dirigent-client 0.9.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.
- dirigent_client/__init__.py +219 -0
- dirigent_client/client.py +143 -0
- dirigent_client/enums.py +215 -0
- dirigent_client/errors.py +140 -0
- dirigent_client/py.typed +0 -0
- dirigent_client/resources/__init__.py +1 -0
- dirigent_client/resources/alerts.py +97 -0
- dirigent_client/resources/auth.py +155 -0
- dirigent_client/resources/base.py +52 -0
- dirigent_client/resources/blocks.py +16 -0
- dirigent_client/resources/connections.py +50 -0
- dirigent_client/resources/pipelines.py +209 -0
- dirigent_client/resources/runs.py +257 -0
- dirigent_client/resources/schedules.py +130 -0
- dirigent_client/resources/schemas.py +45 -0
- dirigent_client/resources/system.py +36 -0
- dirigent_client/resources/trigger_documents.py +20 -0
- dirigent_client/resources/webhooks.py +80 -0
- dirigent_client/schemas/__init__.py +164 -0
- dirigent_client/schemas/alerts.py +90 -0
- dirigent_client/schemas/auth.py +115 -0
- dirigent_client/schemas/catalog.py +66 -0
- dirigent_client/schemas/common.py +38 -0
- dirigent_client/schemas/connections.py +56 -0
- dirigent_client/schemas/pipelines.py +326 -0
- dirigent_client/schemas/runs.py +203 -0
- dirigent_client/schemas/schemas.py +54 -0
- dirigent_client/schemas/system.py +100 -0
- dirigent_client/schemas/triggers.py +208 -0
- dirigent_client/transport.py +191 -0
- dirigent_client-0.9.0.dist-info/METADATA +36 -0
- dirigent_client-0.9.0.dist-info/RECORD +34 -0
- dirigent_client-0.9.0.dist-info/WHEEL +4 -0
- dirigent_client-0.9.0.dist-info/licenses/LICENSE +18 -0
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
"""Pipelines: what a listing shows, what an apply sends, and what an apply reports back."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from enum import StrEnum
|
|
5
|
+
from uuid import UUID
|
|
6
|
+
|
|
7
|
+
from pydantic import AliasChoices, BaseModel, Field, field_validator, model_validator
|
|
8
|
+
|
|
9
|
+
from dirigent_client.enums import DocumentKind, LogLevel, ProvenanceSource, RunPriority, RunStatus
|
|
10
|
+
from dirigent_client.schemas.common import WireModel
|
|
11
|
+
from dirigent_common import JsonMap
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class LastRun(WireModel):
|
|
15
|
+
"""The most recent run of a pipeline, as a listing shows it."""
|
|
16
|
+
|
|
17
|
+
id: UUID
|
|
18
|
+
status: RunStatus
|
|
19
|
+
started_at: datetime | None = None
|
|
20
|
+
finished_at: datetime | None = None
|
|
21
|
+
failed_step: str | None = None
|
|
22
|
+
"""The step whose first failed attempt this run holds, when it did not end well."""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class PipelineOut(WireModel):
|
|
26
|
+
"""A pipeline as a listing shows it."""
|
|
27
|
+
|
|
28
|
+
id: UUID
|
|
29
|
+
code: str
|
|
30
|
+
name: str | None = None
|
|
31
|
+
description: str | None = None
|
|
32
|
+
tags: list[str] = Field(default_factory=list[str])
|
|
33
|
+
"""What the current document says this pipeline is for."""
|
|
34
|
+
|
|
35
|
+
active: bool
|
|
36
|
+
current_version: int | None = None
|
|
37
|
+
active_runs: int = 0
|
|
38
|
+
schedules: int = 0
|
|
39
|
+
"""How many clocks fire this pipeline."""
|
|
40
|
+
|
|
41
|
+
webhooks: int = 0
|
|
42
|
+
"""How many inbound endpoints fire this pipeline."""
|
|
43
|
+
|
|
44
|
+
last_run: LastRun | None = None
|
|
45
|
+
"""How the newest run of this pipeline went, or nothing when it has never run."""
|
|
46
|
+
|
|
47
|
+
created_at: datetime
|
|
48
|
+
updated_at: datetime
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class PipelineVersionOut(WireModel):
|
|
52
|
+
"""One immutable version, its digest, and where it came from."""
|
|
53
|
+
|
|
54
|
+
id: UUID
|
|
55
|
+
version: int
|
|
56
|
+
digest: str
|
|
57
|
+
provenance_source: ProvenanceSource
|
|
58
|
+
provenance_ref: str | None = None
|
|
59
|
+
applied_by: str | None = None
|
|
60
|
+
created_at: datetime
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class PipelineDetail(PipelineOut):
|
|
64
|
+
"""A pipeline plus the document its current version holds."""
|
|
65
|
+
|
|
66
|
+
document: JsonMap | None = None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class ApplyRequest(BaseModel):
|
|
70
|
+
"""A whole ``dirigent/v1`` document, optionally registered under a different code."""
|
|
71
|
+
|
|
72
|
+
document: JsonMap
|
|
73
|
+
code: str | None = Field(default=None, description="Register under this code instead of the document's own.")
|
|
74
|
+
source: ProvenanceSource = ProvenanceSource.API
|
|
75
|
+
source_ref: str | None = Field(default=None, description="The file path or URL the document came from.")
|
|
76
|
+
pause_schedules: bool = Field(
|
|
77
|
+
default=False,
|
|
78
|
+
description="Create the schedules this apply brings into being already paused.",
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class PruneRequest(BaseModel):
|
|
83
|
+
"""The codes a reconcile keeps: everything else it once applied is deactivated."""
|
|
84
|
+
|
|
85
|
+
keep: list[str]
|
|
86
|
+
"""Every code the reconciled directory holds. A directory-provenance pipeline absent
|
|
87
|
+
from this list is deactivated; a pipeline applied any other way is never touched."""
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class PruneResult(BaseModel):
|
|
91
|
+
"""What a prune deactivated and deleted, or would have."""
|
|
92
|
+
|
|
93
|
+
pruned: list[str]
|
|
94
|
+
trigger_documents_removed: list[str] = Field(default_factory=list[str])
|
|
95
|
+
"""The triggers documents a directory no longer holds, deleted with the rows they owned."""
|
|
96
|
+
|
|
97
|
+
dry_run: bool = False
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class RunRequest(BaseModel):
|
|
101
|
+
"""Ad hoc run parameters, validated against the pipeline's own schema."""
|
|
102
|
+
|
|
103
|
+
params: JsonMap = Field(default_factory=dict)
|
|
104
|
+
priority: RunPriority | None = Field(
|
|
105
|
+
default=None,
|
|
106
|
+
description=(
|
|
107
|
+
"How far ahead of other runs this run's attempts are claimed. Omitted takes the pipeline's own priority."
|
|
108
|
+
),
|
|
109
|
+
)
|
|
110
|
+
window_start: datetime | None = Field(
|
|
111
|
+
default=None,
|
|
112
|
+
description="Start of the logical data interval this run covers, inclusive.",
|
|
113
|
+
)
|
|
114
|
+
window_end: datetime | None = Field(
|
|
115
|
+
default=None,
|
|
116
|
+
description="End of that interval, exclusive. Both are given or neither is.",
|
|
117
|
+
)
|
|
118
|
+
log_levels: dict[str, LogLevel] | None = Field(
|
|
119
|
+
default=None,
|
|
120
|
+
description=(
|
|
121
|
+
"Which log levels this run keeps, as a map of block-id pattern to level; the most "
|
|
122
|
+
"specific pattern wins. Omitted keeps info and up."
|
|
123
|
+
),
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
@field_validator("log_levels")
|
|
127
|
+
@classmethod
|
|
128
|
+
def _a_pattern_names_something(cls, value: dict[str, LogLevel] | None) -> dict[str, LogLevel] | None:
|
|
129
|
+
"""Refuse an empty pattern, which would match nothing and say nothing."""
|
|
130
|
+
if value is not None and any(not pattern for pattern in value):
|
|
131
|
+
raise ValueError("log_levels: a pattern may not be empty")
|
|
132
|
+
return value
|
|
133
|
+
|
|
134
|
+
@model_validator(mode="after")
|
|
135
|
+
def _a_window_has_two_ends(self) -> "RunRequest":
|
|
136
|
+
"""Refuse a half-declared window, which no step could read as an interval."""
|
|
137
|
+
if (self.window_start is None) != (self.window_end is None):
|
|
138
|
+
raise ValueError("a window has two ends: give both window_start and window_end, or neither")
|
|
139
|
+
if self.window_start is not None and self.window_end is not None and self.window_start >= self.window_end:
|
|
140
|
+
raise ValueError(
|
|
141
|
+
f"a window runs forwards and covers something: {self.window_start.isoformat()} "
|
|
142
|
+
f"is not before {self.window_end.isoformat()}"
|
|
143
|
+
)
|
|
144
|
+
return self
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class RunAccepted(WireModel):
|
|
148
|
+
"""What starting a run returns: the run id, or why nothing started."""
|
|
149
|
+
|
|
150
|
+
run_id: UUID | None = None
|
|
151
|
+
status: str
|
|
152
|
+
detail: str | None = None
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class BackfillRequest(BaseModel):
|
|
156
|
+
"""One backfill: whose cadence defines the windows, what interval to fill, and with what.
|
|
157
|
+
|
|
158
|
+
``from_`` and ``to`` bound the firings enumerated, half-open, and each enumerated firing
|
|
159
|
+
becomes one run carrying the window that firing would have covered.
|
|
160
|
+
"""
|
|
161
|
+
|
|
162
|
+
schedule: str = Field(description="The schedule on this pipeline whose cadence defines the windows.")
|
|
163
|
+
#: ``from`` is a Python keyword, so the field is ``from_`` here and ``from`` on the wire.
|
|
164
|
+
from_: datetime = Field(
|
|
165
|
+
serialization_alias="from",
|
|
166
|
+
validation_alias=AliasChoices("from", "from_"),
|
|
167
|
+
description="First instant of the interval to enumerate, inclusive.",
|
|
168
|
+
)
|
|
169
|
+
to: datetime = Field(description="Last instant of the interval to enumerate, exclusive.")
|
|
170
|
+
params: JsonMap | None = Field(
|
|
171
|
+
default=None,
|
|
172
|
+
description="Parameters every run created here is given; the schedule's own pins when omitted.",
|
|
173
|
+
)
|
|
174
|
+
dry_run: bool = Field(default=False, description="Answer with the plan and create nothing.")
|
|
175
|
+
|
|
176
|
+
@model_validator(mode="after")
|
|
177
|
+
def _runs_forwards(self) -> "BackfillRequest":
|
|
178
|
+
"""Refuse an empty or backwards interval, which encloses no firing at all."""
|
|
179
|
+
if self.from_ >= self.to:
|
|
180
|
+
raise ValueError(
|
|
181
|
+
f"a backfill runs forwards and covers something: {self.from_.isoformat()} "
|
|
182
|
+
f"is not before {self.to.isoformat()}"
|
|
183
|
+
)
|
|
184
|
+
return self
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
class BackfilledRun(WireModel):
|
|
188
|
+
"""One window a backfill enumerated, and the run it created for it."""
|
|
189
|
+
|
|
190
|
+
window_start: datetime
|
|
191
|
+
window_end: datetime
|
|
192
|
+
run_id: UUID | None = None
|
|
193
|
+
"""The run created for this window, or None on a dry run and where the policy refused one."""
|
|
194
|
+
|
|
195
|
+
detail: str | None = None
|
|
196
|
+
"""Why no run was created, when none was."""
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
class BackfillAccepted(WireModel):
|
|
200
|
+
"""What a backfill amounted to: the windows it enumerated, in chronological order."""
|
|
201
|
+
|
|
202
|
+
pipeline: str
|
|
203
|
+
schedule: str
|
|
204
|
+
dry_run: bool = False
|
|
205
|
+
windows: list[BackfilledRun] = Field(default_factory=list[BackfilledRun])
|
|
206
|
+
|
|
207
|
+
@property
|
|
208
|
+
def created(self) -> int:
|
|
209
|
+
"""How many runs this backfill actually created."""
|
|
210
|
+
return sum(1 for window in self.windows if window.run_id is not None)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
class ValidationIssue(WireModel):
|
|
214
|
+
"""One problem with a document, addressed at the place in it that is wrong."""
|
|
215
|
+
|
|
216
|
+
location: str
|
|
217
|
+
"""Where the problem is, as a dotted document path such as ``steps.push.config.method``."""
|
|
218
|
+
|
|
219
|
+
message: str
|
|
220
|
+
"""What is wrong, in the terms the person editing the document is thinking in."""
|
|
221
|
+
|
|
222
|
+
def __str__(self) -> str:
|
|
223
|
+
"""Render the issue as one line."""
|
|
224
|
+
return f"{self.location}: {self.message}"
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
class PlanAction(StrEnum):
|
|
228
|
+
"""What applying a document would do to the instance."""
|
|
229
|
+
|
|
230
|
+
CREATE = "create"
|
|
231
|
+
"""No pipeline holds this code; applying mints one with version 1."""
|
|
232
|
+
|
|
233
|
+
UPDATE = "update"
|
|
234
|
+
"""The code exists and the document differs; applying writes the next version."""
|
|
235
|
+
|
|
236
|
+
UNCHANGED = "unchanged"
|
|
237
|
+
"""The digest matches the current version; applying writes nothing."""
|
|
238
|
+
|
|
239
|
+
INVALID = "invalid"
|
|
240
|
+
"""The document did not validate against this instance; applying is refused."""
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
class DiffSummary(WireModel):
|
|
244
|
+
"""What changed between the current version and the document being applied."""
|
|
245
|
+
|
|
246
|
+
steps_added: list[str] = Field(default_factory=list[str])
|
|
247
|
+
steps_removed: list[str] = Field(default_factory=list[str])
|
|
248
|
+
steps_changed: list[str] = Field(default_factory=list[str])
|
|
249
|
+
params_changed: bool = False
|
|
250
|
+
triggers_changed: bool = False
|
|
251
|
+
settings_changed: bool = False
|
|
252
|
+
"""Name, description, or concurrency policy differs."""
|
|
253
|
+
|
|
254
|
+
@property
|
|
255
|
+
def empty(self) -> bool:
|
|
256
|
+
"""Report whether the two definitions are the same in every respect summarised here."""
|
|
257
|
+
return not (
|
|
258
|
+
self.steps_added
|
|
259
|
+
or self.steps_removed
|
|
260
|
+
or self.steps_changed
|
|
261
|
+
or self.params_changed
|
|
262
|
+
or self.triggers_changed
|
|
263
|
+
or self.settings_changed
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
class Materialized(WireModel):
|
|
268
|
+
"""What applying a document did to its triggers."""
|
|
269
|
+
|
|
270
|
+
schedules_created: list[str] = Field(default_factory=list[str])
|
|
271
|
+
schedules_updated: list[str] = Field(default_factory=list[str])
|
|
272
|
+
schedules_removed: list[str] = Field(default_factory=list[str])
|
|
273
|
+
webhooks_created: list[str] = Field(default_factory=list[str])
|
|
274
|
+
webhooks_updated: list[str] = Field(default_factory=list[str])
|
|
275
|
+
webhooks_removed: list[str] = Field(default_factory=list[str])
|
|
276
|
+
|
|
277
|
+
@property
|
|
278
|
+
def empty(self) -> bool:
|
|
279
|
+
"""Report whether the apply left every trigger exactly as it was."""
|
|
280
|
+
return not (
|
|
281
|
+
self.schedules_created
|
|
282
|
+
or self.schedules_updated
|
|
283
|
+
or self.schedules_removed
|
|
284
|
+
or self.webhooks_created
|
|
285
|
+
or self.webhooks_updated
|
|
286
|
+
or self.webhooks_removed
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
class PipelinePlan(WireModel):
|
|
291
|
+
"""What applying a document would do, before anything is written."""
|
|
292
|
+
|
|
293
|
+
code: str
|
|
294
|
+
action: PlanAction
|
|
295
|
+
digest: str
|
|
296
|
+
pipeline: str | None = None
|
|
297
|
+
"""The pipeline a triggers document declares clocks for; a pipeline document names none."""
|
|
298
|
+
|
|
299
|
+
current_version: int | None = None
|
|
300
|
+
next_version: int | None = None
|
|
301
|
+
issues: list[ValidationIssue] = Field(default_factory=list[ValidationIssue])
|
|
302
|
+
warnings: list[ValidationIssue] = Field(default_factory=list[ValidationIssue])
|
|
303
|
+
"""What is worth saying but does not refuse the apply, such as a worker tag nobody carries."""
|
|
304
|
+
|
|
305
|
+
diff: DiffSummary | None = None
|
|
306
|
+
|
|
307
|
+
@property
|
|
308
|
+
def ok(self) -> bool:
|
|
309
|
+
"""Report whether this document could be applied as it stands."""
|
|
310
|
+
return self.action is not PlanAction.INVALID
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
class ApplyResult(WireModel):
|
|
314
|
+
"""The outcome of an apply."""
|
|
315
|
+
|
|
316
|
+
kind: DocumentKind = DocumentKind.PIPELINE
|
|
317
|
+
"""Which kind of document was applied; a triggers document writes no version."""
|
|
318
|
+
|
|
319
|
+
plan: PipelinePlan
|
|
320
|
+
pipeline_id: UUID | None = None
|
|
321
|
+
"""The pipeline this apply wrote, or the one a triggers document declares clocks for."""
|
|
322
|
+
|
|
323
|
+
version: int | None = None
|
|
324
|
+
dry_run: bool = False
|
|
325
|
+
triggers: Materialized = Field(default_factory=Materialized)
|
|
326
|
+
"""Which schedules and webhooks the document's ``triggers:`` section created, changed, or retired."""
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
"""Runs: the listing row, the detail with its DAG view model, log pages, and the report."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from uuid import UUID
|
|
5
|
+
|
|
6
|
+
from pydantic import Field
|
|
7
|
+
|
|
8
|
+
from dirigent_client.enums import (
|
|
9
|
+
AttemptKind,
|
|
10
|
+
AttemptStatus,
|
|
11
|
+
LogLevel,
|
|
12
|
+
RunItemStatus,
|
|
13
|
+
RunPriority,
|
|
14
|
+
RunStatus,
|
|
15
|
+
TriggerKind,
|
|
16
|
+
)
|
|
17
|
+
from dirigent_client.schemas.common import WireModel
|
|
18
|
+
from dirigent_common import JsonMap
|
|
19
|
+
|
|
20
|
+
TERMINAL_RUN_STATUSES: frozenset[RunStatus] = frozenset(
|
|
21
|
+
{
|
|
22
|
+
RunStatus.SUCCEEDED,
|
|
23
|
+
RunStatus.COMPLETED_WITH_ERRORS,
|
|
24
|
+
RunStatus.FAILED,
|
|
25
|
+
RunStatus.CANCELLED,
|
|
26
|
+
}
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class RunOut(WireModel):
|
|
31
|
+
"""A run as a listing shows it."""
|
|
32
|
+
|
|
33
|
+
id: UUID
|
|
34
|
+
pipeline: str
|
|
35
|
+
pipeline_version: int
|
|
36
|
+
status: RunStatus
|
|
37
|
+
priority: RunPriority = RunPriority.NORMAL
|
|
38
|
+
"""The priority the claim takes this run's attempts at, pinned when it was created."""
|
|
39
|
+
params: JsonMap = Field(default_factory=dict)
|
|
40
|
+
log_levels: dict[str, LogLevel] | None = None
|
|
41
|
+
"""Which log levels this run keeps, by block-id pattern; None keeps info and up."""
|
|
42
|
+
triggered_by_kind: TriggerKind
|
|
43
|
+
triggered_by_label: str | None = None
|
|
44
|
+
trace_id: str | None = None
|
|
45
|
+
error: str | None = None
|
|
46
|
+
failed_step: str | None = None
|
|
47
|
+
"""The step whose first failed attempt this run holds, when it did not end well."""
|
|
48
|
+
started_at: datetime | None = None
|
|
49
|
+
finished_at: datetime | None = None
|
|
50
|
+
window_start: datetime | None = None
|
|
51
|
+
"""The start of the logical data interval this run covers, when it carries one."""
|
|
52
|
+
|
|
53
|
+
window_end: datetime | None = None
|
|
54
|
+
"""The end of that interval, exclusive. Both are set or neither is."""
|
|
55
|
+
|
|
56
|
+
created_at: datetime
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def terminal(self) -> bool:
|
|
60
|
+
"""Report whether this run has reached a status nothing will move it out of."""
|
|
61
|
+
return self.status in TERMINAL_RUN_STATUSES
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class AttemptOut(WireModel):
|
|
65
|
+
"""One try of one step, which is the unit a retry addresses."""
|
|
66
|
+
|
|
67
|
+
id: UUID
|
|
68
|
+
step_name: str
|
|
69
|
+
block_id: str
|
|
70
|
+
attempt: int
|
|
71
|
+
kind: AttemptKind
|
|
72
|
+
status: AttemptStatus
|
|
73
|
+
run_item_id: UUID | None = None
|
|
74
|
+
error: str | None = None
|
|
75
|
+
error_class: str | None = None
|
|
76
|
+
output: JsonMap | None = None
|
|
77
|
+
output_uri: str | None = None
|
|
78
|
+
"""Where the output was written, when it was too large to inline on the artifact."""
|
|
79
|
+
|
|
80
|
+
output_bytes: int | None = None
|
|
81
|
+
waiting_message: str | None = None
|
|
82
|
+
waiting_progress: float | None = None
|
|
83
|
+
poke_count: int = 0
|
|
84
|
+
"""How many times a probe answered that the work was not done yet."""
|
|
85
|
+
|
|
86
|
+
next_poll_at: datetime | None = None
|
|
87
|
+
available_at: datetime | None = None
|
|
88
|
+
"""When this attempt became claimable, which a retry's backoff pushes into the future."""
|
|
89
|
+
|
|
90
|
+
deadline_at: datetime | None = None
|
|
91
|
+
"""When the engine gives up on this attempt, which is the budget its waiting is spent against."""
|
|
92
|
+
|
|
93
|
+
heartbeat_at: datetime | None = None
|
|
94
|
+
"""When a worker last took this attempt up, which on a parked attempt is its last probe."""
|
|
95
|
+
|
|
96
|
+
started_at: datetime | None = None
|
|
97
|
+
finished_at: datetime | None = None
|
|
98
|
+
created_at: datetime | None = None
|
|
99
|
+
"""When the attempt was written down, which is where its queued time is measured from."""
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class AttemptEvent(AttemptOut):
|
|
103
|
+
"""One attempt state on the run's event stream, with the label a person knows the item by."""
|
|
104
|
+
|
|
105
|
+
item: str | None = None
|
|
106
|
+
item_index: int | None = None
|
|
107
|
+
"""Where the element sits in the fan-out, which is the order a reader lists the elements in."""
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class ItemOut(WireModel):
|
|
111
|
+
"""One element of a fan-out step."""
|
|
112
|
+
|
|
113
|
+
id: UUID
|
|
114
|
+
step_name: str
|
|
115
|
+
item_index: int
|
|
116
|
+
item_key: str
|
|
117
|
+
status: RunItemStatus
|
|
118
|
+
failing_step: str | None = None
|
|
119
|
+
error: str | None = None
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class DagNode(WireModel):
|
|
123
|
+
"""One node of the run's graph."""
|
|
124
|
+
|
|
125
|
+
code: str
|
|
126
|
+
"""The step's key in the document, which is what every edge and every attempt references."""
|
|
127
|
+
|
|
128
|
+
name: str | None = None
|
|
129
|
+
"""The step's human title, when the document gave it one."""
|
|
130
|
+
|
|
131
|
+
block: str
|
|
132
|
+
outcome: str
|
|
133
|
+
depends_on: list[str] = Field(default_factory=list[str])
|
|
134
|
+
rule: str
|
|
135
|
+
fan_out: bool = False
|
|
136
|
+
items_total: int = 0
|
|
137
|
+
items_failed: int = 0
|
|
138
|
+
attempts: int = 0
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class DagView(WireModel):
|
|
142
|
+
"""The run's graph: its nodes and the edges between them."""
|
|
143
|
+
|
|
144
|
+
nodes: list[DagNode] = Field(default_factory=list[DagNode])
|
|
145
|
+
edges: list[tuple[str, str]] = Field(default_factory=list[tuple[str, str]])
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class RunDetail(WireModel):
|
|
149
|
+
"""A run, the DAG view model, and how big the grids its sub-resources page are."""
|
|
150
|
+
|
|
151
|
+
run: RunOut
|
|
152
|
+
dag: DagView
|
|
153
|
+
items_total: int = 0
|
|
154
|
+
attempts_total: int = 0
|
|
155
|
+
waiting_for_workers: list[str] | None = None
|
|
156
|
+
"""The tags this queued run needs that no live worker carries; null when nothing blocks it."""
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class LogEntryOut(WireModel):
|
|
160
|
+
"""One product-telemetry entry."""
|
|
161
|
+
|
|
162
|
+
id: int
|
|
163
|
+
run_id: UUID
|
|
164
|
+
step_name: str | None = None
|
|
165
|
+
step_attempt_id: UUID | None = None
|
|
166
|
+
level: LogLevel
|
|
167
|
+
message: str
|
|
168
|
+
fields: JsonMap | None = None
|
|
169
|
+
created_at: datetime
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class StepReport(WireModel):
|
|
173
|
+
"""What one step amounted to, for the run report."""
|
|
174
|
+
|
|
175
|
+
step: str
|
|
176
|
+
block: str
|
|
177
|
+
outcome: str
|
|
178
|
+
attempts: int
|
|
179
|
+
depends_on: list[str] = Field(default_factory=list[str])
|
|
180
|
+
"""The steps this one waited for, which is why it ran when it did."""
|
|
181
|
+
|
|
182
|
+
warnings: int = 0
|
|
183
|
+
"""How many warnings or errors this step logged, whatever it settled as."""
|
|
184
|
+
|
|
185
|
+
duration_ms: int | None = None
|
|
186
|
+
error: str | None = None
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
class RunReport(WireModel):
|
|
190
|
+
"""A plain summary of one run."""
|
|
191
|
+
|
|
192
|
+
run_id: UUID
|
|
193
|
+
pipeline: str
|
|
194
|
+
pipeline_version: int
|
|
195
|
+
status: RunStatus
|
|
196
|
+
triggered_by: str | None = None
|
|
197
|
+
started_at: datetime | None = None
|
|
198
|
+
finished_at: datetime | None = None
|
|
199
|
+
duration_ms: int | None = None
|
|
200
|
+
steps: list[StepReport] = Field(default_factory=list[StepReport])
|
|
201
|
+
items_total: int = 0
|
|
202
|
+
items_failed: int = 0
|
|
203
|
+
error: str | None = None
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Schemas: named JSON Schemas an instance holds, addressable by code and referenced by name."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from uuid import UUID
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel, Field
|
|
7
|
+
|
|
8
|
+
from dirigent_client.schemas.common import WireModel
|
|
9
|
+
from dirigent_common import EntityName, JsonMap
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SchemaIn(BaseModel):
|
|
13
|
+
"""What storing a schema sends: the JSON Schema itself, and an identity it need not repeat.
|
|
14
|
+
|
|
15
|
+
The body is a JSON Schema in its own right. Its identity is read from the schema's own
|
|
16
|
+
keywords when the caller leaves it out -- ``$id`` for the code, ``title`` for the name,
|
|
17
|
+
``description`` for the description -- so a portable schema file stores as one without a
|
|
18
|
+
dirigent wrapper. A code given here wins over ``$id``; a code given neither way, with no
|
|
19
|
+
``$id`` and no filename to fall back on, is refused.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
code: EntityName | None = None
|
|
23
|
+
name: str | None = None
|
|
24
|
+
description: str | None = None
|
|
25
|
+
body: JsonMap = Field(default_factory=dict)
|
|
26
|
+
"""The JSON Schema, checked to be a valid schema before anything is stored."""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class SchemaUpdate(BaseModel):
|
|
30
|
+
"""What editing a schema may change; the code is fixed once minted.
|
|
31
|
+
|
|
32
|
+
A field left out is left alone, and a field sent as null is cleared. The body, when
|
|
33
|
+
sent, replaces the stored schema and is checked the same way a create's is.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
name: str | None = None
|
|
37
|
+
description: str | None = None
|
|
38
|
+
body: JsonMap | None = None
|
|
39
|
+
|
|
40
|
+
def changing(self, field: str) -> bool:
|
|
41
|
+
"""Report whether the request said anything about this field at all."""
|
|
42
|
+
return field in self.model_fields_set
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class SchemaOut(WireModel):
|
|
46
|
+
"""A schema as every response shows it: the identity quartet and the schema body."""
|
|
47
|
+
|
|
48
|
+
id: UUID
|
|
49
|
+
code: str
|
|
50
|
+
name: str | None = None
|
|
51
|
+
description: str | None = None
|
|
52
|
+
body: JsonMap
|
|
53
|
+
created_at: datetime
|
|
54
|
+
updated_at: datetime
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""What this instance is, whether it is well, and which nodes are picking work up."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from enum import StrEnum
|
|
5
|
+
from typing import Literal
|
|
6
|
+
from uuid import UUID
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel, Field
|
|
9
|
+
|
|
10
|
+
from dirigent_client.enums import WorkerStatus
|
|
11
|
+
from dirigent_client.schemas.common import WireModel
|
|
12
|
+
from dirigent_common import JsonMap
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ConnectionHealth(WireModel):
|
|
16
|
+
"""What one connection reported when it was asked."""
|
|
17
|
+
|
|
18
|
+
code: str
|
|
19
|
+
name: str | None = None
|
|
20
|
+
kind: str
|
|
21
|
+
connected: bool
|
|
22
|
+
detail: str | None = None
|
|
23
|
+
version: str | None = None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class SystemInfo(WireModel):
|
|
27
|
+
"""What this instance is, and whether it is well."""
|
|
28
|
+
|
|
29
|
+
name: str = "dirigent"
|
|
30
|
+
version: str
|
|
31
|
+
environment: str
|
|
32
|
+
database: str
|
|
33
|
+
checked_at: datetime
|
|
34
|
+
plugins: list[str] = Field(default_factory=list[str])
|
|
35
|
+
blocks: int = 0
|
|
36
|
+
storage_schemes: list[str] = Field(default_factory=list[str])
|
|
37
|
+
"""Every scheme a URI in a document may address, including the one core ships."""
|
|
38
|
+
|
|
39
|
+
notifiers: list[str] = Field(default_factory=list[str])
|
|
40
|
+
connection_kinds: list[str] = Field(default_factory=list[str])
|
|
41
|
+
workers_live: int = 0
|
|
42
|
+
unsafe_blocks_enabled: list[str] = Field(default_factory=list[str])
|
|
43
|
+
secrets_configured: bool = False
|
|
44
|
+
connections: list[ConnectionHealth] = Field(default_factory=list[ConnectionHealth])
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class WorkerOut(WireModel):
|
|
48
|
+
"""One worker as the registry knows it."""
|
|
49
|
+
|
|
50
|
+
id: UUID
|
|
51
|
+
name: str
|
|
52
|
+
hostname: str
|
|
53
|
+
version: str
|
|
54
|
+
status: WorkerStatus
|
|
55
|
+
concurrency: int
|
|
56
|
+
tags: list[str] = Field(default_factory=list[str])
|
|
57
|
+
plugins: JsonMap = Field(default_factory=dict)
|
|
58
|
+
catalog_digest: str | None = None
|
|
59
|
+
code_matches_server: bool = True
|
|
60
|
+
"""Whether this worker's catalog is the same code the server is running."""
|
|
61
|
+
|
|
62
|
+
stale: bool = False
|
|
63
|
+
"""Whether its heartbeat is old enough that it is presumed gone."""
|
|
64
|
+
|
|
65
|
+
created_at: datetime
|
|
66
|
+
last_seen_at: datetime
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class CheckStatus(StrEnum):
|
|
70
|
+
"""How one dependency is doing; the worst status across checks becomes the answer."""
|
|
71
|
+
|
|
72
|
+
HEALTHY = "healthy"
|
|
73
|
+
"""Reachable and behaving."""
|
|
74
|
+
|
|
75
|
+
DEGRADED = "degraded"
|
|
76
|
+
"""Usable, but not as it should be; readiness still succeeds."""
|
|
77
|
+
|
|
78
|
+
UNHEALTHY = "unhealthy"
|
|
79
|
+
"""Unusable; readiness fails and the process should not receive traffic."""
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class CheckResult(BaseModel):
|
|
83
|
+
"""What one health check reports."""
|
|
84
|
+
|
|
85
|
+
status: CheckStatus
|
|
86
|
+
detail: str | None = None
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class Health(BaseModel):
|
|
90
|
+
"""The liveness answer: the process is running and can serve requests."""
|
|
91
|
+
|
|
92
|
+
status: Literal["ok"] = "ok"
|
|
93
|
+
version: str
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class Readiness(BaseModel):
|
|
97
|
+
"""The readiness answer: the worst status across the checks, and each check's own."""
|
|
98
|
+
|
|
99
|
+
status: CheckStatus
|
|
100
|
+
checks: dict[str, CheckResult]
|