pixeltable 0.2.13__py3-none-any.whl → 0.2.15__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 +1 -1
- pixeltable/__version__.py +2 -2
- pixeltable/catalog/column.py +8 -3
- pixeltable/catalog/globals.py +8 -0
- pixeltable/catalog/table.py +25 -9
- pixeltable/catalog/table_version.py +30 -55
- pixeltable/catalog/view.py +1 -1
- pixeltable/env.py +4 -4
- pixeltable/exec/__init__.py +2 -1
- pixeltable/exec/row_update_node.py +61 -0
- pixeltable/exec/{sql_scan_node.py → sql_node.py} +120 -56
- pixeltable/exprs/__init__.py +1 -1
- pixeltable/exprs/arithmetic_expr.py +41 -16
- pixeltable/exprs/expr.py +72 -22
- pixeltable/exprs/function_call.py +64 -29
- pixeltable/exprs/globals.py +5 -1
- pixeltable/exprs/inline_array.py +18 -11
- pixeltable/exprs/method_ref.py +63 -0
- pixeltable/ext/__init__.py +9 -0
- pixeltable/ext/functions/__init__.py +8 -0
- pixeltable/ext/functions/whisperx.py +45 -5
- pixeltable/ext/functions/yolox.py +60 -14
- pixeltable/func/callable_function.py +12 -4
- pixeltable/func/expr_template_function.py +1 -1
- pixeltable/func/function.py +12 -2
- pixeltable/func/function_registry.py +24 -9
- pixeltable/func/udf.py +32 -4
- pixeltable/functions/__init__.py +1 -1
- pixeltable/functions/fireworks.py +33 -0
- pixeltable/functions/huggingface.py +96 -6
- pixeltable/functions/image.py +226 -41
- pixeltable/functions/json.py +46 -0
- pixeltable/functions/openai.py +214 -0
- pixeltable/functions/string.py +195 -218
- pixeltable/functions/timestamp.py +210 -0
- pixeltable/functions/together.py +106 -0
- pixeltable/functions/video.py +2 -2
- pixeltable/functions/{eval.py → vision.py} +170 -27
- pixeltable/functions/whisper.py +32 -0
- pixeltable/io/__init__.py +1 -1
- pixeltable/io/external_store.py +2 -2
- pixeltable/io/globals.py +133 -1
- pixeltable/io/pandas.py +82 -31
- pixeltable/iterators/video.py +55 -23
- pixeltable/metadata/__init__.py +1 -1
- pixeltable/metadata/converters/convert_18.py +39 -0
- pixeltable/metadata/notes.py +10 -0
- pixeltable/plan.py +76 -1
- pixeltable/store.py +65 -28
- pixeltable/tool/create_test_db_dump.py +8 -9
- pixeltable/tool/doc_plugins/griffe.py +4 -0
- pixeltable/type_system.py +84 -63
- {pixeltable-0.2.13.dist-info → pixeltable-0.2.15.dist-info}/METADATA +2 -2
- {pixeltable-0.2.13.dist-info → pixeltable-0.2.15.dist-info}/RECORD +57 -51
- pixeltable/exprs/image_member_access.py +0 -96
- {pixeltable-0.2.13.dist-info → pixeltable-0.2.15.dist-info}/LICENSE +0 -0
- {pixeltable-0.2.13.dist-info → pixeltable-0.2.15.dist-info}/WHEEL +0 -0
- {pixeltable-0.2.13.dist-info → pixeltable-0.2.15.dist-info}/entry_points.txt +0 -0
|
@@ -1,28 +1,28 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
-
|
|
2
|
+
|
|
3
|
+
from typing import Any, Optional
|
|
3
4
|
|
|
4
5
|
import sqlalchemy as sql
|
|
5
6
|
|
|
6
|
-
from .globals import ArithmeticOperator
|
|
7
|
-
from .expr import Expr
|
|
8
|
-
from .data_row import DataRow
|
|
9
|
-
from .row_builder import RowBuilder
|
|
10
7
|
import pixeltable.exceptions as excs
|
|
11
|
-
import pixeltable.catalog as catalog
|
|
12
8
|
import pixeltable.type_system as ts
|
|
13
9
|
|
|
10
|
+
from .data_row import DataRow
|
|
11
|
+
from .expr import Expr
|
|
12
|
+
from .globals import ArithmeticOperator
|
|
13
|
+
from .row_builder import RowBuilder
|
|
14
|
+
|
|
14
15
|
|
|
15
16
|
class ArithmeticExpr(Expr):
|
|
16
17
|
"""
|
|
17
18
|
Allows arithmetic exprs on json paths
|
|
18
19
|
"""
|
|
19
20
|
def __init__(self, operator: ArithmeticOperator, op1: Expr, op2: Expr):
|
|
20
|
-
|
|
21
|
-
if op1.col_type.is_json_type() or op2.col_type.is_json_type():
|
|
21
|
+
if op1.col_type.is_json_type() or op2.col_type.is_json_type() or operator == ArithmeticOperator.DIV:
|
|
22
22
|
# we assume it's a float
|
|
23
|
-
super().__init__(ts.FloatType())
|
|
23
|
+
super().__init__(ts.FloatType(nullable=(op1.col_type.nullable or op2.col_type.nullable)))
|
|
24
24
|
else:
|
|
25
|
-
super().__init__(
|
|
25
|
+
super().__init__(op1.col_type.supertype(op2.col_type))
|
|
26
26
|
self.operator = operator
|
|
27
27
|
self.components = [op1, op2]
|
|
28
28
|
|
|
@@ -43,7 +43,7 @@ class ArithmeticExpr(Expr):
|
|
|
43
43
|
def _equals(self, other: ArithmeticExpr) -> bool:
|
|
44
44
|
return self.operator == other.operator
|
|
45
45
|
|
|
46
|
-
def _id_attrs(self) ->
|
|
46
|
+
def _id_attrs(self) -> list[tuple[str, Any]]:
|
|
47
47
|
return super()._id_attrs() + [('operator', self.operator.value)]
|
|
48
48
|
|
|
49
49
|
@property
|
|
@@ -55,6 +55,7 @@ class ArithmeticExpr(Expr):
|
|
|
55
55
|
return self.components[1]
|
|
56
56
|
|
|
57
57
|
def sql_expr(self) -> Optional[sql.ClauseElement]:
|
|
58
|
+
assert self.col_type.is_int_type() or self.col_type.is_float_type() or self.col_type.is_json_type()
|
|
58
59
|
left = self._op1.sql_expr()
|
|
59
60
|
right = self._op2.sql_expr()
|
|
60
61
|
if left is None or right is None:
|
|
@@ -66,14 +67,31 @@ class ArithmeticExpr(Expr):
|
|
|
66
67
|
if self.operator == ArithmeticOperator.MUL:
|
|
67
68
|
return left * right
|
|
68
69
|
if self.operator == ArithmeticOperator.DIV:
|
|
69
|
-
|
|
70
|
+
assert self.col_type.is_float_type()
|
|
71
|
+
# We have to cast to a `float`, or else we'll get a `Decimal`
|
|
72
|
+
return sql.sql.expression.cast(left / right, sql.Float)
|
|
70
73
|
if self.operator == ArithmeticOperator.MOD:
|
|
71
|
-
|
|
74
|
+
if self.col_type.is_int_type():
|
|
75
|
+
return left % right
|
|
76
|
+
if self.col_type.is_float_type():
|
|
77
|
+
# Postgres does not support modulus for floats
|
|
78
|
+
return None
|
|
79
|
+
if self.operator == ArithmeticOperator.FLOORDIV:
|
|
80
|
+
# Postgres has a DIV operator, but it behaves differently from Python's // operator
|
|
81
|
+
# (Postgres rounds toward 0, Python rounds toward negative infinity)
|
|
82
|
+
# We need the behavior to be consistent, so that expressions will evaluate the same way
|
|
83
|
+
# whether or not their operands can be translated to SQL. These SQL clauses should
|
|
84
|
+
# mimic the behavior of Python's // operator.
|
|
85
|
+
if self.col_type.is_int_type():
|
|
86
|
+
return sql.sql.expression.cast(sql.func.floor(left / right), sql.Integer)
|
|
87
|
+
if self.col_type.is_float_type():
|
|
88
|
+
return sql.sql.expression.cast(sql.func.floor(left / right), sql.Float)
|
|
72
89
|
|
|
73
90
|
def eval(self, data_row: DataRow, row_builder: RowBuilder) -> None:
|
|
74
91
|
op1_val = data_row[self._op1.slot_idx]
|
|
75
92
|
op2_val = data_row[self._op2.slot_idx]
|
|
76
|
-
|
|
93
|
+
|
|
94
|
+
# if one or both columns is JsonTyped, we need a dynamic check that they are numeric
|
|
77
95
|
if self._op1.col_type.is_json_type() and not isinstance(op1_val, int) and not isinstance(op1_val, float):
|
|
78
96
|
raise excs.Error(
|
|
79
97
|
f'{self.operator} requires numeric type, but {self._op1} has type {type(op1_val).__name__}')
|
|
@@ -81,6 +99,11 @@ class ArithmeticExpr(Expr):
|
|
|
81
99
|
raise excs.Error(
|
|
82
100
|
f'{self.operator} requires numeric type, but {self._op2} has type {type(op2_val).__name__}')
|
|
83
101
|
|
|
102
|
+
# if either operand is None, always return None
|
|
103
|
+
if op1_val is None or op2_val is None:
|
|
104
|
+
data_row[self.slot_idx] = None
|
|
105
|
+
return
|
|
106
|
+
|
|
84
107
|
if self.operator == ArithmeticOperator.ADD:
|
|
85
108
|
data_row[self.slot_idx] = op1_val + op2_val
|
|
86
109
|
elif self.operator == ArithmeticOperator.SUB:
|
|
@@ -91,12 +114,14 @@ class ArithmeticExpr(Expr):
|
|
|
91
114
|
data_row[self.slot_idx] = op1_val / op2_val
|
|
92
115
|
elif self.operator == ArithmeticOperator.MOD:
|
|
93
116
|
data_row[self.slot_idx] = op1_val % op2_val
|
|
117
|
+
elif self.operator == ArithmeticOperator.FLOORDIV:
|
|
118
|
+
data_row[self.slot_idx] = op1_val // op2_val
|
|
94
119
|
|
|
95
|
-
def _as_dict(self) ->
|
|
120
|
+
def _as_dict(self) -> dict:
|
|
96
121
|
return {'operator': self.operator.value, **super()._as_dict()}
|
|
97
122
|
|
|
98
123
|
@classmethod
|
|
99
|
-
def _from_dict(cls, d:
|
|
124
|
+
def _from_dict(cls, d: dict, components: list[Expr]) -> Expr:
|
|
100
125
|
assert 'operator' in d
|
|
101
126
|
assert len(components) == 2
|
|
102
127
|
return cls(ArithmeticOperator(d['operator']), components[0], components[1])
|
pixeltable/exprs/expr.py
CHANGED
|
@@ -7,7 +7,6 @@ import inspect
|
|
|
7
7
|
import json
|
|
8
8
|
import sys
|
|
9
9
|
import typing
|
|
10
|
-
from itertools import islice
|
|
11
10
|
from typing import Union, Optional, List, Callable, Any, Dict, Tuple, Set, Generator, Type
|
|
12
11
|
from uuid import UUID
|
|
13
12
|
|
|
@@ -16,8 +15,8 @@ import sqlalchemy as sql
|
|
|
16
15
|
import pixeltable
|
|
17
16
|
import pixeltable.catalog as catalog
|
|
18
17
|
import pixeltable.exceptions as excs
|
|
19
|
-
import pixeltable.type_system as ts
|
|
20
18
|
import pixeltable.func as func
|
|
19
|
+
import pixeltable.type_system as ts
|
|
21
20
|
from .data_row import DataRow
|
|
22
21
|
from .globals import ComparisonOperator, LogicalOperator, LiteralPythonTypes, ArithmeticOperator
|
|
23
22
|
|
|
@@ -91,8 +90,8 @@ class Expr(abc.ABC):
|
|
|
91
90
|
|
|
92
91
|
def default_column_name(self) -> Optional[str]:
|
|
93
92
|
"""
|
|
94
|
-
Returns:
|
|
95
|
-
None if this expression lacks a default name,
|
|
93
|
+
Returns:
|
|
94
|
+
None if this expression lacks a default name,
|
|
96
95
|
or a valid identifier (according to catalog.is_valid_identifer) otherwise.
|
|
97
96
|
"""
|
|
98
97
|
return None
|
|
@@ -231,9 +230,8 @@ class Expr(abc.ABC):
|
|
|
231
230
|
self.components[i] = self.components[i]._retarget(tbl_versions)
|
|
232
231
|
return self
|
|
233
232
|
|
|
234
|
-
@abc.abstractmethod
|
|
235
233
|
def __str__(self) -> str:
|
|
236
|
-
|
|
234
|
+
return f'<Expression of type {type(self)}>'
|
|
237
235
|
|
|
238
236
|
def display_str(self, inline: bool = True) -> str:
|
|
239
237
|
"""
|
|
@@ -264,7 +262,7 @@ class Expr(abc.ABC):
|
|
|
264
262
|
if is_match:
|
|
265
263
|
yield self
|
|
266
264
|
|
|
267
|
-
def
|
|
265
|
+
def _contains(self, cls: Optional[Type[Expr]] = None, filter: Optional[Callable[[Expr], bool]] = None) -> bool:
|
|
268
266
|
"""
|
|
269
267
|
Returns True if any subexpr is an instance of cls.
|
|
270
268
|
"""
|
|
@@ -319,17 +317,20 @@ class Expr(abc.ABC):
|
|
|
319
317
|
"""
|
|
320
318
|
if isinstance(o, Expr):
|
|
321
319
|
return o
|
|
322
|
-
#
|
|
320
|
+
# Try to create a literal. We need to check for InlineArray/InlineDict
|
|
321
|
+
# first, to prevent arrays from inappropriately being interpreted as JsonType
|
|
322
|
+
# literals.
|
|
323
|
+
# TODO: general cleanup of InlineArray/InlineDict
|
|
324
|
+
if isinstance(o, list):
|
|
325
|
+
from .inline_array import InlineArray
|
|
326
|
+
return InlineArray(tuple(o))
|
|
327
|
+
if isinstance(o, dict):
|
|
328
|
+
from .inline_dict import InlineDict
|
|
329
|
+
return InlineDict(o)
|
|
323
330
|
obj_type = ts.ColumnType.infer_literal_type(o)
|
|
324
331
|
if obj_type is not None:
|
|
325
332
|
from .literal import Literal
|
|
326
333
|
return Literal(o, col_type=obj_type)
|
|
327
|
-
if isinstance(o, dict):
|
|
328
|
-
from .inline_dict import InlineDict
|
|
329
|
-
return InlineDict(o)
|
|
330
|
-
elif isinstance(o, list):
|
|
331
|
-
from .inline_array import InlineArray
|
|
332
|
-
return InlineArray(tuple(o))
|
|
333
334
|
return None
|
|
334
335
|
|
|
335
336
|
@abc.abstractmethod
|
|
@@ -427,6 +428,14 @@ class Expr(abc.ABC):
|
|
|
427
428
|
# Return a `FunctionCall` obtained by passing this `Expr` to the new `function`.
|
|
428
429
|
return function(self)
|
|
429
430
|
|
|
431
|
+
def __dir__(self) -> list[str]:
|
|
432
|
+
attrs = ['isin', 'astype', 'apply']
|
|
433
|
+
attrs += [
|
|
434
|
+
f.name
|
|
435
|
+
for f in func.FunctionRegistry.get().get_type_methods(self.col_type.type_enum)
|
|
436
|
+
]
|
|
437
|
+
return attrs
|
|
438
|
+
|
|
430
439
|
def __getitem__(self, index: object) -> Expr:
|
|
431
440
|
if self.col_type.is_json_type():
|
|
432
441
|
from .json_path import JsonPath
|
|
@@ -434,19 +443,23 @@ class Expr(abc.ABC):
|
|
|
434
443
|
if self.col_type.is_array_type():
|
|
435
444
|
from .array_slice import ArraySlice
|
|
436
445
|
return ArraySlice(self, index)
|
|
437
|
-
raise
|
|
446
|
+
raise AttributeError(f'Type {self.col_type} is not subscriptable')
|
|
438
447
|
|
|
439
|
-
def __getattr__(self, name: str) -> Union['pixeltable.exprs.
|
|
448
|
+
def __getattr__(self, name: str) -> Union['pixeltable.exprs.MethodRef', 'pixeltable.exprs.FunctionCall', 'pixeltable.exprs.JsonPath']:
|
|
440
449
|
"""
|
|
441
450
|
ex.: <img col>.rotate(60)
|
|
442
451
|
"""
|
|
443
|
-
if self.col_type.is_image_type():
|
|
444
|
-
from .image_member_access import ImageMemberAccess
|
|
445
|
-
return ImageMemberAccess(name, self)
|
|
446
452
|
if self.col_type.is_json_type():
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
453
|
+
return pixeltable.exprs.JsonPath(self).__getattr__(name)
|
|
454
|
+
else:
|
|
455
|
+
method_ref = pixeltable.exprs.MethodRef(self, name)
|
|
456
|
+
if method_ref.fn.is_property:
|
|
457
|
+
# Marked as a property, so autoinvoke the method to obtain a `FunctionCall`
|
|
458
|
+
assert method_ref.fn.arity == 1
|
|
459
|
+
return method_ref.fn(method_ref.base_expr)
|
|
460
|
+
else:
|
|
461
|
+
# Return the `MethodRef` object itself; it requires arguments to become a `FunctionCall`
|
|
462
|
+
return method_ref
|
|
450
463
|
|
|
451
464
|
def __bool__(self) -> bool:
|
|
452
465
|
raise TypeError(
|
|
@@ -490,6 +503,9 @@ class Expr(abc.ABC):
|
|
|
490
503
|
return Comparison(op, self, Literal(other)) # type: ignore[arg-type]
|
|
491
504
|
raise TypeError(f'Other must be Expr or literal: {type(other)}')
|
|
492
505
|
|
|
506
|
+
def __neg__(self) -> 'pixeltable.exprs.ArithmeticExpr':
|
|
507
|
+
return self._make_arithmetic_expr(ArithmeticOperator.MUL, -1)
|
|
508
|
+
|
|
493
509
|
def __add__(self, other: object) -> 'pixeltable.exprs.ArithmeticExpr':
|
|
494
510
|
return self._make_arithmetic_expr(ArithmeticOperator.ADD, other)
|
|
495
511
|
|
|
@@ -505,6 +521,27 @@ class Expr(abc.ABC):
|
|
|
505
521
|
def __mod__(self, other: object) -> 'pixeltable.exprs.ArithmeticExpr':
|
|
506
522
|
return self._make_arithmetic_expr(ArithmeticOperator.MOD, other)
|
|
507
523
|
|
|
524
|
+
def __floordiv__(self, other: object) -> 'pixeltable.exprs.ArithmeticExpr':
|
|
525
|
+
return self._make_arithmetic_expr(ArithmeticOperator.FLOORDIV, other)
|
|
526
|
+
|
|
527
|
+
def __radd__(self, other: object) -> 'pixeltable.exprs.ArithmeticExpr':
|
|
528
|
+
return self._rmake_arithmetic_expr(ArithmeticOperator.ADD, other)
|
|
529
|
+
|
|
530
|
+
def __rsub__(self, other: object) -> 'pixeltable.exprs.ArithmeticExpr':
|
|
531
|
+
return self._rmake_arithmetic_expr(ArithmeticOperator.SUB, other)
|
|
532
|
+
|
|
533
|
+
def __rmul__(self, other: object) -> 'pixeltable.exprs.ArithmeticExpr':
|
|
534
|
+
return self._rmake_arithmetic_expr(ArithmeticOperator.MUL, other)
|
|
535
|
+
|
|
536
|
+
def __rtruediv__(self, other: object) -> 'pixeltable.exprs.ArithmeticExpr':
|
|
537
|
+
return self._rmake_arithmetic_expr(ArithmeticOperator.DIV, other)
|
|
538
|
+
|
|
539
|
+
def __rmod__(self, other: object) -> 'pixeltable.exprs.ArithmeticExpr':
|
|
540
|
+
return self._rmake_arithmetic_expr(ArithmeticOperator.MOD, other)
|
|
541
|
+
|
|
542
|
+
def __rfloordiv__(self, other: object) -> 'pixeltable.exprs.ArithmeticExpr':
|
|
543
|
+
return self._rmake_arithmetic_expr(ArithmeticOperator.FLOORDIV, other)
|
|
544
|
+
|
|
508
545
|
def _make_arithmetic_expr(self, op: ArithmeticOperator, other: object) -> 'pixeltable.exprs.ArithmeticExpr':
|
|
509
546
|
"""
|
|
510
547
|
other: Union[Expr, LiteralPythonTypes]
|
|
@@ -518,6 +555,19 @@ class Expr(abc.ABC):
|
|
|
518
555
|
return ArithmeticExpr(op, self, Literal(other)) # type: ignore[arg-type]
|
|
519
556
|
raise TypeError(f'Other must be Expr or literal: {type(other)}')
|
|
520
557
|
|
|
558
|
+
def _rmake_arithmetic_expr(self, op: ArithmeticOperator, other: object) -> 'pixeltable.exprs.ArithmeticExpr':
|
|
559
|
+
"""
|
|
560
|
+
Right-handed version of _make_arithmetic_expr. other must be a literal; if it were an Expr,
|
|
561
|
+
the operation would have already been evaluated in its left-handed form.
|
|
562
|
+
"""
|
|
563
|
+
# TODO: check for compatibility
|
|
564
|
+
from .arithmetic_expr import ArithmeticExpr
|
|
565
|
+
from .literal import Literal
|
|
566
|
+
assert not isinstance(other, Expr) # Else the left-handed form would have evaluated first
|
|
567
|
+
if isinstance(other, typing.get_args(LiteralPythonTypes)):
|
|
568
|
+
return ArithmeticExpr(op, Literal(other), self) # type: ignore[arg-type]
|
|
569
|
+
raise TypeError(f'Other must be Expr or literal: {type(other)}')
|
|
570
|
+
|
|
521
571
|
def __and__(self, other: object) -> Expr:
|
|
522
572
|
if not isinstance(other, Expr):
|
|
523
573
|
raise TypeError(f'Other needs to be an expression: {type(other)}')
|
|
@@ -20,6 +20,22 @@ from .rowid_ref import RowidRef
|
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
class FunctionCall(Expr):
|
|
23
|
+
|
|
24
|
+
fn: func.Function
|
|
25
|
+
is_method_call: bool
|
|
26
|
+
agg_init_args: Dict[str, Any]
|
|
27
|
+
args: List[Tuple[Optional[int], Optional[Any]]]
|
|
28
|
+
kwargs: Dict[str, Tuple[Optional[int], Optional[Any]]]
|
|
29
|
+
arg_types: List[ts.ColumnType]
|
|
30
|
+
kwarg_types: Dict[str, ts.ColumnType]
|
|
31
|
+
group_by_start_idx: int
|
|
32
|
+
group_by_stop_idx: int
|
|
33
|
+
fn_expr_idx: int
|
|
34
|
+
order_by_start_idx: int
|
|
35
|
+
constant_args: set[str]
|
|
36
|
+
aggregator: Optional[Any]
|
|
37
|
+
current_partition_vals: Optional[List[Any]]
|
|
38
|
+
|
|
23
39
|
def __init__(
|
|
24
40
|
self, fn: func.Function, bound_args: Dict[str, Any], order_by_clause: Optional[List[Any]] = None,
|
|
25
41
|
group_by_clause: Optional[List[Any]] = None, is_method_call: bool = False):
|
|
@@ -31,9 +47,9 @@ class FunctionCall(Expr):
|
|
|
31
47
|
super().__init__(fn.call_return_type(bound_args))
|
|
32
48
|
self.fn = fn
|
|
33
49
|
self.is_method_call = is_method_call
|
|
34
|
-
self.
|
|
50
|
+
self.normalize_args(signature, bound_args)
|
|
35
51
|
|
|
36
|
-
self.agg_init_args
|
|
52
|
+
self.agg_init_args = {}
|
|
37
53
|
if self.is_agg_fn_call:
|
|
38
54
|
# we separate out the init args for the aggregator
|
|
39
55
|
self.agg_init_args = {
|
|
@@ -42,17 +58,16 @@ class FunctionCall(Expr):
|
|
|
42
58
|
bound_args = {arg_name: arg for arg_name, arg in bound_args.items() if arg_name not in fn.init_param_names}
|
|
43
59
|
|
|
44
60
|
# construct components, args, kwargs
|
|
45
|
-
self.components: List[Expr] = []
|
|
46
61
|
|
|
47
62
|
# Tuple[int, Any]:
|
|
48
63
|
# - for Exprs: (index into components, None)
|
|
49
64
|
# - otherwise: (None, val)
|
|
50
|
-
self.args
|
|
51
|
-
self.kwargs
|
|
65
|
+
self.args = []
|
|
66
|
+
self.kwargs = {}
|
|
52
67
|
|
|
53
68
|
# we record the types of non-variable parameters for runtime type checks
|
|
54
|
-
self.arg_types
|
|
55
|
-
self.kwarg_types
|
|
69
|
+
self.arg_types = []
|
|
70
|
+
self.kwarg_types = {}
|
|
56
71
|
# the prefix of parameters that are bound can be passed by position
|
|
57
72
|
for param in fn.signature.py_signature.parameters.values():
|
|
58
73
|
if param.name not in bound_args or param.kind == inspect.Parameter.KEYWORD_ONLY:
|
|
@@ -111,8 +126,8 @@ class FunctionCall(Expr):
|
|
|
111
126
|
|
|
112
127
|
self.constant_args = {param_name for param_name, arg in bound_args.items() if not isinstance(arg, Expr)}
|
|
113
128
|
# execution state for aggregate functions
|
|
114
|
-
self.aggregator
|
|
115
|
-
self.current_partition_vals
|
|
129
|
+
self.aggregator = None
|
|
130
|
+
self.current_partition_vals = None
|
|
116
131
|
|
|
117
132
|
self.id = self._create_id()
|
|
118
133
|
|
|
@@ -120,26 +135,37 @@ class FunctionCall(Expr):
|
|
|
120
135
|
target = tbl._tbl_version_path.tbl_version
|
|
121
136
|
return [RowidRef(target, i) for i in range(target.num_rowid_columns())]
|
|
122
137
|
|
|
138
|
+
def default_column_name(self) -> Optional[str]:
|
|
139
|
+
if self.fn.is_property:
|
|
140
|
+
return self.fn.name
|
|
141
|
+
return super().default_column_name()
|
|
142
|
+
|
|
123
143
|
@classmethod
|
|
124
|
-
def
|
|
125
|
-
"""
|
|
144
|
+
def normalize_args(cls, signature: func.Signature, bound_args: Dict[str, Any]) -> None:
|
|
145
|
+
"""Converts all args to Exprs and checks that they are compatible with signature.
|
|
126
146
|
|
|
127
|
-
|
|
147
|
+
Updates bound_args in place, where necessary.
|
|
128
148
|
"""
|
|
129
149
|
for param_name, arg in bound_args.items():
|
|
130
150
|
param = signature.parameters[param_name]
|
|
151
|
+
is_var_param = param.kind in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD)
|
|
152
|
+
|
|
131
153
|
if isinstance(arg, dict):
|
|
132
154
|
try:
|
|
133
155
|
arg = InlineDict(arg)
|
|
134
156
|
bound_args[param_name] = arg
|
|
157
|
+
continue
|
|
135
158
|
except excs.Error:
|
|
136
159
|
# this didn't work, but it might be a literal
|
|
137
160
|
pass
|
|
161
|
+
|
|
138
162
|
if isinstance(arg, list) or isinstance(arg, tuple):
|
|
139
163
|
try:
|
|
140
164
|
# If the column type is JsonType, force the literal to be JSON
|
|
141
|
-
|
|
165
|
+
is_json = is_var_param or (param.col_type is not None and param.col_type.is_json_type())
|
|
166
|
+
arg = InlineArray(arg, force_json=is_json)
|
|
142
167
|
bound_args[param_name] = arg
|
|
168
|
+
continue
|
|
143
169
|
except excs.Error:
|
|
144
170
|
# this didn't work, but it might be a literal
|
|
145
171
|
pass
|
|
@@ -149,30 +175,39 @@ class FunctionCall(Expr):
|
|
|
149
175
|
try:
|
|
150
176
|
_ = json.dumps(arg)
|
|
151
177
|
except TypeError:
|
|
152
|
-
raise excs.Error(f
|
|
178
|
+
raise excs.Error(f'Argument for parameter {param_name!r} is not json-serializable: {arg}')
|
|
153
179
|
if arg is not None:
|
|
154
180
|
try:
|
|
155
181
|
param_type = param.col_type
|
|
156
182
|
bound_args[param_name] = param_type.create_literal(arg)
|
|
157
183
|
except TypeError as e:
|
|
158
184
|
msg = str(e)
|
|
159
|
-
raise excs.Error(f
|
|
185
|
+
raise excs.Error(f'Argument for parameter {param_name!r}: {msg[0].lower() + msg[1:]}')
|
|
160
186
|
continue
|
|
161
187
|
|
|
162
|
-
#
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
188
|
+
# these checks break the db migration test, because InlineArray isn't serialized correctly (it looses
|
|
189
|
+
# the type information)
|
|
190
|
+
# if is_var_param:
|
|
191
|
+
# if param.kind == inspect.Parameter.VAR_POSITIONAL:
|
|
192
|
+
# if not isinstance(arg, InlineArray) or not arg.col_type.is_json_type():
|
|
193
|
+
# pass
|
|
194
|
+
# assert isinstance(arg, InlineArray), type(arg)
|
|
195
|
+
# assert arg.col_type.is_json_type()
|
|
196
|
+
# if param.kind == inspect.Parameter.VAR_KEYWORD:
|
|
197
|
+
# if not isinstance(arg, InlineDict):
|
|
198
|
+
# pass
|
|
199
|
+
# assert isinstance(arg, InlineDict), type(arg)
|
|
200
|
+
if is_var_param:
|
|
201
|
+
pass
|
|
202
|
+
else:
|
|
203
|
+
assert param.col_type is not None
|
|
204
|
+
# Check that the argument is consistent with the expected parameter type, with the allowance that
|
|
205
|
+
# non-nullable parameters can still accept nullable arguments (since function calls with Nones
|
|
206
|
+
# assigned to non-nullable parameters will always return None)
|
|
207
|
+
if not param.col_type.is_supertype_of(arg.col_type, ignore_nullable=True):
|
|
208
|
+
raise excs.Error(
|
|
209
|
+
f'Parameter {param_name}: argument type {arg.col_type} does not match parameter type '
|
|
210
|
+
f'{param.col_type}')
|
|
176
211
|
|
|
177
212
|
def _equals(self, other: FunctionCall) -> bool:
|
|
178
213
|
if self.fn != other.fn:
|
pixeltable/exprs/globals.py
CHANGED
|
@@ -29,6 +29,8 @@ class ComparisonOperator(enum.Enum):
|
|
|
29
29
|
return '<='
|
|
30
30
|
if self == self.EQ:
|
|
31
31
|
return '=='
|
|
32
|
+
if self == self.NE:
|
|
33
|
+
return '!='
|
|
32
34
|
if self == self.GT:
|
|
33
35
|
return '>'
|
|
34
36
|
if self == self.GE:
|
|
@@ -66,6 +68,7 @@ class ArithmeticOperator(enum.Enum):
|
|
|
66
68
|
MUL = 2
|
|
67
69
|
DIV = 3
|
|
68
70
|
MOD = 4
|
|
71
|
+
FLOORDIV = 5
|
|
69
72
|
|
|
70
73
|
def __str__(self) -> str:
|
|
71
74
|
if self == self.ADD:
|
|
@@ -78,4 +81,5 @@ class ArithmeticOperator(enum.Enum):
|
|
|
78
81
|
return '/'
|
|
79
82
|
if self == self.MOD:
|
|
80
83
|
return '%'
|
|
81
|
-
|
|
84
|
+
if self == self.FLOORDIV:
|
|
85
|
+
return '//'
|
pixeltable/exprs/inline_array.py
CHANGED
|
@@ -21,6 +21,9 @@ class InlineArray(Expr):
|
|
|
21
21
|
is `True`, it will always be cast as a `JsonType`. If `force_json` is `False`, it will be cast as an
|
|
22
22
|
`ArrayType` if it is a homogenous array of scalars or arrays, or a `JsonType` otherwise.
|
|
23
23
|
"""
|
|
24
|
+
|
|
25
|
+
elements: List[Tuple[Optional[int], Any]]
|
|
26
|
+
|
|
24
27
|
def __init__(self, elements: Tuple, force_json: bool = False):
|
|
25
28
|
# we need to call this in order to populate self.components
|
|
26
29
|
super().__init__(ts.ArrayType((len(elements),), ts.IntType()))
|
|
@@ -28,7 +31,7 @@ class InlineArray(Expr):
|
|
|
28
31
|
# elements contains
|
|
29
32
|
# - for Expr elements: (index into components, None)
|
|
30
33
|
# - for non-Expr elements: (None, value)
|
|
31
|
-
self.elements
|
|
34
|
+
self.elements = []
|
|
32
35
|
for el in elements:
|
|
33
36
|
el = copy.deepcopy(el)
|
|
34
37
|
if isinstance(el, list):
|
|
@@ -43,14 +46,16 @@ class InlineArray(Expr):
|
|
|
43
46
|
else:
|
|
44
47
|
self.elements.append((None, el))
|
|
45
48
|
|
|
46
|
-
inferred_element_type = ts.InvalidType()
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
49
|
+
inferred_element_type: Optional[ts.ColumnType] = ts.InvalidType()
|
|
50
|
+
if not force_json:
|
|
51
|
+
# try to infer the element type
|
|
52
|
+
for idx, val in self.elements:
|
|
53
|
+
if idx is not None:
|
|
54
|
+
inferred_element_type = inferred_element_type.supertype(self.components[idx].col_type)
|
|
55
|
+
else:
|
|
56
|
+
inferred_element_type = inferred_element_type.supertype(ts.ColumnType.infer_literal_type(val))
|
|
57
|
+
if inferred_element_type is None:
|
|
58
|
+
break
|
|
54
59
|
|
|
55
60
|
if force_json or inferred_element_type is None:
|
|
56
61
|
# JSON conversion is forced, or there is no common supertype
|
|
@@ -93,7 +98,7 @@ class InlineArray(Expr):
|
|
|
93
98
|
data_row[self.slot_idx] = result
|
|
94
99
|
|
|
95
100
|
def _as_dict(self) -> Dict:
|
|
96
|
-
return {'elements': self.elements, **super()._as_dict()}
|
|
101
|
+
return {'elements': self.elements, 'is_json': self.col_type.is_json_type(), **super()._as_dict()}
|
|
97
102
|
|
|
98
103
|
@classmethod
|
|
99
104
|
def _from_dict(cls, d: Dict, components: List[Expr]) -> Expr:
|
|
@@ -106,4 +111,6 @@ class InlineArray(Expr):
|
|
|
106
111
|
arg.append(components[idx])
|
|
107
112
|
else:
|
|
108
113
|
arg.append(val)
|
|
109
|
-
|
|
114
|
+
# in order to avoid a schema version change, we'll interpret the absence of 'is_json' to indicate an ArrayType
|
|
115
|
+
is_json = d.get('is_json', False)
|
|
116
|
+
return cls(tuple(arg), force_json=is_json)
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
from typing import Any, Optional
|
|
2
|
+
|
|
3
|
+
import sqlalchemy as sql
|
|
4
|
+
|
|
5
|
+
import pixeltable.exceptions as excs
|
|
6
|
+
import pixeltable.type_system as ts
|
|
7
|
+
from pixeltable.exprs import Expr, FunctionCall
|
|
8
|
+
from pixeltable.func import FunctionRegistry, CallableFunction
|
|
9
|
+
from .data_row import DataRow
|
|
10
|
+
from .row_builder import RowBuilder
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class MethodRef(Expr):
|
|
14
|
+
"""
|
|
15
|
+
A method reference. This represents a `Function` instance with its
|
|
16
|
+
first parameter bound to a base expression.
|
|
17
|
+
|
|
18
|
+
When a `MethodRef` is called, it returns a `FunctionCall` with the base expression as the first argument.
|
|
19
|
+
The effective arity of a `MethodRef` is one less than the arity of the underlying `Function`.
|
|
20
|
+
"""
|
|
21
|
+
# TODO: Should this even be an `Expr`? It can't actually be evaluated directly (it has to be first
|
|
22
|
+
# converted to a `FunctionCall` by binding any remaining parameters).
|
|
23
|
+
|
|
24
|
+
def __init__(self, base_expr: Expr, method_name: str):
|
|
25
|
+
super().__init__(ts.InvalidType()) # The `MethodRef` is untyped until it is called.
|
|
26
|
+
self.base_expr = base_expr
|
|
27
|
+
self.method_name = method_name
|
|
28
|
+
self.fn = FunctionRegistry.get().lookup_type_method(base_expr.col_type.type_enum, method_name)
|
|
29
|
+
if self.fn is None:
|
|
30
|
+
# This has to be an `AttributeError`, or tab-completion won't work properly in ipython.
|
|
31
|
+
raise AttributeError(f'Unknown method (of type {base_expr.col_type}): {method_name}')
|
|
32
|
+
self.components = [base_expr]
|
|
33
|
+
self.id = self._create_id()
|
|
34
|
+
|
|
35
|
+
def _as_dict(self) -> dict:
|
|
36
|
+
return {'method_name': self.method_name, **super()._as_dict()}
|
|
37
|
+
|
|
38
|
+
@classmethod
|
|
39
|
+
def _from_dict(cls, d: dict, components: list[Expr]) -> Expr:
|
|
40
|
+
assert 'method_name' in d
|
|
41
|
+
assert len(components) == 1
|
|
42
|
+
return cls(d['method_name'], components[0])
|
|
43
|
+
|
|
44
|
+
def __call__(self, *args, **kwargs) -> FunctionCall:
|
|
45
|
+
result = self.fn(*[self.base_expr, *args], **kwargs)
|
|
46
|
+
assert isinstance(result, FunctionCall)
|
|
47
|
+
result.is_method_call = True
|
|
48
|
+
return result
|
|
49
|
+
|
|
50
|
+
def _equals(self, other: 'MethodRef') -> bool:
|
|
51
|
+
return self.base_expr == other.base_expr and self.method_name == other.method_name
|
|
52
|
+
|
|
53
|
+
def _id_attrs(self) -> list[tuple[str, Any]]:
|
|
54
|
+
return super()._id_attrs() + [('method_name', self.method_name)]
|
|
55
|
+
|
|
56
|
+
def sql_expr(self) -> Optional[sql.ClauseElement]:
|
|
57
|
+
return None
|
|
58
|
+
|
|
59
|
+
def eval(self, data_row: DataRow, row_builder: RowBuilder) -> None:
|
|
60
|
+
assert False, 'MethodRef cannot be evaluated directly'
|
|
61
|
+
|
|
62
|
+
def __str__(self) -> str:
|
|
63
|
+
return f'{self.base_expr}.{self.method_name}'
|
pixeltable/ext/__init__.py
CHANGED
|
@@ -3,3 +3,12 @@ Extended integrations for Pixeltable. This package contains experimental or demo
|
|
|
3
3
|
are not intended for production use. Long-term support cannot be guaranteed, usually because the features
|
|
4
4
|
have dependencies whose future support is unclear.
|
|
5
5
|
"""
|
|
6
|
+
|
|
7
|
+
from pixeltable.utils.code import local_public_names
|
|
8
|
+
from . import functions
|
|
9
|
+
|
|
10
|
+
__all__ = local_public_names(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def __dir__():
|
|
14
|
+
return __all__
|