pixeltable 0.1.2__py3-none-any.whl → 0.2.0__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.
Potentially problematic release.
This version of pixeltable might be problematic. Click here for more details.
- pixeltable/__init__.py +21 -4
- pixeltable/catalog/__init__.py +13 -0
- pixeltable/catalog/catalog.py +159 -0
- pixeltable/catalog/column.py +200 -0
- pixeltable/catalog/dir.py +32 -0
- pixeltable/catalog/globals.py +33 -0
- pixeltable/catalog/insertable_table.py +191 -0
- pixeltable/catalog/named_function.py +36 -0
- pixeltable/catalog/path.py +58 -0
- pixeltable/catalog/path_dict.py +139 -0
- pixeltable/catalog/schema_object.py +39 -0
- pixeltable/catalog/table.py +581 -0
- pixeltable/catalog/table_version.py +749 -0
- pixeltable/catalog/table_version_path.py +133 -0
- pixeltable/catalog/view.py +203 -0
- pixeltable/client.py +520 -31
- pixeltable/dataframe.py +540 -349
- pixeltable/env.py +373 -48
- pixeltable/exceptions.py +12 -21
- pixeltable/exec/__init__.py +9 -0
- pixeltable/exec/aggregation_node.py +78 -0
- pixeltable/exec/cache_prefetch_node.py +113 -0
- pixeltable/exec/component_iteration_node.py +79 -0
- pixeltable/exec/data_row_batch.py +95 -0
- pixeltable/exec/exec_context.py +22 -0
- pixeltable/exec/exec_node.py +61 -0
- pixeltable/exec/expr_eval_node.py +217 -0
- pixeltable/exec/in_memory_data_node.py +69 -0
- pixeltable/exec/media_validation_node.py +43 -0
- pixeltable/exec/sql_scan_node.py +225 -0
- pixeltable/exprs/__init__.py +24 -0
- pixeltable/exprs/arithmetic_expr.py +102 -0
- pixeltable/exprs/array_slice.py +71 -0
- pixeltable/exprs/column_property_ref.py +77 -0
- pixeltable/exprs/column_ref.py +105 -0
- pixeltable/exprs/comparison.py +77 -0
- pixeltable/exprs/compound_predicate.py +98 -0
- pixeltable/exprs/data_row.py +187 -0
- pixeltable/exprs/expr.py +586 -0
- pixeltable/exprs/expr_set.py +39 -0
- pixeltable/exprs/function_call.py +380 -0
- pixeltable/exprs/globals.py +69 -0
- pixeltable/exprs/image_member_access.py +115 -0
- pixeltable/exprs/image_similarity_predicate.py +58 -0
- pixeltable/exprs/inline_array.py +107 -0
- pixeltable/exprs/inline_dict.py +101 -0
- pixeltable/exprs/is_null.py +38 -0
- pixeltable/exprs/json_mapper.py +121 -0
- pixeltable/exprs/json_path.py +159 -0
- pixeltable/exprs/literal.py +54 -0
- pixeltable/exprs/object_ref.py +41 -0
- pixeltable/exprs/predicate.py +44 -0
- pixeltable/exprs/row_builder.py +355 -0
- pixeltable/exprs/rowid_ref.py +94 -0
- pixeltable/exprs/type_cast.py +53 -0
- pixeltable/exprs/variable.py +45 -0
- pixeltable/func/__init__.py +9 -0
- pixeltable/func/aggregate_function.py +194 -0
- pixeltable/func/batched_function.py +53 -0
- pixeltable/func/callable_function.py +69 -0
- pixeltable/func/expr_template_function.py +82 -0
- pixeltable/func/function.py +110 -0
- pixeltable/func/function_registry.py +227 -0
- pixeltable/func/globals.py +36 -0
- pixeltable/func/nos_function.py +202 -0
- pixeltable/func/signature.py +166 -0
- pixeltable/func/udf.py +163 -0
- pixeltable/functions/__init__.py +52 -103
- pixeltable/functions/eval.py +216 -0
- pixeltable/functions/fireworks.py +61 -0
- pixeltable/functions/huggingface.py +120 -0
- pixeltable/functions/image.py +16 -0
- pixeltable/functions/openai.py +88 -0
- pixeltable/functions/pil/image.py +148 -7
- pixeltable/functions/string.py +13 -0
- pixeltable/functions/together.py +27 -0
- pixeltable/functions/util.py +41 -0
- pixeltable/functions/video.py +62 -0
- pixeltable/iterators/__init__.py +3 -0
- pixeltable/iterators/base.py +48 -0
- pixeltable/iterators/document.py +311 -0
- pixeltable/iterators/video.py +89 -0
- pixeltable/metadata/__init__.py +54 -0
- pixeltable/metadata/converters/convert_10.py +18 -0
- pixeltable/metadata/schema.py +211 -0
- pixeltable/plan.py +656 -0
- pixeltable/store.py +413 -182
- pixeltable/tests/conftest.py +143 -86
- pixeltable/tests/test_audio.py +65 -0
- pixeltable/tests/test_catalog.py +27 -0
- pixeltable/tests/test_client.py +14 -14
- pixeltable/tests/test_component_view.py +372 -0
- pixeltable/tests/test_dataframe.py +433 -0
- pixeltable/tests/test_dirs.py +78 -62
- pixeltable/tests/test_document.py +117 -0
- pixeltable/tests/test_exprs.py +591 -135
- pixeltable/tests/test_function.py +297 -67
- pixeltable/tests/test_functions.py +283 -1
- pixeltable/tests/test_migration.py +43 -0
- pixeltable/tests/test_nos.py +54 -0
- pixeltable/tests/test_snapshot.py +208 -0
- pixeltable/tests/test_table.py +1086 -258
- pixeltable/tests/test_transactional_directory.py +42 -0
- pixeltable/tests/test_types.py +5 -11
- pixeltable/tests/test_video.py +149 -34
- pixeltable/tests/test_view.py +530 -0
- pixeltable/tests/utils.py +186 -45
- pixeltable/tool/create_test_db_dump.py +149 -0
- pixeltable/type_system.py +490 -133
- pixeltable/utils/__init__.py +17 -46
- pixeltable/utils/clip.py +12 -15
- pixeltable/utils/coco.py +136 -0
- pixeltable/utils/documents.py +39 -0
- pixeltable/utils/filecache.py +195 -0
- pixeltable/utils/help.py +11 -0
- pixeltable/utils/media_store.py +76 -0
- pixeltable/utils/parquet.py +126 -0
- pixeltable/utils/pytorch.py +172 -0
- pixeltable/utils/s3.py +13 -0
- pixeltable/utils/sql.py +17 -0
- pixeltable/utils/transactional_directory.py +35 -0
- pixeltable-0.2.0.dist-info/LICENSE +18 -0
- pixeltable-0.2.0.dist-info/METADATA +117 -0
- pixeltable-0.2.0.dist-info/RECORD +125 -0
- {pixeltable-0.1.2.dist-info → pixeltable-0.2.0.dist-info}/WHEEL +1 -1
- pixeltable/catalog.py +0 -1421
- pixeltable/exprs.py +0 -1745
- pixeltable/function.py +0 -269
- pixeltable/functions/clip.py +0 -10
- pixeltable/functions/pil/__init__.py +0 -23
- pixeltable/functions/tf.py +0 -21
- pixeltable/index.py +0 -57
- pixeltable/tests/test_dict.py +0 -24
- pixeltable/tests/test_tf.py +0 -69
- pixeltable/tf.py +0 -33
- pixeltable/utils/tf.py +0 -33
- pixeltable/utils/video.py +0 -32
- pixeltable-0.1.2.dist-info/LICENSE +0 -201
- pixeltable-0.1.2.dist-info/METADATA +0 -89
- pixeltable-0.1.2.dist-info/RECORD +0 -37
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import abc
|
|
4
|
+
import importlib
|
|
5
|
+
import inspect
|
|
6
|
+
from typing import Optional, Any, Type, List, Dict
|
|
7
|
+
import itertools
|
|
8
|
+
|
|
9
|
+
import pixeltable.exceptions as excs
|
|
10
|
+
import pixeltable.type_system as ts
|
|
11
|
+
from .function import Function
|
|
12
|
+
from .signature import Signature, Parameter
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Aggregator(abc.ABC):
|
|
16
|
+
def update(self, *args: Any, **kwargs: Any) -> None:
|
|
17
|
+
pass
|
|
18
|
+
def value(self) -> Any:
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class AggregateFunction(Function):
|
|
23
|
+
"""Function interface for an aggregation operation.
|
|
24
|
+
|
|
25
|
+
requires_order_by: if True, the first parameter to an aggregate function defines the order in which the function
|
|
26
|
+
sees rows in update()
|
|
27
|
+
allows_std_agg: if True, the aggregate function can be used as a standard aggregate function w/o a window
|
|
28
|
+
allows_window: if True, the aggregate function can be used with a window
|
|
29
|
+
"""
|
|
30
|
+
ORDER_BY_PARAM = 'order_by'
|
|
31
|
+
GROUP_BY_PARAM = 'group_by'
|
|
32
|
+
RESERVED_PARAMS = {ORDER_BY_PARAM, GROUP_BY_PARAM}
|
|
33
|
+
|
|
34
|
+
def __init__(
|
|
35
|
+
self, aggregator_class: Type[Aggregator], self_path: str,
|
|
36
|
+
init_types: List[ts.ColumnType], update_types: List[ts.ColumnType], value_type: ts.ColumnType,
|
|
37
|
+
requires_order_by: bool, allows_std_agg: bool, allows_window: bool):
|
|
38
|
+
self.agg_cls = aggregator_class
|
|
39
|
+
self.requires_order_by = requires_order_by
|
|
40
|
+
self.allows_std_agg = allows_std_agg
|
|
41
|
+
self.allows_window = allows_window
|
|
42
|
+
|
|
43
|
+
# our signature is the signature of 'update', but without self,
|
|
44
|
+
# plus the parameters of 'init' as keyword-only parameters
|
|
45
|
+
update_params = list(inspect.signature(self.agg_cls.update).parameters.values())[1:] # leave out self
|
|
46
|
+
assert len(update_params) == len(update_types)
|
|
47
|
+
init_params = [
|
|
48
|
+
inspect.Parameter(p.name, inspect.Parameter.KEYWORD_ONLY, default=p.default)
|
|
49
|
+
# starting at 1: leave out self
|
|
50
|
+
for p in itertools.islice(inspect.signature(self.agg_cls.__init__).parameters.values(), 1, None)
|
|
51
|
+
]
|
|
52
|
+
assert len(init_params) == len(init_types)
|
|
53
|
+
duplicate_params = set(p.name for p in init_params) & set(p.name for p in update_params)
|
|
54
|
+
if len(duplicate_params) > 0:
|
|
55
|
+
raise excs.Error(
|
|
56
|
+
f'__init__() and update() cannot have parameters with the same name: '
|
|
57
|
+
f'{", ".join(duplicate_params)}'
|
|
58
|
+
)
|
|
59
|
+
py_params = update_params + init_params # init_params are keyword-only and come last
|
|
60
|
+
py_signature = inspect.Signature(py_params)
|
|
61
|
+
|
|
62
|
+
params = [Parameter(p.name, update_types[i], p.kind, is_batched=False) for i, p in enumerate(update_params)]
|
|
63
|
+
params.extend([Parameter(p.name, init_types[i], p.kind, is_batched=False) for i, p in enumerate(init_params)])
|
|
64
|
+
signature = Signature(value_type, params)
|
|
65
|
+
super().__init__(signature, py_signature=py_signature, self_path=self_path)
|
|
66
|
+
self.init_param_names = [p.name for p in init_params]
|
|
67
|
+
|
|
68
|
+
# make sure the signature doesn't contain reserved parameter names;
|
|
69
|
+
# do this after super().__init__(), otherwise self.name is invalid
|
|
70
|
+
for param in signature.parameters:
|
|
71
|
+
if param.lower() in self.RESERVED_PARAMS:
|
|
72
|
+
raise excs.Error(f'{self.name}(): parameter name {param} is reserved')
|
|
73
|
+
|
|
74
|
+
def help_str(self) -> str:
|
|
75
|
+
res = super().help_str()
|
|
76
|
+
res += '\n\n' + inspect.getdoc(self.agg_cls.update)
|
|
77
|
+
return res
|
|
78
|
+
|
|
79
|
+
def __call__(self, *args: object, **kwargs: object) -> 'pixeltable.exprs.Expr':
|
|
80
|
+
from pixeltable import exprs
|
|
81
|
+
|
|
82
|
+
# perform semantic analysis of special parameters 'order_by' and 'group_by'
|
|
83
|
+
order_by_clause: Optional[Any] = None
|
|
84
|
+
if self.ORDER_BY_PARAM in kwargs:
|
|
85
|
+
if self.requires_order_by:
|
|
86
|
+
raise excs.Error(
|
|
87
|
+
f'{self.display_name}(): order_by invalid, this function requires the first argument to be the '
|
|
88
|
+
f'ordering expression'
|
|
89
|
+
)
|
|
90
|
+
if not self.allows_window:
|
|
91
|
+
raise excs.Error(
|
|
92
|
+
f'{self.display_name}(): order_by invalid with an aggregate function that does not allow windows')
|
|
93
|
+
order_by_clause = kwargs.pop(self.ORDER_BY_PARAM)
|
|
94
|
+
elif self.requires_order_by:
|
|
95
|
+
# the first argument is the order-by expr
|
|
96
|
+
if len(args) == 0:
|
|
97
|
+
raise excs.Error(f'{self.display_name}(): requires an ordering expression as its first argument')
|
|
98
|
+
order_by_clause = args[0]
|
|
99
|
+
if not isinstance(order_by_clause, exprs.Expr):
|
|
100
|
+
raise excs.Error(
|
|
101
|
+
f'{self.display_name}(): the first argument needs to be a Pixeltable expression, but instead is a '
|
|
102
|
+
f'{type(order_by_clause)}'
|
|
103
|
+
)
|
|
104
|
+
# don't pass the first parameter on, the Function doesn't get to see it
|
|
105
|
+
args = args[1:]
|
|
106
|
+
|
|
107
|
+
group_by_clause: Optional[Any] = None
|
|
108
|
+
if self.GROUP_BY_PARAM in kwargs:
|
|
109
|
+
if not self.allows_window:
|
|
110
|
+
raise excs.Error(
|
|
111
|
+
f'{self.display_name}(): group_by invalid with an aggregate function that does not allow windows')
|
|
112
|
+
group_by_clause = kwargs.pop(self.GROUP_BY_PARAM)
|
|
113
|
+
|
|
114
|
+
bound_args = self.py_signature.bind(*args, **kwargs)
|
|
115
|
+
self.validate_call(bound_args.arguments)
|
|
116
|
+
return exprs.FunctionCall(
|
|
117
|
+
self, bound_args.arguments,
|
|
118
|
+
order_by_clause=[order_by_clause] if order_by_clause is not None else [],
|
|
119
|
+
group_by_clause=[group_by_clause] if group_by_clause is not None else [])
|
|
120
|
+
|
|
121
|
+
def validate_call(self, bound_args: Dict[str, Any]) -> None:
|
|
122
|
+
# check that init parameters are not Exprs
|
|
123
|
+
# TODO: do this in the planner (check that init parameters are either constants or only refer to grouping exprs)
|
|
124
|
+
import pixeltable.exprs as exprs
|
|
125
|
+
for param_name in self.init_param_names:
|
|
126
|
+
if param_name in bound_args and isinstance(bound_args[param_name], exprs.Expr):
|
|
127
|
+
raise excs.Error(
|
|
128
|
+
f'{self.display_name}(): init() parameter {param_name} needs to be a constant, not a Pixeltable '
|
|
129
|
+
f'expression'
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def uda(
|
|
134
|
+
*,
|
|
135
|
+
value_type: ts.ColumnType,
|
|
136
|
+
update_types: List[ts.ColumnType],
|
|
137
|
+
init_types: Optional[List[ts.ColumnType]] = None,
|
|
138
|
+
requires_order_by: bool = False, allows_std_agg: bool = True, allows_window: bool = False,
|
|
139
|
+
name: Optional[str] = None
|
|
140
|
+
) -> Type[Aggregator]:
|
|
141
|
+
"""Decorator for user-defined aggregate functions.
|
|
142
|
+
|
|
143
|
+
The decorated class must inherit from Aggregator and implement the following methods:
|
|
144
|
+
- __init__(self, ...) to initialize the aggregator
|
|
145
|
+
- update(self, ...) to update the aggregator with a new value
|
|
146
|
+
- value(self) to return the final result
|
|
147
|
+
|
|
148
|
+
The decorator creates an AggregateFunction instance from the class and adds it
|
|
149
|
+
to the module where the class is defined.
|
|
150
|
+
|
|
151
|
+
Parameters:
|
|
152
|
+
- init_types: list of types for the __init__() parameters; must match the number of parameters
|
|
153
|
+
- update_types: list of types for the update() parameters; must match the number of parameters
|
|
154
|
+
- value_type: return type of the aggregator
|
|
155
|
+
- requires_order_by: if True, the first parameter to the function is the order-by expression
|
|
156
|
+
- allows_std_agg: if True, the function can be used as a standard aggregate function w/o a window
|
|
157
|
+
- allows_window: if True, the function can be used with a window
|
|
158
|
+
- name: name of the AggregateFunction instance; if None, the class name is used
|
|
159
|
+
"""
|
|
160
|
+
if name is not None and not name.isidentifier():
|
|
161
|
+
raise excs.Error(f'Invalid name: {name}')
|
|
162
|
+
if init_types is None:
|
|
163
|
+
init_types = []
|
|
164
|
+
|
|
165
|
+
def decorator(cls: Type[Aggregator]) -> Type[Aggregator]:
|
|
166
|
+
# validate type parameters
|
|
167
|
+
num_init_params = len(inspect.signature(cls.__init__).parameters) - 1
|
|
168
|
+
if num_init_params > 0:
|
|
169
|
+
if len(init_types) != num_init_params:
|
|
170
|
+
raise excs.Error(
|
|
171
|
+
f'init_types must be a list of {num_init_params} types, one for each parameter of __init__()')
|
|
172
|
+
num_update_params = len(inspect.signature(cls.update).parameters) - 1
|
|
173
|
+
if num_update_params == 0:
|
|
174
|
+
raise excs.Error('update() must have at least one parameter')
|
|
175
|
+
if len(update_types) != num_update_params:
|
|
176
|
+
raise excs.Error(
|
|
177
|
+
f'update_types must be a list of {num_update_params} types, one for each parameter of update()')
|
|
178
|
+
assert value_type is not None
|
|
179
|
+
|
|
180
|
+
# the AggregateFunction instance resides in the same module as cls
|
|
181
|
+
module_path = cls.__module__
|
|
182
|
+
nonlocal name
|
|
183
|
+
name = name or cls.__name__
|
|
184
|
+
instance_path = f'{module_path}.{name}'
|
|
185
|
+
|
|
186
|
+
# create the corresponding AggregateFunction instance
|
|
187
|
+
instance = AggregateFunction(
|
|
188
|
+
cls, instance_path, init_types, update_types, value_type, requires_order_by, allows_std_agg, allows_window)
|
|
189
|
+
module = importlib.import_module(module_path)
|
|
190
|
+
setattr(module, name, instance)
|
|
191
|
+
|
|
192
|
+
return cls
|
|
193
|
+
|
|
194
|
+
return decorator
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
from typing import List, Dict, Any, Optional, Callable
|
|
3
|
+
import abc
|
|
4
|
+
|
|
5
|
+
from .function import Function
|
|
6
|
+
from .signature import Signature
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class BatchedFunction(Function):
|
|
10
|
+
"""Base class for functions that can run on batches"""
|
|
11
|
+
|
|
12
|
+
@abc.abstractmethod
|
|
13
|
+
def get_batch_size(self, *args: Any, **kwargs: Any) -> Optional[int]:
|
|
14
|
+
"""Return the batch size for the given arguments, or None if the batch size is unknown.
|
|
15
|
+
args/kwargs might be empty
|
|
16
|
+
"""
|
|
17
|
+
raise NotImplementedError
|
|
18
|
+
|
|
19
|
+
@abc.abstractmethod
|
|
20
|
+
def invoke(self, arg_batches: List[List[Any]], kwarg_batches: Dict[str, List[Any]]) -> List[Any]:
|
|
21
|
+
"""Invoke the function for the given batch and return a batch of results"""
|
|
22
|
+
raise NotImplementedError
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ExplicitBatchedFunction(BatchedFunction):
|
|
26
|
+
"""
|
|
27
|
+
A `BatchedFunction` that is defined by a signature and an explicit python
|
|
28
|
+
`Callable`.
|
|
29
|
+
"""
|
|
30
|
+
def __init__(self, signature: Signature, batch_size: Optional[int], invoker_fn: Callable, self_path: str):
|
|
31
|
+
super().__init__(signature=signature, py_signature=inspect.signature(invoker_fn), self_path=self_path)
|
|
32
|
+
self.batch_size = batch_size
|
|
33
|
+
self.invoker_fn = invoker_fn
|
|
34
|
+
|
|
35
|
+
def get_batch_size(self, *args: Any, **kwargs: Any) -> Optional[int]:
|
|
36
|
+
return self.batch_size
|
|
37
|
+
|
|
38
|
+
def invoke(self, arg_batches: List[List[Any]], kwarg_batches: Dict[str, List[Any]]) -> List[Any]:
|
|
39
|
+
"""Invoke the function for the given batch and return a batch of results"""
|
|
40
|
+
constant_param_names = [p.name for p in self.signature.constant_parameters]
|
|
41
|
+
kwargs = {k: v[0] for k, v in kwarg_batches.items() if k in constant_param_names}
|
|
42
|
+
kwarg_batches = {k: v for k, v in kwarg_batches.items() if k not in constant_param_names}
|
|
43
|
+
return self.invoker_fn(*arg_batches, **kwargs, **kwarg_batches)
|
|
44
|
+
|
|
45
|
+
def validate_call(self, bound_args: Dict[str, Any]) -> None:
|
|
46
|
+
"""Verify constant parameters"""
|
|
47
|
+
import pixeltable.exprs as exprs
|
|
48
|
+
for param in self.signature.constant_parameters:
|
|
49
|
+
if param.name in bound_args and isinstance(bound_args[param.name], exprs.Expr):
|
|
50
|
+
raise ValueError(
|
|
51
|
+
f'{self.display_name}(): '
|
|
52
|
+
f'parameter {param.name} must be a constant value, not a Pixeltable expression'
|
|
53
|
+
)
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import inspect
|
|
4
|
+
import sys
|
|
5
|
+
from typing import Optional, Dict, Callable, List, Tuple
|
|
6
|
+
from uuid import UUID
|
|
7
|
+
import cloudpickle
|
|
8
|
+
|
|
9
|
+
import pixeltable.type_system as ts
|
|
10
|
+
import pixeltable.exceptions as excs
|
|
11
|
+
from .function import Function
|
|
12
|
+
from .function_registry import FunctionRegistry
|
|
13
|
+
from .globals import get_caller_module_path
|
|
14
|
+
from .signature import Signature
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class CallableFunction(Function):
|
|
18
|
+
"""Pixeltable Function backed by a Python Callable.
|
|
19
|
+
|
|
20
|
+
CallableFunctions come in two flavors:
|
|
21
|
+
- references to lambdas and functions defined in notebooks, which are pickled and serialized to the store
|
|
22
|
+
- functions that are defined in modules are serialized via the default mechanism
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(
|
|
26
|
+
self, signature: Signature, py_fn: Callable, self_path: Optional[str] = None,
|
|
27
|
+
self_name: Optional[str] = None):
|
|
28
|
+
assert py_fn is not None
|
|
29
|
+
self.py_fn = py_fn
|
|
30
|
+
self.self_name = self_name
|
|
31
|
+
py_signature = inspect.signature(self.py_fn)
|
|
32
|
+
super().__init__(signature, py_signature, self_path=self_path)
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def display_name(self) -> str:
|
|
36
|
+
return self.self_name
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def name(self) -> str:
|
|
40
|
+
return self.self_name
|
|
41
|
+
|
|
42
|
+
def help_str(self) -> str:
|
|
43
|
+
res = super().help_str()
|
|
44
|
+
res += '\n\n' + inspect.getdoc(self.py_fn)
|
|
45
|
+
return res
|
|
46
|
+
|
|
47
|
+
def _as_dict(self) -> Dict:
|
|
48
|
+
if self.self_path is None:
|
|
49
|
+
# this is not a module function
|
|
50
|
+
from .function_registry import FunctionRegistry
|
|
51
|
+
id = FunctionRegistry.get().create_stored_function(self)
|
|
52
|
+
return {'id': id.hex}
|
|
53
|
+
return super()._as_dict()
|
|
54
|
+
|
|
55
|
+
@classmethod
|
|
56
|
+
def _from_dict(cls, d: Dict) -> Function:
|
|
57
|
+
if 'id' in d:
|
|
58
|
+
from .function_registry import FunctionRegistry
|
|
59
|
+
return FunctionRegistry.get().get_stored_function(UUID(hex=d['id']))
|
|
60
|
+
return super()._from_dict(d)
|
|
61
|
+
|
|
62
|
+
def to_store(self) -> Tuple[Dict, bytes]:
|
|
63
|
+
return (self.signature.as_dict(), cloudpickle.dumps(self.py_fn))
|
|
64
|
+
|
|
65
|
+
@classmethod
|
|
66
|
+
def from_store(cls, name: Optional[str], md: Dict, binary_obj: bytes) -> Function:
|
|
67
|
+
py_fn = cloudpickle.loads(binary_obj)
|
|
68
|
+
assert isinstance(py_fn, Callable)
|
|
69
|
+
return CallableFunction(Signature.from_dict(md), py_fn, self_name=name)
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
from typing import Dict, Optional, Callable, List
|
|
3
|
+
|
|
4
|
+
import pixeltable
|
|
5
|
+
import pixeltable.exceptions as excs
|
|
6
|
+
import pixeltable.type_system as ts
|
|
7
|
+
from .function import Function
|
|
8
|
+
from .signature import Signature, Parameter
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ExprTemplateFunction(Function):
|
|
12
|
+
"""A parameterized expression from which an executable Expr is created with a function call."""
|
|
13
|
+
|
|
14
|
+
def __init__(
|
|
15
|
+
self, expr: 'pixeltable.exprs.Expr', py_signature: inspect.Signature, self_path: Optional[str] = None,
|
|
16
|
+
name: Optional[str] = None):
|
|
17
|
+
import pixeltable.exprs as exprs
|
|
18
|
+
self.expr = expr
|
|
19
|
+
self.self_name = name
|
|
20
|
+
self.param_exprs = list(set(expr.subexprs(expr_class=exprs.Variable)))
|
|
21
|
+
# make sure there are no duplicate names
|
|
22
|
+
assert len(self.param_exprs) == len(set(p.name for p in self.param_exprs))
|
|
23
|
+
self.param_exprs_by_name = {p.name: p for p in self.param_exprs}
|
|
24
|
+
|
|
25
|
+
# verify default values
|
|
26
|
+
self.defaults: Dict[str, exprs.Literal] = {} # key: param name, value: default value converted to a Literal
|
|
27
|
+
for py_param in py_signature.parameters.values():
|
|
28
|
+
if py_param.default is inspect.Parameter.empty:
|
|
29
|
+
continue
|
|
30
|
+
param_expr = self.param_exprs_by_name[py_param.name]
|
|
31
|
+
try:
|
|
32
|
+
literal_default = exprs.Literal(py_param.default, col_type=param_expr.col_type)
|
|
33
|
+
self.defaults[py_param.name] = literal_default
|
|
34
|
+
except TypeError as e:
|
|
35
|
+
msg = str(e)
|
|
36
|
+
raise excs.Error(f"Default value for parameter '{py_param.name}': {msg[0].lower() + msg[1:]}")
|
|
37
|
+
# construct signature
|
|
38
|
+
assert len(self.param_exprs) == len(py_signature.parameters)
|
|
39
|
+
fn_params = [
|
|
40
|
+
Parameter(p.name, self.param_exprs_by_name[p.name].col_type, p.kind)
|
|
41
|
+
for p in py_signature.parameters.values()
|
|
42
|
+
]
|
|
43
|
+
signature = Signature(return_type=expr.col_type, parameters=fn_params)
|
|
44
|
+
|
|
45
|
+
super().__init__(signature, py_signature=py_signature, self_path=self_path)
|
|
46
|
+
|
|
47
|
+
def instantiate(self, *args: object, **kwargs: object) -> 'pixeltable.exprs.Expr':
|
|
48
|
+
bound_args = self.py_signature.bind(*args, **kwargs).arguments
|
|
49
|
+
# apply defaults, otherwise we might have Parameters left over
|
|
50
|
+
bound_args.update(
|
|
51
|
+
{param_name: default for param_name, default in self.defaults.items() if param_name not in bound_args})
|
|
52
|
+
result = self.expr.copy()
|
|
53
|
+
for param_name, arg in bound_args.items():
|
|
54
|
+
param_expr = self.param_exprs_by_name[param_name]
|
|
55
|
+
result = result.substitute(param_expr, arg)
|
|
56
|
+
import pixeltable.exprs as exprs
|
|
57
|
+
assert not result.contains(exprs.Variable)
|
|
58
|
+
return result
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def display_name(self) -> str:
|
|
62
|
+
return self.self_name
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def name(self) -> str:
|
|
66
|
+
return self.self_name
|
|
67
|
+
|
|
68
|
+
def _as_dict(self) -> Dict:
|
|
69
|
+
if self.self_path is not None:
|
|
70
|
+
return super()._as_dict()
|
|
71
|
+
return {
|
|
72
|
+
'name': self.name,
|
|
73
|
+
'expr': self.expr.as_dict(),
|
|
74
|
+
**super()._as_dict()
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
@classmethod
|
|
78
|
+
def _from_dict(cls, d: Dict) -> Function:
|
|
79
|
+
if 'expr' not in d:
|
|
80
|
+
return super()._from_dict(d)
|
|
81
|
+
import pixeltable.exprs as exprs
|
|
82
|
+
return cls(exprs.Expr.from_dict(d['expr']), name=d['name'])
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import abc
|
|
4
|
+
import importlib
|
|
5
|
+
import inspect
|
|
6
|
+
import pixeltable
|
|
7
|
+
from typing import Optional, Dict, Any, Tuple
|
|
8
|
+
|
|
9
|
+
from .globals import resolve_symbol
|
|
10
|
+
from .signature import Signature
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Function(abc.ABC):
|
|
14
|
+
"""Base class for Pixeltable's function interface.
|
|
15
|
+
|
|
16
|
+
A function in Pixeltable is an object that has a signature and implements __call__().
|
|
17
|
+
This base class provides a default serialization mechanism for Function instances provided by Python modules,
|
|
18
|
+
via the member self_path.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self, signature: Signature, py_signature: inspect.Signature, self_path: Optional[str] = None):
|
|
22
|
+
self.signature = signature
|
|
23
|
+
self.py_signature = py_signature
|
|
24
|
+
self.self_path = self_path # fully-qualified path to self
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def name(self) -> str:
|
|
28
|
+
assert self.self_path is not None
|
|
29
|
+
return self.self_path.split('.')[-1]
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def display_name(self) -> str:
|
|
33
|
+
if self.self_path is None:
|
|
34
|
+
return '<anonymous>'
|
|
35
|
+
ptf_prefix = 'pixeltable.functions.'
|
|
36
|
+
if self.self_path.startswith(ptf_prefix):
|
|
37
|
+
return self.self_path[len(ptf_prefix):]
|
|
38
|
+
return self.self_path
|
|
39
|
+
|
|
40
|
+
def help_str(self) -> str:
|
|
41
|
+
return self.display_name + str(self.signature)
|
|
42
|
+
|
|
43
|
+
def __call__(self, *args: object, **kwargs: object) -> 'pixeltable.exprs.Expr':
|
|
44
|
+
from pixeltable import exprs
|
|
45
|
+
bound_args = self.py_signature.bind(*args, **kwargs)
|
|
46
|
+
self.validate_call(bound_args.arguments)
|
|
47
|
+
return exprs.FunctionCall(self, bound_args.arguments)
|
|
48
|
+
|
|
49
|
+
def validate_call(self, bound_args: Dict[str, Any]) -> None:
|
|
50
|
+
"""Override this to do custom validation of the arguments"""
|
|
51
|
+
pass
|
|
52
|
+
|
|
53
|
+
def __eq__(self, other: object) -> bool:
|
|
54
|
+
if not isinstance(other, self.__class__):
|
|
55
|
+
return False
|
|
56
|
+
return self.self_path == other.self_path
|
|
57
|
+
|
|
58
|
+
def source(self) -> None:
|
|
59
|
+
"""Print source code"""
|
|
60
|
+
print('source not available')
|
|
61
|
+
|
|
62
|
+
def as_dict(self) -> Dict:
|
|
63
|
+
"""
|
|
64
|
+
Return a serialized reference to the instance that can be passed to json.dumps() and converted back
|
|
65
|
+
to an instance with from_dict().
|
|
66
|
+
Subclasses can override _as_dict().
|
|
67
|
+
"""
|
|
68
|
+
classpath = f'{self.__class__.__module__}.{self.__class__.__qualname__}'
|
|
69
|
+
return {'_classpath': classpath, **self._as_dict()}
|
|
70
|
+
|
|
71
|
+
def _as_dict(self) -> Dict:
|
|
72
|
+
"""Default serialization: store the path to self (which includes the module path)"""
|
|
73
|
+
assert self.self_path is not None
|
|
74
|
+
return {'path': self.self_path}
|
|
75
|
+
|
|
76
|
+
@classmethod
|
|
77
|
+
def from_dict(cls, d: Dict) -> Function:
|
|
78
|
+
"""
|
|
79
|
+
Turn dict that was produced by calling as_dict() into an instance of the correct Function subclass.
|
|
80
|
+
"""
|
|
81
|
+
assert '_classpath' in d
|
|
82
|
+
module_path, class_name = d['_classpath'].rsplit('.', 1)
|
|
83
|
+
class_module = importlib.import_module(module_path)
|
|
84
|
+
func_class = getattr(class_module, class_name)
|
|
85
|
+
return func_class._from_dict(d)
|
|
86
|
+
|
|
87
|
+
@classmethod
|
|
88
|
+
def _from_dict(cls, d: Dict) -> Function:
|
|
89
|
+
"""Default deserialization: load the symbol indicated by the stored symbol_path"""
|
|
90
|
+
assert 'path' in d and d['path'] is not None
|
|
91
|
+
instance = resolve_symbol(d['path'])
|
|
92
|
+
assert isinstance(instance, Function)
|
|
93
|
+
return instance
|
|
94
|
+
|
|
95
|
+
def to_store(self) -> Tuple[Dict, bytes]:
|
|
96
|
+
"""
|
|
97
|
+
Serialize the function to a format that can be stored in the Pixeltable store
|
|
98
|
+
Returns:
|
|
99
|
+
- a dict that can be passed to json.dumps()
|
|
100
|
+
- additional binary data
|
|
101
|
+
Only Function subclasses that can be stored need to override this.
|
|
102
|
+
"""
|
|
103
|
+
raise NotImplementedError()
|
|
104
|
+
|
|
105
|
+
@classmethod
|
|
106
|
+
def from_store(cls, name: Optional[str], md: Dict, binary_obj: bytes) -> Function:
|
|
107
|
+
"""
|
|
108
|
+
Create a Function instance from the serialized representation returned by to_store()
|
|
109
|
+
"""
|
|
110
|
+
raise NotImplementedError()
|