pixeltable 0.3.14__py3-none-any.whl → 0.5.7__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (220) hide show
  1. pixeltable/__init__.py +42 -8
  2. pixeltable/{dataframe.py → _query.py} +470 -206
  3. pixeltable/_version.py +1 -0
  4. pixeltable/catalog/__init__.py +5 -4
  5. pixeltable/catalog/catalog.py +1785 -432
  6. pixeltable/catalog/column.py +190 -113
  7. pixeltable/catalog/dir.py +2 -4
  8. pixeltable/catalog/globals.py +19 -46
  9. pixeltable/catalog/insertable_table.py +191 -98
  10. pixeltable/catalog/path.py +63 -23
  11. pixeltable/catalog/schema_object.py +11 -15
  12. pixeltable/catalog/table.py +843 -436
  13. pixeltable/catalog/table_metadata.py +103 -0
  14. pixeltable/catalog/table_version.py +978 -657
  15. pixeltable/catalog/table_version_handle.py +72 -16
  16. pixeltable/catalog/table_version_path.py +112 -43
  17. pixeltable/catalog/tbl_ops.py +53 -0
  18. pixeltable/catalog/update_status.py +191 -0
  19. pixeltable/catalog/view.py +134 -90
  20. pixeltable/config.py +134 -22
  21. pixeltable/env.py +471 -157
  22. pixeltable/exceptions.py +6 -0
  23. pixeltable/exec/__init__.py +4 -1
  24. pixeltable/exec/aggregation_node.py +7 -8
  25. pixeltable/exec/cache_prefetch_node.py +83 -110
  26. pixeltable/exec/cell_materialization_node.py +268 -0
  27. pixeltable/exec/cell_reconstruction_node.py +168 -0
  28. pixeltable/exec/component_iteration_node.py +4 -3
  29. pixeltable/exec/data_row_batch.py +8 -65
  30. pixeltable/exec/exec_context.py +16 -4
  31. pixeltable/exec/exec_node.py +13 -36
  32. pixeltable/exec/expr_eval/evaluators.py +11 -7
  33. pixeltable/exec/expr_eval/expr_eval_node.py +27 -12
  34. pixeltable/exec/expr_eval/globals.py +8 -5
  35. pixeltable/exec/expr_eval/row_buffer.py +1 -2
  36. pixeltable/exec/expr_eval/schedulers.py +106 -56
  37. pixeltable/exec/globals.py +35 -0
  38. pixeltable/exec/in_memory_data_node.py +19 -19
  39. pixeltable/exec/object_store_save_node.py +293 -0
  40. pixeltable/exec/row_update_node.py +16 -9
  41. pixeltable/exec/sql_node.py +351 -84
  42. pixeltable/exprs/__init__.py +1 -1
  43. pixeltable/exprs/arithmetic_expr.py +27 -22
  44. pixeltable/exprs/array_slice.py +3 -3
  45. pixeltable/exprs/column_property_ref.py +36 -23
  46. pixeltable/exprs/column_ref.py +213 -89
  47. pixeltable/exprs/comparison.py +5 -5
  48. pixeltable/exprs/compound_predicate.py +5 -4
  49. pixeltable/exprs/data_row.py +164 -54
  50. pixeltable/exprs/expr.py +70 -44
  51. pixeltable/exprs/expr_dict.py +3 -3
  52. pixeltable/exprs/expr_set.py +17 -10
  53. pixeltable/exprs/function_call.py +100 -40
  54. pixeltable/exprs/globals.py +2 -2
  55. pixeltable/exprs/in_predicate.py +4 -4
  56. pixeltable/exprs/inline_expr.py +18 -32
  57. pixeltable/exprs/is_null.py +7 -3
  58. pixeltable/exprs/json_mapper.py +8 -8
  59. pixeltable/exprs/json_path.py +56 -22
  60. pixeltable/exprs/literal.py +27 -5
  61. pixeltable/exprs/method_ref.py +2 -2
  62. pixeltable/exprs/object_ref.py +2 -2
  63. pixeltable/exprs/row_builder.py +167 -67
  64. pixeltable/exprs/rowid_ref.py +25 -10
  65. pixeltable/exprs/similarity_expr.py +58 -40
  66. pixeltable/exprs/sql_element_cache.py +4 -4
  67. pixeltable/exprs/string_op.py +5 -5
  68. pixeltable/exprs/type_cast.py +3 -5
  69. pixeltable/func/__init__.py +1 -0
  70. pixeltable/func/aggregate_function.py +8 -8
  71. pixeltable/func/callable_function.py +9 -9
  72. pixeltable/func/expr_template_function.py +17 -11
  73. pixeltable/func/function.py +18 -20
  74. pixeltable/func/function_registry.py +6 -7
  75. pixeltable/func/globals.py +2 -3
  76. pixeltable/func/mcp.py +74 -0
  77. pixeltable/func/query_template_function.py +29 -27
  78. pixeltable/func/signature.py +46 -19
  79. pixeltable/func/tools.py +31 -13
  80. pixeltable/func/udf.py +18 -20
  81. pixeltable/functions/__init__.py +16 -0
  82. pixeltable/functions/anthropic.py +123 -77
  83. pixeltable/functions/audio.py +147 -10
  84. pixeltable/functions/bedrock.py +13 -6
  85. pixeltable/functions/date.py +7 -4
  86. pixeltable/functions/deepseek.py +35 -43
  87. pixeltable/functions/document.py +81 -0
  88. pixeltable/functions/fal.py +76 -0
  89. pixeltable/functions/fireworks.py +11 -20
  90. pixeltable/functions/gemini.py +195 -39
  91. pixeltable/functions/globals.py +142 -14
  92. pixeltable/functions/groq.py +108 -0
  93. pixeltable/functions/huggingface.py +1056 -24
  94. pixeltable/functions/image.py +115 -57
  95. pixeltable/functions/json.py +1 -1
  96. pixeltable/functions/llama_cpp.py +28 -13
  97. pixeltable/functions/math.py +67 -5
  98. pixeltable/functions/mistralai.py +18 -55
  99. pixeltable/functions/net.py +70 -0
  100. pixeltable/functions/ollama.py +20 -13
  101. pixeltable/functions/openai.py +240 -226
  102. pixeltable/functions/openrouter.py +143 -0
  103. pixeltable/functions/replicate.py +4 -4
  104. pixeltable/functions/reve.py +250 -0
  105. pixeltable/functions/string.py +239 -69
  106. pixeltable/functions/timestamp.py +16 -16
  107. pixeltable/functions/together.py +24 -84
  108. pixeltable/functions/twelvelabs.py +188 -0
  109. pixeltable/functions/util.py +6 -1
  110. pixeltable/functions/uuid.py +30 -0
  111. pixeltable/functions/video.py +1515 -107
  112. pixeltable/functions/vision.py +8 -8
  113. pixeltable/functions/voyageai.py +289 -0
  114. pixeltable/functions/whisper.py +16 -8
  115. pixeltable/functions/whisperx.py +179 -0
  116. pixeltable/{ext/functions → functions}/yolox.py +2 -4
  117. pixeltable/globals.py +362 -115
  118. pixeltable/index/base.py +17 -21
  119. pixeltable/index/btree.py +28 -22
  120. pixeltable/index/embedding_index.py +100 -118
  121. pixeltable/io/__init__.py +4 -2
  122. pixeltable/io/datarows.py +8 -7
  123. pixeltable/io/external_store.py +56 -105
  124. pixeltable/io/fiftyone.py +13 -13
  125. pixeltable/io/globals.py +31 -30
  126. pixeltable/io/hf_datasets.py +61 -16
  127. pixeltable/io/label_studio.py +74 -70
  128. pixeltable/io/lancedb.py +3 -0
  129. pixeltable/io/pandas.py +21 -12
  130. pixeltable/io/parquet.py +25 -105
  131. pixeltable/io/table_data_conduit.py +250 -123
  132. pixeltable/io/utils.py +4 -4
  133. pixeltable/iterators/__init__.py +2 -1
  134. pixeltable/iterators/audio.py +26 -25
  135. pixeltable/iterators/base.py +9 -3
  136. pixeltable/iterators/document.py +112 -78
  137. pixeltable/iterators/image.py +12 -15
  138. pixeltable/iterators/string.py +11 -4
  139. pixeltable/iterators/video.py +523 -120
  140. pixeltable/metadata/__init__.py +14 -3
  141. pixeltable/metadata/converters/convert_13.py +2 -2
  142. pixeltable/metadata/converters/convert_18.py +2 -2
  143. pixeltable/metadata/converters/convert_19.py +2 -2
  144. pixeltable/metadata/converters/convert_20.py +2 -2
  145. pixeltable/metadata/converters/convert_21.py +2 -2
  146. pixeltable/metadata/converters/convert_22.py +2 -2
  147. pixeltable/metadata/converters/convert_24.py +2 -2
  148. pixeltable/metadata/converters/convert_25.py +2 -2
  149. pixeltable/metadata/converters/convert_26.py +2 -2
  150. pixeltable/metadata/converters/convert_29.py +4 -4
  151. pixeltable/metadata/converters/convert_30.py +34 -21
  152. pixeltable/metadata/converters/convert_34.py +2 -2
  153. pixeltable/metadata/converters/convert_35.py +9 -0
  154. pixeltable/metadata/converters/convert_36.py +38 -0
  155. pixeltable/metadata/converters/convert_37.py +15 -0
  156. pixeltable/metadata/converters/convert_38.py +39 -0
  157. pixeltable/metadata/converters/convert_39.py +124 -0
  158. pixeltable/metadata/converters/convert_40.py +73 -0
  159. pixeltable/metadata/converters/convert_41.py +12 -0
  160. pixeltable/metadata/converters/convert_42.py +9 -0
  161. pixeltable/metadata/converters/convert_43.py +44 -0
  162. pixeltable/metadata/converters/util.py +20 -31
  163. pixeltable/metadata/notes.py +9 -0
  164. pixeltable/metadata/schema.py +140 -53
  165. pixeltable/metadata/utils.py +74 -0
  166. pixeltable/mypy/__init__.py +3 -0
  167. pixeltable/mypy/mypy_plugin.py +123 -0
  168. pixeltable/plan.py +382 -115
  169. pixeltable/share/__init__.py +1 -1
  170. pixeltable/share/packager.py +547 -83
  171. pixeltable/share/protocol/__init__.py +33 -0
  172. pixeltable/share/protocol/common.py +165 -0
  173. pixeltable/share/protocol/operation_types.py +33 -0
  174. pixeltable/share/protocol/replica.py +119 -0
  175. pixeltable/share/publish.py +257 -59
  176. pixeltable/store.py +311 -194
  177. pixeltable/type_system.py +373 -211
  178. pixeltable/utils/__init__.py +2 -3
  179. pixeltable/utils/arrow.py +131 -17
  180. pixeltable/utils/av.py +298 -0
  181. pixeltable/utils/azure_store.py +346 -0
  182. pixeltable/utils/coco.py +6 -6
  183. pixeltable/utils/code.py +3 -3
  184. pixeltable/utils/console_output.py +4 -1
  185. pixeltable/utils/coroutine.py +6 -23
  186. pixeltable/utils/dbms.py +32 -6
  187. pixeltable/utils/description_helper.py +4 -5
  188. pixeltable/utils/documents.py +7 -18
  189. pixeltable/utils/exception_handler.py +7 -30
  190. pixeltable/utils/filecache.py +6 -6
  191. pixeltable/utils/formatter.py +86 -48
  192. pixeltable/utils/gcs_store.py +295 -0
  193. pixeltable/utils/http.py +133 -0
  194. pixeltable/utils/http_server.py +2 -3
  195. pixeltable/utils/iceberg.py +1 -2
  196. pixeltable/utils/image.py +17 -0
  197. pixeltable/utils/lancedb.py +90 -0
  198. pixeltable/utils/local_store.py +322 -0
  199. pixeltable/utils/misc.py +5 -0
  200. pixeltable/utils/object_stores.py +573 -0
  201. pixeltable/utils/pydantic.py +60 -0
  202. pixeltable/utils/pytorch.py +5 -6
  203. pixeltable/utils/s3_store.py +527 -0
  204. pixeltable/utils/sql.py +26 -0
  205. pixeltable/utils/system.py +30 -0
  206. pixeltable-0.5.7.dist-info/METADATA +579 -0
  207. pixeltable-0.5.7.dist-info/RECORD +227 -0
  208. {pixeltable-0.3.14.dist-info → pixeltable-0.5.7.dist-info}/WHEEL +1 -1
  209. pixeltable-0.5.7.dist-info/entry_points.txt +2 -0
  210. pixeltable/__version__.py +0 -3
  211. pixeltable/catalog/named_function.py +0 -40
  212. pixeltable/ext/__init__.py +0 -17
  213. pixeltable/ext/functions/__init__.py +0 -11
  214. pixeltable/ext/functions/whisperx.py +0 -77
  215. pixeltable/utils/media_store.py +0 -77
  216. pixeltable/utils/s3.py +0 -17
  217. pixeltable-0.3.14.dist-info/METADATA +0 -434
  218. pixeltable-0.3.14.dist-info/RECORD +0 -186
  219. pixeltable-0.3.14.dist-info/entry_points.txt +0 -3
  220. {pixeltable-0.3.14.dist-info → pixeltable-0.5.7.dist-info/licenses}/LICENSE +0 -0
@@ -4,17 +4,21 @@ import asyncio
4
4
  import datetime
5
5
  import inspect
6
6
  import logging
7
+ import math
7
8
  import sys
8
9
  import time
9
- from typing import Awaitable, Collection, Optional
10
+ from typing import Awaitable, Collection
10
11
 
11
12
  from pixeltable import env, func
12
13
  from pixeltable.config import Config
14
+ from pixeltable.utils.http import exponential_backoff, is_retriable_error
13
15
 
14
16
  from .globals import Dispatcher, ExecCtx, FnCallArgs, Scheduler
15
17
 
16
18
  _logger = logging.getLogger('pixeltable')
17
19
 
20
+ __all__ = ['RateLimitsScheduler', 'RequestRateScheduler']
21
+
18
22
 
19
23
  class RateLimitsScheduler(Scheduler):
20
24
  """
@@ -34,7 +38,7 @@ class RateLimitsScheduler(Scheduler):
34
38
  get_request_resources_param_names: list[str] # names of parameters of RateLimitsInfo.get_request_resources()
35
39
 
36
40
  # scheduling-related state
37
- pool_info: Optional[env.RateLimitsInfo]
41
+ pool_info: env.RateLimitsInfo | None
38
42
  est_usage: dict[str, int] # value per resource; accumulated estimates since the last util. report
39
43
 
40
44
  num_in_flight: int # unfinished tasks
@@ -76,14 +80,15 @@ class RateLimitsScheduler(Scheduler):
76
80
  self.est_usage = dict.fromkeys(self._resources, 0)
77
81
 
78
82
  async def _main_loop(self) -> None:
79
- item: Optional[RateLimitsScheduler.QueueItem] = None
83
+ item: RateLimitsScheduler.QueueItem | None = None
80
84
  while True:
81
85
  if item is None:
82
86
  item = await self.queue.get()
87
+ assert isinstance(item.request.fn_call.fn, func.CallableFunction)
88
+ assert '_runtime_ctx' in item.request.fn_call.fn.signature.system_parameters
83
89
  if item.num_retries > 0:
84
90
  self.total_retried += 1
85
91
 
86
- now = datetime.datetime.now(tz=datetime.timezone.utc)
87
92
  if self.pool_info is None or not self.pool_info.is_initialized():
88
93
  # wait for a single request to get rate limits
89
94
  _logger.debug(f'initializing rate limits for {self.resource_pool}')
@@ -96,14 +101,13 @@ class RateLimitsScheduler(Scheduler):
96
101
  continue
97
102
 
98
103
  # check rate limits
99
- _logger.debug(f'checking rate limits for {self.resource_pool}')
100
104
  request_resources = self._get_request_resources(item.request)
101
- limits_info = self._check_resource_limits(request_resources)
105
+ resource_delay = self._resource_delay(request_resources)
102
106
  aws: list[Awaitable[None]] = []
103
- completed_aw: Optional[asyncio.Task] = None
104
- wait_for_reset: Optional[asyncio.Task] = None
105
- if limits_info is not None:
106
- # limits_info's resource is depleted, wait for capacity to free up
107
+ completed_aw: asyncio.Task | None = None
108
+ wait_for_reset: asyncio.Task | None = None
109
+ if resource_delay > 0:
110
+ # Some resource or resources are nearing depletion
107
111
 
108
112
  if self.num_in_flight > 0:
109
113
  # a completed request can free up capacity
@@ -112,12 +116,10 @@ class RateLimitsScheduler(Scheduler):
112
116
  aws.append(completed_aw)
113
117
  _logger.debug(f'waiting for completed request for {self.resource_pool}')
114
118
 
115
- reset_at = limits_info.reset_at
116
- if reset_at > now:
117
- # we're waiting for the rate limit to reset
118
- wait_for_reset = asyncio.create_task(asyncio.sleep((reset_at - now).total_seconds()))
119
- aws.append(wait_for_reset)
120
- _logger.debug(f'waiting for rate limit reset for {self.resource_pool}')
119
+ # Schedule a sleep until sufficient resources are available
120
+ wait_for_reset = asyncio.create_task(asyncio.sleep(resource_delay))
121
+ aws.append(wait_for_reset)
122
+ _logger.debug(f'waiting {resource_delay:.1f}s for resource availability')
121
123
 
122
124
  if len(aws) > 0:
123
125
  # we have something to wait for
@@ -126,16 +128,12 @@ class RateLimitsScheduler(Scheduler):
126
128
  task.cancel()
127
129
  if completed_aw in done:
128
130
  _logger.debug(f'wait(): completed request for {self.resource_pool}')
129
- if wait_for_reset in done:
130
- _logger.debug(f'wait(): rate limit reset for {self.resource_pool}')
131
- # force waiting for another rate limit report before making any scheduling decisions
132
- self.pool_info.reset()
133
131
  # re-evaluate current capacity for current item
134
132
  continue
135
133
 
136
134
  # we have a new in-flight request
137
135
  for resource, val in request_resources.items():
138
- self.est_usage[resource] += val
136
+ self.est_usage[resource] = self.est_usage.get(resource, 0) + val
139
137
  _logger.debug(f'creating task for {self.resource_pool}')
140
138
  self.num_in_flight += 1
141
139
  task = asyncio.create_task(self._exec(item.request, item.exec_ctx, item.num_retries, is_task=True))
@@ -155,25 +153,30 @@ class RateLimitsScheduler(Scheduler):
155
153
  constant_kwargs, batch_kwargs = request.pxt_fn.create_batch_kwargs(batch_kwargs)
156
154
  return self.pool_info.get_request_resources(**constant_kwargs, **batch_kwargs)
157
155
 
158
- def _check_resource_limits(self, request_resources: dict[str, int]) -> Optional[env.RateLimitInfo]:
159
- """Returns the most depleted resource, relative to its limit, or None if all resources are within limits"""
160
- candidates: list[tuple[env.RateLimitInfo, float]] = [] # (info, relative usage)
156
+ def _resource_delay(self, request_resources: dict[str, int]) -> float:
157
+ """For the provided resources and usage, attempts to estimate the time to wait until sufficient resources are
158
+ available."""
159
+ highest_wait = 0.0
160
+ highest_wait_resource = None
161
161
  for resource, usage in request_resources.items():
162
- # 0.05: leave some headroom, we don't have perfect information
163
162
  info = self.pool_info.resource_limits[resource]
164
- est_remaining = info.remaining - self.est_usage[resource] - usage
165
- if est_remaining < 0.05 * info.limit:
166
- candidates.append((info, est_remaining / info.limit))
167
- if len(candidates) == 0:
168
- return None
169
- return min(candidates, key=lambda x: x[1])[0]
163
+ # Note: usage and est_usage are estimated costs of requests, and it may be way off (for example, if max
164
+ # tokens is unspecified for an openAI request).
165
+ time_until = info.estimated_resource_refill_delay(
166
+ math.ceil(info.limit * env.TARGET_RATE_LIMIT_RESOURCE_FRACT + usage + self.est_usage.get(resource, 0))
167
+ )
168
+ if time_until is not None and highest_wait < time_until:
169
+ highest_wait = time_until
170
+ highest_wait_resource = resource
171
+ _logger.debug(f'Determined wait time of {highest_wait:.1f}s for resource {highest_wait_resource}')
172
+ return highest_wait
170
173
 
171
174
  async def _exec(self, request: FnCallArgs, exec_ctx: ExecCtx, num_retries: int, is_task: bool) -> None:
172
175
  assert all(not row.has_val[request.fn_call.slot_idx] for row in request.rows)
173
176
  assert all(not row.has_exc(request.fn_call.slot_idx) for row in request.rows)
174
177
 
178
+ start_ts = datetime.datetime.now(tz=datetime.timezone.utc)
175
179
  try:
176
- start_ts = datetime.datetime.now(tz=datetime.timezone.utc)
177
180
  pxt_fn = request.fn_call.fn
178
181
  assert isinstance(pxt_fn, func.CallableFunction)
179
182
  _logger.debug(
@@ -187,7 +190,8 @@ class RateLimitsScheduler(Scheduler):
187
190
  for row, result in zip(request.rows, batch_result):
188
191
  row[request.fn_call.slot_idx] = result
189
192
  else:
190
- result = await pxt_fn.aexec(*request.args, **request.kwargs)
193
+ request_kwargs = {**request.kwargs, '_runtime_ctx': env.RuntimeCtx(is_retry=num_retries > 0)}
194
+ result = await pxt_fn.aexec(*request.args, **request_kwargs)
191
195
  request.row[request.fn_call.slot_idx] = result
192
196
  end_ts = datetime.datetime.now(tz=datetime.timezone.utc)
193
197
  _logger.debug(
@@ -200,20 +204,32 @@ class RateLimitsScheduler(Scheduler):
200
204
 
201
205
  self.dispatcher.dispatch(request.rows, exec_ctx)
202
206
  except Exception as exc:
203
- _logger.debug(f'scheduler {self.resource_pool}: exception in slot {request.fn_call.slot_idx}: {exc}')
204
- if self.pool_info is None:
205
- # our pool info should be available at this point
206
- self._set_pool_info()
207
- assert self.pool_info is not None
208
- if num_retries < self.MAX_RETRIES:
209
- retry_delay = self.pool_info.get_retry_delay(exc)
210
- if retry_delay is not None:
211
- self.total_retried += 1
212
- _logger.debug(f'scheduler {self.resource_pool}: retrying in {retry_delay} seconds')
213
- await asyncio.sleep(retry_delay)
214
- self.queue.put_nowait(self.QueueItem(request, num_retries + 1, exec_ctx))
215
- return
216
- # TODO: update resource limits reported in exc.response.headers, if present
207
+ _logger.exception(f'scheduler {self.resource_pool}: exception in slot {request.fn_call.slot_idx}: {exc}')
208
+ if hasattr(exc, 'response') and hasattr(exc.response, 'headers'):
209
+ _logger.debug(f'scheduler {self.resource_pool}: exception headers: {exc.response.headers}')
210
+
211
+ # If pool info is available, attempt to retry based on the resource information
212
+ # Pool info may not be available yet if the exception occurred before the UDF set it
213
+ if self.pool_info is not None:
214
+ self.pool_info.record_exc(start_ts, exc)
215
+
216
+ if num_retries < self.MAX_RETRIES:
217
+ retry_delay = self.pool_info.get_retry_delay(exc, num_retries)
218
+ if retry_delay is None:
219
+ # The resource pool did not recognize it as a retriable error. Try our generic best-effort logic
220
+ # before giving up.
221
+ is_retriable, retry_delay = is_retriable_error(exc)
222
+ if is_retriable:
223
+ retry_delay = retry_delay or exponential_backoff(num_retries)
224
+ if retry_delay is not None:
225
+ self.total_retried += 1
226
+ _logger.debug(
227
+ f'scheduler {self.resource_pool}: sleeping {retry_delay:.2f}s before retrying'
228
+ f' attempt {num_retries} based on the information in the error'
229
+ )
230
+ await asyncio.sleep(retry_delay)
231
+ self.queue.put_nowait(self.QueueItem(request, num_retries + 1, exec_ctx))
232
+ return
217
233
 
218
234
  # record the exception
219
235
  _, _, exc_tb = sys.exc_info()
@@ -248,11 +264,16 @@ class RequestRateScheduler(Scheduler):
248
264
  num_in_flight: int
249
265
  total_requests: int
250
266
  total_retried: int
267
+ total_errors: int
251
268
 
252
269
  TIME_FORMAT = '%H:%M.%S %f'
253
- MAX_RETRIES = 10
270
+ MAX_RETRIES = 3
254
271
  DEFAULT_RATE_LIMIT = 600 # requests per minute
255
272
 
273
+ # Exponential backoff defaults
274
+ BASE_RETRY_DELAY = 1.0 # in seconds
275
+ MAX_RETRY_DELAY = 60.0 # in seconds
276
+
256
277
  def __init__(self, resource_pool: str, dispatcher: Dispatcher):
257
278
  super().__init__(resource_pool, dispatcher)
258
279
  loop_task = asyncio.create_task(self._main_loop())
@@ -260,6 +281,7 @@ class RequestRateScheduler(Scheduler):
260
281
  self.num_in_flight = 0
261
282
  self.total_requests = 0
262
283
  self.total_retried = 0
284
+ self.total_errors = 0
263
285
 
264
286
  # try to get the rate limit from the config
265
287
  elems = resource_pool.split(':')
@@ -278,6 +300,7 @@ class RequestRateScheduler(Scheduler):
278
300
  key = model
279
301
  requests_per_min = Config.get().get_int_value(key, section=section)
280
302
  requests_per_min = requests_per_min or self.DEFAULT_RATE_LIMIT
303
+ _logger.debug(f'rate limit for {self.resource_pool}: {requests_per_min} RPM')
281
304
  self.secs_per_request = 1 / (requests_per_min / 60)
282
305
 
283
306
  @classmethod
@@ -291,8 +314,12 @@ class RequestRateScheduler(Scheduler):
291
314
  if item.num_retries > 0:
292
315
  self.total_retried += 1
293
316
  now = time.monotonic()
317
+ wait_duration = 0.0
318
+ if item.retry_after is not None:
319
+ wait_duration = item.retry_after - now
294
320
  if now - last_request_ts < self.secs_per_request:
295
- wait_duration = self.secs_per_request - (now - last_request_ts)
321
+ wait_duration = max(wait_duration, self.secs_per_request - (now - last_request_ts))
322
+ if wait_duration > 0:
296
323
  _logger.debug(f'waiting for {wait_duration} for {self.resource_pool}')
297
324
  await asyncio.sleep(wait_duration)
298
325
 
@@ -337,15 +364,21 @@ class RequestRateScheduler(Scheduler):
337
364
  self.dispatcher.dispatch(request.rows, exec_ctx)
338
365
 
339
366
  except Exception as exc:
340
- # TODO: which exception can be retried?
341
- _logger.debug(f'exception for {self.resource_pool}: {exc}')
342
- status = getattr(exc, 'status', None)
343
- _logger.debug(f'type={type(exc)} has_status={hasattr(exc, "status")} status={status}')
344
- if num_retries < self.MAX_RETRIES:
345
- self.queue.put_nowait(self.QueueItem(request, num_retries + 1, exec_ctx))
367
+ _logger.exception(f'exception for {self.resource_pool}: type={type(exc)}\n{exc}')
368
+ if hasattr(exc, 'response') and hasattr(exc.response, 'headers'):
369
+ _logger.debug(f'scheduler {self.resource_pool}: exception headers: {exc.response.headers}')
370
+ is_retriable, retry_after = is_retriable_error(exc)
371
+ if is_retriable and num_retries < self.MAX_RETRIES:
372
+ retry_delay = self._compute_retry_delay(num_retries, retry_after)
373
+ _logger.debug(f'scheduler {self.resource_pool}: retrying after {retry_delay}')
374
+ now = time.monotonic()
375
+ # put the request back in the queue right away, which prevents new requests from being generated until
376
+ # this one succeeds or exceeds its retry limit
377
+ self.queue.put_nowait(self.QueueItem(request, num_retries + 1, exec_ctx, retry_after=now + retry_delay))
346
378
  return
347
379
 
348
380
  # record the exception
381
+ self.total_errors += 1
349
382
  _, _, exc_tb = sys.exc_info()
350
383
  for row in request.rows:
351
384
  row.set_exc(request.fn_call.slot_idx, exc)
@@ -353,11 +386,28 @@ class RequestRateScheduler(Scheduler):
353
386
  finally:
354
387
  _logger.debug(
355
388
  f'Scheduler stats: #in-flight={self.num_in_flight} #requests={self.total_requests}, '
356
- f'#retried={self.total_retried}'
389
+ f'#retried={self.total_retried} #errors={self.total_errors}'
357
390
  )
358
391
  if is_task:
359
392
  self.num_in_flight -= 1
360
393
 
394
+ def _compute_retry_delay(self, num_retries: int, retry_after: float | None = None) -> float:
395
+ """
396
+ Calculate exponential backoff delay for rate limit errors.
397
+
398
+ Args:
399
+ retry_count: Number of retries attempted (0-based)
400
+ retry_after: Suggested delay from Retry-After header
401
+
402
+ Returns:
403
+ Delay in seconds
404
+ """
405
+ if retry_after is not None and retry_after > 0:
406
+ # Use server-suggested delay, but cap it at max_delay
407
+ return max(min(retry_after, self.MAX_RETRY_DELAY), self.BASE_RETRY_DELAY)
408
+ else:
409
+ return exponential_backoff(num_retries, max_delay=self.MAX_RETRY_DELAY)
410
+
361
411
 
362
412
  # all concrete Scheduler subclasses that implement matches()
363
413
  SCHEDULERS = [RateLimitsScheduler, RequestRateScheduler]
@@ -0,0 +1,35 @@
1
+ from __future__ import annotations
2
+
3
+ import dataclasses
4
+
5
+ from pixeltable.exprs import ArrayMd, BinaryMd
6
+ from pixeltable.utils.misc import non_none_dict_factory
7
+
8
+ INLINED_OBJECT_MD_KEY = '__pxtinlinedobjmd__'
9
+
10
+
11
+ @dataclasses.dataclass
12
+ class InlinedObjectMd:
13
+ type: str # corresponds to ts.ColumnType.Type
14
+ url_idx: int
15
+ img_start: int | None = None
16
+ img_end: int | None = None
17
+ array_md: ArrayMd | None = None
18
+ binary_md: BinaryMd | None = None
19
+
20
+ @classmethod
21
+ def from_dict(cls, d: dict) -> InlinedObjectMd:
22
+ d = d.copy()
23
+ if 'array_md' in d:
24
+ d['array_md'] = ArrayMd(**d['array_md'])
25
+ if 'binary_md' in d:
26
+ d['binary_md'] = BinaryMd(**d['binary_md'])
27
+ return cls(**d)
28
+
29
+ def as_dict(self) -> dict:
30
+ result = dataclasses.asdict(self, dict_factory=non_none_dict_factory)
31
+ if self.array_md is not None:
32
+ result['array_md'] = self.array_md.as_dict()
33
+ if self.binary_md is not None:
34
+ result['binary_md'] = dataclasses.asdict(self.binary_md)
35
+ return result
@@ -1,8 +1,8 @@
1
1
  import logging
2
- from typing import Any, AsyncIterator, Optional
2
+ from typing import Any, AsyncIterator
3
3
 
4
4
  from pixeltable import catalog, exprs
5
- from pixeltable.utils.media_store import MediaStore
5
+ from pixeltable.utils.local_store import TempStore
6
6
 
7
7
  from .data_row_batch import DataRowBatch
8
8
  from .exec_node import ExecNode
@@ -23,7 +23,7 @@ class InMemoryDataNode(ExecNode):
23
23
 
24
24
  input_rows: list[dict[str, Any]]
25
25
  start_row_id: int
26
- output_rows: Optional[DataRowBatch]
26
+ output_batch: DataRowBatch | None
27
27
 
28
28
  # output_exprs is declared in the superclass, but we redeclare it here with a more specific type
29
29
  output_exprs: list[exprs.ColumnRef]
@@ -38,11 +38,11 @@ class InMemoryDataNode(ExecNode):
38
38
  # we materialize the input slots
39
39
  output_exprs = list(row_builder.input_exprs)
40
40
  super().__init__(row_builder, output_exprs, [], None)
41
- assert tbl.get().is_insertable()
41
+ assert tbl.get().is_insertable
42
42
  self.tbl = tbl
43
43
  self.input_rows = rows
44
44
  self.start_row_id = start_row_id
45
- self.output_rows = None
45
+ self.output_batch = None
46
46
 
47
47
  def _open(self) -> None:
48
48
  """Create row batch and populate with self.input_rows"""
@@ -56,22 +56,21 @@ class InMemoryDataNode(ExecNode):
56
56
  }
57
57
  output_slot_idxs = {e.slot_idx for e in self.output_exprs}
58
58
 
59
- self.output_rows = DataRowBatch(self.tbl, self.row_builder, len(self.input_rows))
60
- for row_idx, input_row in enumerate(self.input_rows):
59
+ self.output_batch = DataRowBatch(self.row_builder)
60
+ for input_row in self.input_rows:
61
+ output_row = self.row_builder.make_row()
61
62
  # populate the output row with the values provided in the input row
62
63
  input_slot_idxs: set[int] = set()
63
64
  for col_name, val in input_row.items():
64
65
  col_info = user_cols_by_name.get(col_name)
65
66
  assert col_info is not None
66
-
67
- if col_info.col.col_type.is_image_type() and isinstance(val, bytes):
68
- # this is a literal image, ie, a sequence of bytes; we save this as a media file and store the path
69
- path = str(MediaStore.prepare_media_path(self.tbl.id, col_info.col.id, self.tbl.get().version))
70
- with open(path, 'wb') as fp:
71
- fp.write(val)
72
- self.output_rows[row_idx][col_info.slot_idx] = path
67
+ col = col_info.col
68
+ if col.col_type.is_image_type() and isinstance(val, bytes):
69
+ # this is a literal media file, ie, a sequence of bytes; save it as a binary file and store the path
70
+ filepath, _ = TempStore.save_media_object(val, col, format=None)
71
+ output_row[col_info.slot_idx] = str(filepath)
73
72
  else:
74
- self.output_rows[row_idx][col_info.slot_idx] = val
73
+ output_row[col_info.slot_idx] = val
75
74
 
76
75
  input_slot_idxs.add(col_info.slot_idx)
77
76
 
@@ -80,10 +79,11 @@ class InMemoryDataNode(ExecNode):
80
79
  for slot_idx in missing_slot_idxs:
81
80
  col_info = output_cols_by_idx.get(slot_idx)
82
81
  assert col_info is not None
83
- self.output_rows[row_idx][col_info.slot_idx] = None
82
+ output_row[col_info.slot_idx] = None
83
+ self.output_batch.add_row(output_row)
84
84
 
85
- self.ctx.num_rows = len(self.output_rows)
85
+ self.ctx.num_rows = len(self.output_batch)
86
86
 
87
87
  async def __aiter__(self) -> AsyncIterator[DataRowBatch]:
88
- _logger.debug(f'InMemoryDataNode: created row batch with {len(self.output_rows)} output_rows')
89
- yield self.output_rows
88
+ _logger.debug(f'InMemoryDataNode: created row batch with {len(self.output_batch)} rows')
89
+ yield self.output_batch