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,20 @@
|
|
|
1
|
+
"""Triggers documents: the clocks and webhooks one document declares for another's pipeline."""
|
|
2
|
+
|
|
3
|
+
from dirigent_client.resources.base import Resource, query
|
|
4
|
+
from dirigent_client.schemas import Page, TriggerDocumentDetail, TriggerDocumentOut
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class TriggerDocuments(Resource):
|
|
8
|
+
"""Read and remove the ``kind: triggers`` documents this instance holds."""
|
|
9
|
+
|
|
10
|
+
async def list(self, *, after: str | None = None, limit: int | None = None) -> Page[TriggerDocumentOut]:
|
|
11
|
+
"""List every triggers document, in code order."""
|
|
12
|
+
return await self._many(TriggerDocumentOut, "GET", "/trigger-documents", params=query(after=after, limit=limit))
|
|
13
|
+
|
|
14
|
+
async def get(self, code: str) -> TriggerDocumentDetail:
|
|
15
|
+
"""Read one triggers document, with the rows it owns."""
|
|
16
|
+
return await self._one(TriggerDocumentDetail, "GET", f"/trigger-documents/{code}")
|
|
17
|
+
|
|
18
|
+
async def delete(self, code: str) -> None:
|
|
19
|
+
"""Remove a triggers document and every schedule and webhook it owns."""
|
|
20
|
+
await self._transport.request("DELETE", f"/trigger-documents/{code}")
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""A pipeline's inbound webhooks: their mapping, their token, and what has arrived."""
|
|
2
|
+
|
|
3
|
+
from pydantic import SecretStr
|
|
4
|
+
|
|
5
|
+
from dirigent_client.enums import RunPriority
|
|
6
|
+
from dirigent_client.resources.base import Resource, query, request_body
|
|
7
|
+
from dirigent_client.schemas import DeliveryOut, Page, WebhookIn, WebhookOut, WebhookTokenOut
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Webhooks(Resource):
|
|
11
|
+
"""Declare, enable, rotate, and read the webhooks hanging from a pipeline."""
|
|
12
|
+
|
|
13
|
+
async def list(self, pipeline: str, *, after: str | None = None, limit: int | None = None) -> Page[WebhookOut]:
|
|
14
|
+
"""List every webhook on a pipeline, with its mapping but never its token."""
|
|
15
|
+
return await self._many(
|
|
16
|
+
WebhookOut,
|
|
17
|
+
"GET",
|
|
18
|
+
f"/pipelines/{pipeline}/triggers/webhooks",
|
|
19
|
+
params=query(after=after, limit=limit),
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
async def get(self, pipeline: str, code: str) -> WebhookOut:
|
|
23
|
+
"""Read one webhook by code, with its mapping but never its token."""
|
|
24
|
+
return await self._one(WebhookOut, "GET", f"/pipelines/{pipeline}/triggers/webhooks/{code}")
|
|
25
|
+
|
|
26
|
+
async def create(
|
|
27
|
+
self,
|
|
28
|
+
pipeline: str,
|
|
29
|
+
code: str,
|
|
30
|
+
*,
|
|
31
|
+
name: str | None = None,
|
|
32
|
+
description: str | None = None,
|
|
33
|
+
params_from_payload: dict[str, str] | None = None,
|
|
34
|
+
hmac_secret: str | None = None,
|
|
35
|
+
rate_limit_per_minute: int = 60,
|
|
36
|
+
priority: RunPriority | None = None,
|
|
37
|
+
) -> WebhookTokenOut:
|
|
38
|
+
"""Declare a webhook and return its token, which is shown here and nowhere else again."""
|
|
39
|
+
payload = WebhookIn(
|
|
40
|
+
code=code,
|
|
41
|
+
name=name,
|
|
42
|
+
description=description,
|
|
43
|
+
params_from_payload=params_from_payload or {},
|
|
44
|
+
hmac_secret=SecretStr(hmac_secret) if hmac_secret else None,
|
|
45
|
+
rate_limit_per_minute=rate_limit_per_minute,
|
|
46
|
+
priority=priority,
|
|
47
|
+
)
|
|
48
|
+
return await self._one(
|
|
49
|
+
WebhookTokenOut,
|
|
50
|
+
"POST",
|
|
51
|
+
f"/pipelines/{pipeline}/triggers/webhooks",
|
|
52
|
+
json=request_body(payload),
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
async def rotate_token(self, pipeline: str, code: str) -> WebhookTokenOut:
|
|
56
|
+
"""Mint a new token and forget the old one immediately; callers must be updated."""
|
|
57
|
+
return await self._one(WebhookTokenOut, "POST", f"/pipelines/{pipeline}/triggers/webhooks/{code}/$rotate-token")
|
|
58
|
+
|
|
59
|
+
async def enable(self, pipeline: str, code: str) -> WebhookOut:
|
|
60
|
+
"""Accept deliveries again on the token that was already issued."""
|
|
61
|
+
return await self._one(WebhookOut, "POST", f"/pipelines/{pipeline}/triggers/webhooks/{code}/$enable")
|
|
62
|
+
|
|
63
|
+
async def disable(self, pipeline: str, code: str) -> WebhookOut:
|
|
64
|
+
"""Refuse deliveries without rotating or losing the token."""
|
|
65
|
+
return await self._one(WebhookOut, "POST", f"/pipelines/{pipeline}/triggers/webhooks/{code}/$disable")
|
|
66
|
+
|
|
67
|
+
async def delete(self, pipeline: str, code: str) -> None:
|
|
68
|
+
"""Remove a webhook, its token, and its delivery history."""
|
|
69
|
+
await self._transport.request("DELETE", f"/pipelines/{pipeline}/triggers/webhooks/{code}")
|
|
70
|
+
|
|
71
|
+
async def deliveries(
|
|
72
|
+
self, pipeline: str, code: str, *, after: str | None = None, limit: int | None = None
|
|
73
|
+
) -> Page[DeliveryOut]:
|
|
74
|
+
"""Read what has arrived, newest first, refusals included."""
|
|
75
|
+
return await self._many(
|
|
76
|
+
DeliveryOut,
|
|
77
|
+
"GET",
|
|
78
|
+
f"/pipelines/{pipeline}/triggers/webhooks/{code}/deliveries",
|
|
79
|
+
params=query(after=after, limit=limit),
|
|
80
|
+
)
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"""The wire schemas: one definition of every shape the REST API accepts and answers with."""
|
|
2
|
+
|
|
3
|
+
from dirigent_client.schemas.alerts import (
|
|
4
|
+
AlertRuleIn,
|
|
5
|
+
AlertRuleOut,
|
|
6
|
+
AlertRuleUpdate,
|
|
7
|
+
NotificationOut,
|
|
8
|
+
TestQueued,
|
|
9
|
+
TestRequest,
|
|
10
|
+
)
|
|
11
|
+
from dirigent_client.schemas.auth import (
|
|
12
|
+
Identity,
|
|
13
|
+
IssuedTokenOut,
|
|
14
|
+
LoginRequest,
|
|
15
|
+
PasswordChangeRequest,
|
|
16
|
+
PasswordResetRequest,
|
|
17
|
+
TokenOut,
|
|
18
|
+
TokenRequest,
|
|
19
|
+
UserIn,
|
|
20
|
+
UserOut,
|
|
21
|
+
UserUpdate,
|
|
22
|
+
)
|
|
23
|
+
from dirigent_client.schemas.catalog import BlockEntry, BlockKind, Catalog, SurfaceEntry
|
|
24
|
+
from dirigent_client.schemas.common import Page, Problem
|
|
25
|
+
from dirigent_client.schemas.connections import ConnectionIn, ConnectionOut, ConnectionUpdate
|
|
26
|
+
from dirigent_client.schemas.pipelines import (
|
|
27
|
+
ApplyRequest,
|
|
28
|
+
ApplyResult,
|
|
29
|
+
BackfillAccepted,
|
|
30
|
+
BackfilledRun,
|
|
31
|
+
BackfillRequest,
|
|
32
|
+
DiffSummary,
|
|
33
|
+
LastRun,
|
|
34
|
+
Materialized,
|
|
35
|
+
PipelineDetail,
|
|
36
|
+
PipelineOut,
|
|
37
|
+
PipelinePlan,
|
|
38
|
+
PipelineVersionOut,
|
|
39
|
+
PlanAction,
|
|
40
|
+
PruneRequest,
|
|
41
|
+
PruneResult,
|
|
42
|
+
RunAccepted,
|
|
43
|
+
RunRequest,
|
|
44
|
+
ValidationIssue,
|
|
45
|
+
)
|
|
46
|
+
from dirigent_client.schemas.runs import (
|
|
47
|
+
TERMINAL_RUN_STATUSES,
|
|
48
|
+
AttemptEvent,
|
|
49
|
+
AttemptOut,
|
|
50
|
+
DagNode,
|
|
51
|
+
DagView,
|
|
52
|
+
ItemOut,
|
|
53
|
+
LogEntryOut,
|
|
54
|
+
RunDetail,
|
|
55
|
+
RunOut,
|
|
56
|
+
RunReport,
|
|
57
|
+
StepReport,
|
|
58
|
+
)
|
|
59
|
+
from dirigent_client.schemas.schemas import SchemaIn, SchemaOut, SchemaUpdate
|
|
60
|
+
from dirigent_client.schemas.system import (
|
|
61
|
+
CheckResult,
|
|
62
|
+
CheckStatus,
|
|
63
|
+
ConnectionHealth,
|
|
64
|
+
Health,
|
|
65
|
+
Readiness,
|
|
66
|
+
SystemInfo,
|
|
67
|
+
WorkerOut,
|
|
68
|
+
)
|
|
69
|
+
from dirigent_client.schemas.triggers import (
|
|
70
|
+
DeliveryOut,
|
|
71
|
+
FiringOut,
|
|
72
|
+
HookAccepted,
|
|
73
|
+
ScheduleIn,
|
|
74
|
+
ScheduleOut,
|
|
75
|
+
SchedulePreview,
|
|
76
|
+
SchedulePreviewRequest,
|
|
77
|
+
TriggerDocumentDetail,
|
|
78
|
+
TriggerDocumentOut,
|
|
79
|
+
WebhookIn,
|
|
80
|
+
WebhookOut,
|
|
81
|
+
WebhookTokenOut,
|
|
82
|
+
)
|
|
83
|
+
from dirigent_common import JsonList, JsonMap
|
|
84
|
+
|
|
85
|
+
__all__ = [
|
|
86
|
+
"TERMINAL_RUN_STATUSES",
|
|
87
|
+
"AlertRuleIn",
|
|
88
|
+
"AlertRuleOut",
|
|
89
|
+
"AlertRuleUpdate",
|
|
90
|
+
"ApplyRequest",
|
|
91
|
+
"ApplyResult",
|
|
92
|
+
"PruneRequest",
|
|
93
|
+
"PruneResult",
|
|
94
|
+
"BackfillAccepted",
|
|
95
|
+
"BackfillRequest",
|
|
96
|
+
"BackfilledRun",
|
|
97
|
+
"AttemptEvent",
|
|
98
|
+
"AttemptOut",
|
|
99
|
+
"BlockEntry",
|
|
100
|
+
"BlockKind",
|
|
101
|
+
"Catalog",
|
|
102
|
+
"CheckResult",
|
|
103
|
+
"CheckStatus",
|
|
104
|
+
"ConnectionHealth",
|
|
105
|
+
"ConnectionIn",
|
|
106
|
+
"ConnectionOut",
|
|
107
|
+
"ConnectionUpdate",
|
|
108
|
+
"SchemaIn",
|
|
109
|
+
"SchemaOut",
|
|
110
|
+
"SchemaUpdate",
|
|
111
|
+
"DagNode",
|
|
112
|
+
"DagView",
|
|
113
|
+
"DeliveryOut",
|
|
114
|
+
"DiffSummary",
|
|
115
|
+
"FiringOut",
|
|
116
|
+
"Health",
|
|
117
|
+
"HookAccepted",
|
|
118
|
+
"Identity",
|
|
119
|
+
"IssuedTokenOut",
|
|
120
|
+
"ItemOut",
|
|
121
|
+
"JsonList",
|
|
122
|
+
"JsonMap",
|
|
123
|
+
"LastRun",
|
|
124
|
+
"LogEntryOut",
|
|
125
|
+
"LoginRequest",
|
|
126
|
+
"Materialized",
|
|
127
|
+
"NotificationOut",
|
|
128
|
+
"Page",
|
|
129
|
+
"PasswordChangeRequest",
|
|
130
|
+
"PasswordResetRequest",
|
|
131
|
+
"PipelineDetail",
|
|
132
|
+
"PipelineOut",
|
|
133
|
+
"PipelinePlan",
|
|
134
|
+
"PipelineVersionOut",
|
|
135
|
+
"PlanAction",
|
|
136
|
+
"Problem",
|
|
137
|
+
"Readiness",
|
|
138
|
+
"RunAccepted",
|
|
139
|
+
"RunDetail",
|
|
140
|
+
"RunOut",
|
|
141
|
+
"RunReport",
|
|
142
|
+
"RunRequest",
|
|
143
|
+
"ScheduleIn",
|
|
144
|
+
"ScheduleOut",
|
|
145
|
+
"SchedulePreview",
|
|
146
|
+
"SchedulePreviewRequest",
|
|
147
|
+
"StepReport",
|
|
148
|
+
"SurfaceEntry",
|
|
149
|
+
"SystemInfo",
|
|
150
|
+
"TestQueued",
|
|
151
|
+
"TestRequest",
|
|
152
|
+
"TokenOut",
|
|
153
|
+
"TokenRequest",
|
|
154
|
+
"TriggerDocumentDetail",
|
|
155
|
+
"TriggerDocumentOut",
|
|
156
|
+
"UserIn",
|
|
157
|
+
"UserOut",
|
|
158
|
+
"UserUpdate",
|
|
159
|
+
"ValidationIssue",
|
|
160
|
+
"WebhookIn",
|
|
161
|
+
"WebhookOut",
|
|
162
|
+
"WebhookTokenOut",
|
|
163
|
+
"WorkerOut",
|
|
164
|
+
]
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""Alert rules, and the notification queue they deliver through."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime, timedelta
|
|
4
|
+
from uuid import UUID
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel, Field
|
|
7
|
+
|
|
8
|
+
from dirigent_client.enums import AlertEvent, AlertScope, NotificationStatus
|
|
9
|
+
from dirigent_client.schemas.common import WireModel
|
|
10
|
+
from dirigent_common import EntityName
|
|
11
|
+
from dirigent_common.durations import Duration
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class AlertRuleIn(BaseModel):
|
|
15
|
+
"""An alert rule as a caller declares it."""
|
|
16
|
+
|
|
17
|
+
code: EntityName
|
|
18
|
+
name: str | None = None
|
|
19
|
+
description: str | None = None
|
|
20
|
+
event: AlertEvent
|
|
21
|
+
notifier: str
|
|
22
|
+
scope: AlertScope = AlertScope.GLOBAL
|
|
23
|
+
pipeline: str | None = None
|
|
24
|
+
connection: str | None = None
|
|
25
|
+
template: str | None = Field(default=None, description="Message subject, reading ${run.*}.")
|
|
26
|
+
throttle: Duration = timedelta(0)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class AlertRuleOut(WireModel):
|
|
30
|
+
"""An alert rule as a listing shows it."""
|
|
31
|
+
|
|
32
|
+
id: UUID
|
|
33
|
+
code: str
|
|
34
|
+
name: str | None = None
|
|
35
|
+
description: str | None = None
|
|
36
|
+
event: AlertEvent
|
|
37
|
+
scope: AlertScope
|
|
38
|
+
pipeline: str | None = None
|
|
39
|
+
notifier: str
|
|
40
|
+
connection: str | None = None
|
|
41
|
+
template: str | None = None
|
|
42
|
+
throttle: str
|
|
43
|
+
active: bool
|
|
44
|
+
paused: bool = False
|
|
45
|
+
last_sent_at: datetime | None = None
|
|
46
|
+
created_at: datetime
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class AlertRuleUpdate(BaseModel):
|
|
50
|
+
"""What a PATCH may change about an alert rule, which is whether it is paused."""
|
|
51
|
+
|
|
52
|
+
paused: bool
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class NotificationOut(WireModel):
|
|
56
|
+
"""One queued or delivered alert."""
|
|
57
|
+
|
|
58
|
+
id: UUID
|
|
59
|
+
event: AlertEvent
|
|
60
|
+
rule: str | None = Field(default=None, description="The rule that raised it; null for a test.")
|
|
61
|
+
notifier: str
|
|
62
|
+
connection: str | None = None
|
|
63
|
+
subject: str
|
|
64
|
+
status: NotificationStatus
|
|
65
|
+
attempt: int
|
|
66
|
+
max_attempts: int
|
|
67
|
+
run_id: UUID | None = None
|
|
68
|
+
run_pipeline: str | None = None
|
|
69
|
+
run_started_at: datetime | None = None
|
|
70
|
+
available_at: datetime
|
|
71
|
+
sent_at: datetime | None = None
|
|
72
|
+
error: str | None = None
|
|
73
|
+
created_at: datetime
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class TestRequest(BaseModel):
|
|
77
|
+
"""Which channel to send a test message through, and what to say."""
|
|
78
|
+
|
|
79
|
+
notifier: str
|
|
80
|
+
connection: str | None = None
|
|
81
|
+
subject: str = "dirigent test alert"
|
|
82
|
+
body: str = "This is a test message sent through the notifier surface."
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class TestQueued(WireModel):
|
|
86
|
+
"""What a test message queued."""
|
|
87
|
+
|
|
88
|
+
notification_id: UUID
|
|
89
|
+
notifier: str
|
|
90
|
+
detail: str = "queued; a worker delivers it on its next pass"
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""Who the caller is, the accounts an instance holds, and the tokens automation uses."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from uuid import UUID
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel, Field, SecretStr
|
|
7
|
+
|
|
8
|
+
from dirigent_client.enums import TokenKind, UserRole
|
|
9
|
+
from dirigent_client.schemas.common import WireModel
|
|
10
|
+
from dirigent_common import Email, EntityName
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class LoginRequest(BaseModel):
|
|
14
|
+
"""What a login presents."""
|
|
15
|
+
|
|
16
|
+
username: str = Field(min_length=1)
|
|
17
|
+
password: SecretStr
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Identity(WireModel):
|
|
21
|
+
"""Who the caller is."""
|
|
22
|
+
|
|
23
|
+
user_id: UUID
|
|
24
|
+
username: str
|
|
25
|
+
role: UserRole
|
|
26
|
+
via: TokenKind | None = None
|
|
27
|
+
token_name: str | None = None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class TokenOut(WireModel):
|
|
31
|
+
"""An API token as a listing shows it: everything except the secret."""
|
|
32
|
+
|
|
33
|
+
id: UUID
|
|
34
|
+
name: str
|
|
35
|
+
username: str
|
|
36
|
+
prefix: str
|
|
37
|
+
created_at: datetime
|
|
38
|
+
last_used_at: datetime | None = None
|
|
39
|
+
expires_at: datetime | None = None
|
|
40
|
+
revoked_at: datetime | None = None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class TokenRequest(BaseModel):
|
|
44
|
+
"""What creating an API token asks for."""
|
|
45
|
+
|
|
46
|
+
name: EntityName
|
|
47
|
+
"""The token's name, which is how it is revoked and how it reads in an audit trail.
|
|
48
|
+
|
|
49
|
+
An entity name like every other named thing an instance holds: it appears in a REST
|
|
50
|
+
path (``DELETE /users/{username}/tokens/{name}`` and ``DELETE /tokens/{name}``), so a
|
|
51
|
+
name with a slash or a space in it is a token nobody can revoke."""
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class IssuedTokenOut(WireModel):
|
|
55
|
+
"""A freshly minted token: the one and only time its secret is returned."""
|
|
56
|
+
|
|
57
|
+
id: UUID
|
|
58
|
+
name: str
|
|
59
|
+
username: str
|
|
60
|
+
prefix: str
|
|
61
|
+
token: str
|
|
62
|
+
"""The secret. It is not stored and cannot be shown again."""
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class UserOut(WireModel):
|
|
66
|
+
"""An account as a listing shows it; the password hash is never rendered."""
|
|
67
|
+
|
|
68
|
+
id: UUID
|
|
69
|
+
username: str
|
|
70
|
+
name: str | None = None
|
|
71
|
+
email: str | None = None
|
|
72
|
+
role: UserRole
|
|
73
|
+
active: bool
|
|
74
|
+
last_login_at: datetime | None = None
|
|
75
|
+
created_at: datetime
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class UserIn(BaseModel):
|
|
79
|
+
"""What creating an account asks for."""
|
|
80
|
+
|
|
81
|
+
username: str = Field(min_length=1, max_length=200)
|
|
82
|
+
password: SecretStr
|
|
83
|
+
name: str | None = None
|
|
84
|
+
email: Email | None = None
|
|
85
|
+
role: UserRole
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class UserUpdate(BaseModel):
|
|
89
|
+
"""What editing an account may change; the username and the password are not among them.
|
|
90
|
+
|
|
91
|
+
A field left out is left alone, and ``name`` or ``email`` sent as null is cleared. Those
|
|
92
|
+
are different requests, so they are told apart by what the body contained rather than by
|
|
93
|
+
the value landing as ``None`` either way. A role is never cleared, only replaced.
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
name: str | None = None
|
|
97
|
+
email: Email | None = None
|
|
98
|
+
role: UserRole | None = None
|
|
99
|
+
|
|
100
|
+
def changing(self, field: str) -> bool:
|
|
101
|
+
"""Report whether the request said anything about this field at all."""
|
|
102
|
+
return field in self.model_fields_set
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class PasswordChangeRequest(BaseModel):
|
|
106
|
+
"""What a person changing their own password presents."""
|
|
107
|
+
|
|
108
|
+
current_password: SecretStr
|
|
109
|
+
new_password: SecretStr
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class PasswordResetRequest(BaseModel):
|
|
113
|
+
"""What an admin resetting another account's password presents."""
|
|
114
|
+
|
|
115
|
+
password: SecretStr
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""The block catalog: what an instance can run, and the schemas a form is built from."""
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import json
|
|
5
|
+
from enum import StrEnum
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, Field
|
|
8
|
+
|
|
9
|
+
from dirigent_common import API_VERSION, JsonMap
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class BlockKind(StrEnum):
|
|
13
|
+
"""Which block surface a catalog entry describes."""
|
|
14
|
+
|
|
15
|
+
OPERATOR = "operator"
|
|
16
|
+
"""Does work; may finish synchronously or return a remote handle."""
|
|
17
|
+
|
|
18
|
+
SENSOR = "sensor"
|
|
19
|
+
"""Waits for the world through durable, scheduled pokes."""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class BlockEntry(BaseModel):
|
|
23
|
+
"""One block in the catalog, with the schemas a form is built from."""
|
|
24
|
+
|
|
25
|
+
id: str
|
|
26
|
+
kind: BlockKind
|
|
27
|
+
summary: str
|
|
28
|
+
group: str
|
|
29
|
+
plugin: str
|
|
30
|
+
idempotent: bool = False
|
|
31
|
+
local_execution: bool = False
|
|
32
|
+
default_poll_seconds: float | None = None
|
|
33
|
+
default_deadline_seconds: float | None = None
|
|
34
|
+
config_schema: JsonMap = Field(default_factory=dict)
|
|
35
|
+
output_schema: JsonMap = Field(default_factory=dict)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class SurfaceEntry(BaseModel):
|
|
39
|
+
"""One catalog entry for a non-block surface: a scheme, a notifier, a connection kind."""
|
|
40
|
+
|
|
41
|
+
id: str
|
|
42
|
+
plugin: str
|
|
43
|
+
config_schema: JsonMap = Field(default_factory=dict)
|
|
44
|
+
secret_fields: list[str] = Field(default_factory=list[str])
|
|
45
|
+
"""Which config fields this surface declares secret, so a form draws them write-only."""
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class Catalog(BaseModel):
|
|
49
|
+
"""The host's merged view of every contribution."""
|
|
50
|
+
|
|
51
|
+
api_version: int = API_VERSION
|
|
52
|
+
plugins: list[str] = Field(default_factory=list[str])
|
|
53
|
+
blocks: list[BlockEntry] = Field(default_factory=list[BlockEntry])
|
|
54
|
+
storage_schemes: list[SurfaceEntry] = Field(default_factory=list[SurfaceEntry])
|
|
55
|
+
notifiers: list[SurfaceEntry] = Field(default_factory=list[SurfaceEntry])
|
|
56
|
+
connection_kinds: list[SurfaceEntry] = Field(default_factory=list[SurfaceEntry])
|
|
57
|
+
|
|
58
|
+
def block(self, block_id: str) -> BlockEntry | None:
|
|
59
|
+
"""Find one catalog entry by block id."""
|
|
60
|
+
return next((entry for entry in self.blocks if entry.id == block_id), None)
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def digest(self) -> str:
|
|
64
|
+
"""Hash the catalog, so a worker registry row can prove code parity across nodes."""
|
|
65
|
+
payload = json.dumps(self.model_dump(mode="json"), sort_keys=True, separators=(",", ":")).encode()
|
|
66
|
+
return f"sha256:{hashlib.sha256(payload).hexdigest()}"
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""What every other wire model is built out of: the base itself, one refusal, and one page."""
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class WireModel(BaseModel):
|
|
7
|
+
"""A frozen shape that crosses the wire."""
|
|
8
|
+
|
|
9
|
+
model_config = ConfigDict(frozen=True)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Problem(WireModel):
|
|
13
|
+
"""One refusal, in the one shape every error response takes."""
|
|
14
|
+
|
|
15
|
+
status: int
|
|
16
|
+
"""The HTTP status, repeated in the body so a logged payload is self-contained."""
|
|
17
|
+
|
|
18
|
+
title: str
|
|
19
|
+
"""The short, stable name of the condition: the status phrase."""
|
|
20
|
+
|
|
21
|
+
detail: str
|
|
22
|
+
"""One sentence a person can act on."""
|
|
23
|
+
|
|
24
|
+
problems: list[str] = Field(default_factory=list[str])
|
|
25
|
+
"""The individual failures, when the refusal is a list of them rather than one."""
|
|
26
|
+
|
|
27
|
+
instance: str | None = None
|
|
28
|
+
"""The path that was asked for, redacted of any credential it carried."""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class Page[T](WireModel):
|
|
32
|
+
"""One page of a listing, and the cursor that continues it."""
|
|
33
|
+
|
|
34
|
+
items: list[T]
|
|
35
|
+
"""The rows of this page, in the listing's own order."""
|
|
36
|
+
|
|
37
|
+
next: str | None = None
|
|
38
|
+
"""The opaque cursor a caller passes as ``after`` to continue, or nothing at the end."""
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Connections: coded credential records of a contributed kind, redacted in every response."""
|
|
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 ConnectionIn(BaseModel):
|
|
13
|
+
"""What creating or replacing a connection sends; secret fields are write-only."""
|
|
14
|
+
|
|
15
|
+
code: EntityName
|
|
16
|
+
name: str | None = None
|
|
17
|
+
kind: str = Field(min_length=1, max_length=100)
|
|
18
|
+
description: str | None = None
|
|
19
|
+
config: JsonMap = Field(default_factory=dict)
|
|
20
|
+
"""The kind's own config, validated against its published model before anything is stored."""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ConnectionUpdate(BaseModel):
|
|
24
|
+
"""What editing a connection may change; the kind and code are fixed once minted.
|
|
25
|
+
|
|
26
|
+
A field left out is left alone, and a field sent as null is cleared. Those are different
|
|
27
|
+
requests, so they are told apart by what the body contained rather than by the value
|
|
28
|
+
landing as ``None`` either way.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
name: str | None = None
|
|
32
|
+
description: str | None = None
|
|
33
|
+
config: JsonMap | None = None
|
|
34
|
+
|
|
35
|
+
def changing(self, field: str) -> bool:
|
|
36
|
+
"""Report whether the request said anything about this field at all."""
|
|
37
|
+
return field in self.model_fields_set
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class ConnectionOut(WireModel):
|
|
41
|
+
"""A connection as every response shows it: settings visible, secrets redacted."""
|
|
42
|
+
|
|
43
|
+
id: UUID
|
|
44
|
+
code: str
|
|
45
|
+
name: str | None = None
|
|
46
|
+
kind: str
|
|
47
|
+
description: str | None = None
|
|
48
|
+
config: JsonMap
|
|
49
|
+
secret_fields: list[str] = Field(default_factory=list[str])
|
|
50
|
+
"""Which fields the kind declares secret, so a form knows what to render as a password."""
|
|
51
|
+
|
|
52
|
+
last_check_at: datetime | None = None
|
|
53
|
+
last_check_healthy: bool | None = None
|
|
54
|
+
last_check_detail: str | None = None
|
|
55
|
+
created_at: datetime
|
|
56
|
+
updated_at: datetime
|