pixeltable 0.2.26__py3-none-any.whl → 0.5.7__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (245) hide show
  1. pixeltable/__init__.py +83 -19
  2. pixeltable/_query.py +1444 -0
  3. pixeltable/_version.py +1 -0
  4. pixeltable/catalog/__init__.py +7 -4
  5. pixeltable/catalog/catalog.py +2394 -119
  6. pixeltable/catalog/column.py +225 -104
  7. pixeltable/catalog/dir.py +38 -9
  8. pixeltable/catalog/globals.py +53 -34
  9. pixeltable/catalog/insertable_table.py +265 -115
  10. pixeltable/catalog/path.py +80 -17
  11. pixeltable/catalog/schema_object.py +28 -43
  12. pixeltable/catalog/table.py +1270 -677
  13. pixeltable/catalog/table_metadata.py +103 -0
  14. pixeltable/catalog/table_version.py +1270 -751
  15. pixeltable/catalog/table_version_handle.py +109 -0
  16. pixeltable/catalog/table_version_path.py +137 -42
  17. pixeltable/catalog/tbl_ops.py +53 -0
  18. pixeltable/catalog/update_status.py +191 -0
  19. pixeltable/catalog/view.py +251 -134
  20. pixeltable/config.py +215 -0
  21. pixeltable/env.py +736 -285
  22. pixeltable/exceptions.py +26 -2
  23. pixeltable/exec/__init__.py +7 -2
  24. pixeltable/exec/aggregation_node.py +39 -21
  25. pixeltable/exec/cache_prefetch_node.py +87 -109
  26. pixeltable/exec/cell_materialization_node.py +268 -0
  27. pixeltable/exec/cell_reconstruction_node.py +168 -0
  28. pixeltable/exec/component_iteration_node.py +25 -28
  29. pixeltable/exec/data_row_batch.py +11 -46
  30. pixeltable/exec/exec_context.py +26 -11
  31. pixeltable/exec/exec_node.py +35 -27
  32. pixeltable/exec/expr_eval/__init__.py +3 -0
  33. pixeltable/exec/expr_eval/evaluators.py +365 -0
  34. pixeltable/exec/expr_eval/expr_eval_node.py +413 -0
  35. pixeltable/exec/expr_eval/globals.py +200 -0
  36. pixeltable/exec/expr_eval/row_buffer.py +74 -0
  37. pixeltable/exec/expr_eval/schedulers.py +413 -0
  38. pixeltable/exec/globals.py +35 -0
  39. pixeltable/exec/in_memory_data_node.py +35 -27
  40. pixeltable/exec/object_store_save_node.py +293 -0
  41. pixeltable/exec/row_update_node.py +44 -29
  42. pixeltable/exec/sql_node.py +414 -115
  43. pixeltable/exprs/__init__.py +8 -5
  44. pixeltable/exprs/arithmetic_expr.py +79 -45
  45. pixeltable/exprs/array_slice.py +5 -5
  46. pixeltable/exprs/column_property_ref.py +40 -26
  47. pixeltable/exprs/column_ref.py +254 -61
  48. pixeltable/exprs/comparison.py +14 -9
  49. pixeltable/exprs/compound_predicate.py +9 -10
  50. pixeltable/exprs/data_row.py +213 -72
  51. pixeltable/exprs/expr.py +270 -104
  52. pixeltable/exprs/expr_dict.py +6 -5
  53. pixeltable/exprs/expr_set.py +20 -11
  54. pixeltable/exprs/function_call.py +383 -284
  55. pixeltable/exprs/globals.py +18 -5
  56. pixeltable/exprs/in_predicate.py +7 -7
  57. pixeltable/exprs/inline_expr.py +37 -37
  58. pixeltable/exprs/is_null.py +8 -4
  59. pixeltable/exprs/json_mapper.py +120 -54
  60. pixeltable/exprs/json_path.py +90 -60
  61. pixeltable/exprs/literal.py +61 -16
  62. pixeltable/exprs/method_ref.py +7 -6
  63. pixeltable/exprs/object_ref.py +19 -8
  64. pixeltable/exprs/row_builder.py +238 -75
  65. pixeltable/exprs/rowid_ref.py +53 -15
  66. pixeltable/exprs/similarity_expr.py +65 -50
  67. pixeltable/exprs/sql_element_cache.py +5 -5
  68. pixeltable/exprs/string_op.py +107 -0
  69. pixeltable/exprs/type_cast.py +25 -13
  70. pixeltable/exprs/variable.py +2 -2
  71. pixeltable/func/__init__.py +9 -5
  72. pixeltable/func/aggregate_function.py +197 -92
  73. pixeltable/func/callable_function.py +119 -35
  74. pixeltable/func/expr_template_function.py +101 -48
  75. pixeltable/func/function.py +375 -62
  76. pixeltable/func/function_registry.py +20 -19
  77. pixeltable/func/globals.py +6 -5
  78. pixeltable/func/mcp.py +74 -0
  79. pixeltable/func/query_template_function.py +151 -35
  80. pixeltable/func/signature.py +178 -49
  81. pixeltable/func/tools.py +164 -0
  82. pixeltable/func/udf.py +176 -53
  83. pixeltable/functions/__init__.py +44 -4
  84. pixeltable/functions/anthropic.py +226 -47
  85. pixeltable/functions/audio.py +148 -11
  86. pixeltable/functions/bedrock.py +137 -0
  87. pixeltable/functions/date.py +188 -0
  88. pixeltable/functions/deepseek.py +113 -0
  89. pixeltable/functions/document.py +81 -0
  90. pixeltable/functions/fal.py +76 -0
  91. pixeltable/functions/fireworks.py +72 -20
  92. pixeltable/functions/gemini.py +249 -0
  93. pixeltable/functions/globals.py +208 -53
  94. pixeltable/functions/groq.py +108 -0
  95. pixeltable/functions/huggingface.py +1088 -95
  96. pixeltable/functions/image.py +155 -84
  97. pixeltable/functions/json.py +8 -11
  98. pixeltable/functions/llama_cpp.py +31 -19
  99. pixeltable/functions/math.py +169 -0
  100. pixeltable/functions/mistralai.py +50 -75
  101. pixeltable/functions/net.py +70 -0
  102. pixeltable/functions/ollama.py +29 -36
  103. pixeltable/functions/openai.py +548 -160
  104. pixeltable/functions/openrouter.py +143 -0
  105. pixeltable/functions/replicate.py +15 -14
  106. pixeltable/functions/reve.py +250 -0
  107. pixeltable/functions/string.py +310 -85
  108. pixeltable/functions/timestamp.py +37 -19
  109. pixeltable/functions/together.py +77 -120
  110. pixeltable/functions/twelvelabs.py +188 -0
  111. pixeltable/functions/util.py +7 -2
  112. pixeltable/functions/uuid.py +30 -0
  113. pixeltable/functions/video.py +1528 -117
  114. pixeltable/functions/vision.py +26 -26
  115. pixeltable/functions/voyageai.py +289 -0
  116. pixeltable/functions/whisper.py +19 -10
  117. pixeltable/functions/whisperx.py +179 -0
  118. pixeltable/functions/yolox.py +112 -0
  119. pixeltable/globals.py +716 -236
  120. pixeltable/index/__init__.py +3 -1
  121. pixeltable/index/base.py +17 -21
  122. pixeltable/index/btree.py +32 -22
  123. pixeltable/index/embedding_index.py +155 -92
  124. pixeltable/io/__init__.py +12 -7
  125. pixeltable/io/datarows.py +140 -0
  126. pixeltable/io/external_store.py +83 -125
  127. pixeltable/io/fiftyone.py +24 -33
  128. pixeltable/io/globals.py +47 -182
  129. pixeltable/io/hf_datasets.py +96 -127
  130. pixeltable/io/label_studio.py +171 -156
  131. pixeltable/io/lancedb.py +3 -0
  132. pixeltable/io/pandas.py +136 -115
  133. pixeltable/io/parquet.py +40 -153
  134. pixeltable/io/table_data_conduit.py +702 -0
  135. pixeltable/io/utils.py +100 -0
  136. pixeltable/iterators/__init__.py +8 -4
  137. pixeltable/iterators/audio.py +207 -0
  138. pixeltable/iterators/base.py +9 -3
  139. pixeltable/iterators/document.py +144 -87
  140. pixeltable/iterators/image.py +17 -38
  141. pixeltable/iterators/string.py +15 -12
  142. pixeltable/iterators/video.py +523 -127
  143. pixeltable/metadata/__init__.py +33 -8
  144. pixeltable/metadata/converters/convert_10.py +2 -3
  145. pixeltable/metadata/converters/convert_13.py +2 -2
  146. pixeltable/metadata/converters/convert_15.py +15 -11
  147. pixeltable/metadata/converters/convert_16.py +4 -5
  148. pixeltable/metadata/converters/convert_17.py +4 -5
  149. pixeltable/metadata/converters/convert_18.py +4 -6
  150. pixeltable/metadata/converters/convert_19.py +6 -9
  151. pixeltable/metadata/converters/convert_20.py +3 -6
  152. pixeltable/metadata/converters/convert_21.py +6 -8
  153. pixeltable/metadata/converters/convert_22.py +3 -2
  154. pixeltable/metadata/converters/convert_23.py +33 -0
  155. pixeltable/metadata/converters/convert_24.py +55 -0
  156. pixeltable/metadata/converters/convert_25.py +19 -0
  157. pixeltable/metadata/converters/convert_26.py +23 -0
  158. pixeltable/metadata/converters/convert_27.py +29 -0
  159. pixeltable/metadata/converters/convert_28.py +13 -0
  160. pixeltable/metadata/converters/convert_29.py +110 -0
  161. pixeltable/metadata/converters/convert_30.py +63 -0
  162. pixeltable/metadata/converters/convert_31.py +11 -0
  163. pixeltable/metadata/converters/convert_32.py +15 -0
  164. pixeltable/metadata/converters/convert_33.py +17 -0
  165. pixeltable/metadata/converters/convert_34.py +21 -0
  166. pixeltable/metadata/converters/convert_35.py +9 -0
  167. pixeltable/metadata/converters/convert_36.py +38 -0
  168. pixeltable/metadata/converters/convert_37.py +15 -0
  169. pixeltable/metadata/converters/convert_38.py +39 -0
  170. pixeltable/metadata/converters/convert_39.py +124 -0
  171. pixeltable/metadata/converters/convert_40.py +73 -0
  172. pixeltable/metadata/converters/convert_41.py +12 -0
  173. pixeltable/metadata/converters/convert_42.py +9 -0
  174. pixeltable/metadata/converters/convert_43.py +44 -0
  175. pixeltable/metadata/converters/util.py +44 -18
  176. pixeltable/metadata/notes.py +21 -0
  177. pixeltable/metadata/schema.py +185 -42
  178. pixeltable/metadata/utils.py +74 -0
  179. pixeltable/mypy/__init__.py +3 -0
  180. pixeltable/mypy/mypy_plugin.py +123 -0
  181. pixeltable/plan.py +616 -225
  182. pixeltable/share/__init__.py +3 -0
  183. pixeltable/share/packager.py +797 -0
  184. pixeltable/share/protocol/__init__.py +33 -0
  185. pixeltable/share/protocol/common.py +165 -0
  186. pixeltable/share/protocol/operation_types.py +33 -0
  187. pixeltable/share/protocol/replica.py +119 -0
  188. pixeltable/share/publish.py +349 -0
  189. pixeltable/store.py +398 -232
  190. pixeltable/type_system.py +730 -267
  191. pixeltable/utils/__init__.py +40 -0
  192. pixeltable/utils/arrow.py +201 -29
  193. pixeltable/utils/av.py +298 -0
  194. pixeltable/utils/azure_store.py +346 -0
  195. pixeltable/utils/coco.py +26 -27
  196. pixeltable/utils/code.py +4 -4
  197. pixeltable/utils/console_output.py +46 -0
  198. pixeltable/utils/coroutine.py +24 -0
  199. pixeltable/utils/dbms.py +92 -0
  200. pixeltable/utils/description_helper.py +11 -12
  201. pixeltable/utils/documents.py +60 -61
  202. pixeltable/utils/exception_handler.py +36 -0
  203. pixeltable/utils/filecache.py +38 -22
  204. pixeltable/utils/formatter.py +88 -51
  205. pixeltable/utils/gcs_store.py +295 -0
  206. pixeltable/utils/http.py +133 -0
  207. pixeltable/utils/http_server.py +14 -13
  208. pixeltable/utils/iceberg.py +13 -0
  209. pixeltable/utils/image.py +17 -0
  210. pixeltable/utils/lancedb.py +90 -0
  211. pixeltable/utils/local_store.py +322 -0
  212. pixeltable/utils/misc.py +5 -0
  213. pixeltable/utils/object_stores.py +573 -0
  214. pixeltable/utils/pydantic.py +60 -0
  215. pixeltable/utils/pytorch.py +20 -20
  216. pixeltable/utils/s3_store.py +527 -0
  217. pixeltable/utils/sql.py +32 -5
  218. pixeltable/utils/system.py +30 -0
  219. pixeltable/utils/transactional_directory.py +4 -3
  220. pixeltable-0.5.7.dist-info/METADATA +579 -0
  221. pixeltable-0.5.7.dist-info/RECORD +227 -0
  222. {pixeltable-0.2.26.dist-info → pixeltable-0.5.7.dist-info}/WHEEL +1 -1
  223. pixeltable-0.5.7.dist-info/entry_points.txt +2 -0
  224. pixeltable/__version__.py +0 -3
  225. pixeltable/catalog/named_function.py +0 -36
  226. pixeltable/catalog/path_dict.py +0 -141
  227. pixeltable/dataframe.py +0 -894
  228. pixeltable/exec/expr_eval_node.py +0 -232
  229. pixeltable/ext/__init__.py +0 -14
  230. pixeltable/ext/functions/__init__.py +0 -8
  231. pixeltable/ext/functions/whisperx.py +0 -77
  232. pixeltable/ext/functions/yolox.py +0 -157
  233. pixeltable/tool/create_test_db_dump.py +0 -311
  234. pixeltable/tool/create_test_video.py +0 -81
  235. pixeltable/tool/doc_plugins/griffe.py +0 -50
  236. pixeltable/tool/doc_plugins/mkdocstrings.py +0 -6
  237. pixeltable/tool/doc_plugins/templates/material/udf.html.jinja +0 -135
  238. pixeltable/tool/embed_udf.py +0 -9
  239. pixeltable/tool/mypy_plugin.py +0 -55
  240. pixeltable/utils/media_store.py +0 -76
  241. pixeltable/utils/s3.py +0 -16
  242. pixeltable-0.2.26.dist-info/METADATA +0 -400
  243. pixeltable-0.2.26.dist-info/RECORD +0 -156
  244. pixeltable-0.2.26.dist-info/entry_points.txt +0 -3
  245. {pixeltable-0.2.26.dist-info → pixeltable-0.5.7.dist-info/licenses}/LICENSE +0 -0
@@ -1,24 +1,27 @@
1
1
  from __future__ import annotations
2
2
 
3
- import abc
4
3
  import importlib
5
4
  import inspect
6
- from typing import TYPE_CHECKING, Any, Callable, Optional
5
+ import typing
6
+ from abc import ABC, abstractmethod
7
+ from copy import copy
8
+ from typing import TYPE_CHECKING, Any, Callable, Sequence, cast
7
9
 
8
10
  import sqlalchemy as sql
11
+ from typing_extensions import Self
9
12
 
10
- import pixeltable as pxt
11
- import pixeltable.exceptions as excs
12
- import pixeltable.type_system as ts
13
+ from pixeltable import exceptions as excs, type_system as ts
13
14
 
14
15
  from .globals import resolve_symbol
15
16
  from .signature import Signature
16
17
 
17
18
  if TYPE_CHECKING:
18
- from .expr_template_function import ExprTemplateFunction
19
+ from pixeltable import exprs
19
20
 
21
+ from .expr_template_function import ExprTemplate, ExprTemplateFunction
20
22
 
21
- class Function(abc.ABC):
23
+
24
+ class Function(ABC):
22
25
  """Base class for Pixeltable's function interface.
23
26
 
24
27
  A function in Pixeltable is an object that has a signature and implements __call__().
@@ -26,29 +29,49 @@ class Function(abc.ABC):
26
29
  via the member self_path.
27
30
  """
28
31
 
29
- signature: Signature
30
- self_path: Optional[str]
32
+ signatures: list[Signature]
33
+ self_path: str | None
31
34
  is_method: bool
32
35
  is_property: bool
33
- _conditional_return_type: Optional[Callable[..., ts.ColumnType]]
36
+ _conditional_return_type: Callable[..., ts.ColumnType] | None
37
+
38
+ # We cache the overload resolutions in self._resolutions. This ensures that each resolution is represented
39
+ # globally by a single Python object. We do this dynamically rather than pre-constructing them in order to
40
+ # avoid circular complexity in the `Function` initialization logic.
41
+ __resolved_fns: list[Self]
34
42
 
35
43
  # Translates a call to this function with the given arguments to its SQLAlchemy equivalent.
36
44
  # Overriden for specific Function instances via the to_sql() decorator. The override must accept the same
37
45
  # parameter names as the original function. Each parameter is going to be of type sql.ColumnElement.
38
- _to_sql: Callable[..., Optional[sql.ColumnElement]]
46
+ _to_sql: Callable[..., sql.ColumnElement | None]
39
47
 
48
+ # Returns the resource pool to use for calling this function with the given arguments.
49
+ # Overriden for specific Function instances via the resource_pool() decorator. The override must accept a subset
50
+ # of the parameters of the original function, with the same type.
51
+ _resource_pool: Callable[..., str | None]
40
52
 
41
53
  def __init__(
42
- self, signature: Signature, self_path: Optional[str] = None, is_method: bool = False, is_property: bool = False
54
+ self,
55
+ signatures: list[Signature],
56
+ self_path: str | None = None,
57
+ is_method: bool = False,
58
+ is_property: bool = False,
43
59
  ):
44
60
  # Check that stored functions cannot be declared using `is_method` or `is_property`:
45
61
  assert not ((is_method or is_property) and self_path is None)
46
- self.signature = signature
62
+ assert isinstance(signatures, list)
63
+ self.signatures = signatures
47
64
  self.self_path = self_path # fully-qualified path to self
48
65
  self.is_method = is_method
49
66
  self.is_property = is_property
50
67
  self._conditional_return_type = None
68
+ self.__resolved_fns = []
51
69
  self._to_sql = self.__default_to_sql
70
+ self._resource_pool = self.__default_resource_pool
71
+
72
+ @property
73
+ def is_valid(self) -> bool:
74
+ return len(self.signatures) > 0
52
75
 
53
76
  @property
54
77
  def name(self) -> str:
@@ -61,52 +84,274 @@ class Function(abc.ABC):
61
84
  return '<anonymous>'
62
85
  ptf_prefix = 'pixeltable.functions.'
63
86
  if self.self_path.startswith(ptf_prefix):
64
- return self.self_path[len(ptf_prefix):]
87
+ return self.self_path[len(ptf_prefix) :]
65
88
  return self.self_path
66
89
 
90
+ @property
91
+ def is_polymorphic(self) -> bool:
92
+ return len(self.signatures) > 1
93
+
94
+ @property
95
+ def signature(self) -> Signature:
96
+ assert not self.is_polymorphic
97
+ return self.signatures[0]
98
+
67
99
  @property
68
100
  def arity(self) -> int:
101
+ assert not self.is_polymorphic
69
102
  return len(self.signature.parameters)
70
103
 
104
+ @property
105
+ @abstractmethod
106
+ def is_async(self) -> bool: ...
107
+
108
+ def comment(self) -> str | None:
109
+ return None
110
+
71
111
  def help_str(self) -> str:
72
- return self.display_name + str(self.signature)
112
+ docstring = self.comment()
113
+ display = self.display_name + str(self.signatures[0])
114
+ if docstring is None:
115
+ return display
116
+ return f'{display}\n\n{docstring}'
117
+
118
+ @property
119
+ def _resolved_fns(self) -> list[Self]:
120
+ """
121
+ Return the list of overload resolutions for this `Function`, constructing it first if necessary.
122
+ Each resolution is a new `Function` instance that retains just the single signature at index `signature_idx`,
123
+ and is otherwise identical to this `Function`.
124
+ """
125
+ if len(self.__resolved_fns) == 0:
126
+ # The list of overload resolutions hasn't been constructed yet; do so now.
127
+ if len(self.signatures) == 1:
128
+ # Only one signature: no need to construct separate resolutions
129
+ self.__resolved_fns.append(self)
130
+ else:
131
+ # Multiple signatures: construct a resolution for each signature
132
+ for idx in range(len(self.signatures)):
133
+ resolution = cast(Self, copy(self))
134
+ resolution.signatures = [self.signatures[idx]]
135
+ resolution.__resolved_fns = [resolution] # Resolves to itself
136
+ resolution._update_as_overload_resolution(idx)
137
+ self.__resolved_fns.append(resolution)
138
+
139
+ return self.__resolved_fns
140
+
141
+ @property
142
+ def _has_resolved_fns(self) -> bool:
143
+ """
144
+ Returns true if the resolved_fns for this `Function` have been constructed (i.e., if self._resolved_fns
145
+ has been accessed).
146
+ """
147
+ return len(self.__resolved_fns) > 0
73
148
 
74
- def __call__(self, *args: Any, **kwargs: Any) -> 'pxt.exprs.FunctionCall':
149
+ def _update_as_overload_resolution(self, signature_idx: int) -> None:
150
+ """
151
+ Subclasses must implement this in order to do any additional work when creating a resolution, beyond
152
+ simply updating `self.signatures`.
153
+ """
154
+ raise NotImplementedError()
155
+
156
+ def __call__(self, *args: Any, **kwargs: Any) -> 'exprs.FunctionCall':
75
157
  from pixeltable import exprs
76
- bound_args = self.signature.py_signature.bind(*args, **kwargs)
77
- self.validate_call(bound_args.arguments)
78
- return exprs.FunctionCall(self, bound_args.arguments)
79
158
 
80
- def validate_call(self, bound_args: dict[str, Any]) -> None:
81
- """Override this to do custom validation of the arguments"""
82
- pass
159
+ args = [exprs.Expr.from_object(arg) for arg in args]
160
+ kwargs = {k: exprs.Expr.from_object(v) for k, v in kwargs.items()}
161
+
162
+ for i, expr in enumerate(args):
163
+ if expr is None:
164
+ raise excs.Error(f'Argument {i + 1} in call to {self.self_path!r} is not a valid Pixeltable expression')
165
+ for param_name, expr in kwargs.items():
166
+ if expr is None:
167
+ raise excs.Error(
168
+ f'Argument {param_name!r} in call to {self.self_path!r} is not a valid Pixeltable expression'
169
+ )
170
+
171
+ resolved_fn, bound_args = self._bind_to_matching_signature(args, kwargs)
172
+ return_type = resolved_fn.call_return_type(bound_args)
173
+
174
+ return exprs.FunctionCall(resolved_fn, args, kwargs, return_type)
175
+
176
+ def _bind_to_matching_signature(self, args: Sequence[Any], kwargs: dict[str, Any]) -> tuple[Self, dict[str, Any]]:
177
+ result: int = -1
178
+ bound_args: dict[str, Any] | None = None
179
+ assert len(self.signatures) > 0
180
+ if len(self.signatures) == 1:
181
+ # Only one signature: call _bind_to_signature() and surface any errors directly
182
+ result = 0
183
+ bound_args = self._bind_to_signature(0, args, kwargs)
184
+ else:
185
+ # Multiple signatures: try each signature in declaration order and trap any errors.
186
+ # If none of them succeed, raise a generic error message.
187
+ for i in range(len(self.signatures)):
188
+ try:
189
+ bound_args = self._bind_to_signature(i, args, kwargs)
190
+ except (TypeError, excs.Error):
191
+ continue
192
+ result = i
193
+ break
194
+ if result == -1:
195
+ raise excs.Error(f'Function {self.name!r} has no matching signature for arguments')
196
+ assert result >= 0
197
+ assert bound_args is not None
198
+ return self._resolved_fns[result], bound_args
199
+
200
+ def _bind_to_signature(self, signature_idx: int, args: Sequence[Any], kwargs: dict[str, Any]) -> dict[str, Any]:
201
+ from pixeltable import exprs
202
+
203
+ signature = self.signatures[signature_idx]
204
+ bound_args = signature.py_signature.bind(*args, **kwargs).arguments
205
+ normalized_args = {k: exprs.Expr.from_object(v) for k, v in bound_args.items()}
206
+ self._resolved_fns[signature_idx].validate_call(normalized_args)
207
+ return normalized_args
83
208
 
84
- def call_return_type(self, kwargs: dict[str, Any]) -> ts.ColumnType:
209
+ def validate_call(self, bound_args: dict[str, 'exprs.Expr' | None]) -> None:
210
+ """Override this to do custom validation of the arguments"""
211
+ assert not self.is_polymorphic
212
+ self.signature.validate_args(bound_args, context=f'in function {self.name!r}')
213
+
214
+ def call_resource_pool(self, bound_args: dict[str, 'exprs.Expr']) -> str:
215
+ """Return the resource pool to use for calling this function with the given arguments"""
216
+ rp_kwargs = self._assemble_callable_args(self._resource_pool, bound_args)
217
+ if rp_kwargs is None:
218
+ # TODO: What to do in this case? An example where this can happen is if model_id is not a constant
219
+ # in a call to one of the OpenAI endpoints.
220
+ raise excs.Error('Could not determine resource pool')
221
+ return self._resource_pool(**rp_kwargs)
222
+
223
+ def call_return_type(self, bound_args: dict[str, 'exprs.Expr']) -> ts.ColumnType:
85
224
  """Return the type of the value returned by calling this function with the given arguments"""
86
225
  if self._conditional_return_type is None:
87
- return self.signature.return_type
88
- bound_args = self.signature.py_signature.bind(**kwargs)
89
- kw_args: dict[str, Any] = {}
90
- sig = inspect.signature(self._conditional_return_type)
91
- for param in sig.parameters.values():
92
- if param.name in bound_args.arguments:
93
- kw_args[param.name] = bound_args.arguments[param.name]
94
- return self._conditional_return_type(**kw_args)
226
+ # No conditional return type specified; use the default return type
227
+ return_type = self.signature.return_type
228
+ else:
229
+ crt_kwargs = self._assemble_callable_args(self._conditional_return_type, bound_args)
230
+ if crt_kwargs is None:
231
+ # A conditional return type is specified, but one of its arguments is not a constant.
232
+ # Use the default return type
233
+ return_type = self.signature.return_type
234
+ else:
235
+ # A conditional return type is specified and all its arguments are constants; use the specific
236
+ # call return type
237
+ return_type = self._conditional_return_type(**crt_kwargs)
238
+
239
+ if return_type.nullable:
240
+ return return_type
241
+
242
+ # If `return_type` is non-nullable, but the function call has a nullable input to any of its non-nullable
243
+ # parameters, then we need to make it nullable. This is because Pixeltable defaults a function output to
244
+ # `None` when any of its non-nullable inputs are `None`.
245
+ for arg_name, arg in bound_args.items():
246
+ param = self.signature.parameters[arg_name]
247
+ if param.kind in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD):
248
+ continue
249
+ if arg.col_type.nullable and not param.col_type.nullable:
250
+ return_type = return_type.copy(nullable=True)
251
+ break
252
+
253
+ return return_type
254
+
255
+ def _assemble_callable_args(self, callable: Callable, bound_args: dict[str, 'exprs.Expr']) -> dict[str, Any] | None:
256
+ """
257
+ Return the kwargs to pass to callable, given bound_args passed to this function.
258
+
259
+ This is used by `conditional_return_type` and `get_resource_pool` to determine call-specific characteristics
260
+ of this function.
261
+
262
+ In both cases, the specified `Callable` takes a subset of the parameters of this Function, which may
263
+ be typed as either `Expr`s or Python values. Any parameters typed as Python values expect to see constants
264
+ (Literals); if the corresponding entries in `bound_args` are not constants, then the return value is None.
265
+ """
266
+ from pixeltable import exprs
267
+
268
+ assert not self.is_polymorphic
269
+
270
+ callable_signature = inspect.signature(callable)
271
+ callable_type_hints = typing.get_type_hints(callable)
272
+ callable_args: dict[str, Any] = {}
273
+
274
+ for param in callable_signature.parameters.values():
275
+ assert param.name in self.signature.parameters
276
+
277
+ arg: exprs.Expr
278
+ if param.name in bound_args:
279
+ arg = bound_args[param.name]
280
+ elif self.signature.parameters[param.name].has_default():
281
+ arg = self.signature.parameters[param.name].default
282
+ else:
283
+ # This parameter is missing from bound_args and has no default value, so return None.
284
+ return None
285
+ assert isinstance(arg, exprs.Expr)
286
+
287
+ expects_expr: type[exprs.Expr] | None = None
288
+ type_hint = callable_type_hints.get(param.name)
289
+ if typing.get_origin(type_hint) is not None:
290
+ type_hint = typing.get_origin(type_hint) # Remove type subscript if one exists
291
+ if isinstance(type_hint, type) and issubclass(type_hint, exprs.Expr):
292
+ # The callable expects an Expr for this parameter. We allow for the case where the
293
+ # callable requests a specific subtype of Expr.
294
+ expects_expr = type_hint
295
+
296
+ if expects_expr is not None:
297
+ # The callable is expecting `param.name` to be an Expr. Validate that it's of the appropriate type;
298
+ # otherwise return None.
299
+ if isinstance(arg, expects_expr):
300
+ callable_args[param.name] = arg
301
+ else:
302
+ return None
303
+ elif isinstance(arg, exprs.Literal):
304
+ # The callable is expecting `param.name` to be a constant Python value. Unpack a Literal if we find
305
+ # one; otherwise return None.
306
+ callable_args[param.name] = arg.val
307
+ else:
308
+ return None
309
+
310
+ return callable_args
95
311
 
96
312
  def conditional_return_type(self, fn: Callable[..., ts.ColumnType]) -> Callable[..., ts.ColumnType]:
97
313
  """Instance decorator for specifying a conditional return type for this function"""
98
314
  # verify that call_return_type only has parameters that are also present in the signature
99
- sig = inspect.signature(fn)
100
- for param in sig.parameters.values():
101
- if param.name not in self.signature.parameters:
102
- raise ValueError(f'`conditional_return_type` has parameter `{param.name}` that is not in the signature')
315
+ fn_sig = inspect.signature(fn)
316
+ for param in fn_sig.parameters.values():
317
+ for self_sig in self.signatures:
318
+ if param.name not in self_sig.parameters:
319
+ raise ValueError(
320
+ f'`conditional_return_type` has parameter `{param.name}` that is not in a signature'
321
+ )
103
322
  self._conditional_return_type = fn
104
323
  return fn
105
324
 
106
325
  def using(self, **kwargs: Any) -> 'ExprTemplateFunction':
326
+ from .expr_template_function import ExprTemplateFunction
327
+
328
+ assert len(self.signatures) > 0
329
+ if len(self.signatures) == 1:
330
+ # Only one signature: call _bind_and_create_template() and surface any errors directly
331
+ template = self._bind_and_create_template(kwargs)
332
+ return ExprTemplateFunction([template])
333
+ else:
334
+ # Multiple signatures: iterate over each signature and generate a template for each
335
+ # successful binding. If there are no successful bindings, raise a generic error.
336
+ # (Note that the resulting ExprTemplateFunction may have strictly fewer signatures than
337
+ # this Function, in the event that only some of the signatures are successfully bound.)
338
+ templates: list['ExprTemplate'] = []
339
+ for i in range(len(self.signatures)):
340
+ try:
341
+ template = self._resolved_fns[i]._bind_and_create_template(kwargs)
342
+ templates.append(template)
343
+ except (TypeError, excs.Error):
344
+ continue
345
+ if len(templates) == 0:
346
+ raise excs.Error(f'Function {self.name!r} has no matching signature for arguments')
347
+ return ExprTemplateFunction(templates)
348
+
349
+ def _bind_and_create_template(self, kwargs: dict[str, Any]) -> 'ExprTemplate':
107
350
  from pixeltable import exprs
108
351
 
109
- from .expr_template_function import ExprTemplateFunction
352
+ from .expr_template_function import ExprTemplate
353
+
354
+ assert not self.is_polymorphic
110
355
 
111
356
  # Resolve each kwarg into a parameter binding
112
357
  bindings: dict[str, exprs.Expr] = {}
@@ -115,62 +360,99 @@ class Function(abc.ABC):
115
360
  raise excs.Error(f'Unknown parameter: {k}')
116
361
  param = self.signature.parameters[k]
117
362
  expr = exprs.Expr.from_object(v)
363
+ if not isinstance(expr, exprs.Literal):
364
+ raise excs.Error(f'Expected a constant value for parameter {k!r} in call to .using()')
118
365
  if not param.col_type.is_supertype_of(expr.col_type):
119
- raise excs.Error(f'Expected type `{param.col_type}` for parameter `{k}`; got `{expr.col_type}`')
120
- bindings[k] = v # Use the original value, not the Expr (The Expr is only for validation)
121
-
122
- residual_params = [
123
- p for p in self.signature.parameters.values() if p.name not in bindings
124
- ]
125
-
126
- # Bind each remaining parameter to a like-named variable
127
- for param in residual_params:
128
- bindings[param.name] = exprs.Variable(param.name, param.col_type)
129
-
130
- call = exprs.FunctionCall(self, bindings)
366
+ raise excs.Error(f'Expected type `{param.col_type}` for parameter {k!r}; got `{expr.col_type}`')
367
+ bindings[k] = expr
368
+
369
+ residual_params = [p for p in self.signature.parameters.values() if p.name not in bindings]
370
+
371
+ # Bind each remaining parameter to a like-named variable.
372
+ # Also construct the call arguments for the template function call. Variables become args when possible;
373
+ # otherwise, they are passed as kwargs.
374
+ template_args: list[exprs.Expr] = []
375
+ template_kwargs: dict[str, exprs.Expr] = {}
376
+ args_ok = True
377
+ for name, param in self.signature.parameters.items():
378
+ if name in bindings:
379
+ template_kwargs[name] = bindings[name]
380
+ args_ok = False
381
+ else:
382
+ var = exprs.Variable(name, param.col_type)
383
+ bindings[name] = var
384
+ if args_ok and param.kind in (
385
+ inspect.Parameter.POSITIONAL_ONLY,
386
+ inspect.Parameter.POSITIONAL_OR_KEYWORD,
387
+ ):
388
+ template_args.append(var)
389
+ else:
390
+ template_kwargs[name] = var
391
+ args_ok = False
392
+
393
+ return_type = self.call_return_type(bindings)
394
+ call = exprs.FunctionCall(self, template_args, template_kwargs, return_type)
131
395
 
132
396
  # Construct the (n-k)-ary signature of the new function. We use `call.col_type` for this, rather than
133
397
  # `self.signature.return_type`, because the return type of the new function may be specialized via a
134
398
  # conditional return type.
135
399
  new_signature = Signature(call.col_type, residual_params, self.signature.is_batched)
136
- return ExprTemplateFunction(call, new_signature)
137
400
 
138
- @abc.abstractmethod
139
- def exec(self, *args: Any, **kwargs: Any) -> Any:
401
+ return ExprTemplate(call, new_signature)
402
+
403
+ def exec(self, args: Sequence[Any], kwargs: dict[str, Any]) -> Any:
404
+ """Execute the function with the given arguments and return the result."""
405
+ raise NotImplementedError()
406
+
407
+ async def aexec(self, *args: Any, **kwargs: Any) -> Any:
140
408
  """Execute the function with the given arguments and return the result."""
141
- pass
409
+ raise NotImplementedError()
142
410
 
143
- def to_sql(self, fn: Callable[..., Optional[sql.ColumnElement]]) -> Callable[..., Optional[sql.ColumnElement]]:
411
+ def to_sql(self, fn: Callable[..., sql.ColumnElement | None]) -> Callable[..., sql.ColumnElement | None]:
144
412
  """Instance decorator for specifying the SQL translation of this function"""
145
413
  self._to_sql = fn
146
414
  return fn
147
415
 
148
- def __default_to_sql(self, *args: Any, **kwargs: Any) -> Optional[sql.ColumnElement]:
416
+ def __default_to_sql(self, *args: Any, **kwargs: Any) -> sql.ColumnElement | None:
149
417
  """The default implementation of SQL translation, which provides no translation"""
150
418
  return None
151
419
 
420
+ def resource_pool(self, fn: Callable[..., str]) -> Callable[..., str]:
421
+ """Instance decorator for specifying the resource pool of this function"""
422
+ # TODO: check that fn's parameters are a subset of our parameters
423
+ self._resource_pool = fn
424
+ return fn
425
+
426
+ def __default_resource_pool(self) -> str | None:
427
+ return None
428
+
152
429
  def __eq__(self, other: object) -> bool:
153
430
  if not isinstance(other, self.__class__):
154
431
  return False
155
432
  return self.self_path == other.self_path
156
433
 
434
+ def __hash__(self) -> int:
435
+ return hash(self.self_path)
436
+
157
437
  def source(self) -> None:
158
438
  """Print source code"""
159
439
  print('source not available')
160
440
 
161
- def as_dict(self) -> dict:
441
+ def as_dict(self) -> dict[str, Any]:
162
442
  """
163
443
  Return a serialized reference to the instance that can be passed to json.dumps() and converted back
164
444
  to an instance with from_dict().
165
445
  Subclasses can override _as_dict().
166
446
  """
447
+ # We currently only ever serialize a function that has a specific signature (not a polymorphic form).
448
+ assert not self.is_polymorphic
167
449
  classpath = f'{self.__class__.__module__}.{self.__class__.__qualname__}'
168
450
  return {'_classpath': classpath, **self._as_dict()}
169
451
 
170
452
  def _as_dict(self) -> dict:
171
- """Default serialization: store the path to self (which includes the module path)"""
453
+ """Default serialization: store the path to self (which includes the module path) and signature."""
172
454
  assert self.self_path is not None
173
- return {'path': self.self_path}
455
+ return {'path': self.self_path, 'signature': self.signature.as_dict()}
174
456
 
175
457
  @classmethod
176
458
  def from_dict(cls, d: dict) -> Function:
@@ -181,15 +463,24 @@ class Function(abc.ABC):
181
463
  module_path, class_name = d['_classpath'].rsplit('.', 1)
182
464
  class_module = importlib.import_module(module_path)
183
465
  func_class = getattr(class_module, class_name)
466
+ assert isinstance(func_class, type) and issubclass(func_class, Function)
184
467
  return func_class._from_dict(d)
185
468
 
186
469
  @classmethod
187
470
  def _from_dict(cls, d: dict) -> Function:
188
471
  """Default deserialization: load the symbol indicated by the stored symbol_path"""
189
- assert 'path' in d and d['path'] is not None
190
- instance = resolve_symbol(d['path'])
191
- assert isinstance(instance, Function)
192
- return instance
472
+ path = d.get('path')
473
+ assert path is not None
474
+ try:
475
+ instance = resolve_symbol(path)
476
+ if isinstance(instance, Function):
477
+ return instance
478
+ else:
479
+ return InvalidFunction(
480
+ path, d, f'the symbol {path!r} is no longer a UDF. (Was the `@pxt.udf` decorator removed?)'
481
+ )
482
+ except (AttributeError, ImportError):
483
+ return InvalidFunction(path, d, f'the symbol {path!r} no longer exists. (Was the UDF moved or renamed?)')
193
484
 
194
485
  def to_store(self) -> tuple[dict, bytes]:
195
486
  """
@@ -202,8 +493,30 @@ class Function(abc.ABC):
202
493
  raise NotImplementedError()
203
494
 
204
495
  @classmethod
205
- def from_store(cls, name: Optional[str], md: dict, binary_obj: bytes) -> Function:
496
+ def from_store(cls, name: str | None, md: dict, binary_obj: bytes) -> Function:
206
497
  """
207
498
  Create a Function instance from the serialized representation returned by to_store()
208
499
  """
209
500
  raise NotImplementedError()
501
+
502
+
503
+ class InvalidFunction(Function):
504
+ fn_dict: dict[str, Any]
505
+ error_msg: str
506
+
507
+ def __init__(self, self_path: str, fn_dict: dict[str, Any], error_msg: str):
508
+ super().__init__([], self_path)
509
+ self.fn_dict = fn_dict
510
+ self.error_msg = error_msg
511
+
512
+ def _as_dict(self) -> dict:
513
+ """
514
+ Here we write out (verbatim) the original metadata that failed to load (and that resulted in the
515
+ InvalidFunction). Note that the InvalidFunction itself is never serialized, so there is no corresponding
516
+ from_dict() method.
517
+ """
518
+ return self.fn_dict
519
+
520
+ @property
521
+ def is_async(self) -> bool:
522
+ return False