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,208 @@
|
|
|
1
|
+
"""A pipeline's persisted triggers: its schedules and its inbound webhooks."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from uuid import UUID
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel, Field, SecretStr, field_validator
|
|
7
|
+
|
|
8
|
+
from dirigent_client.enums import (
|
|
9
|
+
FiringOutcome,
|
|
10
|
+
LogLevel,
|
|
11
|
+
ProvenanceSource,
|
|
12
|
+
RunPriority,
|
|
13
|
+
ScheduleKind,
|
|
14
|
+
WebhookOutcome,
|
|
15
|
+
)
|
|
16
|
+
from dirigent_client.schemas.common import WireModel
|
|
17
|
+
from dirigent_common import EntityName, JsonMap
|
|
18
|
+
from dirigent_common.durations import Duration
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ScheduleIn(BaseModel):
|
|
22
|
+
"""A schedule as a caller declares it: exactly one clock, in its own timezone."""
|
|
23
|
+
|
|
24
|
+
code: EntityName
|
|
25
|
+
name: str | None = None
|
|
26
|
+
description: str | None = None
|
|
27
|
+
cron: str | None = None
|
|
28
|
+
interval: Duration | None = None
|
|
29
|
+
at: datetime | None = None
|
|
30
|
+
timezone: str = "UTC"
|
|
31
|
+
params: JsonMap = Field(default_factory=dict)
|
|
32
|
+
connection_pins: JsonMap = Field(default_factory=dict)
|
|
33
|
+
log_levels: dict[str, LogLevel] | None = Field(
|
|
34
|
+
default=None,
|
|
35
|
+
description=(
|
|
36
|
+
"The log-level map every fired run carries, as block-id patterns; the most "
|
|
37
|
+
"specific pattern wins. Omitted keeps info and up."
|
|
38
|
+
),
|
|
39
|
+
)
|
|
40
|
+
priority: RunPriority | None = Field(
|
|
41
|
+
default=None,
|
|
42
|
+
description="The priority every fired run carries. Omitted takes the pipeline's own.",
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
@field_validator("log_levels")
|
|
46
|
+
@classmethod
|
|
47
|
+
def _a_pattern_names_something(cls, value: dict[str, LogLevel] | None) -> dict[str, LogLevel] | None:
|
|
48
|
+
"""Refuse an empty pattern, which would match nothing and say nothing."""
|
|
49
|
+
if value is not None and any(not pattern for pattern in value):
|
|
50
|
+
raise ValueError("log_levels: a pattern may not be empty")
|
|
51
|
+
return value
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class SchedulePreviewRequest(BaseModel):
|
|
55
|
+
"""A clock as it is being written, for reading back what it would fire.
|
|
56
|
+
|
|
57
|
+
The three expressions are text rather than parsed values: a clock is previewed while it is
|
|
58
|
+
being typed, so a half-written interval is an answer saying what is wrong with it rather
|
|
59
|
+
than a request the server refuses to read.
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
cron: str | None = None
|
|
63
|
+
interval: str | None = None
|
|
64
|
+
at: str | None = None
|
|
65
|
+
timezone: str = "UTC"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class SchedulePreview(WireModel):
|
|
69
|
+
"""The next firings of a clock nothing has declared yet, oldest first."""
|
|
70
|
+
|
|
71
|
+
firings: list[datetime]
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class ScheduleOut(WireModel):
|
|
75
|
+
"""A schedule as a listing shows it."""
|
|
76
|
+
|
|
77
|
+
id: UUID
|
|
78
|
+
code: str
|
|
79
|
+
name: str | None = None
|
|
80
|
+
description: str | None = None
|
|
81
|
+
kind: ScheduleKind
|
|
82
|
+
cron: str | None = None
|
|
83
|
+
interval: str | None = None
|
|
84
|
+
at: datetime | None = None
|
|
85
|
+
timezone: str
|
|
86
|
+
params: JsonMap = Field(default_factory=dict)
|
|
87
|
+
log_levels: dict[str, LogLevel] | None = None
|
|
88
|
+
priority: RunPriority | None = None
|
|
89
|
+
"""The priority every fired run carries, or null when it takes the pipeline's own."""
|
|
90
|
+
paused: bool
|
|
91
|
+
managed: bool
|
|
92
|
+
trigger_document: str | None = None
|
|
93
|
+
"""The triggers document that declares this row, or null when the pipeline's own does."""
|
|
94
|
+
|
|
95
|
+
next_fire_at: datetime | None = None
|
|
96
|
+
last_fired_at: datetime | None = None
|
|
97
|
+
created_at: datetime
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class FiringOut(WireModel):
|
|
101
|
+
"""One recorded firing: what it was due for, and what came of it."""
|
|
102
|
+
|
|
103
|
+
id: int
|
|
104
|
+
scheduled_for: datetime
|
|
105
|
+
created_at: datetime
|
|
106
|
+
outcome: FiringOutcome
|
|
107
|
+
misfired: bool
|
|
108
|
+
run_id: UUID | None = None
|
|
109
|
+
detail: str | None = None
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class WebhookIn(BaseModel):
|
|
113
|
+
"""A webhook as a caller declares it; the token is never supplied, only minted."""
|
|
114
|
+
|
|
115
|
+
code: EntityName
|
|
116
|
+
name: str | None = None
|
|
117
|
+
description: str | None = None
|
|
118
|
+
params_from_payload: dict[str, str] = Field(default_factory=dict[str, str])
|
|
119
|
+
hmac_secret: SecretStr | None = Field(default=None, min_length=1)
|
|
120
|
+
"""The signing secret, or null for an unsigned webhook; an empty string is neither."""
|
|
121
|
+
|
|
122
|
+
rate_limit_per_minute: int = Field(default=60, ge=1)
|
|
123
|
+
priority: RunPriority | None = Field(
|
|
124
|
+
default=None,
|
|
125
|
+
description="The priority every accepted delivery's run carries. Omitted takes the pipeline's own.",
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class WebhookOut(WireModel):
|
|
130
|
+
"""A webhook as a listing shows it: everything about it except the token."""
|
|
131
|
+
|
|
132
|
+
id: UUID
|
|
133
|
+
code: str
|
|
134
|
+
name: str | None = None
|
|
135
|
+
description: str | None = None
|
|
136
|
+
token_prefix: str
|
|
137
|
+
params_from_payload: dict[str, str] = Field(default_factory=dict[str, str])
|
|
138
|
+
signed: bool
|
|
139
|
+
"""Whether this webhook requires an HMAC signature; the secret itself never leaves."""
|
|
140
|
+
|
|
141
|
+
active: bool
|
|
142
|
+
managed: bool
|
|
143
|
+
trigger_document: str | None = None
|
|
144
|
+
"""The triggers document that declares this row, or null when the pipeline's own does."""
|
|
145
|
+
|
|
146
|
+
rate_limit_per_minute: int
|
|
147
|
+
priority: RunPriority | None = None
|
|
148
|
+
"""The priority every accepted delivery's run carries, or null when it takes the pipeline's own."""
|
|
149
|
+
|
|
150
|
+
last_delivery_at: datetime | None = None
|
|
151
|
+
created_at: datetime
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class TriggerDocumentOut(WireModel):
|
|
155
|
+
"""A ``kind: triggers`` document as a listing shows it."""
|
|
156
|
+
|
|
157
|
+
id: UUID
|
|
158
|
+
code: str
|
|
159
|
+
name: str | None = None
|
|
160
|
+
description: str | None = None
|
|
161
|
+
pipeline: str
|
|
162
|
+
"""The code of the pipeline these triggers fire."""
|
|
163
|
+
|
|
164
|
+
digest: str
|
|
165
|
+
provenance_source: ProvenanceSource
|
|
166
|
+
provenance_ref: str | None = None
|
|
167
|
+
applied_by: str | None = None
|
|
168
|
+
created_at: datetime
|
|
169
|
+
updated_at: datetime
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class TriggerDocumentDetail(TriggerDocumentOut):
|
|
173
|
+
"""The document itself, and the rows it owns on the pipeline it names."""
|
|
174
|
+
|
|
175
|
+
document: JsonMap
|
|
176
|
+
schedules: list[str] = Field(default_factory=list[str])
|
|
177
|
+
webhooks: list[str] = Field(default_factory=list[str])
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class WebhookTokenOut(WireModel):
|
|
181
|
+
"""A freshly minted webhook token, returned once and never readable again."""
|
|
182
|
+
|
|
183
|
+
webhook_id: UUID
|
|
184
|
+
code: str
|
|
185
|
+
token: str
|
|
186
|
+
prefix: str
|
|
187
|
+
url_path: str
|
|
188
|
+
"""Where to POST, so a caller can be configured without assembling the path by hand."""
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
class HookAccepted(WireModel):
|
|
192
|
+
"""What an accepted delivery answers with: the run it started, or why it started none."""
|
|
193
|
+
|
|
194
|
+
run_id: UUID | None = None
|
|
195
|
+
outcome: WebhookOutcome
|
|
196
|
+
detail: str | None = None
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
class DeliveryOut(WireModel):
|
|
200
|
+
"""One inbound delivery, and what came of it."""
|
|
201
|
+
|
|
202
|
+
id: int
|
|
203
|
+
created_at: datetime
|
|
204
|
+
outcome: WebhookOutcome
|
|
205
|
+
run_id: UUID | None = None
|
|
206
|
+
reason: str | None = None
|
|
207
|
+
mapped_params: JsonMap | None = None
|
|
208
|
+
source: str | None = None
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"""The one place a request is made, authenticated, retried, and turned into an exception."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from collections.abc import AsyncGenerator, Mapping
|
|
5
|
+
from contextlib import asynccontextmanager
|
|
6
|
+
from typing import Any, Final, Self
|
|
7
|
+
|
|
8
|
+
import httpx2
|
|
9
|
+
|
|
10
|
+
from dirigent_client.errors import (
|
|
11
|
+
VERSION_HEADER,
|
|
12
|
+
DirigentError,
|
|
13
|
+
NotDirigent,
|
|
14
|
+
TransportError,
|
|
15
|
+
error_for,
|
|
16
|
+
parse_problem,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
#: Where the API is unless an instance says otherwise. A server that sets ``api_prefix``
|
|
20
|
+
#: serves it somewhere else, and a connection has to be told the same thing.
|
|
21
|
+
API_PREFIX: Final = "/api/v1"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _normalise_prefix(prefix: str) -> str:
|
|
25
|
+
"""Spell a prefix the one way the server spells it: a leading slash, no trailing one.
|
|
26
|
+
|
|
27
|
+
The server normalises its own setting the same way, so a connection configured by hand
|
|
28
|
+
with "api/v1" or "/api/v1/" addresses the same instance as one configured with neither.
|
|
29
|
+
"""
|
|
30
|
+
trimmed = prefix.strip().strip("/")
|
|
31
|
+
return f"/{trimmed}" if trimmed else ""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
DEFAULT_TIMEOUT: Final = 30.0
|
|
35
|
+
DEFAULT_RETRIES: Final = 2
|
|
36
|
+
BACKOFF_SECONDS: Final = 0.25
|
|
37
|
+
BACKOFF_MAX_SECONDS: Final = 4.0
|
|
38
|
+
|
|
39
|
+
#: A POST or a PATCH can have taken effect before the connection broke, so repeating one
|
|
40
|
+
#: risks a second run, a second token, a second account.
|
|
41
|
+
IDEMPOTENT_METHODS: Final = frozenset({"GET", "HEAD", "OPTIONS", "PUT", "DELETE"})
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def backoff_for(attempt: int) -> float:
|
|
45
|
+
"""Say how long to wait before the given retry."""
|
|
46
|
+
return min(BACKOFF_SECONDS * 2.0**attempt, BACKOFF_MAX_SECONDS)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _retry_after(response: httpx2.Response) -> float | None:
|
|
50
|
+
"""Read a Retry-After header as seconds, ignoring the HTTP-date spelling of it."""
|
|
51
|
+
raw: str | None = response.headers.get("retry-after")
|
|
52
|
+
if raw is None:
|
|
53
|
+
return None
|
|
54
|
+
try:
|
|
55
|
+
return float(raw)
|
|
56
|
+
except ValueError:
|
|
57
|
+
return None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class Transport:
|
|
61
|
+
"""An authenticated connection to one instance."""
|
|
62
|
+
|
|
63
|
+
def __init__(
|
|
64
|
+
self,
|
|
65
|
+
*,
|
|
66
|
+
url: str,
|
|
67
|
+
token: str | None = None,
|
|
68
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
69
|
+
retries: int = DEFAULT_RETRIES,
|
|
70
|
+
headers: Mapping[str, str] | None = None,
|
|
71
|
+
http_transport: httpx2.AsyncBaseTransport | None = None,
|
|
72
|
+
api_prefix: str = API_PREFIX,
|
|
73
|
+
) -> None:
|
|
74
|
+
"""Open a connection pool bound to one base URL and one credential."""
|
|
75
|
+
self.url = url.rstrip("/")
|
|
76
|
+
self.api_prefix = _normalise_prefix(api_prefix)
|
|
77
|
+
self.retries = retries
|
|
78
|
+
sent = {**(headers or {})}
|
|
79
|
+
if token:
|
|
80
|
+
sent["Authorization"] = f"Bearer {token}"
|
|
81
|
+
self._client = httpx2.AsyncClient(
|
|
82
|
+
base_url=self.url,
|
|
83
|
+
headers=sent,
|
|
84
|
+
timeout=timeout,
|
|
85
|
+
transport=http_transport,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
def session_cookie(self, name: str) -> str | None:
|
|
89
|
+
"""Read a cookie the instance set on this connection, or report that it set none."""
|
|
90
|
+
return self._client.cookies.get(name)
|
|
91
|
+
|
|
92
|
+
def present(self, secret: str) -> None:
|
|
93
|
+
"""Present this credential on every later request of this connection.
|
|
94
|
+
|
|
95
|
+
A session arrives as a cookie, and a cookie the instance marked ``Secure`` is never
|
|
96
|
+
sent back over plain HTTP -- so a client that relied on the jar could log in and then
|
|
97
|
+
be refused by the next call. A client is not a browser: it holds the credential
|
|
98
|
+
either way, and presenting it as a bearer token is what makes a session usable
|
|
99
|
+
wherever a token is.
|
|
100
|
+
"""
|
|
101
|
+
self._client.headers["Authorization"] = f"Bearer {secret}"
|
|
102
|
+
|
|
103
|
+
async def __aenter__(self) -> Self:
|
|
104
|
+
"""Enter the connection's context."""
|
|
105
|
+
return self
|
|
106
|
+
|
|
107
|
+
async def __aexit__(self, *_: object) -> None:
|
|
108
|
+
"""Close the underlying connection pool."""
|
|
109
|
+
await self.aclose()
|
|
110
|
+
|
|
111
|
+
async def aclose(self) -> None:
|
|
112
|
+
"""Close the underlying connection pool."""
|
|
113
|
+
await self._client.aclose()
|
|
114
|
+
|
|
115
|
+
def endpoint(self, path: str, *, prefixed: bool = True) -> str:
|
|
116
|
+
"""Render one API path as the absolute URL it is fetched from."""
|
|
117
|
+
return f"{self.url}{self.api_prefix if prefixed else ''}{path}"
|
|
118
|
+
|
|
119
|
+
async def request(
|
|
120
|
+
self,
|
|
121
|
+
method: str,
|
|
122
|
+
path: str,
|
|
123
|
+
*,
|
|
124
|
+
timeout: float | None = None,
|
|
125
|
+
prefixed: bool = True,
|
|
126
|
+
accept: tuple[int, ...] = (),
|
|
127
|
+
**kwargs: Any,
|
|
128
|
+
) -> httpx2.Response:
|
|
129
|
+
"""Make one authenticated request, retrying what is safe and raising what is not."""
|
|
130
|
+
target = f"{self.api_prefix if prefixed else ''}{path}"
|
|
131
|
+
sent: dict[str, Any] = dict(kwargs)
|
|
132
|
+
if timeout is not None:
|
|
133
|
+
sent["timeout"] = timeout
|
|
134
|
+
retryable = method.upper() in IDEMPOTENT_METHODS
|
|
135
|
+
attempts = self.retries + 1 if retryable else 1
|
|
136
|
+
for attempt in range(attempts):
|
|
137
|
+
last = attempt == attempts - 1
|
|
138
|
+
try:
|
|
139
|
+
response = await self._client.request(method, target, **sent)
|
|
140
|
+
except httpx2.HTTPError as error:
|
|
141
|
+
if last:
|
|
142
|
+
raise TransportError(self.url, error) from error
|
|
143
|
+
await asyncio.sleep(backoff_for(attempt))
|
|
144
|
+
continue
|
|
145
|
+
if response.status_code in accept:
|
|
146
|
+
return response
|
|
147
|
+
if response.status_code >= 500 and not last:
|
|
148
|
+
await response.aclose()
|
|
149
|
+
await asyncio.sleep(backoff_for(attempt))
|
|
150
|
+
continue
|
|
151
|
+
if response.status_code >= 400:
|
|
152
|
+
raise self._refusal(response, target)
|
|
153
|
+
return response
|
|
154
|
+
raise DirigentError("the request was never made", url=self.endpoint(path)) # pragma: no cover
|
|
155
|
+
|
|
156
|
+
def _refusal(self, response: httpx2.Response, target: str) -> DirigentError:
|
|
157
|
+
"""Turn an error response into the exception its status and body deserve."""
|
|
158
|
+
if VERSION_HEADER not in response.headers:
|
|
159
|
+
return NotDirigent(self.url, target, response.headers.get("content-type", ""))
|
|
160
|
+
try:
|
|
161
|
+
payload: object = response.json()
|
|
162
|
+
except ValueError:
|
|
163
|
+
payload = None
|
|
164
|
+
return error_for(
|
|
165
|
+
response.status_code,
|
|
166
|
+
f"{self.url}{target}",
|
|
167
|
+
parse_problem(payload),
|
|
168
|
+
retry_after=_retry_after(response),
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
async def json(self, method: str, path: str, **kwargs: Any) -> Any:
|
|
172
|
+
"""Make one request and return its parsed body, or nothing when it answered 204."""
|
|
173
|
+
response = await self.request(method, path, **kwargs)
|
|
174
|
+
return None if response.status_code == 204 else response.json()
|
|
175
|
+
|
|
176
|
+
async def text(self, path: str, **kwargs: Any) -> str:
|
|
177
|
+
"""GET a path that answers with text rather than JSON, such as an export."""
|
|
178
|
+
return (await self.request("GET", path, **kwargs)).text
|
|
179
|
+
|
|
180
|
+
@asynccontextmanager
|
|
181
|
+
async def stream(self, path: str, **kwargs: Any) -> AsyncGenerator[httpx2.Response]:
|
|
182
|
+
"""Open a streaming GET, held open for as long as the caller reads it."""
|
|
183
|
+
target = f"{self.api_prefix}{path}"
|
|
184
|
+
try:
|
|
185
|
+
async with self._client.stream("GET", target, timeout=None, **kwargs) as response:
|
|
186
|
+
if response.status_code >= 400:
|
|
187
|
+
await response.aread()
|
|
188
|
+
raise self._refusal(response, target)
|
|
189
|
+
yield response
|
|
190
|
+
except httpx2.HTTPError as error:
|
|
191
|
+
raise TransportError(self.url, error) from error
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dirigent-client
|
|
3
|
+
Version: 0.9.0
|
|
4
|
+
Summary: The dirigent API contract: typed wire schemas and an async Python client.
|
|
5
|
+
License-Expression: LicenseRef-Proprietary
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Requires-Dist: dirigent-common
|
|
8
|
+
Requires-Dist: httpx2>=2.12.0
|
|
9
|
+
Requires-Dist: pydantic>=2.13.5
|
|
10
|
+
Requires-Dist: pyyaml>=6.0.3
|
|
11
|
+
Requires-Python: >=3.13
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# dirigent-client
|
|
15
|
+
|
|
16
|
+
The dirigent API contract: typed wire schemas and an async Python client.
|
|
17
|
+
|
|
18
|
+
```python
|
|
19
|
+
from datetime import timedelta
|
|
20
|
+
|
|
21
|
+
from dirigent_client import Dirigent
|
|
22
|
+
|
|
23
|
+
async with Dirigent(url="https://dirigent.example.org", token=token) as dg:
|
|
24
|
+
plan = await dg.pipelines.apply("pipelines/daily-climate-load.yaml", dry_run=True)
|
|
25
|
+
run = await dg.pipelines.run("daily-climate-load", params={"day": "2026-08-29"})
|
|
26
|
+
final = await dg.runs.wait(run.run_id, timeout=timedelta(hours=2))
|
|
27
|
+
async for entry in dg.runs.follow_logs(run.run_id):
|
|
28
|
+
print(entry.message)
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
The package holds the pydantic schemas for every request and response the REST API speaks,
|
|
32
|
+
and `dirigent-server` imports them, so the client and the server can never disagree about a
|
|
33
|
+
shape. It depends on `dirigent-common`, `httpx2`, `pydantic`, and `pyyaml`, and on nothing else in the
|
|
34
|
+
workspace.
|
|
35
|
+
|
|
36
|
+
`docs/python.md` has the worked examples, and `examples/python/` has six runnable scripts.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
dirigent_client/__init__.py,sha256=HEhLHTVb0awp1PfJiJBwnJecSDVAQaXSv4bgW9tuICs,4114
|
|
2
|
+
dirigent_client/client.py,sha256=XXPH8Th6aHWqR40cvVWvUPz81WnJVmm8OcRxIf89LPU,5408
|
|
3
|
+
dirigent_client/enums.py,sha256=54XXZvmMx9ejXzXF_mxpp6HILmecCon2Ib5WgYR0Ql0,5543
|
|
4
|
+
dirigent_client/errors.py,sha256=gf3EFi7DeB9ZYYLJUsKRaackbQHn0WWim2XmipkrL9U,5112
|
|
5
|
+
dirigent_client/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
dirigent_client/resources/__init__.py,sha256=Ju5AWYzraLSqLbvOR2QKUBB71FWtW6C0VbejPONUs94,85
|
|
7
|
+
dirigent_client/resources/alerts.py,sha256=UJSbygTMZYWmBjvuuKp_vZqfFe8ccw8guQHUzarVouU,3889
|
|
8
|
+
dirigent_client/resources/auth.py,sha256=upCTGhpf5eHqsyKQgN9YXvnCfvfx2iEVNZ5dQ3v7bP0,6582
|
|
9
|
+
dirigent_client/resources/base.py,sha256=OV-5_7VKaGg_sRqhpSvggvtW6MJNcVqubzLhXkFsVz4,2319
|
|
10
|
+
dirigent_client/resources/blocks.py,sha256=D1V-AMSKICGaSiykALMjCXe6Drpo_rw7O5V4aUJxZGc,824
|
|
11
|
+
dirigent_client/resources/connections.py,sha256=pNWd-XT6EexSNHwtLfTvx5ICjhzjWdi2dyAyzbN8LjI,2326
|
|
12
|
+
dirigent_client/resources/pipelines.py,sha256=V3ny5VMR0_PkR0SPl6WLX8Cu0mLjyPzr1XM7PbAnGMk,8467
|
|
13
|
+
dirigent_client/resources/runs.py,sha256=OmdX9sqGM4ljl4y__MZpn73PvPWTQoQvVlitnFH3huo,9937
|
|
14
|
+
dirigent_client/resources/schedules.py,sha256=LS8mEayld3oWN__YnjuF-X5fZb3Xtr4B9vEjGVLAAWc,4955
|
|
15
|
+
dirigent_client/resources/schemas.py,sha256=x4as80oc5N2MijjqHAkSHjZvabrmyVUPIo9kRaQmBuA,1854
|
|
16
|
+
dirigent_client/resources/system.py,sha256=WrcwNVjXmX_iaoHNVrawUmpoUjzOFozw9VTyHgBkfP8,1574
|
|
17
|
+
dirigent_client/resources/trigger_documents.py,sha256=fnEooykY36R-vSUWeC01HoBKpso5RzK6L2ayBQYI9Dw,1071
|
|
18
|
+
dirigent_client/resources/webhooks.py,sha256=LAZ6wbx6MjrJmhvdStohuQazynIzYup1h68bdxLzmkk,3548
|
|
19
|
+
dirigent_client/schemas/__init__.py,sha256=PW_EXk2_1jEL1KiniS_MCK_74WAPQcHdEeTgOdQ5maQ,3412
|
|
20
|
+
dirigent_client/schemas/alerts.py,sha256=Oxy0DpxRYPZ1UfR7hLiLCegnui5-FYJlSTED8NQnURg,2468
|
|
21
|
+
dirigent_client/schemas/auth.py,sha256=4oQtgrwXCLL0uW77Dn29frAl9S3WqA3HGm2gR5jtdxM,3171
|
|
22
|
+
dirigent_client/schemas/catalog.py,sha256=KGCBeOKUcZ6moSiPGym3N_qhOZoZryI75lFmtwfPHOA,2328
|
|
23
|
+
dirigent_client/schemas/common.py,sha256=Zbdr9BOCA2fYjTMey7vA3k2vhuT9BDJp1CRCl3gWht8,1171
|
|
24
|
+
dirigent_client/schemas/connections.py,sha256=2CBN2VkMvdJ6R0L4pMg983P9bvIDqbb9oPhAx1cWsvs,1895
|
|
25
|
+
dirigent_client/schemas/pipelines.py,sha256=aekxsXFzKgGf25TMppqzCkKLLGM98LAlNfk43rAGTn4,11768
|
|
26
|
+
dirigent_client/schemas/runs.py,sha256=_g62IK_g0LNLy6j67SFjkBH67DYQboBc8pLgio62wsE,6017
|
|
27
|
+
dirigent_client/schemas/schemas.py,sha256=m5MFJxFTMzBubpNfnDkeTOwG6Q4ZS-qGIVSRUsRSL1s,1908
|
|
28
|
+
dirigent_client/schemas/system.py,sha256=iLUxsq8XoYrqgj-UN4q5fNAUkSECJLLpYHFuvgla5k4,2823
|
|
29
|
+
dirigent_client/schemas/triggers.py,sha256=nS7c-xFpL7kHDFR7JpmdFt7vcRaGWMpcgfWqAX--xVM,6489
|
|
30
|
+
dirigent_client/transport.py,sha256=E1jxuqoiwVoRoDE2rJF8eiRYNe2rk7gVoIMUp4i7_zw,7397
|
|
31
|
+
dirigent_client-0.9.0.dist-info/licenses/LICENSE,sha256=LKBm7Cx-WBc1zca4DjGxq99VEpAiWGnZDxIKmntn1hQ,910
|
|
32
|
+
dirigent_client-0.9.0.dist-info/WHEEL,sha256=mru_b36sH6joUMnwf7IlFCun3RoDjrNg9RcfBmEcqsE,81
|
|
33
|
+
dirigent_client-0.9.0.dist-info/METADATA,sha256=T4GYrcnOOtFmWGzdIqyBg99whB0v4fySng5VVW-XU8o,1366
|
|
34
|
+
dirigent_client-0.9.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
Copyright (c) 2026 Morten Olav Hansen <morten@winterop.com>. All rights reserved.
|
|
2
|
+
|
|
3
|
+
This source code and accompanying documentation are the property of
|
|
4
|
+
Morten Olav Hansen. No license, express or implied, is granted to use, copy,
|
|
5
|
+
modify, merge, publish, distribute, sublicense, or sell copies of this
|
|
6
|
+
software or its derivatives.
|
|
7
|
+
|
|
8
|
+
The source is published for reference only. Any use beyond reading
|
|
9
|
+
requires written permission from the copyright holder.
|
|
10
|
+
|
|
11
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
|
12
|
+
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
13
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT.
|
|
14
|
+
IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES,
|
|
15
|
+
OR OTHER LIABILITY ARISING FROM THE USE OF THE SOFTWARE.
|
|
16
|
+
|
|
17
|
+
Third-party components redistributed with this software, and the licences they
|
|
18
|
+
carry, are listed in THIRD_PARTY_NOTICES.md.
|