pixeltable 0.4.6__py3-none-any.whl → 0.4.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.

Potentially problematic release.


This version of pixeltable might be problematic. Click here for more details.

Files changed (53) hide show
  1. pixeltable/__init__.py +4 -2
  2. pixeltable/catalog/__init__.py +1 -1
  3. pixeltable/catalog/catalog.py +3 -3
  4. pixeltable/catalog/column.py +49 -0
  5. pixeltable/catalog/insertable_table.py +0 -7
  6. pixeltable/catalog/schema_object.py +1 -14
  7. pixeltable/catalog/table.py +139 -53
  8. pixeltable/catalog/table_version.py +30 -138
  9. pixeltable/catalog/view.py +2 -1
  10. pixeltable/dataframe.py +2 -3
  11. pixeltable/env.py +43 -5
  12. pixeltable/exec/expr_eval/expr_eval_node.py +2 -2
  13. pixeltable/exec/expr_eval/schedulers.py +36 -15
  14. pixeltable/exprs/array_slice.py +2 -2
  15. pixeltable/exprs/data_row.py +13 -0
  16. pixeltable/exprs/expr.py +9 -9
  17. pixeltable/exprs/function_call.py +2 -2
  18. pixeltable/exprs/globals.py +1 -2
  19. pixeltable/exprs/json_path.py +3 -3
  20. pixeltable/exprs/row_builder.py +14 -16
  21. pixeltable/exprs/string_op.py +3 -3
  22. pixeltable/func/query_template_function.py +2 -2
  23. pixeltable/func/signature.py +30 -3
  24. pixeltable/func/tools.py +2 -2
  25. pixeltable/functions/anthropic.py +75 -25
  26. pixeltable/functions/globals.py +2 -2
  27. pixeltable/functions/llama_cpp.py +9 -1
  28. pixeltable/functions/openai.py +74 -54
  29. pixeltable/functions/video.py +54 -1
  30. pixeltable/functions/vision.py +2 -2
  31. pixeltable/globals.py +74 -12
  32. pixeltable/io/datarows.py +3 -3
  33. pixeltable/io/fiftyone.py +4 -4
  34. pixeltable/io/globals.py +3 -3
  35. pixeltable/io/hf_datasets.py +4 -4
  36. pixeltable/io/pandas.py +6 -6
  37. pixeltable/io/parquet.py +3 -3
  38. pixeltable/io/table_data_conduit.py +2 -2
  39. pixeltable/io/utils.py +2 -2
  40. pixeltable/iterators/document.py +2 -2
  41. pixeltable/iterators/video.py +49 -9
  42. pixeltable/share/packager.py +45 -36
  43. pixeltable/store.py +5 -25
  44. pixeltable/type_system.py +5 -8
  45. pixeltable/utils/__init__.py +2 -2
  46. pixeltable/utils/arrow.py +5 -5
  47. pixeltable/utils/description_helper.py +3 -3
  48. pixeltable/utils/iceberg.py +1 -2
  49. {pixeltable-0.4.6.dist-info → pixeltable-0.4.7.dist-info}/METADATA +70 -19
  50. {pixeltable-0.4.6.dist-info → pixeltable-0.4.7.dist-info}/RECORD +53 -53
  51. {pixeltable-0.4.6.dist-info → pixeltable-0.4.7.dist-info}/WHEEL +0 -0
  52. {pixeltable-0.4.6.dist-info → pixeltable-0.4.7.dist-info}/entry_points.txt +0 -0
  53. {pixeltable-0.4.6.dist-info → pixeltable-0.4.7.dist-info}/licenses/LICENSE +0 -0
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import Any, Optional, Union
3
+ from typing import Any, Optional
4
4
 
5
5
  import jmespath
6
6
  import sqlalchemy as sql
@@ -18,7 +18,7 @@ from .sql_element_cache import SqlElementCache
18
18
 
19
19
  class JsonPath(Expr):
20
20
  def __init__(
21
- self, anchor: Optional[Expr], path_elements: Optional[list[Union[str, int, slice]]] = None, scope_idx: int = 0
21
+ self, anchor: Optional[Expr], path_elements: Optional[list[str | int | slice]] = None, scope_idx: int = 0
22
22
  ) -> None:
23
23
  """
24
24
  anchor can be None, in which case this is a relative JsonPath and the anchor is set later via set_anchor().
@@ -30,7 +30,7 @@ class JsonPath(Expr):
30
30
  super().__init__(ts.JsonType(nullable=True)) # JsonPath expressions are always nullable
31
31
  if anchor is not None:
32
32
  self.components = [anchor]
33
- self.path_elements: list[Union[str, int, slice]] = path_elements
33
+ self.path_elements: list[str | int | slice] = path_elements
34
34
  self.compiled_path = jmespath.compile(self._json_path()) if len(path_elements) > 0 else None
35
35
  self.scope_idx = scope_idx
36
36
  # NOTE: the _create_id() result will change if set_anchor() gets called;
@@ -3,7 +3,7 @@ from __future__ import annotations
3
3
  import sys
4
4
  import time
5
5
  from dataclasses import dataclass
6
- from typing import Any, Iterable, Optional, Sequence
6
+ from typing import Any, Iterable, NamedTuple, Optional, Sequence
7
7
  from uuid import UUID
8
8
 
9
9
  import numpy as np
@@ -34,8 +34,7 @@ class ExecProfile:
34
34
  )
35
35
 
36
36
 
37
- @dataclass
38
- class ColumnSlotIdx:
37
+ class ColumnSlotIdx(NamedTuple):
39
38
  """Info for how to locate materialized column in DataRow
40
39
  TODO: can this be integrated into RowBuilder directly?
41
40
  """
@@ -127,7 +126,7 @@ class RowBuilder:
127
126
  )
128
127
 
129
128
  # if init(columns):
130
- # - we are creating table rows and need to record columns for create_table_row()
129
+ # - we are creating table rows and need to record columns for create_store_table_row()
131
130
  # - output_exprs materialize those columns
132
131
  # - input_exprs are ColumnRefs of the non-computed columns (ie, what needs to be provided as input)
133
132
  # - media validation:
@@ -445,20 +444,20 @@ class RowBuilder:
445
444
  expr, f'expression {expr}', data_row.get_exc(expr.slot_idx), exc_tb, input_vals, 0
446
445
  ) from exc
447
446
 
448
- def create_table_row(
447
+ def create_store_table_row(
449
448
  self, data_row: DataRow, cols_with_excs: Optional[set[int]], pk: tuple[int, ...]
450
449
  ) -> tuple[list[Any], int]:
451
- """Create a table row from the slots that have an output column assigned
450
+ """Create a store table row from the slots that have an output column assigned
452
451
 
453
452
  Return tuple[list of row values in `self.table_columns` order, # of exceptions]
454
453
  This excludes system columns.
454
+ Row values are converted to their store type.
455
455
  """
456
456
  from pixeltable.exprs.column_property_ref import ColumnPropertyRef
457
457
 
458
458
  num_excs = 0
459
459
  table_row: list[Any] = list(pk)
460
- for info in self.table_columns:
461
- col, slot_idx = info.col, info.slot_idx
460
+ for col, slot_idx in self.table_columns:
462
461
  if data_row.has_exc(slot_idx):
463
462
  exc = data_row.get_exc(slot_idx)
464
463
  num_excs += 1
@@ -469,9 +468,11 @@ class RowBuilder:
469
468
  # exceptions get stored in the errortype/-msg properties of the cellmd column
470
469
  table_row.append(ColumnPropertyRef.create_cellmd_exc(exc))
471
470
  else:
472
- if col.col_type.is_image_type() and data_row.file_urls[slot_idx] is None:
473
- # we have yet to store this image
474
- data_row.flush_img(slot_idx, col)
471
+ if col.col_type.is_media_type():
472
+ if col.col_type.is_image_type() and data_row.file_urls[slot_idx] is None:
473
+ # we have yet to store this image
474
+ data_row.flush_img(slot_idx, col)
475
+ data_row.move_tmp_media_file(slot_idx, col)
475
476
  val = data_row.get_stored_val(slot_idx, col.get_sa_col_type())
476
477
  table_row.append(val)
477
478
  if col.stores_cellmd:
@@ -479,7 +480,7 @@ class RowBuilder:
479
480
 
480
481
  return table_row, num_excs
481
482
 
482
- def store_column_names(self) -> tuple[list[str], dict[int, catalog.Column]]:
483
+ def store_column_names(self) -> list[str]:
483
484
  """
484
485
  Returns the list of store column names corresponding to the table_columns of this RowBuilder.
485
486
  The second tuple element of the return value is a dictionary containing all media columns in the
@@ -487,16 +488,13 @@ class RowBuilder:
487
488
  """
488
489
  assert self.tbl is not None, self.table_columns
489
490
  store_col_names: list[str] = [pk_col.name for pk_col in self.tbl.store_tbl.pk_columns()]
490
- media_cols: dict[int, catalog.Column] = {}
491
491
 
492
492
  for col in self.table_columns:
493
- if col.col.col_type.is_media_type():
494
- media_cols[len(store_col_names)] = col.col
495
493
  store_col_names.append(col.col.store_name())
496
494
  if col.col.stores_cellmd:
497
495
  store_col_names.append(col.col.cellmd_store_name())
498
496
 
499
- return store_col_names, media_cols
497
+ return store_col_names
500
498
 
501
499
  def make_row(self) -> exprs.DataRow:
502
500
  """Creates a new DataRow with the current row_builder's configuration."""
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import Any, Optional, Union
3
+ from typing import Any, Optional
4
4
 
5
5
  import sqlalchemy as sql
6
6
 
@@ -76,7 +76,7 @@ class StringOp(Expr):
76
76
  op2_val = data_row[self._op2.slot_idx]
77
77
  data_row[self.slot_idx] = self.eval_nullable(op1_val, op2_val)
78
78
 
79
- def eval_nullable(self, op1_val: Union[str, None], op2_val: Union[int, str, None]) -> Union[str, None]:
79
+ def eval_nullable(self, op1_val: str | None, op2_val: int | str | None) -> str | None:
80
80
  """
81
81
  Return the result of evaluating the expression on two nullable int/float operands,
82
82
  None is interpreted as SQL NULL
@@ -85,7 +85,7 @@ class StringOp(Expr):
85
85
  return None
86
86
  return self.eval_non_null(op1_val, op2_val)
87
87
 
88
- def eval_non_null(self, op1_val: str, op2_val: Union[int, str]) -> str:
88
+ def eval_non_null(self, op1_val: str, op2_val: int | str) -> str:
89
89
  """
90
90
  Return the result of evaluating the expression on two int/float operands
91
91
  """
@@ -2,7 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import inspect
4
4
  from functools import reduce
5
- from typing import TYPE_CHECKING, Any, Callable, Iterable, Optional, Union, overload
5
+ from typing import TYPE_CHECKING, Any, Callable, Iterable, Optional, overload
6
6
 
7
7
  from pixeltable import catalog, exceptions as excs, exprs, func, type_system as ts
8
8
 
@@ -129,7 +129,7 @@ def retrieval_udf(
129
129
  table: catalog.Table,
130
130
  name: Optional[str] = None,
131
131
  description: Optional[str] = None,
132
- parameters: Optional[Iterable[Union[str, exprs.ColumnRef]]] = None,
132
+ parameters: Optional[Iterable[str | exprs.ColumnRef]] = None,
133
133
  limit: Optional[int] = 10,
134
134
  ) -> func.QueryTemplateFunction:
135
135
  """
@@ -84,8 +84,28 @@ class Signature:
84
84
  """
85
85
 
86
86
  SPECIAL_PARAM_NAMES: ClassVar[list[str]] = ['group_by', 'order_by']
87
-
88
- def __init__(self, return_type: ts.ColumnType, parameters: list[Parameter], is_batched: bool = False):
87
+ SYSTEM_PARAM_NAMES: ClassVar[list[str]] = ['_runtime_ctx']
88
+
89
+ return_type: ts.ColumnType
90
+ is_batched: bool
91
+ parameters: dict[str, Parameter] # name -> Parameter
92
+ parameters_by_pos: list[Parameter] # ordered by position in the signature
93
+ constant_parameters: list[Parameter] # parameters that are not batched
94
+ batched_parameters: list[Parameter] # parameters that are batched
95
+ required_parameters: list[Parameter] # parameters that do not have a default value
96
+
97
+ # the names of recognized system parameters in the signature; these are excluded from self.parameters
98
+ system_parameters: list[str]
99
+
100
+ py_signature: inspect.Signature
101
+
102
+ def __init__(
103
+ self,
104
+ return_type: ts.ColumnType,
105
+ parameters: list[Parameter],
106
+ is_batched: bool = False,
107
+ system_parameters: Optional[list[str]] = None,
108
+ ):
89
109
  assert isinstance(return_type, ts.ColumnType)
90
110
  self.return_type = return_type
91
111
  self.is_batched = is_batched
@@ -95,6 +115,7 @@ class Signature:
95
115
  self.constant_parameters = [p for p in parameters if not p.is_batched]
96
116
  self.batched_parameters = [p for p in parameters if p.is_batched]
97
117
  self.required_parameters = [p for p in parameters if not p.has_default()]
118
+ self.system_parameters = system_parameters if system_parameters is not None else []
98
119
  self.py_signature = inspect.Signature([p.to_py_param() for p in self.parameters_by_pos])
99
120
 
100
121
  def get_return_type(self) -> ts.ColumnType:
@@ -237,6 +258,7 @@ class Signature:
237
258
  type_substitutions: Optional[dict] = None,
238
259
  is_cls_method: bool = False,
239
260
  ) -> list[Parameter]:
261
+ """Ignores parameters starting with '_'."""
240
262
  from pixeltable import exprs
241
263
 
242
264
  assert (py_fn is None) != (py_params is None)
@@ -251,6 +273,10 @@ class Signature:
251
273
  for idx, param in enumerate(py_params):
252
274
  if is_cls_method and idx == 0:
253
275
  continue # skip 'self' or 'cls' parameter
276
+ if param.name in cls.SYSTEM_PARAM_NAMES:
277
+ continue # skip system parameters
278
+ if param.name.startswith('_'):
279
+ raise excs.Error(f"{param.name!r}: parameters starting with '_' are reserved")
254
280
  if param.name in cls.SPECIAL_PARAM_NAMES:
255
281
  raise excs.Error(f'{param.name!r} is a reserved parameter name')
256
282
  if param.kind in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD):
@@ -308,5 +334,6 @@ class Signature:
308
334
  raise excs.Error('Cannot infer pixeltable return type')
309
335
  else:
310
336
  _, return_is_batched = cls._infer_type(sig.return_annotation)
337
+ system_params = [param_name for param_name in sig.parameters if param_name in cls.SYSTEM_PARAM_NAMES]
311
338
 
312
- return Signature(return_type, parameters, return_is_batched)
339
+ return Signature(return_type, parameters, return_is_batched, system_parameters=system_params)
pixeltable/func/tools.py CHANGED
@@ -1,5 +1,5 @@
1
1
  import json
2
- from typing import TYPE_CHECKING, Any, Callable, Optional, TypeVar, Union
2
+ from typing import TYPE_CHECKING, Any, Callable, Optional, TypeVar
3
3
 
4
4
  import pydantic
5
5
 
@@ -100,7 +100,7 @@ class Tools(pydantic.BaseModel):
100
100
  self,
101
101
  auto: bool = False,
102
102
  required: bool = False,
103
- tool: Union[str, Function, None] = None,
103
+ tool: str | Function | None = None,
104
104
  parallel_tool_calls: bool = True,
105
105
  ) -> ToolChoice:
106
106
  if sum([auto, required, tool is not None]) != 1:
@@ -38,6 +38,53 @@ def _anthropic_client() -> 'anthropic.AsyncAnthropic':
38
38
  return env.Env.get().get_client('anthropic')
39
39
 
40
40
 
41
+ def _get_header_info(
42
+ headers: httpx.Headers,
43
+ ) -> tuple[
44
+ Optional[tuple[int, int, datetime.datetime]],
45
+ Optional[tuple[int, int, datetime.datetime]],
46
+ Optional[tuple[int, int, datetime.datetime]],
47
+ ]:
48
+ """Extract rate limit info from Anthropic API response headers."""
49
+ requests_limit_str = headers.get('anthropic-ratelimit-requests-limit')
50
+ requests_limit = int(requests_limit_str) if requests_limit_str is not None else None
51
+ requests_remaining_str = headers.get('anthropic-ratelimit-requests-remaining')
52
+ requests_remaining = int(requests_remaining_str) if requests_remaining_str is not None else None
53
+ requests_reset_str = headers.get('anthropic-ratelimit-requests-reset')
54
+ requests_reset = (
55
+ datetime.datetime.fromisoformat(requests_reset_str.replace('Z', '+00:00')) if requests_reset_str else None
56
+ )
57
+ requests_info = (requests_limit, requests_remaining, requests_reset) if requests_reset else None
58
+
59
+ input_tokens_limit_str = headers.get('anthropic-ratelimit-input-tokens-limit')
60
+ input_tokens_limit = int(input_tokens_limit_str) if input_tokens_limit_str is not None else None
61
+ input_tokens_remaining_str = headers.get('anthropic-ratelimit-input-tokens-remaining')
62
+ input_tokens_remaining = int(input_tokens_remaining_str) if input_tokens_remaining_str is not None else None
63
+ input_tokens_reset_str = headers.get('anthropic-ratelimit-input-tokens-reset')
64
+ input_tokens_reset = (
65
+ datetime.datetime.fromisoformat(input_tokens_reset_str.replace('Z', '+00:00'))
66
+ if input_tokens_reset_str
67
+ else None
68
+ )
69
+ input_tokens_info = (input_tokens_limit, input_tokens_remaining, input_tokens_reset) if input_tokens_reset else None
70
+
71
+ output_tokens_limit_str = headers.get('anthropic-ratelimit-output-tokens-limit')
72
+ output_tokens_limit = int(output_tokens_limit_str) if output_tokens_limit_str is not None else None
73
+ output_tokens_remaining_str = headers.get('anthropic-ratelimit-output-tokens-remaining')
74
+ output_tokens_remaining = int(output_tokens_remaining_str) if output_tokens_remaining_str is not None else None
75
+ output_tokens_reset_str = headers.get('anthropic-ratelimit-output-tokens-reset')
76
+ output_tokens_reset = (
77
+ datetime.datetime.fromisoformat(output_tokens_reset_str.replace('Z', '+00:00'))
78
+ if output_tokens_reset_str
79
+ else None
80
+ )
81
+ output_tokens_info = (
82
+ (output_tokens_limit, output_tokens_remaining, output_tokens_reset) if output_tokens_reset else None
83
+ )
84
+
85
+ return requests_info, input_tokens_info, output_tokens_info
86
+
87
+
41
88
  class AnthropicRateLimitsInfo(env.RateLimitsInfo):
42
89
  def __init__(self) -> None:
43
90
  super().__init__(self._get_request_resources)
@@ -51,6 +98,27 @@ class AnthropicRateLimitsInfo(env.RateLimitsInfo):
51
98
  input_len += len(message['content'])
52
99
  return {'requests': 1, 'input_tokens': int(input_len / 4), 'output_tokens': max_tokens}
53
100
 
101
+ def record_exc(self, exc: Exception) -> None:
102
+ import anthropic
103
+
104
+ if (
105
+ not isinstance(exc, anthropic.APIError)
106
+ or not hasattr(exc, 'response')
107
+ or not hasattr(exc.response, 'headers')
108
+ ):
109
+ return
110
+ requests_info, input_tokens_info, output_tokens_info = _get_header_info(exc.response.headers)
111
+ _logger.debug(
112
+ f'record_exc(): requests_info={requests_info} input_tokens_info={input_tokens_info} '
113
+ f'output_tokens_info={output_tokens_info}'
114
+ )
115
+ self.record(requests=requests_info, input_tokens=input_tokens_info, output_tokens=output_tokens_info)
116
+ self.has_exc = True
117
+
118
+ retry_after_str = exc.response.headers.get('retry-after')
119
+ if retry_after_str is not None:
120
+ _logger.debug(f'retry-after: {retry_after_str}')
121
+
54
122
  def get_retry_delay(self, exc: Exception) -> Optional[float]:
55
123
  import anthropic
56
124
 
@@ -77,6 +145,7 @@ async def messages(
77
145
  model_kwargs: Optional[dict[str, Any]] = None,
78
146
  tools: Optional[list[dict[str, Any]]] = None,
79
147
  tool_choice: Optional[dict[str, Any]] = None,
148
+ _runtime_ctx: Optional[env.RuntimeCtx] = None,
80
149
  ) -> dict:
81
150
  """
82
151
  Create a Message.
@@ -151,32 +220,13 @@ async def messages(
151
220
  messages=cast(Iterable[MessageParam], messages), model=model, max_tokens=max_tokens, **model_kwargs
152
221
  )
153
222
 
154
- requests_limit_str = result.headers.get('anthropic-ratelimit-requests-limit')
155
- requests_limit = int(requests_limit_str) if requests_limit_str is not None else None
156
- requests_remaining_str = result.headers.get('anthropic-ratelimit-requests-remaining')
157
- requests_remaining = int(requests_remaining_str) if requests_remaining_str is not None else None
158
- requests_reset_str = result.headers.get('anthropic-ratelimit-requests-reset')
159
- requests_reset = datetime.datetime.fromisoformat(requests_reset_str.replace('Z', '+00:00'))
160
- input_tokens_limit_str = result.headers.get('anthropic-ratelimit-input-tokens-limit')
161
- input_tokens_limit = int(input_tokens_limit_str) if input_tokens_limit_str is not None else None
162
- input_tokens_remaining_str = result.headers.get('anthropic-ratelimit-input-tokens-remaining')
163
- input_tokens_remaining = int(input_tokens_remaining_str) if input_tokens_remaining_str is not None else None
164
- input_tokens_reset_str = result.headers.get('anthropic-ratelimit-input-tokens-reset')
165
- input_tokens_reset = datetime.datetime.fromisoformat(input_tokens_reset_str.replace('Z', '+00:00'))
166
- output_tokens_limit_str = result.headers.get('anthropic-ratelimit-output-tokens-limit')
167
- output_tokens_limit = int(output_tokens_limit_str) if output_tokens_limit_str is not None else None
168
- output_tokens_remaining_str = result.headers.get('anthropic-ratelimit-output-tokens-remaining')
169
- output_tokens_remaining = int(output_tokens_remaining_str) if output_tokens_remaining_str is not None else None
170
- output_tokens_reset_str = result.headers.get('anthropic-ratelimit-output-tokens-reset')
171
- output_tokens_reset = datetime.datetime.fromisoformat(output_tokens_reset_str.replace('Z', '+00:00'))
172
- retry_after_str = result.headers.get('retry-after')
173
- if retry_after_str is not None:
174
- _logger.debug(f'retry-after: {retry_after_str}')
175
-
223
+ requests_info, input_tokens_info, output_tokens_info = _get_header_info(result.headers)
224
+ # retry_after_str = result.headers.get('retry-after')
225
+ # if retry_after_str is not None:
226
+ # _logger.debug(f'retry-after: {retry_after_str}')
227
+ is_retry = _runtime_ctx is not None and _runtime_ctx.is_retry
176
228
  rate_limits_info.record(
177
- requests=(requests_limit, requests_remaining, requests_reset),
178
- input_tokens=(input_tokens_limit, input_tokens_remaining, input_tokens_reset),
179
- output_tokens=(output_tokens_limit, output_tokens_remaining, output_tokens_reset),
229
+ requests=requests_info, input_tokens=input_tokens_info, output_tokens=output_tokens_info, reset_exc=is_retry
180
230
  )
181
231
 
182
232
  result_dict = json.loads(result.text)
@@ -1,6 +1,6 @@
1
1
  import builtins
2
2
  import typing
3
- from typing import Any, Callable, Optional, Union
3
+ from typing import Any, Callable, Optional
4
4
 
5
5
  import sqlalchemy as sql
6
6
 
@@ -11,7 +11,7 @@ from typing import _GenericAlias # type: ignore[attr-defined] # isort: skip
11
11
 
12
12
 
13
13
  # TODO: remove and replace calls with astype()
14
- def cast(expr: exprs.Expr, target_type: Union[ts.ColumnType, type, _GenericAlias]) -> exprs.Expr:
14
+ def cast(expr: exprs.Expr, target_type: ts.ColumnType | type | _GenericAlias) -> exprs.Expr:
15
15
  expr.col_type = ts.ColumnType.normalize_type(target_type)
16
16
  return expr
17
17
 
@@ -93,10 +93,18 @@ def _lookup_pretrained_model(repo_id: str, filename: Optional[str], n_gpu_layers
93
93
  return _model_cache[key]
94
94
 
95
95
 
96
- _model_cache: dict[tuple[str, str, int], Any] = {}
96
+ _model_cache: dict[tuple[str, str, int], 'llama_cpp.Llama'] = {}
97
97
  _IS_GPU_AVAILABLE: Optional[bool] = None
98
98
 
99
99
 
100
+ def cleanup() -> None:
101
+ for model in _model_cache.values():
102
+ if model._sampler is not None:
103
+ model._sampler.close()
104
+ model.close()
105
+ _model_cache.clear()
106
+
107
+
100
108
  __all__ = local_public_names(__name__)
101
109
 
102
110
 
@@ -91,6 +91,49 @@ def _rate_limits_pool(model: str) -> str:
91
91
  return f'rate-limits:openai:{model}'
92
92
 
93
93
 
94
+ # RE pattern for duration in '*-reset' headers;
95
+ # examples: 1d2h3ms, 4m5.6s; # fractional seconds can be reported as 0.5s or 500ms
96
+ _header_duration_pattern = re.compile(r'(?:(\d+)d)?(?:(\d+)h)?(?:(\d+)ms)|(?:(\d+)m)?(?:([\d.]+)s)?')
97
+
98
+
99
+ def _parse_header_duration(duration_str: str) -> datetime.timedelta:
100
+ match = _header_duration_pattern.match(duration_str)
101
+ if not match:
102
+ raise ValueError(f'Invalid duration format: {duration_str}')
103
+
104
+ days = int(match.group(1) or 0)
105
+ hours = int(match.group(2) or 0)
106
+ milliseconds = int(match.group(3) or 0)
107
+ minutes = int(match.group(4) or 0)
108
+ seconds = float(match.group(5) or 0)
109
+
110
+ return datetime.timedelta(days=days, hours=hours, minutes=minutes, seconds=seconds, milliseconds=milliseconds)
111
+
112
+
113
+ def _get_header_info(
114
+ headers: httpx.Headers,
115
+ ) -> tuple[Optional[tuple[int, int, datetime.datetime]], Optional[tuple[int, int, datetime.datetime]]]:
116
+ now = datetime.datetime.now(tz=datetime.timezone.utc)
117
+
118
+ requests_limit_str = headers.get('x-ratelimit-limit-requests')
119
+ requests_limit = int(requests_limit_str) if requests_limit_str is not None else None
120
+ requests_remaining_str = headers.get('x-ratelimit-remaining-requests')
121
+ requests_remaining = int(requests_remaining_str) if requests_remaining_str is not None else None
122
+ requests_reset_str = headers.get('x-ratelimit-reset-requests', '5s') # Default to 5 seconds
123
+ requests_reset_ts = now + _parse_header_duration(requests_reset_str)
124
+ requests_info = (requests_limit, requests_remaining, requests_reset_ts)
125
+
126
+ tokens_limit_str = headers.get('x-ratelimit-limit-tokens')
127
+ tokens_limit = int(tokens_limit_str) if tokens_limit_str is not None else None
128
+ tokens_remaining_str = headers.get('x-ratelimit-remaining-tokens')
129
+ tokens_remaining = int(tokens_remaining_str) if tokens_remaining_str is not None else None
130
+ tokens_reset_str = headers.get('x-ratelimit-reset-tokens', '5s') # Default to 5 seconds
131
+ tokens_reset_ts = now + _parse_header_duration(tokens_reset_str)
132
+ tokens_info = (tokens_limit, tokens_remaining, tokens_reset_ts)
133
+
134
+ return requests_info, tokens_info
135
+
136
+
94
137
  class OpenAIRateLimitsInfo(env.RateLimitsInfo):
95
138
  retryable_errors: tuple[Type[Exception], ...]
96
139
 
@@ -111,61 +154,24 @@ class OpenAIRateLimitsInfo(env.RateLimitsInfo):
111
154
  openai.InternalServerError,
112
155
  )
113
156
 
157
+ def record_exc(self, exc: Exception) -> None:
158
+ import openai
159
+
160
+ _ = isinstance(exc, openai.APIError)
161
+ if not isinstance(exc, openai.APIError) or not hasattr(exc, 'response') or not hasattr(exc.response, 'headers'):
162
+ return
163
+ requests_info, tokens_info = _get_header_info(exc.response.headers)
164
+ _logger.debug(f'record_exc(): requests_info={requests_info} tokens_info={tokens_info}')
165
+ self.record(requests=requests_info, tokens=tokens_info)
166
+ self.has_exc = True
167
+
114
168
  def get_retry_delay(self, exc: Exception) -> Optional[float]:
115
169
  import openai
116
170
 
117
171
  if not isinstance(exc, self.retryable_errors):
118
172
  return None
119
173
  assert isinstance(exc, openai.APIError)
120
- return 1.0
121
-
122
-
123
- # RE pattern for duration in '*-reset' headers;
124
- # examples: 1d2h3ms, 4m5.6s; # fractional seconds can be reported as 0.5s or 500ms
125
- _header_duration_pattern = re.compile(r'(?:(\d+)d)?(?:(\d+)h)?(?:(\d+)ms)|(?:(\d+)m)?(?:([\d.]+)s)?')
126
-
127
-
128
- def _parse_header_duration(duration_str: str) -> datetime.timedelta:
129
- match = _header_duration_pattern.match(duration_str)
130
- if not match:
131
- raise ValueError(f'Invalid duration format: {duration_str}')
132
-
133
- days = int(match.group(1) or 0)
134
- hours = int(match.group(2) or 0)
135
- milliseconds = int(match.group(3) or 0)
136
- minutes = int(match.group(4) or 0)
137
- seconds = float(match.group(5) or 0)
138
-
139
- return datetime.timedelta(days=days, hours=hours, minutes=minutes, seconds=seconds, milliseconds=milliseconds)
140
-
141
-
142
- def _get_header_info(
143
- headers: httpx.Headers, *, requests: bool = True, tokens: bool = True
144
- ) -> tuple[Optional[tuple[int, int, datetime.datetime]], Optional[tuple[int, int, datetime.datetime]]]:
145
- assert requests or tokens
146
- now = datetime.datetime.now(tz=datetime.timezone.utc)
147
-
148
- requests_info: Optional[tuple[int, int, datetime.datetime]] = None
149
- if requests:
150
- requests_limit_str = headers.get('x-ratelimit-limit-requests')
151
- requests_limit = int(requests_limit_str) if requests_limit_str is not None else None
152
- requests_remaining_str = headers.get('x-ratelimit-remaining-requests')
153
- requests_remaining = int(requests_remaining_str) if requests_remaining_str is not None else None
154
- requests_reset_str = headers.get('x-ratelimit-reset-requests', '5s') # Default to 5 seconds
155
- requests_reset_ts = now + _parse_header_duration(requests_reset_str)
156
- requests_info = (requests_limit, requests_remaining, requests_reset_ts)
157
-
158
- tokens_info: Optional[tuple[int, int, datetime.datetime]] = None
159
- if tokens:
160
- tokens_limit_str = headers.get('x-ratelimit-limit-tokens')
161
- tokens_limit = int(tokens_limit_str) if tokens_limit_str is not None else None
162
- tokens_remaining_str = headers.get('x-ratelimit-remaining-tokens')
163
- tokens_remaining = int(tokens_remaining_str) if tokens_remaining_str is not None else None
164
- tokens_reset_str = headers.get('x-ratelimit-reset-tokens', '5s') # Default to 5 seconds
165
- tokens_reset_ts = now + _parse_header_duration(tokens_reset_str)
166
- tokens_info = (tokens_limit, tokens_remaining, tokens_reset_ts)
167
-
168
- return requests_info, tokens_info
174
+ return super().get_retry_delay(exc)
169
175
 
170
176
 
171
177
  #####################################
@@ -355,6 +361,7 @@ async def chat_completions(
355
361
  model_kwargs: Optional[dict[str, Any]] = None,
356
362
  tools: Optional[list[dict[str, Any]]] = None,
357
363
  tool_choice: Optional[dict[str, Any]] = None,
364
+ _runtime_ctx: Optional[env.RuntimeCtx] = None,
358
365
  ) -> dict:
359
366
  """
360
367
  Creates a model response for the given chat conversation.
@@ -418,7 +425,8 @@ async def chat_completions(
418
425
  )
419
426
 
420
427
  requests_info, tokens_info = _get_header_info(result.headers)
421
- rate_limits_info.record(requests=requests_info, tokens=tokens_info)
428
+ is_retry = _runtime_ctx is not None and _runtime_ctx.is_retry
429
+ rate_limits_info.record(requests=requests_info, tokens=tokens_info, reset_exc=is_retry)
422
430
 
423
431
  return json.loads(result.text)
424
432
 
@@ -461,7 +469,12 @@ def _vision_get_request_resources(
461
469
 
462
470
  @pxt.udf
463
471
  async def vision(
464
- prompt: str, image: PIL.Image.Image, *, model: str, model_kwargs: Optional[dict[str, Any]] = None
472
+ prompt: str,
473
+ image: PIL.Image.Image,
474
+ *,
475
+ model: str,
476
+ model_kwargs: Optional[dict[str, Any]] = None,
477
+ _runtime_ctx: Optional[env.RuntimeCtx] = None,
465
478
  ) -> str:
466
479
  """
467
480
  Analyzes an image with the OpenAI vision capability. This is a convenience function that takes an image and
@@ -521,8 +534,10 @@ async def vision(
521
534
  **model_kwargs,
522
535
  )
523
536
 
537
+ # _logger.debug(f'vision(): headers={result.headers}')
524
538
  requests_info, tokens_info = _get_header_info(result.headers)
525
- rate_limits_info.record(requests=requests_info, tokens=tokens_info)
539
+ is_retry = _runtime_ctx is not None and _runtime_ctx.is_retry
540
+ rate_limits_info.record(requests=requests_info, tokens=tokens_info, reset_exc=is_retry)
526
541
 
527
542
  result = json.loads(result.text)
528
543
  return result['choices'][0]['message']['content']
@@ -545,7 +560,11 @@ def _embeddings_get_request_resources(input: list[str]) -> dict[str, int]:
545
560
 
546
561
  @pxt.udf(batch_size=32)
547
562
  async def embeddings(
548
- input: Batch[str], *, model: str, model_kwargs: Optional[dict[str, Any]] = None
563
+ input: Batch[str],
564
+ *,
565
+ model: str,
566
+ model_kwargs: Optional[dict[str, Any]] = None,
567
+ _runtime_ctx: Optional[env.RuntimeCtx] = None,
549
568
  ) -> Batch[pxt.Array[(None,), pxt.Float]]:
550
569
  """
551
570
  Creates an embedding vector representing the input text.
@@ -592,7 +611,8 @@ async def embeddings(
592
611
  input=input, model=model, encoding_format='float', **model_kwargs
593
612
  )
594
613
  requests_info, tokens_info = _get_header_info(result.headers)
595
- rate_limits_info.record(requests=requests_info, tokens=tokens_info)
614
+ is_retry = _runtime_ctx is not None and _runtime_ctx.is_retry
615
+ rate_limits_info.record(requests=requests_info, tokens=tokens_info, reset_exc=is_retry)
596
616
  return [np.array(data['embedding'], dtype=np.float64) for data in json.loads(result.content)['data']]
597
617
 
598
618
 
@@ -12,7 +12,7 @@ import pixeltable as pxt
12
12
  from pixeltable import env
13
13
  from pixeltable.utils.code import local_public_names
14
14
 
15
- _format_defaults = { # format -> (codec, ext)
15
+ _format_defaults: dict[str, tuple[str, str]] = { # format -> (codec, ext)
16
16
  'wav': ('pcm_s16le', 'wav'),
17
17
  'mp3': ('libmp3lame', 'mp3'),
18
18
  'flac': ('flac', 'flac'),
@@ -40,6 +40,59 @@ _format_defaults = { # format -> (codec, ext)
40
40
  class make_video(pxt.Aggregator):
41
41
  """
42
42
  Aggregator that creates a video from a sequence of images.
43
+
44
+ Creates an H.264 encoded MP4 video from a sequence of PIL Image frames. This aggregator requires the input
45
+ frames to be ordered (typically by frame position) and is commonly used with `FrameIterator` views to
46
+ reconstruct videos from processed frames.
47
+
48
+ Args:
49
+ fps: Frames per second for the output video. Default is 25. This is set when the aggregator is created.
50
+
51
+ Returns:
52
+
53
+ - A `pxt.Video` containing the created video file path.
54
+
55
+ Examples:
56
+ Create a video from frames extracted using FrameIterator:
57
+
58
+ >>> import pixeltable as pxt
59
+ >>> from pixeltable.functions.video import make_video
60
+ >>> from pixeltable.iterators import FrameIterator
61
+ >>>
62
+ >>> # Create base table for videos
63
+ >>> videos_table = pxt.create_table('videos', {'video': pxt.Video})
64
+ >>>
65
+ >>> # Create view to extract frames
66
+ >>> frames_view = pxt.create_view(
67
+ ... 'video_frames',
68
+ ... videos_table,
69
+ ... iterator=FrameIterator.create(video=videos_table.video, fps=1)
70
+ ... )
71
+ >>>
72
+ >>> # Reconstruct video from frames
73
+ >>> frames_view.group_by(videos_table).select(
74
+ ... make_video(frames_view.pos, frames_view.frame)
75
+ ... ).show()
76
+
77
+ Apply transformations to frames before creating a video:
78
+
79
+ >>> # Add computed column with transformed frames
80
+ >>> frames_view.add_computed_column(
81
+ ... rotated_frame=frames_view.frame.rotate(30),
82
+ ... stored=True
83
+ ... )
84
+ >>>
85
+ >>> # Create video from transformed frames
86
+ >>> frames_view.group_by(videos_table).select(
87
+ ... make_video(frames_view.pos, frames_view.rotated_frame)
88
+ ... ).show()
89
+
90
+ Compare multiple processed versions side-by-side:
91
+
92
+ >>> frames_view.group_by(videos_table).select(
93
+ ... make_video(frames_view.pos, frames_view.frame),
94
+ ... make_video(frames_view.pos, frames_view.rotated_frame)
95
+ ... ).show()
43
96
  """
44
97
 
45
98
  container: Optional[av.container.OutputContainer]