tilebox-workflows 0.43.0__tar.gz → 0.45.0__tar.gz

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 (50) hide show
  1. {tilebox_workflows-0.43.0 → tilebox_workflows-0.45.0}/PKG-INFO +3 -1
  2. {tilebox_workflows-0.43.0 → tilebox_workflows-0.45.0}/pyproject.toml +2 -0
  3. {tilebox_workflows-0.43.0 → tilebox_workflows-0.45.0}/tilebox/workflows/__init__.py +2 -1
  4. {tilebox_workflows-0.43.0 → tilebox_workflows-0.45.0}/tilebox/workflows/automations/client.py +3 -3
  5. {tilebox_workflows-0.43.0 → tilebox_workflows-0.45.0}/tilebox/workflows/data.py +200 -46
  6. tilebox_workflows-0.45.0/tilebox/workflows/formatting/job.py +402 -0
  7. {tilebox_workflows-0.43.0 → tilebox_workflows-0.45.0}/tilebox/workflows/jobs/client.py +41 -15
  8. {tilebox_workflows-0.43.0 → tilebox_workflows-0.45.0}/tilebox/workflows/jobs/service.py +15 -6
  9. tilebox_workflows-0.45.0/tilebox/workflows/runner/__init__.py +0 -0
  10. {tilebox_workflows-0.43.0 → tilebox_workflows-0.45.0}/tilebox/workflows/runner/task_runner.py +54 -23
  11. {tilebox_workflows-0.43.0 → tilebox_workflows-0.45.0}/tilebox/workflows/runner/task_service.py +4 -2
  12. {tilebox_workflows-0.43.0 → tilebox_workflows-0.45.0}/tilebox/workflows/task.py +95 -14
  13. {tilebox_workflows-0.43.0 → tilebox_workflows-0.45.0}/tilebox/workflows/workflows/v1/automation_pb2.py +22 -22
  14. {tilebox_workflows-0.43.0 → tilebox_workflows-0.45.0}/tilebox/workflows/workflows/v1/automation_pb2.pyi +2 -2
  15. tilebox_workflows-0.45.0/tilebox/workflows/workflows/v1/core_pb2.py +103 -0
  16. {tilebox_workflows-0.43.0 → tilebox_workflows-0.45.0}/tilebox/workflows/workflows/v1/core_pb2.pyi +89 -16
  17. tilebox_workflows-0.45.0/tilebox/workflows/workflows/v1/job_pb2.py +104 -0
  18. {tilebox_workflows-0.43.0 → tilebox_workflows-0.45.0}/tilebox/workflows/workflows/v1/job_pb2.pyi +17 -17
  19. {tilebox_workflows-0.43.0 → tilebox_workflows-0.45.0}/tilebox/workflows/workflows/v1/job_pb2_grpc.py +43 -0
  20. {tilebox_workflows-0.43.0 → tilebox_workflows-0.45.0}/tilebox/workflows/workflows/v1/task_pb2.py +16 -16
  21. {tilebox_workflows-0.43.0 → tilebox_workflows-0.45.0}/tilebox/workflows/workflows/v1/task_pb2.pyi +8 -6
  22. tilebox_workflows-0.43.0/tilebox/workflows/workflows/v1/core_pb2.py +0 -79
  23. tilebox_workflows-0.43.0/tilebox/workflows/workflows/v1/job_pb2.py +0 -106
  24. {tilebox_workflows-0.43.0 → tilebox_workflows-0.45.0}/.gitignore +0 -0
  25. {tilebox_workflows-0.43.0 → tilebox_workflows-0.45.0}/README.md +0 -0
  26. {tilebox_workflows-0.43.0 → tilebox_workflows-0.45.0}/tilebox/workflows/automations/__init__.py +0 -0
  27. {tilebox_workflows-0.43.0 → tilebox_workflows-0.45.0}/tilebox/workflows/automations/cron.py +0 -0
  28. {tilebox_workflows-0.43.0 → tilebox_workflows-0.45.0}/tilebox/workflows/automations/service.py +0 -0
  29. {tilebox_workflows-0.43.0 → tilebox_workflows-0.45.0}/tilebox/workflows/automations/storage_event.py +0 -0
  30. {tilebox_workflows-0.43.0 → tilebox_workflows-0.45.0}/tilebox/workflows/cache.py +0 -0
  31. {tilebox_workflows-0.43.0 → tilebox_workflows-0.45.0}/tilebox/workflows/client.py +0 -0
  32. {tilebox_workflows-0.43.0 → tilebox_workflows-0.45.0}/tilebox/workflows/clusters/__init__.py +0 -0
  33. {tilebox_workflows-0.43.0 → tilebox_workflows-0.45.0}/tilebox/workflows/clusters/client.py +0 -0
  34. {tilebox_workflows-0.43.0 → tilebox_workflows-0.45.0}/tilebox/workflows/clusters/service.py +0 -0
  35. {tilebox_workflows-0.43.0/tilebox/workflows/jobs → tilebox_workflows-0.45.0/tilebox/workflows/formatting}/__init__.py +0 -0
  36. {tilebox_workflows-0.43.0 → tilebox_workflows-0.45.0}/tilebox/workflows/interceptors.py +0 -0
  37. {tilebox_workflows-0.43.0/tilebox/workflows/observability → tilebox_workflows-0.45.0/tilebox/workflows/jobs}/__init__.py +0 -0
  38. {tilebox_workflows-0.43.0/tilebox/workflows/runner → tilebox_workflows-0.45.0/tilebox/workflows/observability}/__init__.py +0 -0
  39. {tilebox_workflows-0.43.0 → tilebox_workflows-0.45.0}/tilebox/workflows/observability/logging.py +0 -0
  40. {tilebox_workflows-0.43.0 → tilebox_workflows-0.45.0}/tilebox/workflows/observability/tracing.py +0 -0
  41. {tilebox_workflows-0.43.0 → tilebox_workflows-0.45.0}/tilebox/workflows/timeseries.py +0 -0
  42. {tilebox_workflows-0.43.0 → tilebox_workflows-0.45.0}/tilebox/workflows/workflows/v1/automation_pb2_grpc.py +0 -0
  43. {tilebox_workflows-0.43.0 → tilebox_workflows-0.45.0}/tilebox/workflows/workflows/v1/core_pb2_grpc.py +0 -0
  44. {tilebox_workflows-0.43.0 → tilebox_workflows-0.45.0}/tilebox/workflows/workflows/v1/diagram_pb2.py +0 -0
  45. {tilebox_workflows-0.43.0 → tilebox_workflows-0.45.0}/tilebox/workflows/workflows/v1/diagram_pb2.pyi +0 -0
  46. {tilebox_workflows-0.43.0 → tilebox_workflows-0.45.0}/tilebox/workflows/workflows/v1/diagram_pb2_grpc.py +0 -0
  47. {tilebox_workflows-0.43.0 → tilebox_workflows-0.45.0}/tilebox/workflows/workflows/v1/task_pb2_grpc.py +0 -0
  48. {tilebox_workflows-0.43.0 → tilebox_workflows-0.45.0}/tilebox/workflows/workflows/v1/workflows_pb2.py +0 -0
  49. {tilebox_workflows-0.43.0 → tilebox_workflows-0.45.0}/tilebox/workflows/workflows/v1/workflows_pb2.pyi +0 -0
  50. {tilebox_workflows-0.43.0 → tilebox_workflows-0.45.0}/tilebox/workflows/workflows/v1/workflows_pb2_grpc.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tilebox-workflows
3
- Version: 0.43.0
3
+ Version: 0.45.0
4
4
  Summary: Workflow client and task runner for Tilebox
5
5
  Project-URL: Homepage, https://tilebox.com
6
6
  Project-URL: Documentation, https://docs.tilebox.com/workflows/introduction
@@ -22,9 +22,11 @@ Requires-Python: >=3.10
22
22
  Requires-Dist: boto3-stubs[essential]>=1.33
23
23
  Requires-Dist: boto3>=1.33
24
24
  Requires-Dist: google-cloud-storage>=2.10
25
+ Requires-Dist: ipywidgets>=8.1.7
25
26
  Requires-Dist: opentelemetry-api>=1.28
26
27
  Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.28
27
28
  Requires-Dist: opentelemetry-sdk>=1.28
29
+ Requires-Dist: python-dateutil>=2.9.0.post0
28
30
  Requires-Dist: tenacity>=8
29
31
  Requires-Dist: tilebox-datasets
30
32
  Requires-Dist: tilebox-grpc>=0.28.0
@@ -30,6 +30,8 @@ dependencies = [
30
30
  "tenacity>=8",
31
31
  "boto3>=1.33",
32
32
  "boto3-stubs[essential]>=1.33",
33
+ "ipywidgets>=8.1.7",
34
+ "python-dateutil>=2.9.0.post0",
33
35
  ]
34
36
 
35
37
  [dependency-groups]
@@ -4,9 +4,10 @@ import sys
4
4
  from loguru import logger
5
5
 
6
6
  from tilebox.workflows.client import Client
7
+ from tilebox.workflows.data import Job
7
8
  from tilebox.workflows.task import ExecutionContext, Task
8
9
 
9
- __all__ = ["Client", "ExecutionContext", "Task"]
10
+ __all__ = ["Client", "ExecutionContext", "Job", "Task"]
10
11
 
11
12
 
12
13
  def _init_logging(level: str = "INFO") -> None:
@@ -7,9 +7,9 @@ from tilebox.workflows.clusters.client import ClusterSlugLike, to_cluster_slug
7
7
  from tilebox.workflows.data import (
8
8
  AutomationPrototype,
9
9
  CronTrigger,
10
+ SingleTaskSubmission,
10
11
  StorageEventTrigger,
11
12
  StorageLocation,
12
- TaskSubmission,
13
13
  )
14
14
  from tilebox.workflows.task import _task_meta
15
15
 
@@ -76,7 +76,7 @@ class AutomationClient:
76
76
  automation = AutomationPrototype(
77
77
  id=UUID(int=0),
78
78
  name=name,
79
- prototype=TaskSubmission(
79
+ prototype=SingleTaskSubmission(
80
80
  cluster_slug=to_cluster_slug(cluster or ""),
81
81
  identifier=_task_meta(task).identifier,
82
82
  input=task._serialize_args(), # noqa: SLF001
@@ -118,7 +118,7 @@ class AutomationClient:
118
118
  automation = AutomationPrototype(
119
119
  id=UUID(int=0),
120
120
  name=name,
121
- prototype=TaskSubmission(
121
+ prototype=SingleTaskSubmission(
122
122
  cluster_slug=to_cluster_slug(cluster or ""),
123
123
  identifier=_task_meta(task).identifier,
124
124
  input=task._serialize_args(), # noqa: SLF001
@@ -1,10 +1,12 @@
1
1
  import re
2
2
  import warnings
3
+ from collections.abc import Callable
3
4
  from dataclasses import dataclass, field
4
5
  from datetime import datetime, timedelta
5
6
  from enum import Enum
6
7
  from functools import lru_cache
7
8
  from pathlib import Path
9
+ from typing import Any, cast
8
10
  from uuid import UUID
9
11
 
10
12
  import boto3
@@ -21,7 +23,7 @@ from tilebox.datasets.query.time_interval import (
21
23
  timedelta_to_duration,
22
24
  timestamp_to_datetime,
23
25
  )
24
- from tilebox.datasets.uuid import uuid_message_to_optional_uuid, uuid_message_to_uuid, uuid_to_uuid_message
26
+ from tilebox.datasets.uuid import must_uuid_to_uuid_message, uuid_message_to_uuid, uuid_to_uuid_message
25
27
 
26
28
  try:
27
29
  # let's not make this a hard dependency, but if it's installed we can use its types
@@ -108,19 +110,19 @@ class TaskLease:
108
110
 
109
111
 
110
112
  @dataclass(order=True)
111
- class ProgressBar:
113
+ class ProgressIndicator:
112
114
  label: str | None
113
115
  total: int
114
116
  done: int
115
117
 
116
118
  @classmethod
117
- def from_message(cls, progress_bar: core_pb2.ProgressBar) -> "ProgressBar":
118
- """Convert a ProgressBar protobuf message to a ProgressBar object."""
119
- return cls(label=progress_bar.label or None, total=progress_bar.total, done=progress_bar.done)
119
+ def from_message(cls, progress_indicator: core_pb2.Progress) -> "ProgressIndicator":
120
+ """Convert a ProgressIndicator protobuf message to a ProgressIndicator object."""
121
+ return cls(label=progress_indicator.label or None, total=progress_indicator.total, done=progress_indicator.done)
120
122
 
121
- def to_message(self) -> core_pb2.ProgressBar:
122
- """Convert a ProgressBar object to a ProgressBar protobuf message."""
123
- return core_pb2.ProgressBar(label=self.label, total=self.total, done=self.done)
123
+ def to_message(self) -> core_pb2.Progress:
124
+ """Convert a ProgressIndicator object to a ProgressIndicator protobuf message."""
125
+ return core_pb2.Progress(label=self.label, total=self.total, done=self.done)
124
126
 
125
127
 
126
128
  @dataclass(order=True)
@@ -160,7 +162,7 @@ class Task:
160
162
  return core_pb2.Task(
161
163
  id=uuid_to_uuid_message(self.id),
162
164
  identifier=self.identifier.to_message(),
163
- state=f"TASK_STATE_{self.state.name}",
165
+ state=cast(core_pb2.TaskState, self.state.value),
164
166
  input=self.input,
165
167
  display=self.display,
166
168
  job=self.job.to_message() if self.job else None,
@@ -187,27 +189,81 @@ class Idling:
187
189
 
188
190
  class JobState(Enum):
189
191
  UNSPECIFIED = 0
190
- QUEUED = 1
191
- STARTED = 2
192
- COMPLETED = 3
192
+ SUBMITTED = 1
193
+ RUNNING = 2
194
+ STARTED = 3
195
+ COMPLETED = 4
196
+ FAILED = 5
197
+ CANCELED = 6
193
198
 
194
199
 
195
200
  _JOB_STATES = {state.value: state for state in JobState}
196
201
 
202
+ # JobState.QUEUED is deprecated and has been renamed to SUBMITTED, but we keep it around for backwards compatibility
203
+ JobState.QUEUED = JobState.SUBMITTED # type: ignore[assignment]
197
204
 
198
- @dataclass(order=True)
205
+
206
+ @dataclass(order=True, frozen=True)
207
+ class ExecutionStats:
208
+ first_task_started_at: datetime | None
209
+ last_task_stopped_at: datetime | None
210
+ compute_time: timedelta
211
+ elapsed_time: timedelta
212
+ parallelism: float
213
+ total_tasks: int
214
+ tasks_by_state: dict[TaskState, int]
215
+
216
+ @classmethod
217
+ def from_message(cls, execution_stats: core_pb2.ExecutionStats) -> "ExecutionStats":
218
+ """Convert a ExecutionStats protobuf message to a ExecutionStats object."""
219
+ return cls(
220
+ first_task_started_at=timestamp_to_datetime(execution_stats.first_task_started_at)
221
+ if execution_stats.HasField("first_task_started_at")
222
+ else None,
223
+ last_task_stopped_at=timestamp_to_datetime(execution_stats.last_task_stopped_at)
224
+ if execution_stats.HasField("last_task_stopped_at")
225
+ else None,
226
+ compute_time=duration_to_timedelta(execution_stats.compute_time),
227
+ elapsed_time=duration_to_timedelta(execution_stats.elapsed_time),
228
+ parallelism=execution_stats.parallelism,
229
+ total_tasks=execution_stats.total_tasks,
230
+ tasks_by_state={_TASK_STATES[state.state]: state.count for state in execution_stats.tasks_by_state},
231
+ )
232
+
233
+ def to_message(self) -> core_pb2.ExecutionStats:
234
+ """Convert a ExecutionStats object to a ExecutionStats protobuf message."""
235
+ return core_pb2.ExecutionStats(
236
+ first_task_started_at=datetime_to_timestamp(self.first_task_started_at)
237
+ if self.first_task_started_at
238
+ else None,
239
+ last_task_stopped_at=datetime_to_timestamp(self.last_task_stopped_at)
240
+ if self.last_task_stopped_at
241
+ else None,
242
+ compute_time=timedelta_to_duration(self.compute_time),
243
+ elapsed_time=timedelta_to_duration(self.elapsed_time),
244
+ parallelism=self.parallelism,
245
+ total_tasks=self.total_tasks,
246
+ tasks_by_state=[
247
+ core_pb2.TaskStateCount(state=cast(core_pb2.TaskState, state.value), count=count)
248
+ for state, count in self.tasks_by_state.items()
249
+ ],
250
+ )
251
+
252
+
253
+ @dataclass(order=True, frozen=True)
199
254
  class Job:
200
255
  id: UUID
201
256
  name: str
202
257
  trace_parent: str
203
258
  state: JobState
204
259
  submitted_at: datetime
205
- started_at: datetime | None
206
- canceled: bool
207
- progress_bars: list[ProgressBar]
260
+ progress: list[ProgressIndicator]
261
+ execution_stats: ExecutionStats
208
262
 
209
263
  @classmethod
210
- def from_message(cls, job: core_pb2.Job) -> "Job": # lets use typing.Self once we require python >= 3.11
264
+ def from_message(
265
+ cls, job: core_pb2.Job, **extra_kwargs: Any
266
+ ) -> "Job": # lets use typing.Self once we require python >= 3.11
211
267
  """Convert a Job protobuf message to a Job object."""
212
268
  return cls(
213
269
  id=uuid_message_to_uuid(job.id),
@@ -215,9 +271,9 @@ class Job:
215
271
  trace_parent=job.trace_parent,
216
272
  state=_JOB_STATES[job.state],
217
273
  submitted_at=timestamp_to_datetime(job.submitted_at),
218
- started_at=timestamp_to_datetime(job.started_at) if job.HasField("started_at") else None,
219
- canceled=job.canceled,
220
- progress_bars=[ProgressBar.from_message(progress_bar) for progress_bar in job.progress_bars],
274
+ progress=[ProgressIndicator.from_message(progress) for progress in job.progress],
275
+ execution_stats=ExecutionStats.from_message(job.execution_stats),
276
+ **extra_kwargs,
221
277
  )
222
278
 
223
279
  def to_message(self) -> core_pb2.Job:
@@ -226,12 +282,32 @@ class Job:
226
282
  id=uuid_to_uuid_message(self.id),
227
283
  name=self.name,
228
284
  trace_parent=self.trace_parent,
229
- state=f"JOB_STATE_{self.state.name}",
285
+ state=cast(core_pb2.JobState, self.state.value),
230
286
  submitted_at=datetime_to_timestamp(self.submitted_at),
231
- started_at=datetime_to_timestamp(self.started_at) if self.started_at else None,
232
- canceled=self.canceled,
233
- progress_bars=[progress_bar.to_message() for progress_bar in self.progress_bars],
287
+ progress=[progress.to_message() for progress in self.progress],
288
+ execution_stats=self.execution_stats.to_message(),
289
+ )
290
+
291
+ @property
292
+ def canceled(self) -> bool:
293
+ warnings.warn(
294
+ "The canceled property on a job has been deprecated, and will be removed in a future version. "
295
+ "Use job.state == JobState.CANCELED, or job.state == JobState.FAILED instead.",
296
+ DeprecationWarning,
297
+ stacklevel=2,
298
+ )
299
+ # the deprecated canceled property was also true for failed jobs, so we keep that behavior
300
+ return self.state in (JobState.CANCELED, JobState.FAILED)
301
+
302
+ @property
303
+ def started_at(self) -> datetime | None:
304
+ warnings.warn(
305
+ "The started_at property on a job has been deprecated, use `job.execution_stats.first_task_started_at` "
306
+ "instead.",
307
+ DeprecationWarning,
308
+ stacklevel=2,
234
309
  )
310
+ return self.execution_stats.first_task_started_at
235
311
 
236
312
 
237
313
  @dataclass(order=True)
@@ -266,7 +342,69 @@ class NextTaskToRun:
266
342
 
267
343
 
268
344
  @dataclass
269
- class TaskSubmission:
345
+ class TaskSubmissionGroup:
346
+ dependencies_on_other_groups: list[int]
347
+ inputs: list[bytes] = field(default_factory=list)
348
+ identifier_pointers: list[int] = field(default_factory=list)
349
+ cluster_slug_pointers: list[int] = field(default_factory=list)
350
+ display_pointers: list[int] = field(default_factory=list)
351
+ max_retries_values: list[int] = field(default_factory=list)
352
+
353
+ @classmethod
354
+ def from_message(cls, group: core_pb2.TaskSubmissionGroup) -> "TaskSubmissionGroup":
355
+ """Convert a TaskSubmissionGroup protobuf message to a TaskSubmissionGroup object."""
356
+ return cls(
357
+ dependencies_on_other_groups=list(group.dependencies_on_other_groups),
358
+ inputs=list(group.inputs),
359
+ identifier_pointers=list(group.identifier_pointers),
360
+ cluster_slug_pointers=list(group.cluster_slug_pointers),
361
+ display_pointers=list(group.display_pointers),
362
+ max_retries_values=list(group.max_retries_values),
363
+ )
364
+
365
+ def to_message(self) -> core_pb2.TaskSubmissionGroup:
366
+ """Convert a TaskSubmissionGroup object to a TaskSubmissionGroup protobuf message."""
367
+ return core_pb2.TaskSubmissionGroup(
368
+ dependencies_on_other_groups=self.dependencies_on_other_groups,
369
+ inputs=self.inputs,
370
+ identifier_pointers=self.identifier_pointers,
371
+ cluster_slug_pointers=self.cluster_slug_pointers,
372
+ display_pointers=self.display_pointers,
373
+ max_retries_values=self.max_retries_values,
374
+ )
375
+
376
+
377
+ @dataclass
378
+ class TaskSubmissions:
379
+ task_groups: list[TaskSubmissionGroup]
380
+ cluster_slug_lookup: list[str]
381
+ identifier_lookup: list[TaskIdentifier]
382
+ display_lookup: list[str]
383
+
384
+ @classmethod
385
+ def from_message(cls, sub_task: core_pb2.TaskSubmissions) -> "TaskSubmissions":
386
+ """Convert a TaskSubmission protobuf message to a TaskSubmission object."""
387
+ return cls(
388
+ task_groups=[TaskSubmissionGroup.from_message(group) for group in sub_task.task_groups],
389
+ cluster_slug_lookup=list(sub_task.cluster_slug_lookup),
390
+ identifier_lookup=[TaskIdentifier.from_message(identifier) for identifier in sub_task.identifier_lookup],
391
+ display_lookup=list(sub_task.display_lookup),
392
+ )
393
+
394
+ def to_message(self) -> core_pb2.TaskSubmissions:
395
+ """Convert a TaskSubmissions object to a TaskSubmissions protobuf message."""
396
+ return core_pb2.TaskSubmissions(
397
+ task_groups=[group.to_message() for group in self.task_groups],
398
+ cluster_slug_lookup=self.cluster_slug_lookup,
399
+ identifier_lookup=[identifier.to_message() for identifier in self.identifier_lookup],
400
+ display_lookup=self.display_lookup,
401
+ )
402
+
403
+
404
+ @dataclass
405
+ class SingleTaskSubmission:
406
+ """A submission of a single task. Used for automations."""
407
+
270
408
  cluster_slug: str
271
409
  identifier: TaskIdentifier
272
410
  input: bytes
@@ -275,8 +413,8 @@ class TaskSubmission:
275
413
  max_retries: int = 0
276
414
 
277
415
  @classmethod
278
- def from_message(cls, sub_task: core_pb2.TaskSubmission) -> "TaskSubmission":
279
- """Convert a TaskSubmission protobuf message to a TaskSubmission object."""
416
+ def from_message(cls, sub_task: core_pb2.SingleTaskSubmission) -> "SingleTaskSubmission":
417
+ """Convert a TaskSubmission protobuf message to a SingleTaskSubmission object."""
280
418
  return cls(
281
419
  cluster_slug=sub_task.cluster_slug,
282
420
  identifier=TaskIdentifier.from_message(sub_task.identifier),
@@ -286,9 +424,9 @@ class TaskSubmission:
286
424
  max_retries=sub_task.max_retries,
287
425
  )
288
426
 
289
- def to_message(self) -> core_pb2.TaskSubmission:
290
- """Convert a TaskSubmission object to a TaskSubmission protobuf message."""
291
- return core_pb2.TaskSubmission(
427
+ def to_message(self) -> core_pb2.SingleTaskSubmission:
428
+ """Convert a SingleTaskSubmission object to a TaskSubmission protobuf message."""
429
+ return core_pb2.SingleTaskSubmission(
292
430
  cluster_slug=self.cluster_slug,
293
431
  identifier=self.identifier.to_message(),
294
432
  input=self.input,
@@ -302,8 +440,8 @@ class TaskSubmission:
302
440
  class ComputedTask:
303
441
  id: UUID
304
442
  display: str | None
305
- sub_tasks: list[TaskSubmission]
306
- progress_updates: list[ProgressBar]
443
+ sub_tasks: TaskSubmissions | None
444
+ progress_updates: list[ProgressIndicator]
307
445
 
308
446
  @classmethod
309
447
  def from_message(cls, computed_task: task_pb2.ComputedTask) -> "ComputedTask":
@@ -311,8 +449,10 @@ class ComputedTask:
311
449
  return cls(
312
450
  id=uuid_message_to_uuid(computed_task.id),
313
451
  display=computed_task.display,
314
- sub_tasks=[TaskSubmission.from_message(sub_task) for sub_task in computed_task.sub_tasks],
315
- progress_updates=[ProgressBar.from_message(progress) for progress in computed_task.progress_updates],
452
+ sub_tasks=TaskSubmissions.from_message(computed_task.sub_tasks)
453
+ if computed_task.HasField("sub_tasks")
454
+ else None,
455
+ progress_updates=[ProgressIndicator.from_message(progress) for progress in computed_task.progress_updates],
316
456
  )
317
457
 
318
458
  def to_message(self) -> task_pb2.ComputedTask:
@@ -320,7 +460,7 @@ class ComputedTask:
320
460
  return task_pb2.ComputedTask(
321
461
  id=uuid_to_uuid_message(self.id),
322
462
  display=self.display,
323
- sub_tasks=[sub_task.to_message() for sub_task in self.sub_tasks],
463
+ sub_tasks=self.sub_tasks.to_message() if self.sub_tasks else None,
324
464
  progress_updates=[progress.to_message() for progress in self.progress_updates],
325
465
  )
326
466
 
@@ -500,7 +640,7 @@ class TriggeredCronEvent:
500
640
  class AutomationPrototype:
501
641
  id: UUID
502
642
  name: str
503
- prototype: TaskSubmission
643
+ prototype: SingleTaskSubmission
504
644
  storage_event_triggers: list[StorageEventTrigger]
505
645
  cron_triggers: list[CronTrigger]
506
646
 
@@ -510,7 +650,7 @@ class AutomationPrototype:
510
650
  return cls(
511
651
  id=uuid_message_to_uuid(task.id),
512
652
  name=task.name,
513
- prototype=TaskSubmission.from_message(task.prototype),
653
+ prototype=SingleTaskSubmission.from_message(task.prototype),
514
654
  storage_event_triggers=[
515
655
  StorageEventTrigger.from_message(trigger) for trigger in task.storage_event_triggers
516
656
  ],
@@ -571,9 +711,13 @@ class QueryJobsResponse:
571
711
  next_page: Pagination
572
712
 
573
713
  @classmethod
574
- def from_message(cls, page: job_pb2.QueryJobsResponse) -> "QueryJobsResponse":
714
+ def from_message(
715
+ cls,
716
+ page: job_pb2.QueryJobsResponse,
717
+ job_factory: Callable[[core_pb2.Job], Job] = Job.from_message,
718
+ ) -> "QueryJobsResponse":
575
719
  return cls(
576
- jobs=[Job.from_message(job) for job in page.jobs],
720
+ jobs=[job_factory(job) for job in page.jobs],
577
721
  next_page=Pagination.from_message(page.next_page),
578
722
  )
579
723
 
@@ -586,21 +730,31 @@ class QueryJobsResponse:
586
730
 
587
731
  @dataclass(frozen=True)
588
732
  class QueryFilters:
589
- time_interval: TimeInterval | None = None
590
- id_interval: IDInterval | None = None
591
- automation_id: UUID | None = None
733
+ time_interval: TimeInterval | None
734
+ id_interval: IDInterval | None
735
+ automation_ids: list[UUID]
736
+ job_states: list[JobState]
737
+ name: str | None
592
738
 
593
739
  @classmethod
594
740
  def from_message(cls, filters: job_pb2.QueryFilters) -> "QueryFilters":
595
741
  return cls(
596
- time_interval=TimeInterval.from_message(filters.time_interval),
597
- id_interval=IDInterval.from_message(filters.id_interval),
598
- automation_id=uuid_message_to_optional_uuid(filters.automation_id),
742
+ time_interval=TimeInterval.from_message(filters.time_interval)
743
+ if filters.HasField("time_interval")
744
+ else None,
745
+ id_interval=IDInterval.from_message(filters.id_interval) if filters.HasField("id_interval") else None,
746
+ automation_ids=[uuid_message_to_uuid(uuid) for uuid in filters.automation_ids],
747
+ job_states=[_JOB_STATES[state] for state in filters.states],
748
+ name=filters.name or None,
599
749
  )
600
750
 
601
751
  def to_message(self) -> job_pb2.QueryFilters:
602
752
  return job_pb2.QueryFilters(
603
753
  time_interval=self.time_interval.to_message() if self.time_interval else None,
604
754
  id_interval=self.id_interval.to_message() if self.id_interval else None,
605
- automation_id=uuid_to_uuid_message(self.automation_id) if self.automation_id else None,
755
+ automation_ids=[must_uuid_to_uuid_message(uuid) for uuid in self.automation_ids]
756
+ if self.automation_ids
757
+ else None,
758
+ states=[cast(core_pb2.JobState, state.value) for state in self.job_states] if self.job_states else None,
759
+ name=self.name or None, # empty string becomes None
606
760
  )