dirigent-client 0.27.0__tar.gz → 0.28.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.
- {dirigent_client-0.27.0 → dirigent_client-0.28.0}/PKG-INFO +2 -2
- {dirigent_client-0.27.0 → dirigent_client-0.28.0}/pyproject.toml +2 -2
- {dirigent_client-0.27.0 → dirigent_client-0.28.0}/pyproject.toml.orig +2 -2
- {dirigent_client-0.27.0 → dirigent_client-0.28.0}/src/dirigent_client/__init__.py +8 -0
- {dirigent_client-0.27.0 → dirigent_client-0.28.0}/src/dirigent_client/enums.py +6 -0
- {dirigent_client-0.27.0 → dirigent_client-0.28.0}/src/dirigent_client/resources/alerts.py +5 -5
- {dirigent_client-0.27.0 → dirigent_client-0.28.0}/src/dirigent_client/resources/auth.py +19 -18
- {dirigent_client-0.27.0 → dirigent_client-0.28.0}/src/dirigent_client/resources/base.py +14 -0
- {dirigent_client-0.27.0 → dirigent_client-0.28.0}/src/dirigent_client/resources/blocks.py +2 -2
- {dirigent_client-0.27.0 → dirigent_client-0.28.0}/src/dirigent_client/resources/connections.py +9 -9
- {dirigent_client-0.27.0 → dirigent_client-0.28.0}/src/dirigent_client/resources/examples.py +2 -2
- {dirigent_client-0.27.0 → dirigent_client-0.28.0}/src/dirigent_client/resources/pipelines.py +25 -19
- {dirigent_client-0.27.0 → dirigent_client-0.28.0}/src/dirigent_client/resources/runs.py +57 -13
- {dirigent_client-0.27.0 → dirigent_client-0.28.0}/src/dirigent_client/resources/schedules.py +13 -9
- {dirigent_client-0.27.0 → dirigent_client-0.28.0}/src/dirigent_client/resources/schemas.py +4 -4
- {dirigent_client-0.27.0 → dirigent_client-0.28.0}/src/dirigent_client/resources/sources.py +10 -8
- {dirigent_client-0.27.0 → dirigent_client-0.28.0}/src/dirigent_client/resources/trigger_documents.py +3 -3
- {dirigent_client-0.27.0 → dirigent_client-0.28.0}/src/dirigent_client/schemas/__init__.py +27 -1
- {dirigent_client-0.27.0 → dirigent_client-0.28.0}/src/dirigent_client/schemas/alerts.py +5 -5
- {dirigent_client-0.27.0 → dirigent_client-0.28.0}/src/dirigent_client/schemas/auth.py +18 -10
- {dirigent_client-0.27.0 → dirigent_client-0.28.0}/src/dirigent_client/schemas/common.py +13 -2
- {dirigent_client-0.27.0 → dirigent_client-0.28.0}/src/dirigent_client/schemas/connections.py +37 -4
- {dirigent_client-0.27.0 → dirigent_client-0.28.0}/src/dirigent_client/schemas/pipelines.py +10 -7
- {dirigent_client-0.27.0 → dirigent_client-0.28.0}/src/dirigent_client/schemas/runs.py +85 -3
- {dirigent_client-0.27.0 → dirigent_client-0.28.0}/src/dirigent_client/schemas/schemas.py +4 -4
- {dirigent_client-0.27.0 → dirigent_client-0.28.0}/src/dirigent_client/schemas/sources.py +13 -2
- {dirigent_client-0.27.0 → dirigent_client-0.28.0}/src/dirigent_client/schemas/triggers.py +4 -4
- {dirigent_client-0.27.0 → dirigent_client-0.28.0}/LICENSE +0 -0
- {dirigent_client-0.27.0 → dirigent_client-0.28.0}/README.md +0 -0
- {dirigent_client-0.27.0 → dirigent_client-0.28.0}/src/dirigent_client/client.py +0 -0
- {dirigent_client-0.27.0 → dirigent_client-0.28.0}/src/dirigent_client/errors.py +0 -0
- {dirigent_client-0.27.0 → dirigent_client-0.28.0}/src/dirigent_client/messages.py +0 -0
- {dirigent_client-0.27.0 → dirigent_client-0.28.0}/src/dirigent_client/py.typed +0 -0
- {dirigent_client-0.27.0 → dirigent_client-0.28.0}/src/dirigent_client/resources/__init__.py +0 -0
- {dirigent_client-0.27.0 → dirigent_client-0.28.0}/src/dirigent_client/resources/system.py +0 -0
- {dirigent_client-0.27.0 → dirigent_client-0.28.0}/src/dirigent_client/schemas/catalog.py +0 -0
- {dirigent_client-0.27.0 → dirigent_client-0.28.0}/src/dirigent_client/schemas/examples.py +0 -0
- {dirigent_client-0.27.0 → dirigent_client-0.28.0}/src/dirigent_client/schemas/labels.py +0 -0
- {dirigent_client-0.27.0 → dirigent_client-0.28.0}/src/dirigent_client/schemas/system.py +0 -0
- {dirigent_client-0.27.0 → dirigent_client-0.28.0}/src/dirigent_client/transport.py +0 -0
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dirigent-client
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.28.0
|
|
4
4
|
Summary: The dirigent API contract: typed wire schemas and an async Python client.
|
|
5
5
|
License-Expression: LicenseRef-Proprietary
|
|
6
6
|
License-File: LICENSE
|
|
7
7
|
Classifier: Programming Language :: Python :: 3
|
|
8
8
|
Classifier: Programming Language :: Python :: 3.13
|
|
9
|
-
Requires-Dist: dirigent-common==0.
|
|
9
|
+
Requires-Dist: dirigent-common==0.28.0
|
|
10
10
|
Requires-Dist: httpx2>=2.12.0
|
|
11
11
|
Requires-Dist: pydantic>=2.13.5
|
|
12
12
|
Requires-Dist: pyyaml>=6.0.3
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "dirigent-client"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.28.0"
|
|
4
4
|
description = "The dirigent API contract: typed wire schemas and an async Python client."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.13"
|
|
@@ -11,7 +11,7 @@ classifiers = [
|
|
|
11
11
|
"Programming Language :: Python :: 3.13",
|
|
12
12
|
]
|
|
13
13
|
dependencies = [
|
|
14
|
-
"dirigent-common==0.
|
|
14
|
+
"dirigent-common==0.28.0",
|
|
15
15
|
"httpx2>=2.12.0",
|
|
16
16
|
"pydantic>=2.13.5",
|
|
17
17
|
"pyyaml>=6.0.3",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "dirigent-client"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.28.0"
|
|
4
4
|
description = "The dirigent API contract: typed wire schemas and an async Python client."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.13"
|
|
@@ -11,7 +11,7 @@ classifiers = [
|
|
|
11
11
|
"Programming Language :: Python :: 3.13",
|
|
12
12
|
]
|
|
13
13
|
dependencies = [
|
|
14
|
-
"dirigent-common==0.
|
|
14
|
+
"dirigent-common==0.28.0",
|
|
15
15
|
"httpx2>=2.12.0",
|
|
16
16
|
"pydantic>=2.13.5",
|
|
17
17
|
"pyyaml>=6.0.3",
|
|
@@ -61,6 +61,7 @@ from dirigent_client.schemas import (
|
|
|
61
61
|
Catalog,
|
|
62
62
|
CheckResult,
|
|
63
63
|
CheckStatus,
|
|
64
|
+
ConnectionDetail,
|
|
64
65
|
ConnectionHealth,
|
|
65
66
|
ConnectionIn,
|
|
66
67
|
ConnectionOut,
|
|
@@ -93,11 +94,13 @@ from dirigent_client.schemas import (
|
|
|
93
94
|
Readiness,
|
|
94
95
|
RequestFacts,
|
|
95
96
|
Requirements,
|
|
97
|
+
RerunRequest,
|
|
96
98
|
RunAccepted,
|
|
97
99
|
RunDetail,
|
|
98
100
|
RunOut,
|
|
99
101
|
RunReport,
|
|
100
102
|
RunRequest,
|
|
103
|
+
RunStats,
|
|
101
104
|
ScheduleIn,
|
|
102
105
|
ScheduleOut,
|
|
103
106
|
SchedulePreview,
|
|
@@ -115,6 +118,7 @@ from dirigent_client.schemas import (
|
|
|
115
118
|
SystemInfo,
|
|
116
119
|
TestQueued,
|
|
117
120
|
TestRequest,
|
|
121
|
+
TokenLifetime,
|
|
118
122
|
TokenOut,
|
|
119
123
|
TokenRequest,
|
|
120
124
|
TriggerDocumentDetail,
|
|
@@ -165,6 +169,7 @@ __all__ = [
|
|
|
165
169
|
"CheckResult",
|
|
166
170
|
"CheckStatus",
|
|
167
171
|
"Conflict",
|
|
172
|
+
"ConnectionDetail",
|
|
168
173
|
"ConnectionHealth",
|
|
169
174
|
"ConnectionIn",
|
|
170
175
|
"ConnectionOut",
|
|
@@ -219,6 +224,7 @@ __all__ = [
|
|
|
219
224
|
"ProvenanceSource",
|
|
220
225
|
"RateLimited",
|
|
221
226
|
"Readiness",
|
|
227
|
+
"RerunRequest",
|
|
222
228
|
"Requirements",
|
|
223
229
|
"RunAccepted",
|
|
224
230
|
"RunDetail",
|
|
@@ -227,6 +233,7 @@ __all__ = [
|
|
|
227
233
|
"RunPriority",
|
|
228
234
|
"RunReport",
|
|
229
235
|
"RunRequest",
|
|
236
|
+
"RunStats",
|
|
230
237
|
"RunStatus",
|
|
231
238
|
"ScheduleIn",
|
|
232
239
|
"ScheduleKind",
|
|
@@ -242,6 +249,7 @@ __all__ = [
|
|
|
242
249
|
"TestRequest",
|
|
243
250
|
"TokenKind",
|
|
244
251
|
"TokenOut",
|
|
252
|
+
"TokenLifetime",
|
|
245
253
|
"TokenRequest",
|
|
246
254
|
"TriggerDocumentDetail",
|
|
247
255
|
"TriggerDocumentOut",
|
|
@@ -8,6 +8,9 @@ class RunStatus(StrEnum):
|
|
|
8
8
|
|
|
9
9
|
QUEUED = "queued"
|
|
10
10
|
RUNNING = "running"
|
|
11
|
+
PAUSED = "paused"
|
|
12
|
+
"""Held by an operator: nothing new is claimed and its deadlines stand still until it resumes."""
|
|
13
|
+
|
|
11
14
|
SUCCEEDED = "succeeded"
|
|
12
15
|
COMPLETED_WITH_ERRORS = "completed_with_errors"
|
|
13
16
|
FAILED = "failed"
|
|
@@ -114,6 +117,9 @@ class TriggerKind(StrEnum):
|
|
|
114
117
|
SOURCE = "source"
|
|
115
118
|
"""An input arrived at a source node, and ``triggered_by_id`` names the source."""
|
|
116
119
|
|
|
120
|
+
RERUN = "rerun"
|
|
121
|
+
"""An operator reran a settled run, and ``triggered_by_id`` names that run."""
|
|
122
|
+
|
|
117
123
|
|
|
118
124
|
class ScheduleKind(StrEnum):
|
|
119
125
|
"""How a schedule computes its next firing."""
|
|
@@ -4,7 +4,7 @@ from datetime import timedelta
|
|
|
4
4
|
from uuid import UUID
|
|
5
5
|
|
|
6
6
|
from dirigent_client.enums import AlertEvent, AlertScope, Importance
|
|
7
|
-
from dirigent_client.resources.base import Resource, query, request_body
|
|
7
|
+
from dirigent_client.resources.base import Resource, query, request_body, segment
|
|
8
8
|
from dirigent_client.schemas import (
|
|
9
9
|
AlertRuleIn,
|
|
10
10
|
AlertRuleOut,
|
|
@@ -76,11 +76,11 @@ class Alerts(Resource):
|
|
|
76
76
|
"""
|
|
77
77
|
named = query(paused=paused, template=template, body=body, importance=importance, connection=connection)
|
|
78
78
|
body_json = AlertRuleUpdate.model_validate(named).model_dump(mode="json", exclude_unset=True)
|
|
79
|
-
return await self._one(AlertRuleOut, "PATCH", f"/alert-rules/{code}", json=body_json)
|
|
79
|
+
return await self._one(AlertRuleOut, "PATCH", f"/alert-rules/{segment(code)}", json=body_json)
|
|
80
80
|
|
|
81
81
|
async def delete_rule(self, code: str) -> None:
|
|
82
82
|
"""Remove an alert rule; the notifications it already raised are kept."""
|
|
83
|
-
await self._transport.request("DELETE", f"/alert-rules/{code}")
|
|
83
|
+
await self._transport.request("DELETE", f"/alert-rules/{segment(code)}")
|
|
84
84
|
|
|
85
85
|
async def test(
|
|
86
86
|
self,
|
|
@@ -98,11 +98,11 @@ class Alerts(Resource):
|
|
|
98
98
|
|
|
99
99
|
async def notification(self, notification_id: UUID | str) -> NotificationOut:
|
|
100
100
|
"""Read one notification, which is how a caller watches a delivery it just queued."""
|
|
101
|
-
return await self._one(NotificationOut, "GET", f"/notifications/{notification_id}")
|
|
101
|
+
return await self._one(NotificationOut, "GET", f"/notifications/{segment(notification_id)}")
|
|
102
102
|
|
|
103
103
|
async def retry(self, notification_id: UUID | str) -> NotificationOut:
|
|
104
104
|
"""Put one notification back on the queue, due now."""
|
|
105
|
-
return await self._one(NotificationOut, "POST", f"/notifications/{notification_id}/$retry")
|
|
105
|
+
return await self._one(NotificationOut, "POST", f"/notifications/{segment(notification_id)}/$retry")
|
|
106
106
|
|
|
107
107
|
async def notifications(
|
|
108
108
|
self, *, run_id: UUID | str | None = None, after: str | None = None, limit: int | None = None
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
"""Logging in, logging out, asking who you are, and the accounts and tokens an admin manages."""
|
|
2
2
|
|
|
3
|
+
from datetime import timedelta
|
|
4
|
+
from uuid import UUID
|
|
5
|
+
|
|
3
6
|
from pydantic import SecretStr
|
|
4
7
|
|
|
5
8
|
from dirigent_client.enums import UserRole
|
|
6
|
-
from dirigent_client.resources.base import Resource, query, request_body
|
|
9
|
+
from dirigent_client.resources.base import Resource, query, request_body, segment
|
|
7
10
|
from dirigent_client.schemas import (
|
|
8
11
|
Identity,
|
|
9
12
|
IssuedTokenOut,
|
|
@@ -100,33 +103,31 @@ class Users(Resource):
|
|
|
100
103
|
if role is not None:
|
|
101
104
|
named["role"] = role
|
|
102
105
|
body = UserUpdate.model_validate(named).model_dump(mode="json", exclude_unset=True)
|
|
103
|
-
return await self._one(UserOut, "PATCH", f"/users/{username}", json=body)
|
|
106
|
+
return await self._one(UserOut, "PATCH", f"/users/{segment(username)}", json=body)
|
|
104
107
|
|
|
105
108
|
async def deactivate(self, username: str) -> UserOut:
|
|
106
109
|
"""Bar an account from logging in and revoke the sessions it already holds."""
|
|
107
|
-
return await self._one(UserOut, "POST", f"/users/{username}/$deactivate")
|
|
110
|
+
return await self._one(UserOut, "POST", f"/users/{segment(username)}/$deactivate")
|
|
108
111
|
|
|
109
112
|
async def activate(self, username: str) -> UserOut:
|
|
110
113
|
"""Let an account log in again; the sessions it lost are not restored."""
|
|
111
|
-
return await self._one(UserOut, "POST", f"/users/{username}/$activate")
|
|
114
|
+
return await self._one(UserOut, "POST", f"/users/{segment(username)}/$activate")
|
|
112
115
|
|
|
113
116
|
async def reset_password(self, username: str, password: str) -> None:
|
|
114
117
|
"""Set an account's password without its old one, ending every session it holds."""
|
|
115
118
|
payload = PasswordResetRequest(password=SecretStr(password))
|
|
116
|
-
await self._transport.request("POST", f"/users/{username}/$reset-password", json=request_body(payload))
|
|
119
|
+
await self._transport.request("POST", f"/users/{segment(username)}/$reset-password", json=request_body(payload))
|
|
117
120
|
|
|
118
121
|
async def tokens(self, username: str, *, after: str | None = None, limit: int | None = None) -> Page[TokenOut]:
|
|
119
122
|
"""List the API tokens one account holds, without their secrets."""
|
|
120
|
-
return await self._many(
|
|
123
|
+
return await self._many(
|
|
124
|
+
TokenOut, "GET", f"/users/{segment(username)}/tokens", params=query(after=after, limit=limit)
|
|
125
|
+
)
|
|
121
126
|
|
|
122
|
-
async def create_token(self, username: str, name: str) -> IssuedTokenOut:
|
|
127
|
+
async def create_token(self, username: str, name: str, *, expires_in: timedelta | None = None) -> IssuedTokenOut:
|
|
123
128
|
"""Mint a bearer token for another account; its secret is returned exactly once."""
|
|
124
|
-
payload = TokenRequest(name=name)
|
|
125
|
-
return await self._one(IssuedTokenOut, "POST", f"/users/{username}/tokens", json=request_body(payload))
|
|
126
|
-
|
|
127
|
-
async def revoke_token(self, username: str, name: str) -> None:
|
|
128
|
-
"""Revoke that account's live tokens of the given name."""
|
|
129
|
-
await self._transport.request("DELETE", f"/users/{username}/tokens/{name}")
|
|
129
|
+
payload = TokenRequest(name=name, expires_in=expires_in)
|
|
130
|
+
return await self._one(IssuedTokenOut, "POST", f"/users/{segment(username)}/tokens", json=request_body(payload))
|
|
130
131
|
|
|
131
132
|
|
|
132
133
|
class Tokens(Resource):
|
|
@@ -136,14 +137,14 @@ class Tokens(Resource):
|
|
|
136
137
|
"""List every account's API tokens, without their secrets; sessions are not listed."""
|
|
137
138
|
return await self._many(TokenOut, "GET", "/tokens", params=query(after=after, limit=limit))
|
|
138
139
|
|
|
139
|
-
async def create(self, name: str) -> IssuedTokenOut:
|
|
140
|
+
async def create(self, name: str, *, expires_in: timedelta | None = None) -> IssuedTokenOut:
|
|
140
141
|
"""Mint a bearer token for this account; its secret is returned exactly once."""
|
|
141
|
-
payload = TokenRequest(name=name)
|
|
142
|
+
payload = TokenRequest(name=name, expires_in=expires_in)
|
|
142
143
|
return await self._one(IssuedTokenOut, "POST", "/tokens", json=request_body(payload))
|
|
143
144
|
|
|
144
|
-
async def revoke(self,
|
|
145
|
-
"""Revoke
|
|
146
|
-
await self._transport.request("DELETE", f"/tokens/{
|
|
145
|
+
async def revoke(self, token_id: UUID | str) -> None:
|
|
146
|
+
"""Revoke the one live token with this id, whichever account holds it."""
|
|
147
|
+
await self._transport.request("DELETE", f"/tokens/{segment(str(token_id))}")
|
|
147
148
|
|
|
148
149
|
|
|
149
150
|
class Admin:
|
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
from enum import Enum
|
|
4
4
|
from typing import Any, Final
|
|
5
|
+
from urllib.parse import quote
|
|
5
6
|
|
|
6
7
|
from pydantic import BaseModel, SecretStr
|
|
7
8
|
|
|
9
|
+
from dirigent_client.errors import DirigentError
|
|
8
10
|
from dirigent_client.schemas import Page
|
|
9
11
|
from dirigent_client.transport import Transport
|
|
10
12
|
|
|
@@ -40,6 +42,18 @@ class Resource:
|
|
|
40
42
|
return Page(items=[model.model_validate(row) for row in payload["items"]], next=payload.get("next"))
|
|
41
43
|
|
|
42
44
|
|
|
45
|
+
def segment(value: object) -> str:
|
|
46
|
+
"""Percent-encode an identifier as exactly one path segment of a request.
|
|
47
|
+
|
|
48
|
+
A value that cannot be one is refused: an empty one or a dot segment is collapsed out of the
|
|
49
|
+
path, and a slash is decoded back into a separator by the server that routes the request.
|
|
50
|
+
"""
|
|
51
|
+
text = str(value)
|
|
52
|
+
if text in ("", ".", "..") or "/" in text:
|
|
53
|
+
raise DirigentError(f"{text!r} is not an identifier this instance could hold")
|
|
54
|
+
return quote(text, safe="")
|
|
55
|
+
|
|
56
|
+
|
|
43
57
|
def query(**values: object) -> dict[str, Any]:
|
|
44
58
|
"""Build a query string from the arguments a caller actually supplied."""
|
|
45
59
|
return {name: value for name, value in values.items() if value is not None}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""The block catalog: what this instance can run, and the schemas a form is built from."""
|
|
2
2
|
|
|
3
|
-
from dirigent_client.resources.base import Resource, query
|
|
3
|
+
from dirigent_client.resources.base import Resource, query, segment
|
|
4
4
|
from dirigent_client.schemas import BlockEntry, BlockKind, Catalog
|
|
5
5
|
|
|
6
6
|
|
|
@@ -13,4 +13,4 @@ class Blocks(Resource):
|
|
|
13
13
|
|
|
14
14
|
async def get(self, block_id: str) -> BlockEntry:
|
|
15
15
|
"""Read one catalog entry, which is what a step's configuration form is built from."""
|
|
16
|
-
return await self._one(BlockEntry, "GET", f"/blocks/{block_id}")
|
|
16
|
+
return await self._one(BlockEntry, "GET", f"/blocks/{segment(block_id)}")
|
{dirigent_client-0.27.0 → dirigent_client-0.28.0}/src/dirigent_client/resources/connections.py
RENAMED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""Connections: coded credential records of a contributed kind, redacted in every response."""
|
|
2
2
|
|
|
3
|
-
from dirigent_client.resources.base import Resource, edit_body, query, request_body
|
|
4
|
-
from dirigent_client.schemas import ConnectionIn, ConnectionOut, ConnectionUpdate, Page
|
|
3
|
+
from dirigent_client.resources.base import Resource, edit_body, query, request_body, segment
|
|
4
|
+
from dirigent_client.schemas import ConnectionDetail, ConnectionIn, ConnectionOut, ConnectionUpdate, Page
|
|
5
5
|
from dirigent_common import HealthReport, JsonMap
|
|
6
6
|
|
|
7
7
|
|
|
@@ -12,9 +12,9 @@ class Connections(Resource):
|
|
|
12
12
|
"""List every stored credential record, with its secrets redacted."""
|
|
13
13
|
return await self._many(ConnectionOut, "GET", "/connections", params=query(after=after, limit=limit))
|
|
14
14
|
|
|
15
|
-
async def get(self, code: str) ->
|
|
16
|
-
"""Read one credential record, with its secrets redacted."""
|
|
17
|
-
return await self._one(
|
|
15
|
+
async def get(self, code: str) -> ConnectionDetail:
|
|
16
|
+
"""Read one credential record, with its secrets redacted and what uses it."""
|
|
17
|
+
return await self._one(ConnectionDetail, "GET", f"/connections/{segment(code)}")
|
|
18
18
|
|
|
19
19
|
async def create(
|
|
20
20
|
self,
|
|
@@ -43,12 +43,12 @@ class Connections(Resource):
|
|
|
43
43
|
their own keep the name and the description the credential was created with.
|
|
44
44
|
"""
|
|
45
45
|
sent = edit_body(ConnectionUpdate, name=name, description=description, config=config)
|
|
46
|
-
return await self._one(ConnectionOut, "PATCH", f"/connections/{code}", json=sent)
|
|
46
|
+
return await self._one(ConnectionOut, "PATCH", f"/connections/{segment(code)}", json=sent)
|
|
47
47
|
|
|
48
48
|
async def delete(self, code: str) -> None:
|
|
49
|
-
"""Remove a credential record."""
|
|
50
|
-
await self._transport.request("DELETE", f"/connections/{code}")
|
|
49
|
+
"""Remove a credential record, which the server refuses while anything still uses it."""
|
|
50
|
+
await self._transport.request("DELETE", f"/connections/{segment(code)}")
|
|
51
51
|
|
|
52
52
|
async def check(self, code: str) -> HealthReport:
|
|
53
53
|
"""Open the credential and ask its kind whether the external system answers."""
|
|
54
|
-
return await self._one(HealthReport, "POST", f"/connections/{code}/$check")
|
|
54
|
+
return await self._one(HealthReport, "POST", f"/connections/{segment(code)}/$check")
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from collections.abc import Sequence
|
|
4
4
|
|
|
5
|
-
from dirigent_client.resources.base import Resource, query
|
|
5
|
+
from dirigent_client.resources.base import Resource, query, segment
|
|
6
6
|
from dirigent_client.schemas import ExampleDetail, ExampleOut, Page
|
|
7
7
|
|
|
8
8
|
|
|
@@ -25,4 +25,4 @@ class Examples(Resource):
|
|
|
25
25
|
|
|
26
26
|
async def get(self, code: str) -> ExampleDetail:
|
|
27
27
|
"""Read one example by its code, with the text a copy of it copies."""
|
|
28
|
-
return await self._one(ExampleDetail, "GET", f"/examples/{code}")
|
|
28
|
+
return await self._one(ExampleDetail, "GET", f"/examples/{segment(code)}")
|
{dirigent_client-0.27.0 → dirigent_client-0.28.0}/src/dirigent_client/resources/pipelines.py
RENAMED
|
@@ -11,7 +11,7 @@ from pydantic import JsonValue
|
|
|
11
11
|
|
|
12
12
|
from dirigent_client.enums import LogLevel, ProvenanceSource, RunPriority
|
|
13
13
|
from dirigent_client.errors import DirigentError
|
|
14
|
-
from dirigent_client.resources.base import Resource, query, request_body
|
|
14
|
+
from dirigent_client.resources.base import Resource, query, request_body, segment
|
|
15
15
|
from dirigent_client.schemas import (
|
|
16
16
|
ApplyRequest,
|
|
17
17
|
ApplyResult,
|
|
@@ -28,6 +28,7 @@ from dirigent_client.schemas import (
|
|
|
28
28
|
RunRequest,
|
|
29
29
|
ValidationIssue,
|
|
30
30
|
)
|
|
31
|
+
from dirigent_common import bounded_load
|
|
31
32
|
|
|
32
33
|
type DocumentSource = JsonMap | str | Path
|
|
33
34
|
|
|
@@ -48,7 +49,7 @@ class Pipelines(Resource):
|
|
|
48
49
|
|
|
49
50
|
async def get(self, code: str) -> PipelineDetail:
|
|
50
51
|
"""Read a pipeline and the document its current version holds."""
|
|
51
|
-
return await self._one(PipelineDetail, "GET", f"/pipelines/{code}")
|
|
52
|
+
return await self._one(PipelineDetail, "GET", f"/pipelines/{segment(code)}")
|
|
52
53
|
|
|
53
54
|
async def apply(
|
|
54
55
|
self,
|
|
@@ -58,13 +59,14 @@ class Pipelines(Resource):
|
|
|
58
59
|
source: ProvenanceSource | None = None,
|
|
59
60
|
source_ref: str | None = None,
|
|
60
61
|
dry_run: bool = False,
|
|
61
|
-
|
|
62
|
+
paused: bool = False,
|
|
62
63
|
) -> ApplyResult:
|
|
63
64
|
"""Validate a document against this instance and commit a new version, or plan one.
|
|
64
65
|
|
|
65
66
|
A document this instance cannot run arrives as a 200 with ``action`` of ``invalid``
|
|
66
|
-
and the issues on the plan, not as a refusal. ``
|
|
67
|
-
|
|
67
|
+
and the issues on the plan, not as a refusal. ``paused`` creates the schedules and the
|
|
68
|
+
polled or held sources this apply brings into being paused, and leaves a received source
|
|
69
|
+
and everything the instance already holds alone.
|
|
68
70
|
"""
|
|
69
71
|
parsed, provenance, ref = read_document(document)
|
|
70
72
|
payload = ApplyRequest(
|
|
@@ -72,7 +74,7 @@ class Pipelines(Resource):
|
|
|
72
74
|
code=code,
|
|
73
75
|
source=source if source is not None else provenance,
|
|
74
76
|
source_ref=source_ref if source_ref is not None else ref,
|
|
75
|
-
|
|
77
|
+
paused=paused,
|
|
76
78
|
)
|
|
77
79
|
return await self._one(
|
|
78
80
|
ApplyResult,
|
|
@@ -102,12 +104,12 @@ class Pipelines(Resource):
|
|
|
102
104
|
) -> Page[PipelineVersionOut]:
|
|
103
105
|
"""List every immutable version, newest first, with its provenance."""
|
|
104
106
|
return await self._many(
|
|
105
|
-
PipelineVersionOut, "GET", f"/pipelines/{code}/versions", params=query(after=after, limit=limit)
|
|
107
|
+
PipelineVersionOut, "GET", f"/pipelines/{segment(code)}/versions", params=query(after=after, limit=limit)
|
|
106
108
|
)
|
|
107
109
|
|
|
108
110
|
async def export(self, code: str, *, version: int | None = None) -> str:
|
|
109
111
|
"""Render a stored pipeline as the canonical YAML a git repository holds."""
|
|
110
|
-
return await self._transport.text(f"/pipelines/{code}/$export", params=query(version=version))
|
|
112
|
+
return await self._transport.text(f"/pipelines/{segment(code)}/$export", params=query(version=version))
|
|
111
113
|
|
|
112
114
|
async def validate(self, code: str, *, version: int | None = None) -> builtins.list[ValidationIssue]:
|
|
113
115
|
"""Report what a stored version would fail on if it ran now, against this instance.
|
|
@@ -115,20 +117,22 @@ class Pipelines(Resource):
|
|
|
115
117
|
The issues are bounded by the document, so this one answers a bare array, not a page.
|
|
116
118
|
builtins names the type because this class has a method called ``list``.
|
|
117
119
|
"""
|
|
118
|
-
rows = await self._transport.json(
|
|
120
|
+
rows = await self._transport.json(
|
|
121
|
+
"POST", f"/pipelines/{segment(code)}/$validate", params=query(version=version)
|
|
122
|
+
)
|
|
119
123
|
return [ValidationIssue.model_validate(row) for row in rows]
|
|
120
124
|
|
|
121
125
|
async def activate(self, code: str) -> PipelineOut:
|
|
122
126
|
"""Make a pipeline runnable again, and let its schedules fire."""
|
|
123
|
-
return await self._one(PipelineOut, "POST", f"/pipelines/{code}/$activate")
|
|
127
|
+
return await self._one(PipelineOut, "POST", f"/pipelines/{segment(code)}/$activate")
|
|
124
128
|
|
|
125
129
|
async def deactivate(self, code: str) -> PipelineOut:
|
|
126
130
|
"""Deregister a pipeline: schedules pause, it stops being runnable, history is kept."""
|
|
127
|
-
return await self._one(PipelineOut, "POST", f"/pipelines/{code}/$deactivate")
|
|
131
|
+
return await self._one(PipelineOut, "POST", f"/pipelines/{segment(code)}/$deactivate")
|
|
128
132
|
|
|
129
133
|
async def delete(self, code: str) -> None:
|
|
130
134
|
"""Delete a pipeline and every run ever attributed to it, refusing while any is in flight."""
|
|
131
|
-
await self._transport.request("DELETE", f"/pipelines/{code}")
|
|
135
|
+
await self._transport.request("DELETE", f"/pipelines/{segment(code)}")
|
|
132
136
|
|
|
133
137
|
async def run(
|
|
134
138
|
self,
|
|
@@ -167,7 +171,7 @@ class Pipelines(Resource):
|
|
|
167
171
|
log_levels=log_levels,
|
|
168
172
|
priority=priority,
|
|
169
173
|
)
|
|
170
|
-
return await self._one(RunAccepted, "POST", f"/pipelines/{code}/$run", json=request_body(payload))
|
|
174
|
+
return await self._one(RunAccepted, "POST", f"/pipelines/{segment(code)}/$run", json=request_body(payload))
|
|
171
175
|
|
|
172
176
|
async def backfill(
|
|
173
177
|
self,
|
|
@@ -191,7 +195,9 @@ class Pipelines(Resource):
|
|
|
191
195
|
params=params,
|
|
192
196
|
dry_run=dry_run,
|
|
193
197
|
)
|
|
194
|
-
return await self._one(
|
|
198
|
+
return await self._one(
|
|
199
|
+
BackfillAccepted, "POST", f"/pipelines/{segment(code)}/$backfill", json=request_body(payload)
|
|
200
|
+
)
|
|
195
201
|
|
|
196
202
|
|
|
197
203
|
def read_document(document: DocumentSource) -> tuple[JsonMap, ProvenanceSource, str | None]:
|
|
@@ -199,17 +205,17 @@ def read_document(document: DocumentSource) -> tuple[JsonMap, ProvenanceSource,
|
|
|
199
205
|
if isinstance(document, dict):
|
|
200
206
|
return dict(document), ProvenanceSource.API, None
|
|
201
207
|
if isinstance(document, Path):
|
|
202
|
-
return
|
|
208
|
+
return parse_document(document.read_text()), ProvenanceSource.FILE, str(document)
|
|
203
209
|
path = Path(document)
|
|
204
210
|
if len(document) < 4096 and "\n" not in document and path.is_file():
|
|
205
|
-
return
|
|
206
|
-
return
|
|
211
|
+
return parse_document(path.read_text()), ProvenanceSource.FILE, document
|
|
212
|
+
return parse_document(document), ProvenanceSource.API, None
|
|
207
213
|
|
|
208
214
|
|
|
209
|
-
def
|
|
215
|
+
def parse_document(text: str) -> JsonMap:
|
|
210
216
|
"""Read a document's text as YAML, which accepts its JSON form verbatim."""
|
|
211
217
|
try:
|
|
212
|
-
loaded: object =
|
|
218
|
+
loaded: object = bounded_load(text)
|
|
213
219
|
except yaml.YAMLError as error:
|
|
214
220
|
raise DirigentError(f"the document is not valid YAML or JSON: {error}") from error
|
|
215
221
|
if not isinstance(loaded, dict):
|
|
@@ -12,7 +12,7 @@ import httpx2
|
|
|
12
12
|
|
|
13
13
|
from dirigent_client.enums import AttemptStatus, RunStatus
|
|
14
14
|
from dirigent_client.errors import TransportError, WaitTimeout
|
|
15
|
-
from dirigent_client.resources.base import Resource, query
|
|
15
|
+
from dirigent_client.resources.base import Resource, query, request_body, segment
|
|
16
16
|
from dirigent_client.schemas import (
|
|
17
17
|
ArtifactOut,
|
|
18
18
|
AttemptEvent,
|
|
@@ -20,9 +20,12 @@ from dirigent_client.schemas import (
|
|
|
20
20
|
ItemOut,
|
|
21
21
|
LogEntryOut,
|
|
22
22
|
Page,
|
|
23
|
+
RerunRequest,
|
|
24
|
+
RunAccepted,
|
|
23
25
|
RunDetail,
|
|
24
26
|
RunOut,
|
|
25
27
|
RunReport,
|
|
28
|
+
RunStats,
|
|
26
29
|
)
|
|
27
30
|
|
|
28
31
|
#: What a run's own report document is stored as, which is how it is told from a step's output.
|
|
@@ -67,9 +70,25 @@ class Runs(Resource):
|
|
|
67
70
|
),
|
|
68
71
|
)
|
|
69
72
|
|
|
73
|
+
async def stats(
|
|
74
|
+
self,
|
|
75
|
+
*,
|
|
76
|
+
since: str | None = None,
|
|
77
|
+
pipeline: str | None = None,
|
|
78
|
+
tags: Sequence[str] = (),
|
|
79
|
+
) -> RunStats:
|
|
80
|
+
"""Summarise the runs that settled within a window, ``7d`` when none is named.
|
|
81
|
+
|
|
82
|
+
Per day by status, per pipeline how long they took, and the errors and steps that
|
|
83
|
+
failed most. Naming more than one tag narrows, as it does on the listing.
|
|
84
|
+
"""
|
|
85
|
+
return await self._one(
|
|
86
|
+
RunStats, "GET", "/runs/$stats", params=query(since=since, pipeline=pipeline, tag=[*tags] or None)
|
|
87
|
+
)
|
|
88
|
+
|
|
70
89
|
async def get(self, run_id: UUID | str) -> RunDetail:
|
|
71
90
|
"""Read a run with the DAG view model, and how many items and attempts it has."""
|
|
72
|
-
return await self._one(RunDetail, "GET", f"/runs/{run_id}")
|
|
91
|
+
return await self._one(RunDetail, "GET", f"/runs/{segment(run_id)}")
|
|
73
92
|
|
|
74
93
|
async def items(
|
|
75
94
|
self,
|
|
@@ -79,7 +98,9 @@ class Runs(Resource):
|
|
|
79
98
|
limit: int | None = None,
|
|
80
99
|
) -> Page[ItemOut]:
|
|
81
100
|
"""List a run's fan-out items in the order they were created, which is grid order."""
|
|
82
|
-
return await self._many(
|
|
101
|
+
return await self._many(
|
|
102
|
+
ItemOut, "GET", f"/runs/{segment(run_id)}/items", params=query(after=after, limit=limit)
|
|
103
|
+
)
|
|
83
104
|
|
|
84
105
|
async def attempts(
|
|
85
106
|
self,
|
|
@@ -94,7 +115,7 @@ class Runs(Resource):
|
|
|
94
115
|
return await self._many(
|
|
95
116
|
AttemptOut,
|
|
96
117
|
"GET",
|
|
97
|
-
f"/runs/{run_id}/attempts",
|
|
118
|
+
f"/runs/{segment(run_id)}/attempts",
|
|
98
119
|
params=query(step=step, status=status.value if status else None, after=after, limit=limit),
|
|
99
120
|
)
|
|
100
121
|
|
|
@@ -106,11 +127,13 @@ class Runs(Resource):
|
|
|
106
127
|
limit: int | None = None,
|
|
107
128
|
) -> Page[ArtifactOut]:
|
|
108
129
|
"""List what a run wrote down: each step's stored output, and its report document."""
|
|
109
|
-
return await self._many(
|
|
130
|
+
return await self._many(
|
|
131
|
+
ArtifactOut, "GET", f"/runs/{segment(run_id)}/artifacts", params=query(after=after, limit=limit)
|
|
132
|
+
)
|
|
110
133
|
|
|
111
134
|
async def artifact_text(self, artifact_id: UUID | str) -> str:
|
|
112
135
|
"""Read one artifact's content as the text it was stored as."""
|
|
113
|
-
return await self._transport.text(f"/artifacts/{artifact_id}")
|
|
136
|
+
return await self._transport.text(f"/artifacts/{segment(artifact_id)}")
|
|
114
137
|
|
|
115
138
|
async def report_document(self, run_id: UUID | str) -> str | None:
|
|
116
139
|
"""Read the markdown document this run rendered when it settled, if it rendered one."""
|
|
@@ -123,12 +146,31 @@ class Runs(Resource):
|
|
|
123
146
|
|
|
124
147
|
async def cancel(self, run_id: UUID | str) -> RunOut:
|
|
125
148
|
"""Stop what has not started, and tell the remote about what has."""
|
|
126
|
-
return await self._one(RunOut, "POST", f"/runs/{run_id}/$cancel")
|
|
149
|
+
return await self._one(RunOut, "POST", f"/runs/{segment(run_id)}/$cancel")
|
|
150
|
+
|
|
151
|
+
async def rerun(self, run_id: UUID | str, *, current: bool = False) -> RunAccepted:
|
|
152
|
+
"""Run a settled run again, with its parameters and, at a source, its input.
|
|
153
|
+
|
|
154
|
+
The new run pins the version the original ran, or the pipeline's current one with
|
|
155
|
+
``current``. Its ``triggered_by_id`` names the original.
|
|
156
|
+
"""
|
|
157
|
+
body = RerunRequest(version="current" if current else "pinned")
|
|
158
|
+
return await self._one(RunAccepted, "POST", f"/runs/{segment(run_id)}/$rerun", json=request_body(body))
|
|
159
|
+
|
|
160
|
+
async def pause(self, run_id: UUID | str) -> RunOut:
|
|
161
|
+
"""Hold a queued or running run: nothing more of it starts, and its deadlines stand still."""
|
|
162
|
+
return await self._one(RunOut, "POST", f"/runs/{segment(run_id)}/$pause")
|
|
163
|
+
|
|
164
|
+
async def resume(self, run_id: UUID | str) -> RunOut:
|
|
165
|
+
"""Let a paused run go on, its open deadlines moved on by as long as it was paused."""
|
|
166
|
+
return await self._one(RunOut, "POST", f"/runs/{segment(run_id)}/$resume")
|
|
127
167
|
|
|
128
168
|
async def retry(self, attempt_id: UUID | str, *, idempotency_key: str | None = None) -> AttemptOut:
|
|
129
169
|
"""Create one manual attempt of a failed step, reading its upstream stored outputs."""
|
|
130
170
|
key = idempotency_key or str(uuid.uuid4())
|
|
131
|
-
return await self._one(
|
|
171
|
+
return await self._one(
|
|
172
|
+
AttemptOut, "POST", f"/attempts/{segment(attempt_id)}/$retry", headers={"Idempotency-Key": key}
|
|
173
|
+
)
|
|
132
174
|
|
|
133
175
|
async def logs(
|
|
134
176
|
self,
|
|
@@ -140,12 +182,12 @@ class Runs(Resource):
|
|
|
140
182
|
) -> Page[LogEntryOut]:
|
|
141
183
|
"""Read one page of a run's log entries, in write order."""
|
|
142
184
|
return await self._many(
|
|
143
|
-
LogEntryOut, "GET", f"/runs/{run_id}/$logs", params=query(after=after, limit=limit, step=step)
|
|
185
|
+
LogEntryOut, "GET", f"/runs/{segment(run_id)}/$logs", params=query(after=after, limit=limit, step=step)
|
|
144
186
|
)
|
|
145
187
|
|
|
146
188
|
async def report(self, run_id: UUID | str) -> RunReport:
|
|
147
189
|
"""Summarise a run: what each step amounted to, and how long the whole thing took."""
|
|
148
|
-
return await self._one(RunReport, "GET", f"/runs/{run_id}/$report")
|
|
190
|
+
return await self._one(RunReport, "GET", f"/runs/{segment(run_id)}/$report")
|
|
149
191
|
|
|
150
192
|
async def wait(
|
|
151
193
|
self,
|
|
@@ -164,7 +206,7 @@ class Runs(Resource):
|
|
|
164
206
|
if time.monotonic() + interval > deadline:
|
|
165
207
|
raise WaitTimeout(
|
|
166
208
|
f"run {run_id} was still {run.status.value} after {timeout}",
|
|
167
|
-
url=self._transport.endpoint(f"/runs/{run_id}"),
|
|
209
|
+
url=self._transport.endpoint(f"/runs/{segment(run_id)}"),
|
|
168
210
|
)
|
|
169
211
|
await asyncio.sleep(interval)
|
|
170
212
|
interval = min(interval * POLL_BACKOFF, POLL_MAX_SECONDS)
|
|
@@ -194,7 +236,9 @@ class Runs(Resource):
|
|
|
194
236
|
expired = False
|
|
195
237
|
advanced = False
|
|
196
238
|
try:
|
|
197
|
-
async with self._transport.stream(
|
|
239
|
+
async with self._transport.stream(
|
|
240
|
+
f"/runs/{segment(run_id)}/$events", params=query(after=cursor)
|
|
241
|
+
) as response:
|
|
198
242
|
async for event in httpx2.EventSource(response):
|
|
199
243
|
if event.event == "end":
|
|
200
244
|
ended = True
|
|
@@ -254,7 +298,7 @@ class Runs(Resource):
|
|
|
254
298
|
advanced = False
|
|
255
299
|
try:
|
|
256
300
|
params = query(follow="sse", after=cursor, step=step)
|
|
257
|
-
async with self._transport.stream(f"/runs/{run_id}/$logs", params=params) as response:
|
|
301
|
+
async with self._transport.stream(f"/runs/{segment(run_id)}/$logs", params=params) as response:
|
|
258
302
|
async for event in httpx2.EventSource(response):
|
|
259
303
|
if event.event == "end":
|
|
260
304
|
ended = True
|