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.
- pixeltable/__init__.py +83 -19
- pixeltable/_query.py +1444 -0
- pixeltable/_version.py +1 -0
- pixeltable/catalog/__init__.py +7 -4
- pixeltable/catalog/catalog.py +2394 -119
- pixeltable/catalog/column.py +225 -104
- pixeltable/catalog/dir.py +38 -9
- pixeltable/catalog/globals.py +53 -34
- pixeltable/catalog/insertable_table.py +265 -115
- pixeltable/catalog/path.py +80 -17
- pixeltable/catalog/schema_object.py +28 -43
- pixeltable/catalog/table.py +1270 -677
- pixeltable/catalog/table_metadata.py +103 -0
- pixeltable/catalog/table_version.py +1270 -751
- pixeltable/catalog/table_version_handle.py +109 -0
- pixeltable/catalog/table_version_path.py +137 -42
- pixeltable/catalog/tbl_ops.py +53 -0
- pixeltable/catalog/update_status.py +191 -0
- pixeltable/catalog/view.py +251 -134
- pixeltable/config.py +215 -0
- pixeltable/env.py +736 -285
- pixeltable/exceptions.py +26 -2
- pixeltable/exec/__init__.py +7 -2
- pixeltable/exec/aggregation_node.py +39 -21
- pixeltable/exec/cache_prefetch_node.py +87 -109
- pixeltable/exec/cell_materialization_node.py +268 -0
- pixeltable/exec/cell_reconstruction_node.py +168 -0
- pixeltable/exec/component_iteration_node.py +25 -28
- pixeltable/exec/data_row_batch.py +11 -46
- pixeltable/exec/exec_context.py +26 -11
- pixeltable/exec/exec_node.py +35 -27
- pixeltable/exec/expr_eval/__init__.py +3 -0
- pixeltable/exec/expr_eval/evaluators.py +365 -0
- pixeltable/exec/expr_eval/expr_eval_node.py +413 -0
- pixeltable/exec/expr_eval/globals.py +200 -0
- pixeltable/exec/expr_eval/row_buffer.py +74 -0
- pixeltable/exec/expr_eval/schedulers.py +413 -0
- pixeltable/exec/globals.py +35 -0
- pixeltable/exec/in_memory_data_node.py +35 -27
- pixeltable/exec/object_store_save_node.py +293 -0
- pixeltable/exec/row_update_node.py +44 -29
- pixeltable/exec/sql_node.py +414 -115
- pixeltable/exprs/__init__.py +8 -5
- pixeltable/exprs/arithmetic_expr.py +79 -45
- pixeltable/exprs/array_slice.py +5 -5
- pixeltable/exprs/column_property_ref.py +40 -26
- pixeltable/exprs/column_ref.py +254 -61
- pixeltable/exprs/comparison.py +14 -9
- pixeltable/exprs/compound_predicate.py +9 -10
- pixeltable/exprs/data_row.py +213 -72
- pixeltable/exprs/expr.py +270 -104
- pixeltable/exprs/expr_dict.py +6 -5
- pixeltable/exprs/expr_set.py +20 -11
- pixeltable/exprs/function_call.py +383 -284
- pixeltable/exprs/globals.py +18 -5
- pixeltable/exprs/in_predicate.py +7 -7
- pixeltable/exprs/inline_expr.py +37 -37
- pixeltable/exprs/is_null.py +8 -4
- pixeltable/exprs/json_mapper.py +120 -54
- pixeltable/exprs/json_path.py +90 -60
- pixeltable/exprs/literal.py +61 -16
- pixeltable/exprs/method_ref.py +7 -6
- pixeltable/exprs/object_ref.py +19 -8
- pixeltable/exprs/row_builder.py +238 -75
- pixeltable/exprs/rowid_ref.py +53 -15
- pixeltable/exprs/similarity_expr.py +65 -50
- pixeltable/exprs/sql_element_cache.py +5 -5
- pixeltable/exprs/string_op.py +107 -0
- pixeltable/exprs/type_cast.py +25 -13
- pixeltable/exprs/variable.py +2 -2
- pixeltable/func/__init__.py +9 -5
- pixeltable/func/aggregate_function.py +197 -92
- pixeltable/func/callable_function.py +119 -35
- pixeltable/func/expr_template_function.py +101 -48
- pixeltable/func/function.py +375 -62
- pixeltable/func/function_registry.py +20 -19
- pixeltable/func/globals.py +6 -5
- pixeltable/func/mcp.py +74 -0
- pixeltable/func/query_template_function.py +151 -35
- pixeltable/func/signature.py +178 -49
- pixeltable/func/tools.py +164 -0
- pixeltable/func/udf.py +176 -53
- pixeltable/functions/__init__.py +44 -4
- pixeltable/functions/anthropic.py +226 -47
- pixeltable/functions/audio.py +148 -11
- pixeltable/functions/bedrock.py +137 -0
- pixeltable/functions/date.py +188 -0
- pixeltable/functions/deepseek.py +113 -0
- pixeltable/functions/document.py +81 -0
- pixeltable/functions/fal.py +76 -0
- pixeltable/functions/fireworks.py +72 -20
- pixeltable/functions/gemini.py +249 -0
- pixeltable/functions/globals.py +208 -53
- pixeltable/functions/groq.py +108 -0
- pixeltable/functions/huggingface.py +1088 -95
- pixeltable/functions/image.py +155 -84
- pixeltable/functions/json.py +8 -11
- pixeltable/functions/llama_cpp.py +31 -19
- pixeltable/functions/math.py +169 -0
- pixeltable/functions/mistralai.py +50 -75
- pixeltable/functions/net.py +70 -0
- pixeltable/functions/ollama.py +29 -36
- pixeltable/functions/openai.py +548 -160
- pixeltable/functions/openrouter.py +143 -0
- pixeltable/functions/replicate.py +15 -14
- pixeltable/functions/reve.py +250 -0
- pixeltable/functions/string.py +310 -85
- pixeltable/functions/timestamp.py +37 -19
- pixeltable/functions/together.py +77 -120
- pixeltable/functions/twelvelabs.py +188 -0
- pixeltable/functions/util.py +7 -2
- pixeltable/functions/uuid.py +30 -0
- pixeltable/functions/video.py +1528 -117
- pixeltable/functions/vision.py +26 -26
- pixeltable/functions/voyageai.py +289 -0
- pixeltable/functions/whisper.py +19 -10
- pixeltable/functions/whisperx.py +179 -0
- pixeltable/functions/yolox.py +112 -0
- pixeltable/globals.py +716 -236
- pixeltable/index/__init__.py +3 -1
- pixeltable/index/base.py +17 -21
- pixeltable/index/btree.py +32 -22
- pixeltable/index/embedding_index.py +155 -92
- pixeltable/io/__init__.py +12 -7
- pixeltable/io/datarows.py +140 -0
- pixeltable/io/external_store.py +83 -125
- pixeltable/io/fiftyone.py +24 -33
- pixeltable/io/globals.py +47 -182
- pixeltable/io/hf_datasets.py +96 -127
- pixeltable/io/label_studio.py +171 -156
- pixeltable/io/lancedb.py +3 -0
- pixeltable/io/pandas.py +136 -115
- pixeltable/io/parquet.py +40 -153
- pixeltable/io/table_data_conduit.py +702 -0
- pixeltable/io/utils.py +100 -0
- pixeltable/iterators/__init__.py +8 -4
- pixeltable/iterators/audio.py +207 -0
- pixeltable/iterators/base.py +9 -3
- pixeltable/iterators/document.py +144 -87
- pixeltable/iterators/image.py +17 -38
- pixeltable/iterators/string.py +15 -12
- pixeltable/iterators/video.py +523 -127
- pixeltable/metadata/__init__.py +33 -8
- pixeltable/metadata/converters/convert_10.py +2 -3
- pixeltable/metadata/converters/convert_13.py +2 -2
- pixeltable/metadata/converters/convert_15.py +15 -11
- pixeltable/metadata/converters/convert_16.py +4 -5
- pixeltable/metadata/converters/convert_17.py +4 -5
- pixeltable/metadata/converters/convert_18.py +4 -6
- pixeltable/metadata/converters/convert_19.py +6 -9
- pixeltable/metadata/converters/convert_20.py +3 -6
- pixeltable/metadata/converters/convert_21.py +6 -8
- pixeltable/metadata/converters/convert_22.py +3 -2
- pixeltable/metadata/converters/convert_23.py +33 -0
- pixeltable/metadata/converters/convert_24.py +55 -0
- pixeltable/metadata/converters/convert_25.py +19 -0
- pixeltable/metadata/converters/convert_26.py +23 -0
- pixeltable/metadata/converters/convert_27.py +29 -0
- pixeltable/metadata/converters/convert_28.py +13 -0
- pixeltable/metadata/converters/convert_29.py +110 -0
- pixeltable/metadata/converters/convert_30.py +63 -0
- pixeltable/metadata/converters/convert_31.py +11 -0
- pixeltable/metadata/converters/convert_32.py +15 -0
- pixeltable/metadata/converters/convert_33.py +17 -0
- pixeltable/metadata/converters/convert_34.py +21 -0
- pixeltable/metadata/converters/convert_35.py +9 -0
- pixeltable/metadata/converters/convert_36.py +38 -0
- pixeltable/metadata/converters/convert_37.py +15 -0
- pixeltable/metadata/converters/convert_38.py +39 -0
- pixeltable/metadata/converters/convert_39.py +124 -0
- pixeltable/metadata/converters/convert_40.py +73 -0
- pixeltable/metadata/converters/convert_41.py +12 -0
- pixeltable/metadata/converters/convert_42.py +9 -0
- pixeltable/metadata/converters/convert_43.py +44 -0
- pixeltable/metadata/converters/util.py +44 -18
- pixeltable/metadata/notes.py +21 -0
- pixeltable/metadata/schema.py +185 -42
- pixeltable/metadata/utils.py +74 -0
- pixeltable/mypy/__init__.py +3 -0
- pixeltable/mypy/mypy_plugin.py +123 -0
- pixeltable/plan.py +616 -225
- pixeltable/share/__init__.py +3 -0
- pixeltable/share/packager.py +797 -0
- pixeltable/share/protocol/__init__.py +33 -0
- pixeltable/share/protocol/common.py +165 -0
- pixeltable/share/protocol/operation_types.py +33 -0
- pixeltable/share/protocol/replica.py +119 -0
- pixeltable/share/publish.py +349 -0
- pixeltable/store.py +398 -232
- pixeltable/type_system.py +730 -267
- pixeltable/utils/__init__.py +40 -0
- pixeltable/utils/arrow.py +201 -29
- pixeltable/utils/av.py +298 -0
- pixeltable/utils/azure_store.py +346 -0
- pixeltable/utils/coco.py +26 -27
- pixeltable/utils/code.py +4 -4
- pixeltable/utils/console_output.py +46 -0
- pixeltable/utils/coroutine.py +24 -0
- pixeltable/utils/dbms.py +92 -0
- pixeltable/utils/description_helper.py +11 -12
- pixeltable/utils/documents.py +60 -61
- pixeltable/utils/exception_handler.py +36 -0
- pixeltable/utils/filecache.py +38 -22
- pixeltable/utils/formatter.py +88 -51
- pixeltable/utils/gcs_store.py +295 -0
- pixeltable/utils/http.py +133 -0
- pixeltable/utils/http_server.py +14 -13
- pixeltable/utils/iceberg.py +13 -0
- pixeltable/utils/image.py +17 -0
- pixeltable/utils/lancedb.py +90 -0
- pixeltable/utils/local_store.py +322 -0
- pixeltable/utils/misc.py +5 -0
- pixeltable/utils/object_stores.py +573 -0
- pixeltable/utils/pydantic.py +60 -0
- pixeltable/utils/pytorch.py +20 -20
- pixeltable/utils/s3_store.py +527 -0
- pixeltable/utils/sql.py +32 -5
- pixeltable/utils/system.py +30 -0
- pixeltable/utils/transactional_directory.py +4 -3
- pixeltable-0.5.7.dist-info/METADATA +579 -0
- pixeltable-0.5.7.dist-info/RECORD +227 -0
- {pixeltable-0.2.26.dist-info → pixeltable-0.5.7.dist-info}/WHEEL +1 -1
- pixeltable-0.5.7.dist-info/entry_points.txt +2 -0
- pixeltable/__version__.py +0 -3
- pixeltable/catalog/named_function.py +0 -36
- pixeltable/catalog/path_dict.py +0 -141
- pixeltable/dataframe.py +0 -894
- pixeltable/exec/expr_eval_node.py +0 -232
- pixeltable/ext/__init__.py +0 -14
- pixeltable/ext/functions/__init__.py +0 -8
- pixeltable/ext/functions/whisperx.py +0 -77
- pixeltable/ext/functions/yolox.py +0 -157
- pixeltable/tool/create_test_db_dump.py +0 -311
- pixeltable/tool/create_test_video.py +0 -81
- pixeltable/tool/doc_plugins/griffe.py +0 -50
- pixeltable/tool/doc_plugins/mkdocstrings.py +0 -6
- pixeltable/tool/doc_plugins/templates/material/udf.html.jinja +0 -135
- pixeltable/tool/embed_udf.py +0 -9
- pixeltable/tool/mypy_plugin.py +0 -55
- pixeltable/utils/media_store.py +0 -76
- pixeltable/utils/s3.py +0 -16
- pixeltable-0.2.26.dist-info/METADATA +0 -400
- pixeltable-0.2.26.dist-info/RECORD +0 -156
- pixeltable-0.2.26.dist-info/entry_points.txt +0 -3
- {pixeltable-0.2.26.dist-info → pixeltable-0.5.7.dist-info/licenses}/LICENSE +0 -0
pixeltable/func/function.py
CHANGED
|
@@ -1,24 +1,27 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import abc
|
|
4
3
|
import importlib
|
|
5
4
|
import inspect
|
|
6
|
-
|
|
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
|
-
|
|
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
|
|
19
|
+
from pixeltable import exprs
|
|
19
20
|
|
|
21
|
+
from .expr_template_function import ExprTemplate, ExprTemplateFunction
|
|
20
22
|
|
|
21
|
-
|
|
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
|
-
|
|
30
|
-
self_path:
|
|
32
|
+
signatures: list[Signature]
|
|
33
|
+
self_path: str | None
|
|
31
34
|
is_method: bool
|
|
32
35
|
is_property: bool
|
|
33
|
-
_conditional_return_type:
|
|
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[...,
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
|
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
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
100
|
-
for param in
|
|
101
|
-
|
|
102
|
-
|
|
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
|
|
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
|
|
120
|
-
bindings[k] =
|
|
121
|
-
|
|
122
|
-
residual_params = [
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
#
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
|
|
139
|
-
|
|
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
|
-
|
|
409
|
+
raise NotImplementedError()
|
|
142
410
|
|
|
143
|
-
def to_sql(self, fn: Callable[...,
|
|
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) ->
|
|
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
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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:
|
|
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
|