tilebox-workflows 0.43.0__tar.gz → 0.44.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.
- {tilebox_workflows-0.43.0 → tilebox_workflows-0.44.0}/PKG-INFO +3 -1
- {tilebox_workflows-0.43.0 → tilebox_workflows-0.44.0}/pyproject.toml +2 -0
- {tilebox_workflows-0.43.0 → tilebox_workflows-0.44.0}/tilebox/workflows/__init__.py +2 -1
- {tilebox_workflows-0.43.0 → tilebox_workflows-0.44.0}/tilebox/workflows/data.py +25 -16
- tilebox_workflows-0.44.0/tilebox/workflows/formatting/job.py +389 -0
- {tilebox_workflows-0.43.0 → tilebox_workflows-0.44.0}/tilebox/workflows/jobs/client.py +3 -3
- {tilebox_workflows-0.43.0 → tilebox_workflows-0.44.0}/tilebox/workflows/jobs/service.py +12 -3
- tilebox_workflows-0.44.0/tilebox/workflows/runner/__init__.py +0 -0
- {tilebox_workflows-0.43.0 → tilebox_workflows-0.44.0}/tilebox/workflows/runner/task_runner.py +16 -14
- {tilebox_workflows-0.43.0 → tilebox_workflows-0.44.0}/tilebox/workflows/runner/task_service.py +4 -2
- {tilebox_workflows-0.43.0 → tilebox_workflows-0.44.0}/tilebox/workflows/task.py +4 -4
- tilebox_workflows-0.44.0/tilebox/workflows/workflows/v1/core_pb2.py +79 -0
- {tilebox_workflows-0.43.0 → tilebox_workflows-0.44.0}/tilebox/workflows/workflows/v1/core_pb2.pyi +5 -5
- {tilebox_workflows-0.43.0 → tilebox_workflows-0.44.0}/tilebox/workflows/workflows/v1/job_pb2.py +27 -29
- {tilebox_workflows-0.43.0 → tilebox_workflows-0.44.0}/tilebox/workflows/workflows/v1/job_pb2.pyi +0 -6
- {tilebox_workflows-0.43.0 → tilebox_workflows-0.44.0}/tilebox/workflows/workflows/v1/job_pb2_grpc.py +43 -0
- {tilebox_workflows-0.43.0 → tilebox_workflows-0.44.0}/tilebox/workflows/workflows/v1/task_pb2.py +14 -14
- {tilebox_workflows-0.43.0 → tilebox_workflows-0.44.0}/tilebox/workflows/workflows/v1/task_pb2.pyi +4 -4
- tilebox_workflows-0.43.0/tilebox/workflows/workflows/v1/core_pb2.py +0 -79
- {tilebox_workflows-0.43.0 → tilebox_workflows-0.44.0}/.gitignore +0 -0
- {tilebox_workflows-0.43.0 → tilebox_workflows-0.44.0}/README.md +0 -0
- {tilebox_workflows-0.43.0 → tilebox_workflows-0.44.0}/tilebox/workflows/automations/__init__.py +0 -0
- {tilebox_workflows-0.43.0 → tilebox_workflows-0.44.0}/tilebox/workflows/automations/client.py +0 -0
- {tilebox_workflows-0.43.0 → tilebox_workflows-0.44.0}/tilebox/workflows/automations/cron.py +0 -0
- {tilebox_workflows-0.43.0 → tilebox_workflows-0.44.0}/tilebox/workflows/automations/service.py +0 -0
- {tilebox_workflows-0.43.0 → tilebox_workflows-0.44.0}/tilebox/workflows/automations/storage_event.py +0 -0
- {tilebox_workflows-0.43.0 → tilebox_workflows-0.44.0}/tilebox/workflows/cache.py +0 -0
- {tilebox_workflows-0.43.0 → tilebox_workflows-0.44.0}/tilebox/workflows/client.py +0 -0
- {tilebox_workflows-0.43.0 → tilebox_workflows-0.44.0}/tilebox/workflows/clusters/__init__.py +0 -0
- {tilebox_workflows-0.43.0 → tilebox_workflows-0.44.0}/tilebox/workflows/clusters/client.py +0 -0
- {tilebox_workflows-0.43.0 → tilebox_workflows-0.44.0}/tilebox/workflows/clusters/service.py +0 -0
- {tilebox_workflows-0.43.0/tilebox/workflows/jobs → tilebox_workflows-0.44.0/tilebox/workflows/formatting}/__init__.py +0 -0
- {tilebox_workflows-0.43.0 → tilebox_workflows-0.44.0}/tilebox/workflows/interceptors.py +0 -0
- {tilebox_workflows-0.43.0/tilebox/workflows/observability → tilebox_workflows-0.44.0/tilebox/workflows/jobs}/__init__.py +0 -0
- {tilebox_workflows-0.43.0/tilebox/workflows/runner → tilebox_workflows-0.44.0/tilebox/workflows/observability}/__init__.py +0 -0
- {tilebox_workflows-0.43.0 → tilebox_workflows-0.44.0}/tilebox/workflows/observability/logging.py +0 -0
- {tilebox_workflows-0.43.0 → tilebox_workflows-0.44.0}/tilebox/workflows/observability/tracing.py +0 -0
- {tilebox_workflows-0.43.0 → tilebox_workflows-0.44.0}/tilebox/workflows/timeseries.py +0 -0
- {tilebox_workflows-0.43.0 → tilebox_workflows-0.44.0}/tilebox/workflows/workflows/v1/automation_pb2.py +0 -0
- {tilebox_workflows-0.43.0 → tilebox_workflows-0.44.0}/tilebox/workflows/workflows/v1/automation_pb2.pyi +0 -0
- {tilebox_workflows-0.43.0 → tilebox_workflows-0.44.0}/tilebox/workflows/workflows/v1/automation_pb2_grpc.py +0 -0
- {tilebox_workflows-0.43.0 → tilebox_workflows-0.44.0}/tilebox/workflows/workflows/v1/core_pb2_grpc.py +0 -0
- {tilebox_workflows-0.43.0 → tilebox_workflows-0.44.0}/tilebox/workflows/workflows/v1/diagram_pb2.py +0 -0
- {tilebox_workflows-0.43.0 → tilebox_workflows-0.44.0}/tilebox/workflows/workflows/v1/diagram_pb2.pyi +0 -0
- {tilebox_workflows-0.43.0 → tilebox_workflows-0.44.0}/tilebox/workflows/workflows/v1/diagram_pb2_grpc.py +0 -0
- {tilebox_workflows-0.43.0 → tilebox_workflows-0.44.0}/tilebox/workflows/workflows/v1/task_pb2_grpc.py +0 -0
- {tilebox_workflows-0.43.0 → tilebox_workflows-0.44.0}/tilebox/workflows/workflows/v1/workflows_pb2.py +0 -0
- {tilebox_workflows-0.43.0 → tilebox_workflows-0.44.0}/tilebox/workflows/workflows/v1/workflows_pb2.pyi +0 -0
- {tilebox_workflows-0.43.0 → tilebox_workflows-0.44.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.
|
|
3
|
+
Version: 0.44.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
|
|
@@ -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:
|
|
@@ -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
|
|
8
10
|
from uuid import UUID
|
|
9
11
|
|
|
10
12
|
import boto3
|
|
@@ -108,19 +110,19 @@ class TaskLease:
|
|
|
108
110
|
|
|
109
111
|
|
|
110
112
|
@dataclass(order=True)
|
|
111
|
-
class
|
|
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,
|
|
118
|
-
"""Convert a
|
|
119
|
-
return cls(label=
|
|
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.
|
|
122
|
-
"""Convert a
|
|
123
|
-
return core_pb2.
|
|
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)
|
|
@@ -195,7 +197,7 @@ class JobState(Enum):
|
|
|
195
197
|
_JOB_STATES = {state.value: state for state in JobState}
|
|
196
198
|
|
|
197
199
|
|
|
198
|
-
@dataclass(order=True)
|
|
200
|
+
@dataclass(order=True, frozen=True)
|
|
199
201
|
class Job:
|
|
200
202
|
id: UUID
|
|
201
203
|
name: str
|
|
@@ -204,10 +206,12 @@ class Job:
|
|
|
204
206
|
submitted_at: datetime
|
|
205
207
|
started_at: datetime | None
|
|
206
208
|
canceled: bool
|
|
207
|
-
|
|
209
|
+
progress: list[ProgressIndicator]
|
|
208
210
|
|
|
209
211
|
@classmethod
|
|
210
|
-
def from_message(
|
|
212
|
+
def from_message(
|
|
213
|
+
cls, job: core_pb2.Job, **extra_kwargs: Any
|
|
214
|
+
) -> "Job": # lets use typing.Self once we require python >= 3.11
|
|
211
215
|
"""Convert a Job protobuf message to a Job object."""
|
|
212
216
|
return cls(
|
|
213
217
|
id=uuid_message_to_uuid(job.id),
|
|
@@ -217,7 +221,8 @@ class Job:
|
|
|
217
221
|
submitted_at=timestamp_to_datetime(job.submitted_at),
|
|
218
222
|
started_at=timestamp_to_datetime(job.started_at) if job.HasField("started_at") else None,
|
|
219
223
|
canceled=job.canceled,
|
|
220
|
-
|
|
224
|
+
progress=[ProgressIndicator.from_message(progress) for progress in job.progress],
|
|
225
|
+
**extra_kwargs,
|
|
221
226
|
)
|
|
222
227
|
|
|
223
228
|
def to_message(self) -> core_pb2.Job:
|
|
@@ -230,7 +235,7 @@ class Job:
|
|
|
230
235
|
submitted_at=datetime_to_timestamp(self.submitted_at),
|
|
231
236
|
started_at=datetime_to_timestamp(self.started_at) if self.started_at else None,
|
|
232
237
|
canceled=self.canceled,
|
|
233
|
-
|
|
238
|
+
progress=[progress.to_message() for progress in self.progress],
|
|
234
239
|
)
|
|
235
240
|
|
|
236
241
|
|
|
@@ -303,7 +308,7 @@ class ComputedTask:
|
|
|
303
308
|
id: UUID
|
|
304
309
|
display: str | None
|
|
305
310
|
sub_tasks: list[TaskSubmission]
|
|
306
|
-
progress_updates: list[
|
|
311
|
+
progress_updates: list[ProgressIndicator]
|
|
307
312
|
|
|
308
313
|
@classmethod
|
|
309
314
|
def from_message(cls, computed_task: task_pb2.ComputedTask) -> "ComputedTask":
|
|
@@ -312,7 +317,7 @@ class ComputedTask:
|
|
|
312
317
|
id=uuid_message_to_uuid(computed_task.id),
|
|
313
318
|
display=computed_task.display,
|
|
314
319
|
sub_tasks=[TaskSubmission.from_message(sub_task) for sub_task in computed_task.sub_tasks],
|
|
315
|
-
progress_updates=[
|
|
320
|
+
progress_updates=[ProgressIndicator.from_message(progress) for progress in computed_task.progress_updates],
|
|
316
321
|
)
|
|
317
322
|
|
|
318
323
|
def to_message(self) -> task_pb2.ComputedTask:
|
|
@@ -571,9 +576,13 @@ class QueryJobsResponse:
|
|
|
571
576
|
next_page: Pagination
|
|
572
577
|
|
|
573
578
|
@classmethod
|
|
574
|
-
def from_message(
|
|
579
|
+
def from_message(
|
|
580
|
+
cls,
|
|
581
|
+
page: job_pb2.QueryJobsResponse,
|
|
582
|
+
job_factory: Callable[[core_pb2.Job], Job] = Job.from_message,
|
|
583
|
+
) -> "QueryJobsResponse":
|
|
575
584
|
return cls(
|
|
576
|
-
jobs=[
|
|
585
|
+
jobs=[job_factory(job) for job in page.jobs],
|
|
577
586
|
next_page=Pagination.from_message(page.next_page),
|
|
578
587
|
)
|
|
579
588
|
|
|
@@ -0,0 +1,389 @@
|
|
|
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(
|
|
39
|
+
progress.label or self.job.name, progress.done, progress.total, self.job.canceled
|
|
40
|
+
)
|
|
41
|
+
for progress in self.job.progress
|
|
42
|
+
)
|
|
43
|
+
self.layout = VBox(self.widgets)
|
|
44
|
+
self.refresh_thread = Thread(target=self._refresh_worker)
|
|
45
|
+
self.refresh_thread.start()
|
|
46
|
+
|
|
47
|
+
return self.layout._repr_mimebundle_(*args, **kwargs) if self.layout is not None else None
|
|
48
|
+
|
|
49
|
+
def stop(self) -> None:
|
|
50
|
+
self._stop_refresh.set()
|
|
51
|
+
|
|
52
|
+
def _refresh_worker(self) -> None:
|
|
53
|
+
"""Refresh the job's progress display, intended to be run in a background thread."""
|
|
54
|
+
|
|
55
|
+
if self.job is None or self.refresh_callback is None or self.layout is None:
|
|
56
|
+
return
|
|
57
|
+
|
|
58
|
+
last_progress: Job | None = None
|
|
59
|
+
|
|
60
|
+
while True:
|
|
61
|
+
progress = self.refresh_callback(self.job.id)
|
|
62
|
+
updated = False
|
|
63
|
+
if last_progress is None: # first time, don't add the refresh time
|
|
64
|
+
self.widgets[1] = HTML(_render_job_progress(progress, False))
|
|
65
|
+
updated = True
|
|
66
|
+
elif progress.state != last_progress.state or progress.started_at != last_progress.started_at:
|
|
67
|
+
self.widgets[1] = HTML(_render_job_progress(progress, True))
|
|
68
|
+
updated = True
|
|
69
|
+
|
|
70
|
+
if last_progress is None or progress.progress != last_progress.progress:
|
|
71
|
+
self.widgets[2:] = [
|
|
72
|
+
_progress_indicator_bar(
|
|
73
|
+
progress.label or self.job.name, progress.done, progress.total, self.job.canceled
|
|
74
|
+
)
|
|
75
|
+
for progress in progress.progress
|
|
76
|
+
]
|
|
77
|
+
updated = True
|
|
78
|
+
|
|
79
|
+
if updated:
|
|
80
|
+
self.layout.children = self.widgets # trigger a rerender of the ipywidgets
|
|
81
|
+
|
|
82
|
+
last_progress = progress
|
|
83
|
+
|
|
84
|
+
if last_progress.state == JobState.COMPLETED:
|
|
85
|
+
self.stop()
|
|
86
|
+
return # no more refreshing needed
|
|
87
|
+
|
|
88
|
+
refresh_wait = 5 + random.uniform(0, 2) # noqa: S311 # wait between 5 and 7 seconds to refresh progress
|
|
89
|
+
if self._stop_refresh.wait(refresh_wait): # check if the event to stop is set, if so exit the thread
|
|
90
|
+
return
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@dataclass(order=True, frozen=True)
|
|
94
|
+
class RichDisplayJob(Job):
|
|
95
|
+
_widget: JobWidget = field(compare=False, repr=False)
|
|
96
|
+
|
|
97
|
+
def __repr__(self) -> str:
|
|
98
|
+
return super().__repr__().replace(RichDisplayJob.__name__, Job.__name__)
|
|
99
|
+
|
|
100
|
+
def _repr_mimebundle_(self, *args: Any, **kwargs: Any) -> dict[str, str] | None:
|
|
101
|
+
"""Called by the IPython MimeBundleFormatter for interactive display of ipywidgets.
|
|
102
|
+
|
|
103
|
+
By overriding this and forwarding to an ipywidget, we can utilize the interactive display capabilities of
|
|
104
|
+
the widget, without having to inherit from a widget as base class of the Job.
|
|
105
|
+
"""
|
|
106
|
+
self._widget.job = self # initialize the widget the first time we want to interactively display it
|
|
107
|
+
return self._widget._repr_mimebundle_(*args, **kwargs)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
_CSS = """
|
|
111
|
+
:root {
|
|
112
|
+
--tbx-font-color0: var(
|
|
113
|
+
--jp-content-font-color0,
|
|
114
|
+
var(--pst-color-text-base rgba(0, 0, 0, 1))
|
|
115
|
+
);
|
|
116
|
+
--tbx-font-color2: var(
|
|
117
|
+
--jp-content-font-color2,
|
|
118
|
+
var(--pst-color-text-base, rgba(0, 0, 0, 0.54))
|
|
119
|
+
);
|
|
120
|
+
--tbx-border-color: var(
|
|
121
|
+
--jp-border-color2,
|
|
122
|
+
hsl(from var(--pst-color-on-background, white) h s calc(l - 10))
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
html[theme="dark"],
|
|
127
|
+
html[data-theme="dark"],
|
|
128
|
+
body[data-theme="dark"],
|
|
129
|
+
body.vscode-dark {
|
|
130
|
+
--tbx-font-color0: var(
|
|
131
|
+
--jp-content-font-color0,
|
|
132
|
+
var(--pst-color-text-base, rgba(255, 255, 255, 1))
|
|
133
|
+
);
|
|
134
|
+
--tbx-font-color2: var(
|
|
135
|
+
--jp-content-font-color2,
|
|
136
|
+
var(--pst-color-text-base, rgba(255, 255, 255, 0.54))
|
|
137
|
+
);
|
|
138
|
+
--tbx-border-color: var(
|
|
139
|
+
--jp-border-color2,
|
|
140
|
+
hsl(from var(--pst-color-on-background, #111111) h s calc(l + 10))
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.tbx-wrap {
|
|
145
|
+
display: block !important;
|
|
146
|
+
min-width: 300px;
|
|
147
|
+
max-width: 540px;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.tbx-text-repr-fallback {
|
|
151
|
+
/* fallback to plain text repr when CSS is not injected (untrusted notebook) */
|
|
152
|
+
display: none;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.tbx-header {
|
|
156
|
+
display: flex;
|
|
157
|
+
flex-direction: row;
|
|
158
|
+
justify-content: space-between;
|
|
159
|
+
align-items: center;
|
|
160
|
+
padding-bottom: 2px;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.tbx-header > .tbx-obj-type {
|
|
164
|
+
padding: 3px 5px;
|
|
165
|
+
line-height: 1;
|
|
166
|
+
border-bottom: solid 1px var(--tbx-border-color);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
.tbx-obj-type {
|
|
171
|
+
display: flex;
|
|
172
|
+
flex-direction: row;
|
|
173
|
+
align-items: center;
|
|
174
|
+
gap: 4px;
|
|
175
|
+
color: var(--tbx-font-color2);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.tbx-detail-key {
|
|
179
|
+
color: var(--tbx-font-color2);
|
|
180
|
+
}
|
|
181
|
+
.tbx-detail-mono {
|
|
182
|
+
font-family: monospace;
|
|
183
|
+
}
|
|
184
|
+
.tbx-detail-value {
|
|
185
|
+
color: var(--tbx-font-color0);
|
|
186
|
+
}
|
|
187
|
+
.tbx-detail-value-muted {
|
|
188
|
+
color: var(--tbx-font-color2);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
.tbx-job-state {
|
|
192
|
+
border-radius: 10px;
|
|
193
|
+
padding: 2px 10px;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
.tbx-job-state-queued {
|
|
197
|
+
background-color: #f1f5f9;
|
|
198
|
+
color: #0f172a;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
.tbx-job-state-running {
|
|
202
|
+
background-color: #0066ff;
|
|
203
|
+
color: #f8fafc;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
.tbx-job-state-completed {
|
|
207
|
+
background-color: #21c45d;
|
|
208
|
+
color: #f8fafc;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
.tbx-job-state-failed {
|
|
212
|
+
background-color: #f43e5d;
|
|
213
|
+
color: #f8fafc;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
.tbx-job-progress a {
|
|
217
|
+
text-decoration: underline;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
.tbx-detail-button a{
|
|
221
|
+
display: flex;
|
|
222
|
+
align-items: center;
|
|
223
|
+
flex-direction: row;
|
|
224
|
+
gap: 8px;
|
|
225
|
+
font-size: 13px;
|
|
226
|
+
background-color: #f43f5e;
|
|
227
|
+
padding: 2px 10px;
|
|
228
|
+
color: white;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
.tbx-detail-button a svg {
|
|
232
|
+
fill: white;
|
|
233
|
+
stroke: white;
|
|
234
|
+
stroke-width: 2px;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
.tbx-detail-button a span {
|
|
238
|
+
display: inline;
|
|
239
|
+
vertical-align: middle;
|
|
240
|
+
margin: 0;
|
|
241
|
+
padding: 0;
|
|
242
|
+
line-height: 1.8;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
.tbx-detail-button a:hover {
|
|
246
|
+
background-color: #cc4e63;
|
|
247
|
+
color: white;
|
|
248
|
+
}
|
|
249
|
+
"""
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _render_job_details_html(job: Job) -> str:
|
|
253
|
+
"""Render a job as HTML."""
|
|
254
|
+
return f"""
|
|
255
|
+
<style>
|
|
256
|
+
{_CSS}
|
|
257
|
+
</style>
|
|
258
|
+
<div class="tbx-text-repr-fallback"></div>
|
|
259
|
+
<div class="tbx-wrap">
|
|
260
|
+
<div class="tbx-header">
|
|
261
|
+
<div class="tbx-obj-type">tilebox.workflows.Job</div>
|
|
262
|
+
<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>
|
|
263
|
+
</div>
|
|
264
|
+
<div class="tbx-job-details">
|
|
265
|
+
<div><span class="tbx-detail-key tbx-detail-mono">id:</span> <span class="tbx-detail-value tbx-detail-mono">{job.id!s}</span><div>
|
|
266
|
+
<div><span class="tbx-detail-key tbx-detail-mono">name:</span> <span class="tbx-detail-value">{job.name}</span><div>
|
|
267
|
+
<div><span class="tbx-detail-key tbx-detail-mono">submitted_at:</span> {_render_datetime(job.submitted_at)}<div>
|
|
268
|
+
</div>
|
|
269
|
+
</div>
|
|
270
|
+
"""
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def _render_datetime(dt: datetime) -> str:
|
|
274
|
+
local = dt.astimezone(tzlocal())
|
|
275
|
+
time_part = local.strftime("%Y-%m-%d %H:%M:%S")
|
|
276
|
+
tz_part = local.strftime("%z")
|
|
277
|
+
tz_part = "(UTC)" if tz_part == "+0000" else f"(UTC{tz_part})"
|
|
278
|
+
return f"<span class='tbx-detail-value'>{time_part}</span> <span class='tbx-detail-value-muted'>{tz_part}</span>"
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def _render_job_progress(job: Job, include_refresh_time: bool) -> str:
|
|
282
|
+
refresh = ""
|
|
283
|
+
if include_refresh_time:
|
|
284
|
+
current_time = datetime.now(tzlocal())
|
|
285
|
+
refresh = f" <span class='tbx-detail-value-muted'>(refreshed at {current_time.strftime('%H:%M:%S')})</span> {_info_icon}"
|
|
286
|
+
|
|
287
|
+
state_name = job.state.name
|
|
288
|
+
if job.state == JobState.STARTED:
|
|
289
|
+
state_name = "RUNNING" if not job.canceled else "FAILED"
|
|
290
|
+
|
|
291
|
+
no_progress = ""
|
|
292
|
+
if not job.progress:
|
|
293
|
+
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>"
|
|
294
|
+
|
|
295
|
+
"""Render a job's progress as HTML, needs to be called after render_job_details_html since that injects the necessary CSS."""
|
|
296
|
+
return f"""
|
|
297
|
+
<div class="tbx-wrap">
|
|
298
|
+
<div class="tbx-obj-type">Progress{refresh}</div>
|
|
299
|
+
<div class="tbx-job-progress">
|
|
300
|
+
<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>
|
|
301
|
+
<div><span class="tbx-detail-key tbx-detail-mono">started_at:</span> {_render_datetime(job.started_at) if job.started_at else "<span class='tbx-detail-value-muted tbx-detail-mono'>None</span>"}<div>
|
|
302
|
+
<div><span class="tbx-detail-key tbx-detail-mono">progress:</span> {no_progress}</div>
|
|
303
|
+
</div>
|
|
304
|
+
</div>
|
|
305
|
+
""".strip()
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
_BAR_COLORS = {
|
|
309
|
+
"running": "#0066ff",
|
|
310
|
+
"failed": "#f43e5d",
|
|
311
|
+
"completed": "#21c45d",
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def _progress_indicator_bar(label: str, done: int, total: int, job_cancelled: bool) -> HBox:
|
|
316
|
+
percentage = done / total if total > 0 else 0 if done <= total else 1
|
|
317
|
+
non_completed_color = _BAR_COLORS["running"] if not job_cancelled else _BAR_COLORS["failed"]
|
|
318
|
+
progress = IntProgress(
|
|
319
|
+
min=0,
|
|
320
|
+
max=total,
|
|
321
|
+
value=done,
|
|
322
|
+
description=label,
|
|
323
|
+
tooltip=label,
|
|
324
|
+
style={"bar_color": non_completed_color if percentage < 1 else _BAR_COLORS["completed"]},
|
|
325
|
+
layout={"width": "400px"},
|
|
326
|
+
)
|
|
327
|
+
label_html = (
|
|
328
|
+
f"<span class='tbx-detail-mono'><span class='tbx-detail-value'>{percentage:.0%}</span> "
|
|
329
|
+
f"<span class='tbx-detail-value-muted'>({done} / {total})</span></span>"
|
|
330
|
+
)
|
|
331
|
+
label = HTML(label_html)
|
|
332
|
+
return HBox([progress, label])
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
_eye_icon = """
|
|
336
|
+
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16px"
|
|
337
|
+
height="16px" viewBox="0 0 72 72" enable-background="new 0 0 72 72" xml:space="preserve">
|
|
338
|
+
<g>
|
|
339
|
+
<g>
|
|
340
|
+
<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
|
|
341
|
+
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
|
|
342
|
+
c11.113,0,29.5-8.18,29.5-17.5C65.501,32.929,47.114,24.75,36.001,24.75z"/>
|
|
343
|
+
</g>
|
|
344
|
+
<g>
|
|
345
|
+
<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
|
|
346
|
+
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
|
|
347
|
+
c4.686,0,8.5-3.813,8.5-8.5C44.501,37.731,40.687,33.917,36.001,33.917z"/>
|
|
348
|
+
</g>
|
|
349
|
+
<g>
|
|
350
|
+
<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
|
|
351
|
+
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
|
|
352
|
+
c-0.158,0.129-0.307,0.27-0.444,0.418C32.612,39.7,32.342,39.809,32.073,39.809z"/>
|
|
353
|
+
</g>
|
|
354
|
+
<g>
|
|
355
|
+
<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
|
|
356
|
+
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"/>
|
|
357
|
+
</g>
|
|
358
|
+
<g>
|
|
359
|
+
<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
|
|
360
|
+
C37.876,17.354,36.979,18.25,35.876,18.25z"/>
|
|
361
|
+
</g>
|
|
362
|
+
<g>
|
|
363
|
+
<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
|
|
364
|
+
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"
|
|
365
|
+
/>
|
|
366
|
+
</g>
|
|
367
|
+
<g>
|
|
368
|
+
<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
|
|
369
|
+
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"/>
|
|
370
|
+
</g>
|
|
371
|
+
<g>
|
|
372
|
+
<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
|
|
373
|
+
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
|
|
374
|
+
"/>
|
|
375
|
+
</g>
|
|
376
|
+
<g>
|
|
377
|
+
<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
|
|
378
|
+
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"/>
|
|
379
|
+
</g>
|
|
380
|
+
</g>
|
|
381
|
+
</svg>
|
|
382
|
+
""".strip()
|
|
383
|
+
|
|
384
|
+
_info_icon = """
|
|
385
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="24px" height="24px">
|
|
386
|
+
<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>
|
|
387
|
+
<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"/>
|
|
388
|
+
</svg>
|
|
389
|
+
""".strip()
|
|
@@ -125,19 +125,19 @@ class JobClient:
|
|
|
125
125
|
"""
|
|
126
126
|
return self._service.get_by_id(_to_uuid(job_id))
|
|
127
127
|
|
|
128
|
-
def display(self,
|
|
128
|
+
def display(self, job_id: JobIDLike, direction: str = "down", layout: str = "dagre", sketchy: bool = True) -> None:
|
|
129
129
|
"""Create a visualization of the job as a diagram and display it in an interactive environment.
|
|
130
130
|
|
|
131
131
|
Requires an IPython environment such as a Jupyter notebook.
|
|
132
132
|
|
|
133
133
|
Args:
|
|
134
|
-
|
|
134
|
+
job_id: The job or job id to visualize.
|
|
135
135
|
direction: The direction of the diagram. Defaults to "down". See https://d2lang.com/tour/layouts/#direction
|
|
136
136
|
layout: The layout to use for the diagram. Defaults to "dagre". Currently supported layouts are
|
|
137
137
|
"dagre" and "elk". See https://d2lang.com/tour/layouts/
|
|
138
138
|
sketchy: Whether to render the diagram in a sketchy hand drawn style. Defaults to True.
|
|
139
139
|
"""
|
|
140
|
-
display(HTML(self.visualize(
|
|
140
|
+
display(HTML(self.visualize(job_id, direction, layout, sketchy)))
|
|
141
141
|
|
|
142
142
|
def visualize(self, job: JobIDLike, direction: str = "down", layout: str = "dagre", sketchy: bool = True) -> str:
|
|
143
143
|
"""Create a visualization of the job as a diagram.
|
|
@@ -11,10 +11,12 @@ from tilebox.workflows.data import (
|
|
|
11
11
|
TaskSubmission,
|
|
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,
|
|
@@ -43,12 +45,16 @@ class JobService:
|
|
|
43
45
|
job_name=job_name,
|
|
44
46
|
trace_parent=trace_parent,
|
|
45
47
|
)
|
|
46
|
-
return
|
|
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
|
|
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
|
-
|
|
82
|
+
|
|
83
|
+
return QueryJobsResponse.from_message(
|
|
84
|
+
response, job_factory=lambda job: RichDisplayJob.from_message(job, _widget=JobWidget(self.get_progress))
|
|
85
|
+
)
|
|
File without changes
|