dploydb 0.1.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,389 @@
1
+ """Typed candidate-application lifecycle contracts."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ import threading
7
+ from collections.abc import Mapping, Sequence
8
+ from dataclasses import dataclass
9
+ from pathlib import Path
10
+ from typing import Protocol
11
+
12
+ from dploydb.models import ProductionApplicationHandle
13
+ from dploydb.subprocesses import CommandResult
14
+
15
+ _OPERATION_ID = re.compile(r"op_[0-9a-f]{32}\Z")
16
+ _RELEASE_IDENTIFIER = re.compile(r"[A-Za-z0-9][A-Za-z0-9._-]{0,63}\Z")
17
+
18
+
19
+ def validate_operation_id(value: str) -> str:
20
+ """Validate an opaque operation identifier before deriving resource names."""
21
+ if not isinstance(value, str):
22
+ raise TypeError("operation_id must be a string")
23
+ if _OPERATION_ID.fullmatch(value) is None:
24
+ raise ValueError("operation_id must be an opaque DployDB operation ID")
25
+ return value
26
+
27
+
28
+ def validate_release_identifier(value: str) -> str:
29
+ """Validate the bounded value made available to Compose interpolation."""
30
+ if not isinstance(value, str):
31
+ raise TypeError("version must be a string")
32
+ if _RELEASE_IDENTIFIER.fullmatch(value) is None or ".." in value:
33
+ raise ValueError(
34
+ "version must be 1-64 letters, digits, dots, underscores, or hyphens, "
35
+ "start with a letter or digit, and contain no traversal sequence"
36
+ )
37
+ return value
38
+
39
+
40
+ class CommandExecutor(Protocol):
41
+ """Subset of the bounded subprocess runner used by application runners."""
42
+
43
+ def run(
44
+ self,
45
+ command: Sequence[str],
46
+ *,
47
+ timeout_seconds: float,
48
+ environment: Mapping[str, str],
49
+ working_directory: Path | None = None,
50
+ cancellation_event: threading.Event | None = None,
51
+ ) -> CommandResult: ...
52
+
53
+
54
+ @dataclass(frozen=True, slots=True)
55
+ class CandidateHandle:
56
+ """Opaque identity and expected isolation boundary for one candidate."""
57
+
58
+ operation_id: str
59
+ version: str
60
+ compose_project: str
61
+ container_name: str
62
+ rehearsal_database_path: Path
63
+ candidate_database_path: str
64
+
65
+
66
+ @dataclass(frozen=True, slots=True)
67
+ class CandidateMount:
68
+ """One selected mount from live Docker inspection evidence."""
69
+
70
+ mount_type: str
71
+ source: str
72
+ destination: str
73
+ read_write: bool
74
+
75
+
76
+ @dataclass(frozen=True, slots=True)
77
+ class CandidateInspection:
78
+ """Validated live evidence for an isolated running candidate."""
79
+
80
+ container_id: str
81
+ container_name: str
82
+ running: bool
83
+ compose_project: str
84
+ compose_service: str
85
+ operation_id: str
86
+ host_ip: str
87
+ host_port: int
88
+ container_port: int
89
+ mounts: tuple[CandidateMount, ...]
90
+ command: CommandResult
91
+
92
+
93
+ @dataclass(frozen=True, slots=True)
94
+ class CandidateStart:
95
+ """Evidence returned after Compose creates the isolated candidate."""
96
+
97
+ handle: CandidateHandle
98
+ container_reference: str
99
+ command: CommandResult
100
+
101
+
102
+ @dataclass(frozen=True, slots=True)
103
+ class CandidateLogs:
104
+ """Bounded redacted application-log capture."""
105
+
106
+ handle: CandidateHandle
107
+ command: CommandResult
108
+
109
+
110
+ @dataclass(frozen=True, slots=True)
111
+ class CandidateCleanupProof:
112
+ """Read-only proof that no candidate container or project network remains."""
113
+
114
+ container_absent: bool
115
+ networks_absent: bool
116
+ container_query: CommandResult
117
+ network_query: CommandResult
118
+
119
+ @property
120
+ def proven(self) -> bool:
121
+ return self.container_absent and self.networks_absent
122
+
123
+
124
+ @dataclass(frozen=True, slots=True)
125
+ class CandidateCleanup:
126
+ """Commands and terminal proof from idempotent isolated cleanup."""
127
+
128
+ presence_query: CommandResult
129
+ remove_command: CommandResult | None
130
+ compose_down: CommandResult
131
+ proof: CandidateCleanupProof
132
+
133
+
134
+ class CandidateRunnerError(RuntimeError):
135
+ """Typed low-level failure containing only bounded redacted evidence."""
136
+
137
+ def __init__(
138
+ self,
139
+ message: str,
140
+ *,
141
+ command: CommandResult | None = None,
142
+ cleanup: CandidateCleanup | None = None,
143
+ ) -> None:
144
+ super().__init__(message)
145
+ self.command = command
146
+ self.cleanup = cleanup
147
+
148
+ @property
149
+ def cleanup_proven(self) -> bool | None:
150
+ return None if self.cleanup is None else self.cleanup.proof.proven
151
+
152
+
153
+ class CandidateStartError(CandidateRunnerError):
154
+ """Candidate creation failed; cleanup evidence states whether retry is safe."""
155
+
156
+
157
+ class CandidateInspectionError(CandidateRunnerError):
158
+ """Live Docker state contradicted the required candidate isolation boundary."""
159
+
160
+
161
+ class CandidateCleanupError(CandidateRunnerError):
162
+ """Candidate cleanup could not be proven."""
163
+
164
+
165
+ class ApplicationRunner(Protocol):
166
+ """Small Milestone 4A candidate lifecycle; production controls come later."""
167
+
168
+ def start(
169
+ self,
170
+ *,
171
+ operation_id: str,
172
+ version: str,
173
+ rehearsal_database_path: Path,
174
+ cancellation_event: threading.Event | None = None,
175
+ ) -> CandidateStart: ...
176
+
177
+ def inspect(
178
+ self,
179
+ handle: CandidateHandle,
180
+ *,
181
+ cancellation_event: threading.Event | None = None,
182
+ ) -> CandidateInspection: ...
183
+
184
+ def collect_logs(
185
+ self,
186
+ handle: CandidateHandle,
187
+ *,
188
+ cancellation_event: threading.Event | None = None,
189
+ ) -> CandidateLogs: ...
190
+
191
+ def stop(self, handle: CandidateHandle) -> CandidateCleanup: ...
192
+
193
+ def prove_cleanup(self, handle: CandidateHandle) -> CandidateCleanupProof: ...
194
+
195
+
196
+ @dataclass(frozen=True, slots=True)
197
+ class ProductionInspection:
198
+ """Validated live Docker evidence for one exact production application."""
199
+
200
+ handle: ProductionApplicationHandle
201
+ running: bool
202
+ mounts: tuple[CandidateMount, ...]
203
+ command: CommandResult
204
+
205
+
206
+ @dataclass(frozen=True, slots=True)
207
+ class ProductionDiscovery:
208
+ """Configured Compose lookup plus validated current-application inspection."""
209
+
210
+ query: CommandResult
211
+ inspection: ProductionInspection
212
+
213
+
214
+ @dataclass(frozen=True, slots=True)
215
+ class ProductionStop:
216
+ """Exact-container stop command and proof that the previous app is stopped."""
217
+
218
+ handle: ProductionApplicationHandle
219
+ command: CommandResult
220
+ inspection: ProductionInspection
221
+
222
+
223
+ @dataclass(frozen=True, slots=True)
224
+ class ProductionStart:
225
+ """Evidence returned after a new release container is created."""
226
+
227
+ handle: ProductionApplicationHandle
228
+ container_reference: str
229
+ command: CommandResult
230
+ inspection: ProductionInspection
231
+
232
+
233
+ @dataclass(frozen=True, slots=True)
234
+ class ProductionNetworkRefresh:
235
+ """Exact stopped-container network endpoint refresh evidence."""
236
+
237
+ network_name: str
238
+ aliases: tuple[str, ...]
239
+ disconnect: CommandResult
240
+ connect: CommandResult
241
+
242
+
243
+ @dataclass(frozen=True, slots=True)
244
+ class ProductionRestart:
245
+ """Exact previous-container restart plus running-state proof."""
246
+
247
+ handle: ProductionApplicationHandle
248
+ command: CommandResult
249
+ inspection: ProductionInspection
250
+ network_refreshes: tuple[ProductionNetworkRefresh, ...] = ()
251
+
252
+
253
+ @dataclass(frozen=True, slots=True)
254
+ class ProductionLogs:
255
+ """Bounded logs collected from one exact production container."""
256
+
257
+ handle: ProductionApplicationHandle
258
+ command: CommandResult
259
+
260
+
261
+ @dataclass(frozen=True, slots=True)
262
+ class ProductionCleanupProof:
263
+ """Proof that a failed new release container and network are absent."""
264
+
265
+ container_absent: bool
266
+ networks_absent: bool
267
+ container_query: CommandResult
268
+ network_query: CommandResult
269
+
270
+ @property
271
+ def proven(self) -> bool:
272
+ return self.container_absent and self.networks_absent
273
+
274
+
275
+ @dataclass(frozen=True, slots=True)
276
+ class ProductionCleanup:
277
+ """Idempotent exact-target cleanup evidence for a failed new release."""
278
+
279
+ presence_query: CommandResult
280
+ remove_command: CommandResult | None
281
+ compose_down: CommandResult
282
+ proof: ProductionCleanupProof
283
+
284
+
285
+ class ProductionRunnerError(RuntimeError):
286
+ """Base production lifecycle failure with bounded redacted command evidence."""
287
+
288
+ def __init__(
289
+ self,
290
+ message: str,
291
+ *,
292
+ command: CommandResult | None = None,
293
+ cleanup: ProductionCleanup | None = None,
294
+ ) -> None:
295
+ super().__init__(message)
296
+ self.command = command
297
+ self.cleanup = cleanup
298
+
299
+ @property
300
+ def cleanup_proven(self) -> bool | None:
301
+ return None if self.cleanup is None else self.cleanup.proof.proven
302
+
303
+
304
+ class ProductionDiscoveryError(ProductionRunnerError):
305
+ """Configured current application could not be uniquely and safely identified."""
306
+
307
+
308
+ class ProductionInspectionError(ProductionRunnerError):
309
+ """Live state contradicted the recorded production application identity."""
310
+
311
+
312
+ class ProductionStopError(ProductionRunnerError):
313
+ """The current application could not be proven stopped."""
314
+
315
+
316
+ class ProductionStartError(ProductionRunnerError):
317
+ """The new release could not be started with proven cleanup evidence."""
318
+
319
+
320
+ class ProductionRestartError(ProductionRunnerError):
321
+ """The exact previous application could not be proven restarted."""
322
+
323
+
324
+ class ProductionCleanupError(ProductionRunnerError):
325
+ """Failed new-release resources could not be proven absent."""
326
+
327
+
328
+ class ProductionApplicationRunner(Protocol):
329
+ """Milestone 5 production application lifecycle boundary."""
330
+
331
+ def discover_current(
332
+ self,
333
+ *,
334
+ cancellation_event: threading.Event | None = None,
335
+ ) -> ProductionDiscovery: ...
336
+
337
+ def inspect(
338
+ self,
339
+ handle: ProductionApplicationHandle,
340
+ *,
341
+ expected_running: bool,
342
+ cancellation_event: threading.Event | None = None,
343
+ ) -> ProductionInspection: ...
344
+
345
+ def inspect_live(
346
+ self,
347
+ handle: ProductionApplicationHandle,
348
+ *,
349
+ cancellation_event: threading.Event | None = None,
350
+ ) -> ProductionInspection: ...
351
+
352
+ def stop_current(
353
+ self,
354
+ handle: ProductionApplicationHandle,
355
+ *,
356
+ cancellation_event: threading.Event | None = None,
357
+ ) -> ProductionStop: ...
358
+
359
+ def start_new(
360
+ self,
361
+ *,
362
+ operation_id: str,
363
+ release_id: str,
364
+ version: str,
365
+ cancellation_event: threading.Event | None = None,
366
+ ) -> ProductionStart: ...
367
+
368
+ def collect_logs(
369
+ self,
370
+ handle: ProductionApplicationHandle,
371
+ *,
372
+ cancellation_event: threading.Event | None = None,
373
+ ) -> ProductionLogs: ...
374
+
375
+ def remove_new(self, handle: ProductionApplicationHandle) -> ProductionCleanup: ...
376
+
377
+ def prove_release_absent(
378
+ self,
379
+ *,
380
+ release_id: str,
381
+ version: str,
382
+ ) -> ProductionCleanupProof: ...
383
+
384
+ def restart_previous(
385
+ self,
386
+ handle: ProductionApplicationHandle,
387
+ *,
388
+ cancellation_event: threading.Event | None = None,
389
+ ) -> ProductionRestart: ...