dirigent-server 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_server/__init__.py +7 -0
- dirigent_server/app.py +171 -0
- dirigent_server/dependencies.py +51 -0
- dirigent_server/errors.py +116 -0
- dirigent_server/health.py +97 -0
- dirigent_server/logging.py +5 -0
- dirigent_server/pagination.py +56 -0
- dirigent_server/py.typed +0 -0
- dirigent_server/routes/__init__.py +79 -0
- dirigent_server/routes/alerts.py +289 -0
- dirigent_server/routes/auth.py +218 -0
- dirigent_server/routes/blocks.py +39 -0
- dirigent_server/routes/connections.py +294 -0
- dirigent_server/routes/hooks.py +114 -0
- dirigent_server/routes/pipelines.py +427 -0
- dirigent_server/routes/runs.py +818 -0
- dirigent_server/routes/schema.py +27 -0
- dirigent_server/routes/schemas.py +127 -0
- dirigent_server/routes/system.py +63 -0
- dirigent_server/routes/trigger_documents.py +109 -0
- dirigent_server/routes/triggers.py +537 -0
- dirigent_server/routes/users.py +226 -0
- dirigent_server/routes/workers.py +52 -0
- dirigent_server/security.py +186 -0
- dirigent_server/static/.gitkeep +0 -0
- dirigent_server/transactions.py +34 -0
- dirigent_server/ui.py +237 -0
- dirigent_server-0.9.0.dist-info/METADATA +17 -0
- dirigent_server-0.9.0.dist-info/RECORD +32 -0
- dirigent_server-0.9.0.dist-info/WHEEL +4 -0
- dirigent_server-0.9.0.dist-info/licenses/LICENSE +18 -0
- dirigent_server-0.9.0.dist-info/licenses/THIRD_PARTY_NOTICES.md +631 -0
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
"""Pipelines: apply, export, versions, activation, deletion, the ad hoc run, and backfill."""
|
|
2
|
+
|
|
3
|
+
from typing import Annotated
|
|
4
|
+
|
|
5
|
+
from fastapi import APIRouter, HTTPException, Query, Response, status
|
|
6
|
+
from fastapi.responses import PlainTextResponse
|
|
7
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
8
|
+
|
|
9
|
+
from dirigent_client.schemas import (
|
|
10
|
+
ApplyRequest,
|
|
11
|
+
ApplyResult,
|
|
12
|
+
BackfillAccepted,
|
|
13
|
+
BackfilledRun,
|
|
14
|
+
BackfillRequest,
|
|
15
|
+
LastRun,
|
|
16
|
+
Page,
|
|
17
|
+
PipelineDetail,
|
|
18
|
+
PipelineOut,
|
|
19
|
+
PipelineVersionOut,
|
|
20
|
+
PruneRequest,
|
|
21
|
+
PruneResult,
|
|
22
|
+
RunAccepted,
|
|
23
|
+
RunRequest,
|
|
24
|
+
ValidationIssue,
|
|
25
|
+
)
|
|
26
|
+
from dirigent_core.directory import prune_absent
|
|
27
|
+
from dirigent_core.documents import DocumentError, carried_refusal, load_document
|
|
28
|
+
from dirigent_core.engine import Attribution, ParameterError, create_run
|
|
29
|
+
from dirigent_core.engine.definition import load_definition
|
|
30
|
+
from dirigent_core.engine.runs import Provenance, RunCreationError, RunWindow
|
|
31
|
+
from dirigent_core.models import Pipeline
|
|
32
|
+
from dirigent_core.pipelines import (
|
|
33
|
+
NO_COUNTS,
|
|
34
|
+
PipelineCounts,
|
|
35
|
+
PipelineError,
|
|
36
|
+
PipelineInUse,
|
|
37
|
+
UnknownPipeline,
|
|
38
|
+
apply_document,
|
|
39
|
+
delete_pipeline,
|
|
40
|
+
export_pipeline,
|
|
41
|
+
get_version,
|
|
42
|
+
last_runs,
|
|
43
|
+
list_pipelines,
|
|
44
|
+
list_versions,
|
|
45
|
+
listing_counts,
|
|
46
|
+
require_pipeline,
|
|
47
|
+
revalidate,
|
|
48
|
+
set_active,
|
|
49
|
+
)
|
|
50
|
+
from dirigent_core.triggers.backfill import BackfillError, backfill
|
|
51
|
+
from dirigent_core.triggers.schedules import find_schedule
|
|
52
|
+
from dirigent_server.dependencies import ServicesDep, SessionDep
|
|
53
|
+
from dirigent_server.pagination import DEFAULT_PAGE, AfterParam, LimitParam, clip, int_cursor
|
|
54
|
+
from dirigent_server.security import AdminDep, OperatorDep, PrincipalDep
|
|
55
|
+
from dirigent_server.transactions import Transactional
|
|
56
|
+
|
|
57
|
+
router = APIRouter(route_class=Transactional, tags=["pipelines"])
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def render(row: Pipeline, counts: PipelineCounts = NO_COUNTS, last: LastRun | None = None) -> PipelineOut:
|
|
61
|
+
"""Render a pipeline row with the counts and the last run a listing draws beside it."""
|
|
62
|
+
return PipelineOut.model_validate(row, from_attributes=True).model_copy(
|
|
63
|
+
update={
|
|
64
|
+
"active_runs": counts.active_runs,
|
|
65
|
+
"schedules": counts.schedules,
|
|
66
|
+
"webhooks": counts.webhooks,
|
|
67
|
+
"last_run": last,
|
|
68
|
+
}
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@router.get(
|
|
73
|
+
"/pipelines",
|
|
74
|
+
operation_id="listPipelines",
|
|
75
|
+
summary="List pipelines",
|
|
76
|
+
response_model=Page[PipelineOut],
|
|
77
|
+
)
|
|
78
|
+
async def list_all(
|
|
79
|
+
session: SessionDep,
|
|
80
|
+
principal: PrincipalDep,
|
|
81
|
+
after: AfterParam = None,
|
|
82
|
+
limit: LimitParam = DEFAULT_PAGE,
|
|
83
|
+
tag: Annotated[
|
|
84
|
+
list[str] | None, Query(description="Only pipelines wearing this tag; repeat it to name more.")
|
|
85
|
+
] = None,
|
|
86
|
+
) -> Page[PipelineOut]:
|
|
87
|
+
"""List every pipeline, what fires it, how many runs are in flight, and how the last one went.
|
|
88
|
+
|
|
89
|
+
``tag`` repeats, and repeating it narrows: a pipeline is listed only if it wears every
|
|
90
|
+
tag named.
|
|
91
|
+
"""
|
|
92
|
+
rows = await list_pipelines(session, after=after, limit=limit + 1, tags=tag or ())
|
|
93
|
+
page, following = clip(rows, limit, lambda row: row.code)
|
|
94
|
+
ids = [row.id for row in page]
|
|
95
|
+
counts = await listing_counts(session, ids)
|
|
96
|
+
latest = await last_runs(session, ids)
|
|
97
|
+
found = [render(row, counts.get(row.id, NO_COUNTS), latest.get(row.id)) for row in page]
|
|
98
|
+
return Page(items=found, next=following)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@router.post(
|
|
102
|
+
"/pipelines/$apply",
|
|
103
|
+
operation_id="applyPipeline",
|
|
104
|
+
summary="Apply a pipeline document",
|
|
105
|
+
response_model=ApplyResult,
|
|
106
|
+
)
|
|
107
|
+
async def apply(
|
|
108
|
+
payload: ApplyRequest,
|
|
109
|
+
session: SessionDep,
|
|
110
|
+
services: ServicesDep,
|
|
111
|
+
principal: OperatorDep,
|
|
112
|
+
dry_run: Annotated[bool, Query(description="Report the plan without writing anything.")] = False,
|
|
113
|
+
) -> ApplyResult:
|
|
114
|
+
"""Validate a document against this instance and commit a new version, or plan one.
|
|
115
|
+
|
|
116
|
+
``pause_schedules`` on the request creates this apply's new schedules paused; one the
|
|
117
|
+
instance already holds keeps the paused state it has.
|
|
118
|
+
"""
|
|
119
|
+
raw = dict(payload.document)
|
|
120
|
+
if payload.code is not None:
|
|
121
|
+
raw["code"] = payload.code
|
|
122
|
+
try:
|
|
123
|
+
definition = load_document(raw)
|
|
124
|
+
except DocumentError as error:
|
|
125
|
+
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, detail=error.problems) from error
|
|
126
|
+
refusal = carried_refusal(definition)
|
|
127
|
+
if refusal is not None:
|
|
128
|
+
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, detail=[refusal])
|
|
129
|
+
return await apply_document(
|
|
130
|
+
session,
|
|
131
|
+
services,
|
|
132
|
+
definition,
|
|
133
|
+
provenance=Provenance(source=payload.source, ref=payload.source_ref, applied_by=principal.label),
|
|
134
|
+
dry_run=dry_run,
|
|
135
|
+
pause_schedules=payload.pause_schedules,
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@router.post(
|
|
140
|
+
"/pipelines/$prune",
|
|
141
|
+
operation_id="prunePipelines",
|
|
142
|
+
summary="Deactivate directory pipelines absent from a set",
|
|
143
|
+
response_model=PruneResult,
|
|
144
|
+
)
|
|
145
|
+
async def prune(
|
|
146
|
+
payload: PruneRequest,
|
|
147
|
+
session: SessionDep,
|
|
148
|
+
principal: OperatorDep,
|
|
149
|
+
dry_run: Annotated[bool, Query(description="Report what would be deactivated without writing.")] = False,
|
|
150
|
+
) -> PruneResult:
|
|
151
|
+
"""The reconcile half of a directory apply.
|
|
152
|
+
|
|
153
|
+
Every active pipeline whose current version a directory apply wrote, and whose code is
|
|
154
|
+
not in ``keep``, is deactivated -- never deleted. A directory-provenance triggers document
|
|
155
|
+
absent from ``keep`` is deleted instead, and takes the rows it owned with it. An empty
|
|
156
|
+
``keep`` is refused outside a dry run: an empty directory is an accident, not an
|
|
157
|
+
instruction to turn everything off.
|
|
158
|
+
"""
|
|
159
|
+
if not payload.keep and not dry_run:
|
|
160
|
+
raise HTTPException(
|
|
161
|
+
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
|
|
162
|
+
detail=["keep names no codes, and pruning against an empty set would deactivate every directory pipeline"],
|
|
163
|
+
)
|
|
164
|
+
return await prune_absent(session, set(payload.keep), dry_run=dry_run)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
@router.get(
|
|
168
|
+
"/pipelines/{code}",
|
|
169
|
+
operation_id="getPipeline",
|
|
170
|
+
summary="Read a pipeline",
|
|
171
|
+
response_model=PipelineDetail,
|
|
172
|
+
)
|
|
173
|
+
async def get_one(code: str, session: SessionDep, principal: PrincipalDep) -> PipelineDetail:
|
|
174
|
+
"""Read a pipeline and the document its current version holds."""
|
|
175
|
+
pipeline = await _require(session, code)
|
|
176
|
+
document = None
|
|
177
|
+
if pipeline.current_version is not None:
|
|
178
|
+
document = (await get_version(session, pipeline)).ordered_document
|
|
179
|
+
counts = await listing_counts(session, [pipeline.id])
|
|
180
|
+
latest = await last_runs(session, [pipeline.id])
|
|
181
|
+
base = render(pipeline, counts.get(pipeline.id, NO_COUNTS), latest.get(pipeline.id))
|
|
182
|
+
return PipelineDetail(**base.model_dump(), document=document)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
@router.get(
|
|
186
|
+
"/pipelines/{code}/versions",
|
|
187
|
+
operation_id="listPipelineVersions",
|
|
188
|
+
summary="List a pipeline's versions",
|
|
189
|
+
response_model=Page[PipelineVersionOut],
|
|
190
|
+
)
|
|
191
|
+
async def versions(
|
|
192
|
+
code: str,
|
|
193
|
+
session: SessionDep,
|
|
194
|
+
principal: PrincipalDep,
|
|
195
|
+
after: AfterParam = None,
|
|
196
|
+
limit: LimitParam = DEFAULT_PAGE,
|
|
197
|
+
) -> Page[PipelineVersionOut]:
|
|
198
|
+
"""List every immutable version, newest first, with its provenance."""
|
|
199
|
+
pipeline = await _require(session, code)
|
|
200
|
+
rows = await list_versions(session, pipeline.id, after=int_cursor(after), limit=limit + 1)
|
|
201
|
+
found = [PipelineVersionOut.model_validate(row, from_attributes=True) for row in rows]
|
|
202
|
+
items, following = clip(found, limit, lambda row: row.version)
|
|
203
|
+
return Page(items=items, next=following)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
@router.get(
|
|
207
|
+
"/pipelines/{code}/$export",
|
|
208
|
+
operation_id="exportPipeline",
|
|
209
|
+
summary="Export a pipeline as canonical YAML",
|
|
210
|
+
response_class=PlainTextResponse,
|
|
211
|
+
responses={200: {"content": {"application/yaml": {}}, "description": "The canonical document."}},
|
|
212
|
+
)
|
|
213
|
+
async def export(
|
|
214
|
+
code: str,
|
|
215
|
+
session: SessionDep,
|
|
216
|
+
principal: PrincipalDep,
|
|
217
|
+
version: Annotated[int | None, Query(description="Export this version instead of the current one.")] = None,
|
|
218
|
+
) -> PlainTextResponse:
|
|
219
|
+
"""Render a stored pipeline as the canonical YAML a git repository holds."""
|
|
220
|
+
try:
|
|
221
|
+
text = await export_pipeline(session, code, version=version)
|
|
222
|
+
except PipelineError as error:
|
|
223
|
+
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(error)) from error
|
|
224
|
+
return PlainTextResponse(text, media_type="application/yaml")
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
@router.post(
|
|
228
|
+
"/pipelines/{code}/$validate",
|
|
229
|
+
operation_id="validatePipeline",
|
|
230
|
+
summary="Re-check a stored pipeline against this instance",
|
|
231
|
+
response_model=list[ValidationIssue],
|
|
232
|
+
)
|
|
233
|
+
async def validate_stored(
|
|
234
|
+
code: str,
|
|
235
|
+
session: SessionDep,
|
|
236
|
+
services: ServicesDep,
|
|
237
|
+
principal: PrincipalDep,
|
|
238
|
+
version: Annotated[int | None, Query(description="Check this version instead of the current one.")] = None,
|
|
239
|
+
) -> list[ValidationIssue]:
|
|
240
|
+
"""Report what a stored version would fail on if it ran now, or an empty list."""
|
|
241
|
+
try:
|
|
242
|
+
return await revalidate(session, services, code, version=version)
|
|
243
|
+
except PipelineError as error:
|
|
244
|
+
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(error)) from error
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
@router.post(
|
|
248
|
+
"/pipelines/{code}/$activate",
|
|
249
|
+
operation_id="activatePipeline",
|
|
250
|
+
summary="Activate a pipeline",
|
|
251
|
+
response_model=PipelineOut,
|
|
252
|
+
)
|
|
253
|
+
async def activate(code: str, session: SessionDep, principal: OperatorDep) -> PipelineOut:
|
|
254
|
+
"""Make a pipeline runnable again, and let its schedules fire."""
|
|
255
|
+
return render(await _act(session, code, active=True))
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
@router.post(
|
|
259
|
+
"/pipelines/{code}/$deactivate",
|
|
260
|
+
operation_id="deactivatePipeline",
|
|
261
|
+
summary="Deactivate a pipeline",
|
|
262
|
+
response_model=PipelineOut,
|
|
263
|
+
)
|
|
264
|
+
async def deactivate(code: str, session: SessionDep, principal: OperatorDep) -> PipelineOut:
|
|
265
|
+
"""Deregister a pipeline: schedules pause, it stops being runnable, history is kept."""
|
|
266
|
+
return render(await _act(session, code, active=False))
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
@router.delete(
|
|
270
|
+
"/pipelines/{code}",
|
|
271
|
+
operation_id="deletePipeline",
|
|
272
|
+
summary="Delete a pipeline and its history",
|
|
273
|
+
status_code=status.HTTP_204_NO_CONTENT,
|
|
274
|
+
)
|
|
275
|
+
async def delete(code: str, session: SessionDep, principal: AdminDep) -> Response:
|
|
276
|
+
"""Delete a pipeline and every run ever attributed to it, in one transaction.
|
|
277
|
+
|
|
278
|
+
Admin, unlike every other pipeline verb, and it takes the history with it: the runs,
|
|
279
|
+
their items, attempts, logs and artifact references, plus the versions, schedules and
|
|
280
|
+
webhooks the definition owns. Runs still in flight refuse the delete with a 409 --
|
|
281
|
+
finish or cancel them first. Deactivating is the reversible verb an operator has.
|
|
282
|
+
"""
|
|
283
|
+
try:
|
|
284
|
+
await delete_pipeline(session, code)
|
|
285
|
+
except UnknownPipeline as error:
|
|
286
|
+
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(error)) from error
|
|
287
|
+
except PipelineInUse as error:
|
|
288
|
+
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=str(error)) from error
|
|
289
|
+
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
@router.post(
|
|
293
|
+
"/pipelines/{code}/$run",
|
|
294
|
+
operation_id="runPipeline",
|
|
295
|
+
summary="Start an ad hoc run",
|
|
296
|
+
response_model=RunAccepted,
|
|
297
|
+
status_code=status.HTTP_202_ACCEPTED,
|
|
298
|
+
)
|
|
299
|
+
async def start_run(
|
|
300
|
+
code: str,
|
|
301
|
+
payload: RunRequest,
|
|
302
|
+
session: SessionDep,
|
|
303
|
+
services: ServicesDep,
|
|
304
|
+
principal: OperatorDep,
|
|
305
|
+
) -> RunAccepted:
|
|
306
|
+
"""Validate parameters against the pipeline's schema and instantiate a run."""
|
|
307
|
+
pipeline = await _require(session, code)
|
|
308
|
+
if not pipeline.active:
|
|
309
|
+
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=f"pipeline {code!r} is deactivated")
|
|
310
|
+
if pipeline.current_version is None:
|
|
311
|
+
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=f"pipeline {code!r} has no versions yet")
|
|
312
|
+
version = await get_version(session, pipeline)
|
|
313
|
+
definition = load_definition(version.document)
|
|
314
|
+
try:
|
|
315
|
+
definition.validate_params(payload.params, services.format_checker)
|
|
316
|
+
except ParameterError as error:
|
|
317
|
+
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, detail=str(error)) from error
|
|
318
|
+
try:
|
|
319
|
+
run = await create_run(
|
|
320
|
+
session,
|
|
321
|
+
services,
|
|
322
|
+
version,
|
|
323
|
+
params=payload.params,
|
|
324
|
+
attribution=Attribution(
|
|
325
|
+
kind=principal.trigger_kind,
|
|
326
|
+
id=principal.token_id or principal.user_id,
|
|
327
|
+
label=principal.label,
|
|
328
|
+
),
|
|
329
|
+
window=_window(payload),
|
|
330
|
+
log_levels={pattern: level.value for pattern, level in payload.log_levels.items()}
|
|
331
|
+
if payload.log_levels
|
|
332
|
+
else None,
|
|
333
|
+
priority=payload.priority,
|
|
334
|
+
)
|
|
335
|
+
except RunCreationError as error:
|
|
336
|
+
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=str(error)) from error
|
|
337
|
+
if run is None:
|
|
338
|
+
return RunAccepted(status="skipped", detail="a run of this pipeline is already in flight")
|
|
339
|
+
return RunAccepted(run_id=run.id, status=run.status.value)
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def _window(payload: RunRequest) -> RunWindow | None:
|
|
343
|
+
"""Read the window a run was asked for, which the body has both ends of or neither."""
|
|
344
|
+
if payload.window_start is None or payload.window_end is None:
|
|
345
|
+
return None
|
|
346
|
+
return RunWindow(start=payload.window_start, end=payload.window_end)
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
@router.post(
|
|
350
|
+
"/pipelines/{code}/$backfill",
|
|
351
|
+
operation_id="backfillPipeline",
|
|
352
|
+
summary="Fill the windows a schedule's cadence has already gone past",
|
|
353
|
+
response_model=BackfillAccepted,
|
|
354
|
+
status_code=status.HTTP_202_ACCEPTED,
|
|
355
|
+
)
|
|
356
|
+
async def start_backfill(
|
|
357
|
+
code: str,
|
|
358
|
+
payload: BackfillRequest,
|
|
359
|
+
session: SessionDep,
|
|
360
|
+
services: ServicesDep,
|
|
361
|
+
principal: OperatorDep,
|
|
362
|
+
) -> BackfillAccepted:
|
|
363
|
+
"""Enumerate a schedule's firings inside an interval and create one run per window.
|
|
364
|
+
|
|
365
|
+
The schedule's own clock is untouched: this fills what has already gone past, and every
|
|
366
|
+
run it creates is attributed to the backfill rather than to a firing.
|
|
367
|
+
"""
|
|
368
|
+
del principal
|
|
369
|
+
pipeline = await _require(session, code)
|
|
370
|
+
if not pipeline.active:
|
|
371
|
+
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=f"pipeline {code!r} is deactivated")
|
|
372
|
+
if pipeline.current_version is None:
|
|
373
|
+
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=f"pipeline {code!r} has no versions yet")
|
|
374
|
+
schedule = await find_schedule(session, pipeline.id, payload.schedule)
|
|
375
|
+
if schedule is None:
|
|
376
|
+
raise HTTPException(
|
|
377
|
+
status_code=status.HTTP_404_NOT_FOUND,
|
|
378
|
+
detail=f"pipeline {code!r} has no schedule coded {payload.schedule!r}",
|
|
379
|
+
)
|
|
380
|
+
version = await get_version(session, pipeline)
|
|
381
|
+
try:
|
|
382
|
+
filled = await backfill(
|
|
383
|
+
session,
|
|
384
|
+
services,
|
|
385
|
+
version,
|
|
386
|
+
schedule,
|
|
387
|
+
start=payload.from_,
|
|
388
|
+
end=payload.to,
|
|
389
|
+
params=payload.params,
|
|
390
|
+
dry_run=payload.dry_run,
|
|
391
|
+
)
|
|
392
|
+
except BackfillError as error:
|
|
393
|
+
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, detail=str(error)) from error
|
|
394
|
+
except ParameterError as error:
|
|
395
|
+
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, detail=str(error)) from error
|
|
396
|
+
except RunCreationError as error:
|
|
397
|
+
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=str(error)) from error
|
|
398
|
+
return BackfillAccepted(
|
|
399
|
+
pipeline=pipeline.code,
|
|
400
|
+
schedule=schedule.code,
|
|
401
|
+
dry_run=payload.dry_run,
|
|
402
|
+
windows=[
|
|
403
|
+
BackfilledRun(
|
|
404
|
+
window_start=one.window.start,
|
|
405
|
+
window_end=one.window.end,
|
|
406
|
+
run_id=one.run_id,
|
|
407
|
+
detail=one.detail,
|
|
408
|
+
)
|
|
409
|
+
for one in filled
|
|
410
|
+
],
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
async def _require(session: AsyncSession, code: str) -> Pipeline:
|
|
415
|
+
"""Read a pipeline by code, translating "no such thing" into a 404."""
|
|
416
|
+
try:
|
|
417
|
+
return await require_pipeline(session, code)
|
|
418
|
+
except UnknownPipeline as error:
|
|
419
|
+
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(error)) from error
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
async def _act(session: AsyncSession, code: str, *, active: bool) -> Pipeline:
|
|
423
|
+
"""Activate or deactivate, translating "no such thing" into a 404."""
|
|
424
|
+
try:
|
|
425
|
+
return await set_active(session, code, active=active)
|
|
426
|
+
except UnknownPipeline as error:
|
|
427
|
+
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(error)) from error
|