patchshuttle 0.1.0a2__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.
- patchshuttle/__init__.py +98 -0
- patchshuttle/_diff.py +317 -0
- patchshuttle/_process.py +198 -0
- patchshuttle/_version.py +3 -0
- patchshuttle/actions/__init__.py +80 -0
- patchshuttle/actions/constructors.py +211 -0
- patchshuttle/actions/create.py +155 -0
- patchshuttle/actions/modify.py +174 -0
- patchshuttle/audit.py +588 -0
- patchshuttle/backup.py +712 -0
- patchshuttle/checks/__init__.py +37 -0
- patchshuttle/checks/constructors.py +67 -0
- patchshuttle/checks/runner.py +233 -0
- patchshuttle/cli.py +766 -0
- patchshuttle/config.py +247 -0
- patchshuttle/context.py +370 -0
- patchshuttle/errors.py +291 -0
- patchshuttle/execution.py +651 -0
- patchshuttle/formatters/__init__.py +25 -0
- patchshuttle/formatters/runner.py +240 -0
- patchshuttle/identifiers.py +20 -0
- patchshuttle/inventory.py +331 -0
- patchshuttle/logging.py +741 -0
- patchshuttle/models.py +496 -0
- patchshuttle/operations.py +292 -0
- patchshuttle/parser.py +243 -0
- patchshuttle/planner.py +1144 -0
- patchshuttle/policy.py +377 -0
- patchshuttle/py.typed +1 -0
- patchshuttle/registry.py +275 -0
- patchshuttle/resources/AI_GUIDE.md +163 -0
- patchshuttle/resources/AUDIT-EXAMPLE.psh.yaml +10 -0
- patchshuttle/resources/PATCH-EXAMPLE.psh.yaml +17 -0
- patchshuttle/resources/PATCHSHUTTLE_PROTOCOL.md +109 -0
- patchshuttle/resources/__init__.py +1 -0
- patchshuttle/rollback.py +306 -0
- patchshuttle/runner.py +880 -0
- patchshuttle/verification.py +107 -0
- patchshuttle/workspace.py +382 -0
- patchshuttle-0.1.0a2.dist-info/METADATA +535 -0
- patchshuttle-0.1.0a2.dist-info/RECORD +44 -0
- patchshuttle-0.1.0a2.dist-info/WHEEL +4 -0
- patchshuttle-0.1.0a2.dist-info/entry_points.txt +2 -0
- patchshuttle-0.1.0a2.dist-info/licenses/LICENSE +21 -0
patchshuttle/models.py
ADDED
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
"""Immutable declarative models for PatchShuttle jobs."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from collections.abc import Mapping
|
|
7
|
+
from enum import Enum
|
|
8
|
+
from typing import Annotated, Literal, TypeAlias, cast
|
|
9
|
+
|
|
10
|
+
from pydantic import (
|
|
11
|
+
BaseModel,
|
|
12
|
+
ConfigDict,
|
|
13
|
+
Field,
|
|
14
|
+
RootModel,
|
|
15
|
+
field_validator,
|
|
16
|
+
model_validator,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
from patchshuttle.identifiers import ProjectId
|
|
20
|
+
|
|
21
|
+
StrictString: TypeAlias = Annotated[str, Field(strict=True)]
|
|
22
|
+
NonEmptyString: TypeAlias = Annotated[str, Field(strict=True, min_length=1)]
|
|
23
|
+
PositiveInteger: TypeAlias = Annotated[int, Field(strict=True, ge=1)]
|
|
24
|
+
Depth: TypeAlias = Annotated[int, Field(strict=True, ge=1, le=10)]
|
|
25
|
+
DiffStrip: TypeAlias = Annotated[int, Field(strict=True, ge=0, le=2)]
|
|
26
|
+
QuietLevel: TypeAlias = Annotated[int, Field(strict=True, ge=0, le=2)]
|
|
27
|
+
NonEmptyStringTuple: TypeAlias = Annotated[
|
|
28
|
+
tuple[NonEmptyString, ...], Field(min_length=1)
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class _FrozenModel(BaseModel):
|
|
33
|
+
"""Shared configuration for immutable models with a closed schema."""
|
|
34
|
+
|
|
35
|
+
model_config = ConfigDict(extra="forbid", frozen=True, validate_default=True)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class TreeParameters(_FrozenModel):
|
|
39
|
+
path: NonEmptyString = "."
|
|
40
|
+
depth: Depth = 4
|
|
41
|
+
max_entries: PositiveInteger = 500
|
|
42
|
+
include_hidden: bool = Field(default=False, strict=True)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class ReadParameters(_FrozenModel):
|
|
46
|
+
path: NonEmptyString
|
|
47
|
+
start_line: PositiveInteger = 1
|
|
48
|
+
end_line: PositiveInteger | None = None
|
|
49
|
+
max_bytes: PositiveInteger | None = None
|
|
50
|
+
|
|
51
|
+
@model_validator(mode="after")
|
|
52
|
+
def validate_line_range(self) -> ReadParameters:
|
|
53
|
+
if self.end_line is not None and self.end_line < self.start_line:
|
|
54
|
+
raise ValueError("end_line must be greater than or equal to start_line")
|
|
55
|
+
return self
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class SearchParameters(_FrozenModel):
|
|
59
|
+
path: NonEmptyString = "."
|
|
60
|
+
text: NonEmptyString
|
|
61
|
+
glob: StrictString | None = None
|
|
62
|
+
case_sensitive: bool = Field(default=True, strict=True)
|
|
63
|
+
max_results: PositiveInteger = 200
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class FindFilesParameters(_FrozenModel):
|
|
67
|
+
path: NonEmptyString = "."
|
|
68
|
+
glob: NonEmptyString
|
|
69
|
+
max_results: PositiveInteger = 500
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class FileInfoParameters(_FrozenModel):
|
|
73
|
+
path: NonEmptyString
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class HashParameters(_FrozenModel):
|
|
77
|
+
path: NonEmptyString
|
|
78
|
+
algorithm: Literal["sha256"] = "sha256"
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class GitStatusParameters(_FrozenModel):
|
|
82
|
+
pass
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class EnvironmentParameters(_FrozenModel):
|
|
86
|
+
pass
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class CreateDirectoryParameters(_FrozenModel):
|
|
90
|
+
path: NonEmptyString
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class CreateFileParameters(_FrozenModel):
|
|
94
|
+
path: NonEmptyString
|
|
95
|
+
content: StrictString
|
|
96
|
+
encoding: NonEmptyString = "utf-8"
|
|
97
|
+
newline: Literal["lf", "crlf"] = "lf"
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class ReplaceExactParameters(_FrozenModel):
|
|
101
|
+
path: NonEmptyString
|
|
102
|
+
old: NonEmptyString
|
|
103
|
+
new: StrictString
|
|
104
|
+
expected_count: PositiveInteger = 1
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class InsertBeforeParameters(_FrozenModel):
|
|
108
|
+
path: NonEmptyString
|
|
109
|
+
anchor: NonEmptyString
|
|
110
|
+
content: StrictString
|
|
111
|
+
expected_count: PositiveInteger = 1
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class InsertAfterParameters(_FrozenModel):
|
|
115
|
+
path: NonEmptyString
|
|
116
|
+
anchor: NonEmptyString
|
|
117
|
+
content: StrictString
|
|
118
|
+
expected_count: PositiveInteger = 1
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class DeleteExactParameters(_FrozenModel):
|
|
122
|
+
path: NonEmptyString
|
|
123
|
+
text: NonEmptyString
|
|
124
|
+
expected_count: PositiveInteger = 1
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class ApplyDiffParameters(_FrozenModel):
|
|
128
|
+
diff: NonEmptyString
|
|
129
|
+
strip: DiffStrip = 1
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class CompileallParameters(_FrozenModel):
|
|
133
|
+
paths: NonEmptyStringTuple
|
|
134
|
+
quiet: QuietLevel = 1
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class PytestParameters(_FrozenModel):
|
|
138
|
+
paths: tuple[NonEmptyString, ...] = ()
|
|
139
|
+
args: tuple[StrictString, ...] = ()
|
|
140
|
+
timeout_seconds: PositiveInteger | None = None
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class UnittestParameters(_FrozenModel):
|
|
144
|
+
discover: NonEmptyString
|
|
145
|
+
pattern: NonEmptyString
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class DjangoCheckParameters(_FrozenModel):
|
|
149
|
+
manage_py: NonEmptyString
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class DjangoMigrationsCheckParameters(_FrozenModel):
|
|
153
|
+
manage_py: NonEmptyString
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class DjangoTestParameters(_FrozenModel):
|
|
157
|
+
manage_py: NonEmptyString
|
|
158
|
+
labels: tuple[NonEmptyString, ...] = ()
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
class ImportCheckParameters(_FrozenModel):
|
|
162
|
+
modules: NonEmptyStringTuple
|
|
163
|
+
|
|
164
|
+
@field_validator("modules")
|
|
165
|
+
@classmethod
|
|
166
|
+
def validate_module_names(cls, modules: tuple[str, ...]) -> tuple[str, ...]:
|
|
167
|
+
invalid = [module for module in modules if not _MODULE_NAME.fullmatch(module)]
|
|
168
|
+
if invalid:
|
|
169
|
+
raise ValueError("modules must contain only dotted Python identifiers")
|
|
170
|
+
return modules
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
class ProfileParameters(_FrozenModel):
|
|
174
|
+
name: NonEmptyString
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
class TreeAction(_FrozenModel):
|
|
178
|
+
tree: TreeParameters
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
class ReadAction(_FrozenModel):
|
|
182
|
+
read: ReadParameters
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
class SearchAction(_FrozenModel):
|
|
186
|
+
search: SearchParameters
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
class FindFilesAction(_FrozenModel):
|
|
190
|
+
find_files: FindFilesParameters
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
class FileInfoAction(_FrozenModel):
|
|
194
|
+
file_info: FileInfoParameters
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
class HashAction(_FrozenModel):
|
|
198
|
+
hash: HashParameters
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
class GitStatusAction(_FrozenModel):
|
|
202
|
+
git_status: GitStatusParameters
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
class EnvironmentAction(_FrozenModel):
|
|
206
|
+
environment: EnvironmentParameters
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
class CreateDirectoryAction(_FrozenModel):
|
|
210
|
+
create_directory: CreateDirectoryParameters
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
class CreateFileAction(_FrozenModel):
|
|
214
|
+
create_file: CreateFileParameters
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
class ReplaceExactAction(_FrozenModel):
|
|
218
|
+
replace_exact: ReplaceExactParameters
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
class InsertBeforeAction(_FrozenModel):
|
|
222
|
+
insert_before: InsertBeforeParameters
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
class InsertAfterAction(_FrozenModel):
|
|
226
|
+
insert_after: InsertAfterParameters
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
class DeleteExactAction(_FrozenModel):
|
|
230
|
+
delete_exact: DeleteExactParameters
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
class ApplyDiffAction(_FrozenModel):
|
|
234
|
+
apply_diff: ApplyDiffParameters
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
ActionValue: TypeAlias = (
|
|
238
|
+
TreeAction
|
|
239
|
+
| ReadAction
|
|
240
|
+
| SearchAction
|
|
241
|
+
| FindFilesAction
|
|
242
|
+
| FileInfoAction
|
|
243
|
+
| HashAction
|
|
244
|
+
| GitStatusAction
|
|
245
|
+
| EnvironmentAction
|
|
246
|
+
| CreateDirectoryAction
|
|
247
|
+
| CreateFileAction
|
|
248
|
+
| ReplaceExactAction
|
|
249
|
+
| InsertBeforeAction
|
|
250
|
+
| InsertAfterAction
|
|
251
|
+
| DeleteExactAction
|
|
252
|
+
| ApplyDiffAction
|
|
253
|
+
)
|
|
254
|
+
ActionName: TypeAlias = Literal[
|
|
255
|
+
"tree",
|
|
256
|
+
"read",
|
|
257
|
+
"search",
|
|
258
|
+
"find_files",
|
|
259
|
+
"file_info",
|
|
260
|
+
"hash",
|
|
261
|
+
"git_status",
|
|
262
|
+
"environment",
|
|
263
|
+
"create_directory",
|
|
264
|
+
"create_file",
|
|
265
|
+
"replace_exact",
|
|
266
|
+
"insert_before",
|
|
267
|
+
"insert_after",
|
|
268
|
+
"delete_exact",
|
|
269
|
+
"apply_diff",
|
|
270
|
+
]
|
|
271
|
+
|
|
272
|
+
_ACTION_MODELS: dict[str, type[_FrozenModel]] = {
|
|
273
|
+
"tree": TreeAction,
|
|
274
|
+
"read": ReadAction,
|
|
275
|
+
"search": SearchAction,
|
|
276
|
+
"find_files": FindFilesAction,
|
|
277
|
+
"file_info": FileInfoAction,
|
|
278
|
+
"hash": HashAction,
|
|
279
|
+
"git_status": GitStatusAction,
|
|
280
|
+
"environment": EnvironmentAction,
|
|
281
|
+
"create_directory": CreateDirectoryAction,
|
|
282
|
+
"create_file": CreateFileAction,
|
|
283
|
+
"replace_exact": ReplaceExactAction,
|
|
284
|
+
"insert_before": InsertBeforeAction,
|
|
285
|
+
"insert_after": InsertAfterAction,
|
|
286
|
+
"delete_exact": DeleteExactAction,
|
|
287
|
+
"apply_diff": ApplyDiffAction,
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
AUDIT_ACTION_NAMES = frozenset(
|
|
291
|
+
{
|
|
292
|
+
"tree",
|
|
293
|
+
"read",
|
|
294
|
+
"search",
|
|
295
|
+
"find_files",
|
|
296
|
+
"file_info",
|
|
297
|
+
"hash",
|
|
298
|
+
"git_status",
|
|
299
|
+
"environment",
|
|
300
|
+
}
|
|
301
|
+
)
|
|
302
|
+
CHANGE_ACTION_NAMES = frozenset(
|
|
303
|
+
{
|
|
304
|
+
"create_directory",
|
|
305
|
+
"create_file",
|
|
306
|
+
"replace_exact",
|
|
307
|
+
"insert_before",
|
|
308
|
+
"insert_after",
|
|
309
|
+
"delete_exact",
|
|
310
|
+
"apply_diff",
|
|
311
|
+
}
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
class Action(RootModel[ActionValue]):
|
|
316
|
+
"""One YAML-shaped action entry containing exactly one action name."""
|
|
317
|
+
|
|
318
|
+
model_config = ConfigDict(frozen=True)
|
|
319
|
+
|
|
320
|
+
@model_validator(mode="before")
|
|
321
|
+
@classmethod
|
|
322
|
+
def select_action_model(cls, value: object) -> object:
|
|
323
|
+
if isinstance(value, Mapping):
|
|
324
|
+
keys = list(value)
|
|
325
|
+
if len(keys) != 1:
|
|
326
|
+
raise ValueError("an action entry must contain exactly one action name")
|
|
327
|
+
action_model = _ACTION_MODELS.get(keys[0])
|
|
328
|
+
if action_model is None:
|
|
329
|
+
raise ValueError(f"unknown action name {keys[0]!r}")
|
|
330
|
+
return action_model.model_validate(value)
|
|
331
|
+
return value
|
|
332
|
+
|
|
333
|
+
@property
|
|
334
|
+
def name(self) -> ActionName:
|
|
335
|
+
return cast(ActionName, next(iter(type(self.root).model_fields)))
|
|
336
|
+
|
|
337
|
+
@property
|
|
338
|
+
def parameters(self) -> _FrozenModel:
|
|
339
|
+
return cast(_FrozenModel, getattr(self.root, self.name))
|
|
340
|
+
|
|
341
|
+
@property
|
|
342
|
+
def is_audit(self) -> bool:
|
|
343
|
+
return self.name in AUDIT_ACTION_NAMES
|
|
344
|
+
|
|
345
|
+
@property
|
|
346
|
+
def is_change(self) -> bool:
|
|
347
|
+
return self.name in CHANGE_ACTION_NAMES
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
class CompileallCheck(_FrozenModel):
|
|
351
|
+
compileall: CompileallParameters
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
class PytestCheck(_FrozenModel):
|
|
355
|
+
pytest: PytestParameters
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
class UnittestCheck(_FrozenModel):
|
|
359
|
+
unittest: UnittestParameters
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
class DjangoCheck(_FrozenModel):
|
|
363
|
+
django_check: DjangoCheckParameters
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
class DjangoMigrationsCheck(_FrozenModel):
|
|
367
|
+
django_migrations_check: DjangoMigrationsCheckParameters
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
class DjangoTestCheck(_FrozenModel):
|
|
371
|
+
django_test: DjangoTestParameters
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
class ImportCheck(_FrozenModel):
|
|
375
|
+
import_check: ImportCheckParameters
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
class ProfileCheck(_FrozenModel):
|
|
379
|
+
profile: ProfileParameters
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
CheckValue: TypeAlias = (
|
|
383
|
+
CompileallCheck
|
|
384
|
+
| PytestCheck
|
|
385
|
+
| UnittestCheck
|
|
386
|
+
| DjangoCheck
|
|
387
|
+
| DjangoMigrationsCheck
|
|
388
|
+
| DjangoTestCheck
|
|
389
|
+
| ImportCheck
|
|
390
|
+
| ProfileCheck
|
|
391
|
+
)
|
|
392
|
+
CheckName: TypeAlias = Literal[
|
|
393
|
+
"compileall",
|
|
394
|
+
"pytest",
|
|
395
|
+
"unittest",
|
|
396
|
+
"django_check",
|
|
397
|
+
"django_migrations_check",
|
|
398
|
+
"django_test",
|
|
399
|
+
"import_check",
|
|
400
|
+
"profile",
|
|
401
|
+
]
|
|
402
|
+
|
|
403
|
+
_CHECK_MODELS: dict[str, type[_FrozenModel]] = {
|
|
404
|
+
"compileall": CompileallCheck,
|
|
405
|
+
"pytest": PytestCheck,
|
|
406
|
+
"unittest": UnittestCheck,
|
|
407
|
+
"django_check": DjangoCheck,
|
|
408
|
+
"django_migrations_check": DjangoMigrationsCheck,
|
|
409
|
+
"django_test": DjangoTestCheck,
|
|
410
|
+
"import_check": ImportCheck,
|
|
411
|
+
"profile": ProfileCheck,
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
class Check(RootModel[CheckValue]):
|
|
416
|
+
"""One YAML-shaped check entry containing exactly one check name."""
|
|
417
|
+
|
|
418
|
+
model_config = ConfigDict(frozen=True)
|
|
419
|
+
|
|
420
|
+
@model_validator(mode="before")
|
|
421
|
+
@classmethod
|
|
422
|
+
def select_check_model(cls, value: object) -> object:
|
|
423
|
+
if isinstance(value, Mapping):
|
|
424
|
+
keys = list(value)
|
|
425
|
+
if len(keys) != 1:
|
|
426
|
+
raise ValueError("a check entry must contain exactly one check name")
|
|
427
|
+
check_model = _CHECK_MODELS.get(keys[0])
|
|
428
|
+
if check_model is None:
|
|
429
|
+
raise ValueError(f"unknown check name {keys[0]!r}")
|
|
430
|
+
return check_model.model_validate(value)
|
|
431
|
+
return value
|
|
432
|
+
|
|
433
|
+
@property
|
|
434
|
+
def name(self) -> CheckName:
|
|
435
|
+
return cast(CheckName, next(iter(type(self.root).model_fields)))
|
|
436
|
+
|
|
437
|
+
@property
|
|
438
|
+
def parameters(self) -> _FrozenModel:
|
|
439
|
+
return cast(_FrozenModel, getattr(self.root, self.name))
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
class JobKind(str, Enum):
|
|
443
|
+
"""Supported v0.1 job kinds."""
|
|
444
|
+
|
|
445
|
+
AUDIT = "audit"
|
|
446
|
+
PATCH = "patch"
|
|
447
|
+
VERIFY = "verify"
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
class Job(_FrozenModel):
|
|
451
|
+
"""One structurally validated PatchShuttle protocol v1 job."""
|
|
452
|
+
|
|
453
|
+
protocol: Literal[1]
|
|
454
|
+
project_id: ProjectId
|
|
455
|
+
id: Annotated[str, Field(strict=True, pattern=r"^[A-Z][A-Z0-9_-]{2,63}$")]
|
|
456
|
+
kind: JobKind
|
|
457
|
+
title: StrictString | None = None
|
|
458
|
+
description: StrictString | None = None
|
|
459
|
+
actions: tuple[Action, ...] = ()
|
|
460
|
+
checks: tuple[Check, ...] = ()
|
|
461
|
+
|
|
462
|
+
@field_validator("protocol", mode="before")
|
|
463
|
+
@classmethod
|
|
464
|
+
def validate_protocol_is_strict_integer(cls, value: object) -> object:
|
|
465
|
+
if type(value) is not int:
|
|
466
|
+
raise ValueError("protocol must be the integer 1")
|
|
467
|
+
return value
|
|
468
|
+
|
|
469
|
+
@model_validator(mode="after")
|
|
470
|
+
def validate_kind_contract(self) -> Job:
|
|
471
|
+
if self.kind is JobKind.AUDIT:
|
|
472
|
+
if not self.actions:
|
|
473
|
+
raise ValueError("audit jobs require at least one action")
|
|
474
|
+
if any(not action.is_audit for action in self.actions):
|
|
475
|
+
raise ValueError("audit jobs may contain only audit actions")
|
|
476
|
+
if self.checks:
|
|
477
|
+
raise ValueError("audit jobs may not contain checks")
|
|
478
|
+
|
|
479
|
+
elif self.kind is JobKind.PATCH:
|
|
480
|
+
if not self.actions:
|
|
481
|
+
raise ValueError("patch jobs require at least one action")
|
|
482
|
+
if any(not action.is_change for action in self.actions):
|
|
483
|
+
raise ValueError("patch jobs may contain only change actions")
|
|
484
|
+
|
|
485
|
+
else:
|
|
486
|
+
if self.actions:
|
|
487
|
+
raise ValueError("verify jobs may not contain actions")
|
|
488
|
+
if not self.checks:
|
|
489
|
+
raise ValueError("verify jobs require at least one check")
|
|
490
|
+
|
|
491
|
+
return self
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
_MODULE_NAME = re.compile(r"[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*")
|
|
495
|
+
|
|
496
|
+
__all__ = ["Action", "ActionName", "Check", "CheckName", "Job", "JobKind"]
|