pixeltable 0.1.1__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.

Files changed (139) hide show
  1. pixeltable/__init__.py +34 -6
  2. pixeltable/catalog/__init__.py +13 -0
  3. pixeltable/catalog/catalog.py +159 -0
  4. pixeltable/catalog/column.py +200 -0
  5. pixeltable/catalog/dir.py +32 -0
  6. pixeltable/catalog/globals.py +33 -0
  7. pixeltable/catalog/insertable_table.py +191 -0
  8. pixeltable/catalog/named_function.py +36 -0
  9. pixeltable/catalog/path.py +58 -0
  10. pixeltable/catalog/path_dict.py +139 -0
  11. pixeltable/catalog/schema_object.py +39 -0
  12. pixeltable/catalog/table.py +581 -0
  13. pixeltable/catalog/table_version.py +749 -0
  14. pixeltable/catalog/table_version_path.py +133 -0
  15. pixeltable/catalog/view.py +203 -0
  16. pixeltable/client.py +520 -30
  17. pixeltable/dataframe.py +540 -349
  18. pixeltable/env.py +373 -45
  19. pixeltable/exceptions.py +12 -21
  20. pixeltable/exec/__init__.py +9 -0
  21. pixeltable/exec/aggregation_node.py +78 -0
  22. pixeltable/exec/cache_prefetch_node.py +113 -0
  23. pixeltable/exec/component_iteration_node.py +79 -0
  24. pixeltable/exec/data_row_batch.py +95 -0
  25. pixeltable/exec/exec_context.py +22 -0
  26. pixeltable/exec/exec_node.py +61 -0
  27. pixeltable/exec/expr_eval_node.py +217 -0
  28. pixeltable/exec/in_memory_data_node.py +69 -0
  29. pixeltable/exec/media_validation_node.py +43 -0
  30. pixeltable/exec/sql_scan_node.py +225 -0
  31. pixeltable/exprs/__init__.py +24 -0
  32. pixeltable/exprs/arithmetic_expr.py +102 -0
  33. pixeltable/exprs/array_slice.py +71 -0
  34. pixeltable/exprs/column_property_ref.py +77 -0
  35. pixeltable/exprs/column_ref.py +105 -0
  36. pixeltable/exprs/comparison.py +77 -0
  37. pixeltable/exprs/compound_predicate.py +98 -0
  38. pixeltable/exprs/data_row.py +187 -0
  39. pixeltable/exprs/expr.py +586 -0
  40. pixeltable/exprs/expr_set.py +39 -0
  41. pixeltable/exprs/function_call.py +380 -0
  42. pixeltable/exprs/globals.py +69 -0
  43. pixeltable/exprs/image_member_access.py +115 -0
  44. pixeltable/exprs/image_similarity_predicate.py +58 -0
  45. pixeltable/exprs/inline_array.py +107 -0
  46. pixeltable/exprs/inline_dict.py +101 -0
  47. pixeltable/exprs/is_null.py +38 -0
  48. pixeltable/exprs/json_mapper.py +121 -0
  49. pixeltable/exprs/json_path.py +159 -0
  50. pixeltable/exprs/literal.py +54 -0
  51. pixeltable/exprs/object_ref.py +41 -0
  52. pixeltable/exprs/predicate.py +44 -0
  53. pixeltable/exprs/row_builder.py +355 -0
  54. pixeltable/exprs/rowid_ref.py +94 -0
  55. pixeltable/exprs/type_cast.py +53 -0
  56. pixeltable/exprs/variable.py +45 -0
  57. pixeltable/func/__init__.py +9 -0
  58. pixeltable/func/aggregate_function.py +194 -0
  59. pixeltable/func/batched_function.py +53 -0
  60. pixeltable/func/callable_function.py +69 -0
  61. pixeltable/func/expr_template_function.py +82 -0
  62. pixeltable/func/function.py +110 -0
  63. pixeltable/func/function_registry.py +227 -0
  64. pixeltable/func/globals.py +36 -0
  65. pixeltable/func/nos_function.py +202 -0
  66. pixeltable/func/signature.py +166 -0
  67. pixeltable/func/udf.py +163 -0
  68. pixeltable/functions/__init__.py +52 -103
  69. pixeltable/functions/eval.py +216 -0
  70. pixeltable/functions/fireworks.py +61 -0
  71. pixeltable/functions/huggingface.py +120 -0
  72. pixeltable/functions/image.py +16 -0
  73. pixeltable/functions/openai.py +88 -0
  74. pixeltable/functions/pil/image.py +148 -7
  75. pixeltable/functions/string.py +13 -0
  76. pixeltable/functions/together.py +27 -0
  77. pixeltable/functions/util.py +41 -0
  78. pixeltable/functions/video.py +62 -0
  79. pixeltable/iterators/__init__.py +3 -0
  80. pixeltable/iterators/base.py +48 -0
  81. pixeltable/iterators/document.py +311 -0
  82. pixeltable/iterators/video.py +89 -0
  83. pixeltable/metadata/__init__.py +54 -0
  84. pixeltable/metadata/converters/convert_10.py +18 -0
  85. pixeltable/metadata/schema.py +211 -0
  86. pixeltable/plan.py +656 -0
  87. pixeltable/store.py +413 -182
  88. pixeltable/tests/conftest.py +143 -87
  89. pixeltable/tests/test_audio.py +65 -0
  90. pixeltable/tests/test_catalog.py +27 -0
  91. pixeltable/tests/test_client.py +14 -14
  92. pixeltable/tests/test_component_view.py +372 -0
  93. pixeltable/tests/test_dataframe.py +433 -0
  94. pixeltable/tests/test_dirs.py +78 -62
  95. pixeltable/tests/test_document.py +117 -0
  96. pixeltable/tests/test_exprs.py +591 -135
  97. pixeltable/tests/test_function.py +297 -67
  98. pixeltable/tests/test_functions.py +283 -1
  99. pixeltable/tests/test_migration.py +43 -0
  100. pixeltable/tests/test_nos.py +54 -0
  101. pixeltable/tests/test_snapshot.py +208 -0
  102. pixeltable/tests/test_table.py +1085 -262
  103. pixeltable/tests/test_transactional_directory.py +42 -0
  104. pixeltable/tests/test_types.py +5 -11
  105. pixeltable/tests/test_video.py +149 -34
  106. pixeltable/tests/test_view.py +530 -0
  107. pixeltable/tests/utils.py +186 -45
  108. pixeltable/tool/create_test_db_dump.py +149 -0
  109. pixeltable/type_system.py +490 -126
  110. pixeltable/utils/__init__.py +17 -46
  111. pixeltable/utils/clip.py +12 -15
  112. pixeltable/utils/coco.py +136 -0
  113. pixeltable/utils/documents.py +39 -0
  114. pixeltable/utils/filecache.py +195 -0
  115. pixeltable/utils/help.py +11 -0
  116. pixeltable/utils/media_store.py +76 -0
  117. pixeltable/utils/parquet.py +126 -0
  118. pixeltable/utils/pytorch.py +172 -0
  119. pixeltable/utils/s3.py +13 -0
  120. pixeltable/utils/sql.py +17 -0
  121. pixeltable/utils/transactional_directory.py +35 -0
  122. pixeltable-0.2.0.dist-info/LICENSE +18 -0
  123. pixeltable-0.2.0.dist-info/METADATA +117 -0
  124. pixeltable-0.2.0.dist-info/RECORD +125 -0
  125. {pixeltable-0.1.1.dist-info → pixeltable-0.2.0.dist-info}/WHEEL +1 -1
  126. pixeltable/catalog.py +0 -1421
  127. pixeltable/exprs.py +0 -1745
  128. pixeltable/function.py +0 -269
  129. pixeltable/functions/clip.py +0 -10
  130. pixeltable/functions/pil/__init__.py +0 -23
  131. pixeltable/functions/tf.py +0 -21
  132. pixeltable/index.py +0 -57
  133. pixeltable/tests/test_dict.py +0 -24
  134. pixeltable/tests/test_tf.py +0 -69
  135. pixeltable/tf.py +0 -33
  136. pixeltable/utils/tf.py +0 -33
  137. pixeltable/utils/video.py +0 -32
  138. pixeltable-0.1.1.dist-info/METADATA +0 -31
  139. pixeltable-0.1.1.dist-info/RECORD +0 -36
@@ -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()