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.
@@ -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]