tilebox-workflows 0.43.0__py3-none-any.whl → 0.45.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,402 @@
1
+ """HTML formatting and ipywidgets for interactive display of Tilebox Workflow jobs."""
2
+
3
+ # some CSS helpers for our Jupyter HTML snippets - inspired by xarray's interactive display
4
+ import random
5
+ from collections.abc import Callable
6
+ from dataclasses import dataclass, field
7
+ from datetime import datetime
8
+ from threading import Event, Thread
9
+ from typing import Any
10
+ from uuid import UUID
11
+
12
+ from dateutil.tz import tzlocal
13
+ from ipywidgets import HTML, HBox, IntProgress, VBox
14
+
15
+ from tilebox.workflows.data import Job, JobState
16
+
17
+
18
+ class JobWidget:
19
+ def __init__(self, refresh_callback: Callable[[UUID], Job] | None = None) -> None:
20
+ self.job: Job | None = None
21
+ self.refresh_callback = refresh_callback
22
+ self.layout: VBox | None = None
23
+ self.widgets = []
24
+ self.refresh_thread: Thread | None = None
25
+ self._stop_refresh = Event()
26
+
27
+ def __del__(self) -> None:
28
+ self.stop()
29
+
30
+ def _repr_mimebundle_(self, *args: Any, **kwargs: Any) -> dict[str, str] | None:
31
+ if self.job is None: # no job to display
32
+ return None
33
+
34
+ if self.layout is None: # initialize the widget the first time we want to interactively display it
35
+ self.widgets.append(HTML(_render_job_details_html(self.job)))
36
+ self.widgets.append(HTML(_render_job_progress(self.job, False)))
37
+ self.widgets.extend(
38
+ _progress_indicator_bar(progress.label or self.job.name, progress.done, progress.total, self.job.state)
39
+ for progress in self.job.progress
40
+ )
41
+ self.layout = VBox(self.widgets)
42
+ self.refresh_thread = Thread(target=self._refresh_worker)
43
+ self.refresh_thread.start()
44
+
45
+ return self.layout._repr_mimebundle_(*args, **kwargs) if self.layout is not None else None
46
+
47
+ def stop(self) -> None:
48
+ self._stop_refresh.set()
49
+
50
+ def _refresh_worker(self) -> None:
51
+ """Refresh the job's progress display, intended to be run in a background thread."""
52
+
53
+ if self.job is None or self.refresh_callback is None or self.layout is None:
54
+ return
55
+
56
+ last_progress: Job | None = None
57
+
58
+ while True:
59
+ progress = self.refresh_callback(self.job.id)
60
+ updated = False
61
+ if last_progress is None: # first time, don't add the refresh time
62
+ self.widgets[1] = HTML(_render_job_progress(progress, False))
63
+ updated = True
64
+ elif (
65
+ progress.state != last_progress.state
66
+ or progress.execution_stats.first_task_started_at != last_progress.execution_stats.first_task_started_at
67
+ ):
68
+ self.widgets[1] = HTML(_render_job_progress(progress, True))
69
+ updated = True
70
+
71
+ if last_progress is None or progress.progress != last_progress.progress:
72
+ self.widgets[2:] = [
73
+ _progress_indicator_bar(
74
+ progress.label or self.job.name, progress.done, progress.total, self.job.state
75
+ )
76
+ for progress in progress.progress
77
+ ]
78
+ updated = True
79
+
80
+ if updated:
81
+ self.layout.children = self.widgets # trigger a rerender of the ipywidgets
82
+
83
+ last_progress = progress
84
+
85
+ if last_progress.state == JobState.COMPLETED:
86
+ self.stop()
87
+ return # no more refreshing needed
88
+
89
+ refresh_wait = 5 + random.uniform(0, 2) # noqa: S311 # wait between 5 and 7 seconds to refresh progress
90
+ if self._stop_refresh.wait(refresh_wait): # check if the event to stop is set, if so exit the thread
91
+ return
92
+
93
+
94
+ @dataclass(order=True, frozen=True)
95
+ class RichDisplayJob(Job):
96
+ _widget: JobWidget = field(compare=False, repr=False)
97
+
98
+ def __repr__(self) -> str:
99
+ return super().__repr__().replace(RichDisplayJob.__name__, Job.__name__)
100
+
101
+ def _repr_mimebundle_(self, *args: Any, **kwargs: Any) -> dict[str, str] | None:
102
+ """Called by the IPython MimeBundleFormatter for interactive display of ipywidgets.
103
+
104
+ By overriding this and forwarding to an ipywidget, we can utilize the interactive display capabilities of
105
+ the widget, without having to inherit from a widget as base class of the Job.
106
+ """
107
+ self._widget.job = self # initialize the widget the first time we want to interactively display it
108
+ return self._widget._repr_mimebundle_(*args, **kwargs)
109
+
110
+
111
+ _CSS = """
112
+ :root {
113
+ --tbx-font-color0: var(
114
+ --jp-content-font-color0,
115
+ var(--pst-color-text-base rgba(0, 0, 0, 1))
116
+ );
117
+ --tbx-font-color2: var(
118
+ --jp-content-font-color2,
119
+ var(--pst-color-text-base, rgba(0, 0, 0, 0.54))
120
+ );
121
+ --tbx-border-color: var(
122
+ --jp-border-color2,
123
+ hsl(from var(--pst-color-on-background, white) h s calc(l - 10))
124
+ );
125
+ }
126
+
127
+ html[theme="dark"],
128
+ html[data-theme="dark"],
129
+ body[data-theme="dark"],
130
+ body.vscode-dark {
131
+ --tbx-font-color0: var(
132
+ --jp-content-font-color0,
133
+ var(--pst-color-text-base, rgba(255, 255, 255, 1))
134
+ );
135
+ --tbx-font-color2: var(
136
+ --jp-content-font-color2,
137
+ var(--pst-color-text-base, rgba(255, 255, 255, 0.54))
138
+ );
139
+ --tbx-border-color: var(
140
+ --jp-border-color2,
141
+ hsl(from var(--pst-color-on-background, #111111) h s calc(l + 10))
142
+ );
143
+ }
144
+
145
+ .tbx-wrap {
146
+ display: block !important;
147
+ min-width: 300px;
148
+ max-width: 540px;
149
+ }
150
+
151
+ .tbx-text-repr-fallback {
152
+ /* fallback to plain text repr when CSS is not injected (untrusted notebook) */
153
+ display: none;
154
+ }
155
+
156
+ .tbx-header {
157
+ display: flex;
158
+ flex-direction: row;
159
+ justify-content: space-between;
160
+ align-items: center;
161
+ padding-bottom: 2px;
162
+ }
163
+
164
+ .tbx-header > .tbx-obj-type {
165
+ padding: 3px 5px;
166
+ line-height: 1;
167
+ border-bottom: solid 1px var(--tbx-border-color);
168
+ }
169
+
170
+
171
+ .tbx-obj-type {
172
+ display: flex;
173
+ flex-direction: row;
174
+ align-items: center;
175
+ gap: 4px;
176
+ color: var(--tbx-font-color2);
177
+ }
178
+
179
+ .tbx-detail-key {
180
+ color: var(--tbx-font-color2);
181
+ }
182
+ .tbx-detail-mono {
183
+ font-family: monospace;
184
+ }
185
+ .tbx-detail-value {
186
+ color: var(--tbx-font-color0);
187
+ }
188
+ .tbx-detail-value-muted {
189
+ color: var(--tbx-font-color2);
190
+ }
191
+
192
+ .tbx-job-state {
193
+ border-radius: 10px;
194
+ padding: 2px 10px;
195
+ }
196
+
197
+ .tbx-job-state-submitted {
198
+ background-color: #f1f5f9;
199
+ color: #0f172a;
200
+ }
201
+
202
+ .tbx-job-state-running {
203
+ background-color: #0066ff;
204
+ color: #f8fafc;
205
+ }
206
+
207
+ .tbx-job-state-started {
208
+ background-color: #fd9b11;
209
+ color: #f8fafc;
210
+ }
211
+
212
+ .tbx-job-state-completed {
213
+ background-color: #21c45d;
214
+ color: #f8fafc;
215
+ }
216
+
217
+ .tbx-job-state-failed {
218
+ background-color: #f43e5d;
219
+ color: #f8fafc;
220
+ }
221
+
222
+ .tbx-job-state-canceled {
223
+ background-color: #94a2b3;
224
+ color: #f8fafc;
225
+ }
226
+
227
+ .tbx-job-progress a {
228
+ text-decoration: underline;
229
+ }
230
+
231
+ .tbx-detail-button a{
232
+ display: flex;
233
+ align-items: center;
234
+ flex-direction: row;
235
+ gap: 8px;
236
+ font-size: 13px;
237
+ background-color: #f43f5e;
238
+ padding: 2px 10px;
239
+ color: white;
240
+ }
241
+
242
+ .tbx-detail-button a svg {
243
+ fill: white;
244
+ stroke: white;
245
+ stroke-width: 2px;
246
+ }
247
+
248
+ .tbx-detail-button a span {
249
+ display: inline;
250
+ vertical-align: middle;
251
+ margin: 0;
252
+ padding: 0;
253
+ line-height: 1.8;
254
+ }
255
+
256
+ .tbx-detail-button a:hover {
257
+ background-color: #cc4e63;
258
+ color: white;
259
+ }
260
+ """
261
+
262
+
263
+ def _render_job_details_html(job: Job) -> str:
264
+ """Render a job as HTML."""
265
+ return f"""
266
+ <style>
267
+ {_CSS}
268
+ </style>
269
+ <div class="tbx-text-repr-fallback"></div>
270
+ <div class="tbx-wrap">
271
+ <div class="tbx-header">
272
+ <div class="tbx-obj-type">tilebox.workflows.Job</div>
273
+ <div class="tbx-detail-button"><a href="https://console.tilebox.com/workflows/jobs/{job.id!s}" target="_blank">{_eye_icon} <span>Tilebox Console</span></a></div>
274
+ </div>
275
+ <div class="tbx-job-details">
276
+ <div><span class="tbx-detail-key tbx-detail-mono">id:</span> <span class="tbx-detail-value tbx-detail-mono">{job.id!s}</span><div>
277
+ <div><span class="tbx-detail-key tbx-detail-mono">name:</span> <span class="tbx-detail-value">{job.name}</span><div>
278
+ <div><span class="tbx-detail-key tbx-detail-mono">submitted_at:</span> {_render_datetime(job.submitted_at)}<div>
279
+ </div>
280
+ </div>
281
+ """
282
+
283
+
284
+ def _render_datetime(dt: datetime) -> str:
285
+ local = dt.astimezone(tzlocal())
286
+ time_part = local.strftime("%Y-%m-%d %H:%M:%S")
287
+ tz_part = local.strftime("%z")
288
+ tz_part = "(UTC)" if tz_part == "+0000" else f"(UTC{tz_part})"
289
+ return f"<span class='tbx-detail-value'>{time_part}</span> <span class='tbx-detail-value-muted'>{tz_part}</span>"
290
+
291
+
292
+ def _render_job_progress(job: Job, include_refresh_time: bool) -> str:
293
+ refresh = ""
294
+ if include_refresh_time:
295
+ current_time = datetime.now(tzlocal())
296
+ refresh = f" <span class='tbx-detail-value-muted'>(refreshed at {current_time.strftime('%H:%M:%S')})</span> {_info_icon}"
297
+
298
+ state_name = job.state.name
299
+
300
+ no_progress = ""
301
+ if not job.progress:
302
+ no_progress = "<span class='tbx-detail-value-muted'>No user defined progress indicators. <a href='https://docs.tilebox.com/workflows/progress' target='_blank'>Learn more</a></span>"
303
+
304
+ started_at = job.execution_stats.first_task_started_at
305
+
306
+ """Render a job's progress as HTML, needs to be called after render_job_details_html since that injects the necessary CSS."""
307
+ return f"""
308
+ <div class="tbx-wrap">
309
+ <div class="tbx-obj-type">Progress{refresh}</div>
310
+ <div class="tbx-job-progress">
311
+ <div><span class="tbx-detail-key tbx-detail-mono">state:</span> <span class="tbx-job-state tbx-job-state-{state_name.lower()}">{state_name}</span><div>
312
+ <div><span class="tbx-detail-key tbx-detail-mono">started_at:</span> {_render_datetime(started_at) if started_at else "<span class='tbx-detail-value-muted tbx-detail-mono'>None</span>"}<div>
313
+ <div><span class="tbx-detail-key tbx-detail-mono">progress:</span> {no_progress}</div>
314
+ </div>
315
+ </div>
316
+ """.strip()
317
+
318
+
319
+ _BAR_COLORS = {
320
+ "running": "#0066ff",
321
+ "failed": "#f43e5d",
322
+ "completed": "#21c45d",
323
+ }
324
+
325
+
326
+ def _progress_indicator_bar(label: str, done: int, total: int, state: JobState) -> HBox:
327
+ percentage = done / total if total > 0 else 0 if done <= total else 1
328
+ non_completed_color = (
329
+ _BAR_COLORS["failed"] if state in (JobState.FAILED, JobState.CANCELED) else _BAR_COLORS["running"]
330
+ )
331
+ progress = IntProgress(
332
+ min=0,
333
+ max=total,
334
+ value=done,
335
+ description=label,
336
+ tooltip=label,
337
+ style={"bar_color": non_completed_color if percentage < 1 else _BAR_COLORS["completed"]},
338
+ layout={"width": "400px"},
339
+ )
340
+ label_html = (
341
+ f"<span class='tbx-detail-mono'><span class='tbx-detail-value'>{percentage:.0%}</span> "
342
+ f"<span class='tbx-detail-value-muted'>({done} / {total})</span></span>"
343
+ )
344
+ label = HTML(label_html)
345
+ return HBox([progress, label])
346
+
347
+
348
+ _eye_icon = """
349
+ <svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16px"
350
+ height="16px" viewBox="0 0 72 72" enable-background="new 0 0 72 72" xml:space="preserve">
351
+ <g>
352
+ <g>
353
+ <path d="M36.001,63.75C24.233,63.75,2.5,54.883,2.5,42.25c0-12.634,21.733-21.5,33.501-21.5c11.766,0,33.5,8.866,33.5,21.5
354
+ C69.501,54.883,47.767,63.75,36.001,63.75z M36.001,24.75C24.886,24.75,6.5,32.929,6.5,42.25c0,9.32,18.387,17.5,29.501,17.5
355
+ c11.113,0,29.5-8.18,29.5-17.5C65.501,32.929,47.114,24.75,36.001,24.75z"/>
356
+ </g>
357
+ <g>
358
+ <path d="M36.001,52.917c-5.791,0-10.501-4.709-10.501-10.5c0-5.79,4.711-10.5,10.501-10.5c5.789,0,10.5,4.71,10.5,10.5
359
+ C46.501,48.208,41.79,52.917,36.001,52.917z M36.001,33.917c-4.688,0-8.501,3.814-8.501,8.5c0,4.688,3.813,8.5,8.501,8.5
360
+ c4.686,0,8.5-3.813,8.5-8.5C44.501,37.731,40.687,33.917,36.001,33.917z"/>
361
+ </g>
362
+ <g>
363
+ <path d="M32.073,39.809c-0.242,0-0.484-0.088-0.677-0.264c-0.406-0.375-0.433-1.008-0.059-1.414
364
+ c0.2-0.217,0.415-0.422,0.644-0.609c0.428-0.352,1.058-0.291,1.408,0.137c0.352,0.426,0.29,1.057-0.136,1.408
365
+ c-0.158,0.129-0.307,0.27-0.444,0.418C32.612,39.7,32.342,39.809,32.073,39.809z"/>
366
+ </g>
367
+ <g>
368
+ <path d="M36.001,48.75c-3.494,0-6.335-2.842-6.335-6.334c0-0.553,0.448-1,1-1c0.553,0,1,0.447,1,1
369
+ c0,2.391,1.945,4.334,4.335,4.334c0.553,0,1,0.447,1,1S36.554,48.75,36.001,48.75z"/>
370
+ </g>
371
+ <g>
372
+ <path d="M35.876,18.25c-1.105,0-2-0.896-2-2v-6c0-1.104,0.895-2,2-2c1.104,0,2,0.896,2,2v6
373
+ C37.876,17.354,36.979,18.25,35.876,18.25z"/>
374
+ </g>
375
+ <g>
376
+ <path d="M24.353,18.93c-0.732,0-1.437-0.402-1.788-1.101l-1.852-3.68c-0.497-0.987-0.1-2.189,0.888-2.686
377
+ c0.985-0.498,2.188-0.101,2.686,0.887l1.852,3.68c0.496,0.987,0.099,2.189-0.888,2.686C24.962,18.861,24.655,18.93,24.353,18.93z"
378
+ />
379
+ </g>
380
+ <g>
381
+ <path d="M12.684,23.567c-0.548,0-1.094-0.224-1.488-0.663l-2.6-2.894c-0.738-0.822-0.671-2.087,0.151-2.824
382
+ c0.82-0.74,2.085-0.672,2.824,0.15l2.6,2.894c0.738,0.822,0.67,2.087-0.151,2.824C13.638,23.398,13.16,23.567,12.684,23.567z"/>
383
+ </g>
384
+ <g>
385
+ <path d="M46.581,18.93c-0.303,0-0.609-0.068-0.898-0.214c-0.986-0.496-1.383-1.698-0.887-2.686l1.852-3.68
386
+ c0.494-0.985,1.695-1.386,2.686-0.887c0.986,0.496,1.385,1.698,0.887,2.686l-1.852,3.68C48.019,18.527,47.313,18.93,46.581,18.93z
387
+ "/>
388
+ </g>
389
+ <g>
390
+ <path d="M58.249,23.567c-0.475,0-0.953-0.169-1.336-0.513c-0.82-0.737-0.889-2.002-0.15-2.824l2.6-2.894
391
+ c0.738-0.82,2.002-0.89,2.824-0.15c0.822,0.737,0.889,2.002,0.15,2.824l-2.6,2.894C59.343,23.344,58.798,23.567,58.249,23.567z"/>
392
+ </g>
393
+ </g>
394
+ </svg>
395
+ """.strip()
396
+
397
+ _info_icon = """
398
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="24px" height="24px">
399
+ <title>The progress information below is a live update and will refresh automatically. These live updates are not reflected in the underlying job object variables.</title>
400
+ <path d="M 32 10 C 19.85 10 10 19.85 10 32 C 10 44.15 19.85 54 32 54 C 44.15 54 54 44.15 54 32 C 54 19.85 44.15 10 32 10 z M 32 14 C 41.941 14 50 22.059 50 32 C 50 41.941 41.941 50 32 50 C 22.059 50 14 41.941 14 32 C 14 22.059 22.059 14 32 14 z M 32 21 C 30.343 21 29 22.343 29 24 C 29 25.657 30.343 27 32 27 C 33.657 27 35 25.657 35 24 C 35 22.343 33.657 21 32 21 z M 32 30 C 30.895 30 30 30.896 30 32 L 30 42 C 30 43.104 30.895 44 32 44 C 33.105 44 34 43.104 34 42 L 34 32 C 34 30.896 33.105 30 32 30 z"/>
401
+ </svg>
402
+ """.strip()
@@ -9,12 +9,13 @@ from tilebox.datasets.query.time_interval import TimeInterval, TimeIntervalLike
9
9
  from tilebox.workflows.clusters.client import ClusterSlugLike, to_cluster_slug
10
10
  from tilebox.workflows.data import (
11
11
  Job,
12
+ JobState,
12
13
  QueryFilters,
13
14
  QueryJobsResponse,
14
15
  )
15
16
  from tilebox.workflows.jobs.service import JobService
16
17
  from tilebox.workflows.observability.tracing import WorkflowTracer, get_trace_parent_of_current_span
17
- from tilebox.workflows.task import FutureTask
18
+ from tilebox.workflows.task import FutureTask, merge_future_tasks_to_submissions
18
19
  from tilebox.workflows.task import Task as TaskInstance
19
20
 
20
21
  try:
@@ -64,8 +65,9 @@ class JobClient:
64
65
  """
65
66
  tasks = root_task_or_tasks if isinstance(root_task_or_tasks, list) else [root_task_or_tasks]
66
67
 
68
+ default_cluster = ""
67
69
  if isinstance(cluster, ClusterSlugLike | None):
68
- slugs = [to_cluster_slug(cluster or "")] * len(tasks)
70
+ slugs = [to_cluster_slug(cluster or default_cluster)] * len(tasks)
69
71
  else:
70
72
  slugs = [to_cluster_slug(c) for c in cluster]
71
73
 
@@ -75,13 +77,14 @@ class JobClient:
75
77
  f"or exactly one cluster per task. But got {len(tasks)} tasks and {len(slugs)} clusters."
76
78
  )
77
79
 
78
- task_submissions = [
79
- FutureTask(i, task, [], slugs[i], max_retries).to_submission() for i, task in enumerate(tasks)
80
- ]
80
+ task_submissions = [FutureTask(i, task, [], slugs[i], max_retries) for i, task in enumerate(tasks)]
81
+ submissions_merged = merge_future_tasks_to_submissions(task_submissions, default_cluster)
82
+ if submissions_merged is None:
83
+ raise ValueError("At least one task must be submitted.")
81
84
 
82
85
  with self._tracer.start_as_current_span(f"job/{job_name}"):
83
86
  trace_parent = get_trace_parent_of_current_span()
84
- return self._service.submit(job_name, trace_parent, task_submissions)
87
+ return self._service.submit(job_name, trace_parent, submissions_merged)
85
88
 
86
89
  def retry(self, job_or_id: JobIDLike) -> int:
87
90
  """Retry a job.
@@ -125,19 +128,19 @@ class JobClient:
125
128
  """
126
129
  return self._service.get_by_id(_to_uuid(job_id))
127
130
 
128
- def display(self, job: JobIDLike, direction: str = "down", layout: str = "dagre", sketchy: bool = True) -> None:
131
+ def display(self, job_id: JobIDLike, direction: str = "down", layout: str = "dagre", sketchy: bool = True) -> None:
129
132
  """Create a visualization of the job as a diagram and display it in an interactive environment.
130
133
 
131
134
  Requires an IPython environment such as a Jupyter notebook.
132
135
 
133
136
  Args:
134
- job: The job or job id to visualize.
137
+ job_id: The job or job id to visualize.
135
138
  direction: The direction of the diagram. Defaults to "down". See https://d2lang.com/tour/layouts/#direction
136
139
  layout: The layout to use for the diagram. Defaults to "dagre". Currently supported layouts are
137
140
  "dagre" and "elk". See https://d2lang.com/tour/layouts/
138
141
  sketchy: Whether to render the diagram in a sketchy hand drawn style. Defaults to True.
139
142
  """
140
- display(HTML(self.visualize(job, direction, layout, sketchy)))
143
+ display(HTML(self.visualize(job_id, direction, layout, sketchy)))
141
144
 
142
145
  def visualize(self, job: JobIDLike, direction: str = "down", layout: str = "dagre", sketchy: bool = True) -> str:
143
146
  """Create a visualization of the job as a diagram.
@@ -154,7 +157,13 @@ class JobClient:
154
157
  """
155
158
  return self._service.visualize(_to_uuid(job), direction, layout, sketchy)
156
159
 
157
- def query(self, temporal_extent: TimeIntervalLike | IDIntervalLike, automation_id: UUID | None = None) -> list[Job]:
160
+ def query(
161
+ self,
162
+ temporal_extent: TimeIntervalLike | IDIntervalLike,
163
+ automation_ids: UUID | list[UUID] | None = None,
164
+ job_states: JobState | list[JobState] | None = None,
165
+ name: str | None = None,
166
+ ) -> list[Job]:
158
167
  """List jobs in the given temporal extent.
159
168
 
160
169
  Args:
@@ -170,11 +179,14 @@ class JobClient:
170
179
  - tuple of two UUIDs: [start, end) -> Construct an IDInterval with the given start and end id
171
180
  - tuple of two strings: [start, end) -> Construct an IDInterval with the given start and end id
172
181
  parsed from the strings
173
- automation_id: The automation id to filter jobs by. If specified, only jobs created by the given automation
174
- are returned.
175
-
182
+ automation_ids: An automation id or list of automation ids to filter jobs by.
183
+ If specified, only jobs created by one of the selected automations are returned.
184
+ job_states: A job state or list of job states to filter jobs by. If specified, only jobs in one of the
185
+ selected states are returned.
186
+ name: A name to filter jobs by. If specified, only jobs with a matching name are returned. The match is
187
+ case-insensitive and uses a fuzzy matching scheme.
176
188
  Returns:
177
- A list of jobs.
189
+ A list of jobs matching the given filters.
178
190
  """
179
191
  time_interval: TimeInterval | None = None
180
192
  id_interval: IDInterval | None = None
@@ -202,7 +214,21 @@ class JobClient:
202
214
  end_inclusive=dataset_time_interval.end_inclusive,
203
215
  )
204
216
 
205
- filters = QueryFilters(time_interval=time_interval, id_interval=id_interval, automation_id=automation_id)
217
+ automation_ids = automation_ids or []
218
+ if not isinstance(automation_ids, list):
219
+ automation_ids = [automation_ids]
220
+
221
+ job_states = job_states or []
222
+ if not isinstance(job_states, list):
223
+ job_states = [job_states]
224
+
225
+ filters = QueryFilters(
226
+ time_interval=time_interval,
227
+ id_interval=id_interval,
228
+ automation_ids=automation_ids,
229
+ job_states=job_states,
230
+ name=name,
231
+ )
206
232
 
207
233
  def request(page: PaginationProtocol) -> QueryJobsResponse:
208
234
  query_page = Pagination(page.limit, page.starting_after)
@@ -8,13 +8,15 @@ from tilebox.workflows.data import (
8
8
  Job,
9
9
  QueryFilters,
10
10
  QueryJobsResponse,
11
- TaskSubmission,
11
+ TaskSubmissions,
12
12
  uuid_to_uuid_message,
13
13
  )
14
+ from tilebox.workflows.formatting.job import JobWidget, RichDisplayJob
14
15
  from tilebox.workflows.workflows.v1.core_pb2 import Job as JobMessage
15
16
  from tilebox.workflows.workflows.v1.diagram_pb2 import Diagram, RenderOptions
16
17
  from tilebox.workflows.workflows.v1.job_pb2 import (
17
18
  CancelJobRequest,
19
+ GetJobProgressRequest,
18
20
  GetJobRequest,
19
21
  QueryJobsRequest,
20
22
  RetryJobRequest,
@@ -37,18 +39,22 @@ class JobService:
37
39
  """
38
40
  self.service = with_pythonic_errors(JobServiceStub(channel))
39
41
 
40
- def submit(self, job_name: str, trace_parent: str, tasks: list[TaskSubmission]) -> Job:
42
+ def submit(self, job_name: str, trace_parent: str, tasks: TaskSubmissions) -> Job:
41
43
  request = SubmitJobRequest(
42
- tasks=[task.to_message() for task in tasks],
44
+ tasks=tasks.to_message(),
43
45
  job_name=job_name,
44
46
  trace_parent=trace_parent,
45
47
  )
46
- return Job.from_message(self.service.SubmitJob(request))
48
+ return RichDisplayJob.from_message(self.service.SubmitJob(request), _widget=JobWidget(self.get_progress))
47
49
 
48
50
  def get_by_id(self, job_id: UUID) -> Job:
49
51
  request = GetJobRequest(job_id=uuid_to_uuid_message(job_id))
50
52
  response: JobMessage = self.service.GetJob(request)
51
- return Job.from_message(response)
53
+ return RichDisplayJob.from_message(response, _widget=JobWidget(self.get_progress))
54
+
55
+ def get_progress(self, job_id: UUID) -> Job:
56
+ request = GetJobProgressRequest(job_id=uuid_to_uuid_message(job_id))
57
+ return Job.from_message(self.service.GetJobProgress(request))
52
58
 
53
59
  def retry(self, job_id: UUID) -> int:
54
60
  request = RetryJobRequest(job_id=uuid_to_uuid_message(job_id))
@@ -73,4 +79,7 @@ class JobService:
73
79
  page=page.to_message() if page is not None else None,
74
80
  )
75
81
  response: QueryJobsResponseMessage = self.service.QueryJobs(request)
76
- return QueryJobsResponse.from_message(response)
82
+
83
+ return QueryJobsResponse.from_message(
84
+ response, job_factory=lambda job: RichDisplayJob.from_message(job, _widget=JobWidget(self.get_progress))
85
+ )