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,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