dirigent-core 0.17.2__tar.gz → 0.17.3__tar.gz
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_core-0.17.2 → dirigent_core-0.17.3}/PKG-INFO +4 -4
- {dirigent_core-0.17.2 → dirigent_core-0.17.3}/pyproject.toml +4 -4
- {dirigent_core-0.17.2 → dirigent_core-0.17.3}/pyproject.toml.orig +4 -4
- {dirigent_core-0.17.2 → dirigent_core-0.17.3}/src/dirigent_core/alembic/versions/0001_baseline_schema.py +3 -0
- {dirigent_core-0.17.2 → dirigent_core-0.17.3}/src/dirigent_core/engine/claim.py +3 -0
- {dirigent_core-0.17.2 → dirigent_core-0.17.3}/src/dirigent_core/engine/context.py +36 -2
- {dirigent_core-0.17.2 → dirigent_core-0.17.3}/src/dirigent_core/engine/executor.py +32 -5
- {dirigent_core-0.17.2 → dirigent_core-0.17.3}/src/dirigent_core/models.py +4 -0
- {dirigent_core-0.17.2 → dirigent_core-0.17.3}/LICENSE +0 -0
- {dirigent_core-0.17.2 → dirigent_core-0.17.3}/README.md +0 -0
- {dirigent_core-0.17.2 → dirigent_core-0.17.3}/src/dirigent_core/__init__.py +0 -0
- {dirigent_core-0.17.2 → dirigent_core-0.17.3}/src/dirigent_core/alembic/env.py +0 -0
- {dirigent_core-0.17.2 → dirigent_core-0.17.3}/src/dirigent_core/alembic/script.py.mako +0 -0
- {dirigent_core-0.17.2 → dirigent_core-0.17.3}/src/dirigent_core/alerting.py +0 -0
- {dirigent_core-0.17.2 → dirigent_core-0.17.3}/src/dirigent_core/artifacts.py +0 -0
- {dirigent_core-0.17.2 → dirigent_core-0.17.3}/src/dirigent_core/auth.py +0 -0
- {dirigent_core-0.17.2 → dirigent_core-0.17.3}/src/dirigent_core/blockdocs.py +0 -0
- {dirigent_core-0.17.2 → dirigent_core-0.17.3}/src/dirigent_core/config.py +0 -0
- {dirigent_core-0.17.2 → dirigent_core-0.17.3}/src/dirigent_core/configdocs.py +0 -0
- {dirigent_core-0.17.2 → dirigent_core-0.17.3}/src/dirigent_core/database.py +0 -0
- {dirigent_core-0.17.2 → dirigent_core-0.17.3}/src/dirigent_core/directory.py +0 -0
- {dirigent_core-0.17.2 → dirigent_core-0.17.3}/src/dirigent_core/documents.py +0 -0
- {dirigent_core-0.17.2 → dirigent_core-0.17.3}/src/dirigent_core/documentschema.py +0 -0
- {dirigent_core-0.17.2 → dirigent_core-0.17.3}/src/dirigent_core/engine/__init__.py +0 -0
- {dirigent_core-0.17.2 → dirigent_core-0.17.3}/src/dirigent_core/engine/definition.py +0 -0
- {dirigent_core-0.17.2 → dirigent_core-0.17.3}/src/dirigent_core/engine/failure.py +0 -0
- {dirigent_core-0.17.2 → dirigent_core-0.17.3}/src/dirigent_core/engine/recovery.py +0 -0
- {dirigent_core-0.17.2 → dirigent_core-0.17.3}/src/dirigent_core/engine/references.py +0 -0
- {dirigent_core-0.17.2 → dirigent_core-0.17.3}/src/dirigent_core/engine/runs.py +0 -0
- {dirigent_core-0.17.2 → dirigent_core-0.17.3}/src/dirigent_core/engine/services.py +0 -0
- {dirigent_core-0.17.2 → dirigent_core-0.17.3}/src/dirigent_core/engine/state.py +0 -0
- {dirigent_core-0.17.2 → dirigent_core-0.17.3}/src/dirigent_core/errors.py +0 -0
- {dirigent_core-0.17.2 → dirigent_core-0.17.3}/src/dirigent_core/examples.py +0 -0
- {dirigent_core-0.17.2 → dirigent_core-0.17.3}/src/dirigent_core/ids.py +0 -0
- {dirigent_core-0.17.2 → dirigent_core-0.17.3}/src/dirigent_core/logging.py +0 -0
- {dirigent_core-0.17.2 → dirigent_core-0.17.3}/src/dirigent_core/messages.py +0 -0
- {dirigent_core-0.17.2 → dirigent_core-0.17.3}/src/dirigent_core/migrations.py +0 -0
- {dirigent_core-0.17.2 → dirigent_core-0.17.3}/src/dirigent_core/pipelines.py +0 -0
- {dirigent_core-0.17.2 → dirigent_core-0.17.3}/src/dirigent_core/plugins.py +0 -0
- {dirigent_core-0.17.2 → dirigent_core-0.17.3}/src/dirigent_core/protocol.py +0 -0
- {dirigent_core-0.17.2 → dirigent_core-0.17.3}/src/dirigent_core/py.typed +0 -0
- {dirigent_core-0.17.2 → dirigent_core-0.17.3}/src/dirigent_core/ratelimit.py +0 -0
- {dirigent_core-0.17.2 → dirigent_core-0.17.3}/src/dirigent_core/registry.py +0 -0
- {dirigent_core-0.17.2 → dirigent_core-0.17.3}/src/dirigent_core/reporting.py +0 -0
- {dirigent_core-0.17.2 → dirigent_core-0.17.3}/src/dirigent_core/retention.py +0 -0
- {dirigent_core-0.17.2 → dirigent_core-0.17.3}/src/dirigent_core/scheduler.py +0 -0
- {dirigent_core-0.17.2 → dirigent_core-0.17.3}/src/dirigent_core/schemas.py +0 -0
- {dirigent_core-0.17.2 → dirigent_core-0.17.3}/src/dirigent_core/secrets.py +0 -0
- {dirigent_core-0.17.2 → dirigent_core-0.17.3}/src/dirigent_core/storage.py +0 -0
- {dirigent_core-0.17.2 → dirigent_core-0.17.3}/src/dirigent_core/telemetry.py +0 -0
- {dirigent_core-0.17.2 → dirigent_core-0.17.3}/src/dirigent_core/trigger_documents.py +0 -0
- {dirigent_core-0.17.2 → dirigent_core-0.17.3}/src/dirigent_core/triggers/__init__.py +0 -0
- {dirigent_core-0.17.2 → dirigent_core-0.17.3}/src/dirigent_core/triggers/backfill.py +0 -0
- {dirigent_core-0.17.2 → dirigent_core-0.17.3}/src/dirigent_core/triggers/materialize.py +0 -0
- {dirigent_core-0.17.2 → dirigent_core-0.17.3}/src/dirigent_core/triggers/schedules.py +0 -0
- {dirigent_core-0.17.2 → dirigent_core-0.17.3}/src/dirigent_core/triggers/webhooks.py +0 -0
- {dirigent_core-0.17.2 → dirigent_core-0.17.3}/src/dirigent_core/types.py +0 -0
- {dirigent_core-0.17.2 → dirigent_core-0.17.3}/src/dirigent_core/worker.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dirigent-core
|
|
3
|
-
Version: 0.17.
|
|
3
|
+
Version: 0.17.3
|
|
4
4
|
Summary: Dirigent engine core: schema, configuration, and plugin host.
|
|
5
5
|
License-Expression: LicenseRef-Proprietary
|
|
6
6
|
License-File: LICENSE
|
|
@@ -12,9 +12,9 @@ Requires-Dist: argon2-cffi>=25.1.0
|
|
|
12
12
|
Requires-Dist: asyncpg>=0.31.0
|
|
13
13
|
Requires-Dist: cronsim>=2.7
|
|
14
14
|
Requires-Dist: cryptography>=46.0.5
|
|
15
|
-
Requires-Dist: dirigent-client==0.17.
|
|
16
|
-
Requires-Dist: dirigent-common==0.17.
|
|
17
|
-
Requires-Dist: dirigent-plugin==0.17.
|
|
15
|
+
Requires-Dist: dirigent-client==0.17.3
|
|
16
|
+
Requires-Dist: dirigent-common==0.17.3
|
|
17
|
+
Requires-Dist: dirigent-plugin==0.17.3
|
|
18
18
|
Requires-Dist: jsonschema>=4.26.0
|
|
19
19
|
Requires-Dist: opentelemetry-api>=1.44.0
|
|
20
20
|
Requires-Dist: pydantic-settings>=2.15.0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "dirigent-core"
|
|
3
|
-
version = "0.17.
|
|
3
|
+
version = "0.17.3"
|
|
4
4
|
description = "Dirigent engine core: schema, configuration, and plugin host."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.13"
|
|
@@ -17,9 +17,9 @@ dependencies = [
|
|
|
17
17
|
"asyncpg>=0.31.0",
|
|
18
18
|
"cronsim>=2.7",
|
|
19
19
|
"cryptography>=46.0.5",
|
|
20
|
-
"dirigent-client==0.17.
|
|
21
|
-
"dirigent-common==0.17.
|
|
22
|
-
"dirigent-plugin==0.17.
|
|
20
|
+
"dirigent-client==0.17.3",
|
|
21
|
+
"dirigent-common==0.17.3",
|
|
22
|
+
"dirigent-plugin==0.17.3",
|
|
23
23
|
"jsonschema>=4.26.0",
|
|
24
24
|
"opentelemetry-api>=1.44.0",
|
|
25
25
|
"pydantic-settings>=2.15.0",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "dirigent-core"
|
|
3
|
-
version = "0.17.
|
|
3
|
+
version = "0.17.3"
|
|
4
4
|
description = "Dirigent engine core: schema, configuration, and plugin host."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.13"
|
|
@@ -17,9 +17,9 @@ dependencies = [
|
|
|
17
17
|
"asyncpg>=0.31.0",
|
|
18
18
|
"cronsim>=2.7",
|
|
19
19
|
"cryptography>=46.0.5",
|
|
20
|
-
"dirigent-client==0.17.
|
|
21
|
-
"dirigent-common==0.17.
|
|
22
|
-
"dirigent-plugin==0.17.
|
|
20
|
+
"dirigent-client==0.17.3",
|
|
21
|
+
"dirigent-common==0.17.3",
|
|
22
|
+
"dirigent-plugin==0.17.3",
|
|
23
23
|
"jsonschema>=4.26.0",
|
|
24
24
|
"opentelemetry-api>=1.44.0",
|
|
25
25
|
"pydantic-settings>=2.15.0",
|
|
@@ -808,6 +808,9 @@ def upgrade() -> None:
|
|
|
808
808
|
sa.Column(
|
|
809
809
|
"remote_handle", sa.JSON().with_variant(postgresql.JSONB(astext_type=Text()), "postgresql"), nullable=True
|
|
810
810
|
),
|
|
811
|
+
sa.Column(
|
|
812
|
+
"fetched_output", sa.JSON().with_variant(postgresql.JSONB(astext_type=Text()), "postgresql"), nullable=True
|
|
813
|
+
),
|
|
811
814
|
sa.Column(
|
|
812
815
|
"poke_cursor", sa.JSON().with_variant(postgresql.JSONB(astext_type=Text()), "postgresql"), nullable=True
|
|
813
816
|
),
|
|
@@ -60,6 +60,9 @@ class ClaimedUnit(BaseModel):
|
|
|
60
60
|
"""The run's trace context, so an attempt's span joins the trace the run started in."""
|
|
61
61
|
|
|
62
62
|
remote_handle: RemoteHandle | None = None
|
|
63
|
+
fetched_output: JsonMap | None = None
|
|
64
|
+
"""The result a previous call already fetched and committed, which settles this claim."""
|
|
65
|
+
|
|
63
66
|
poke_cursor: JsonMap | None = None
|
|
64
67
|
"""The cursor the last committed poke returned, handed to the next one."""
|
|
65
68
|
|
|
@@ -6,7 +6,8 @@ long step is visible working and its last lines still land with the outcome.
|
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
import asyncio
|
|
9
|
-
from collections.abc import Mapping
|
|
9
|
+
from collections.abc import AsyncGenerator, Mapping
|
|
10
|
+
from contextlib import AbstractAsyncContextManager, asynccontextmanager
|
|
10
11
|
from datetime import UTC, datetime, timedelta
|
|
11
12
|
from fnmatch import fnmatch
|
|
12
13
|
from pathlib import Path
|
|
@@ -26,7 +27,7 @@ from dirigent_core.messages import RUN_UNKNOWN_CONNECTION, RUN_UNKNOWN_SCHEMA, U
|
|
|
26
27
|
from dirigent_core.models import Connection, Schema
|
|
27
28
|
from dirigent_core.secrets import SecretBox
|
|
28
29
|
from dirigent_core.storage import AttemptStorage, Storage, connection_binder, work_dir
|
|
29
|
-
from dirigent_plugin import BlockFailure, ConnectionRef, ErrorClass, Logger, Runs
|
|
30
|
+
from dirigent_plugin import BlockFailure, ByteSink, Capture, ConnectionRef, ErrorClass, Logger, Runs
|
|
30
31
|
|
|
31
32
|
#: What an HTTP connection is assumed to call its fields, so core can build a client for
|
|
32
33
|
#: any connection kind without importing the package that contributed it.
|
|
@@ -230,6 +231,19 @@ class BufferedLogger(BaseModel):
|
|
|
230
231
|
self.full.set()
|
|
231
232
|
|
|
232
233
|
|
|
234
|
+
class CapturedStream:
|
|
235
|
+
"""A sink that carries the URI the engine opened it under, for a block to put in its output."""
|
|
236
|
+
|
|
237
|
+
def __init__(self, uri: str, sink: ByteSink) -> None:
|
|
238
|
+
"""Bind the storage sink to the URI naming it."""
|
|
239
|
+
self.uri = uri
|
|
240
|
+
self._sink = sink
|
|
241
|
+
|
|
242
|
+
async def write(self, data: bytes) -> int:
|
|
243
|
+
"""Append bytes to the stream and return how many were accepted."""
|
|
244
|
+
return await self._sink.write(data)
|
|
245
|
+
|
|
246
|
+
|
|
233
247
|
class EngineStepContext:
|
|
234
248
|
"""The concrete StepContext the engine hands a block for exactly one call."""
|
|
235
249
|
|
|
@@ -297,6 +311,26 @@ class EngineStepContext:
|
|
|
297
311
|
"""Return the run-scoped URI prefix for intermediate artifacts."""
|
|
298
312
|
return self._scratch
|
|
299
313
|
|
|
314
|
+
def capture(self, name: str, *, content_type: str = "text/plain") -> AbstractAsyncContextManager[Capture]:
|
|
315
|
+
"""Open the storage object a block's named stream is written to, under the run's scratch."""
|
|
316
|
+
uri = f"{self._scratch.rstrip('/')}/{self._segment()}-{name}"
|
|
317
|
+
|
|
318
|
+
@asynccontextmanager
|
|
319
|
+
async def opened() -> AsyncGenerator[Capture]:
|
|
320
|
+
async with self.storage.open_write(uri, content_type=content_type) as sink:
|
|
321
|
+
yield CapturedStream(uri, sink)
|
|
322
|
+
|
|
323
|
+
return opened()
|
|
324
|
+
|
|
325
|
+
def _segment(self) -> str:
|
|
326
|
+
"""The path this attempt's captures sit under: the step, the fan-out item, and the attempt.
|
|
327
|
+
|
|
328
|
+
``{step}[/{item}]/attempt-{n}``. The step key is unique within the run and the item id
|
|
329
|
+
within a fan-out, so no two attempts of one run write over each other's streams.
|
|
330
|
+
"""
|
|
331
|
+
item = f"/{self.run_item_id}" if self.run_item_id is not None else ""
|
|
332
|
+
return f"{self.step}{item}/attempt-{self.attempt}"
|
|
333
|
+
|
|
300
334
|
@property
|
|
301
335
|
def work(self) -> Path:
|
|
302
336
|
"""Return the run's directory on this worker's own filesystem, made on first read.
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
"""The engine proper: claim, call one block, record the outcome, ready the dependents.
|
|
2
2
|
|
|
3
3
|
Two invariants carry everything here. One transaction per state transition, so there is no
|
|
4
|
-
window in which a step is finished but its dependents have not been told. And a remote
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
window in which a step is finished but its dependents have not been told. And what a remote
|
|
5
|
+
call hands back is committed on its own the moment it arrives -- the handle ``execute``
|
|
6
|
+
returns, and the result ``fetch`` returns -- so a crash recovers into polling or settling
|
|
7
|
+
rather than into a duplicate job or a second fetch.
|
|
7
8
|
"""
|
|
8
9
|
|
|
9
10
|
import asyncio
|
|
@@ -97,6 +98,8 @@ class Produced(BaseModel):
|
|
|
97
98
|
model_config = ConfigDict(frozen=True)
|
|
98
99
|
|
|
99
100
|
output: JsonMap
|
|
101
|
+
fetched: bool = False
|
|
102
|
+
"""True when a fetch returned this output and nothing has committed it yet."""
|
|
100
103
|
|
|
101
104
|
|
|
102
105
|
class Submitted(BaseModel):
|
|
@@ -320,6 +323,7 @@ class Engine:
|
|
|
320
323
|
schemas=await load_schemas(session),
|
|
321
324
|
traceparent=run.traceparent,
|
|
322
325
|
remote_handle=RemoteHandle.model_validate(attempt.remote_handle) if attempt.remote_handle else None,
|
|
326
|
+
fetched_output=attempt.fetched_output,
|
|
323
327
|
poke_cursor=attempt.poke_cursor,
|
|
324
328
|
deadline_at=attempt.deadline_at,
|
|
325
329
|
gone_probes=attempt.gone_probes,
|
|
@@ -376,6 +380,8 @@ class Engine:
|
|
|
376
380
|
telemetry.record_failure(span, result.failure.message)
|
|
377
381
|
if isinstance(result, Submitted):
|
|
378
382
|
await self.record_handle(unit, result.handle)
|
|
383
|
+
if isinstance(result, Produced) and result.fetched:
|
|
384
|
+
await self._record_fetched(unit, result.output)
|
|
379
385
|
await self._record(
|
|
380
386
|
unit,
|
|
381
387
|
result,
|
|
@@ -440,6 +446,8 @@ class Engine:
|
|
|
440
446
|
unit: ClaimedUnit,
|
|
441
447
|
) -> CallResult:
|
|
442
448
|
"""Drive an operator's half of the contract: execute once, then probe and fetch."""
|
|
449
|
+
if unit.fetched_output is not None:
|
|
450
|
+
return Produced(output=unit.fetched_output)
|
|
443
451
|
if not unit.is_probe or unit.remote_handle is None:
|
|
444
452
|
with self._timed(unit.block_id, "execute"):
|
|
445
453
|
produced = await operator.execute(config, context)
|
|
@@ -460,8 +468,8 @@ class Engine:
|
|
|
460
468
|
)
|
|
461
469
|
case ProbeStatus.SUCCEEDED:
|
|
462
470
|
with self._timed(unit.block_id, "fetch"):
|
|
463
|
-
|
|
464
|
-
return Produced(output=
|
|
471
|
+
retrieved = await operator.fetch(handle, config, context)
|
|
472
|
+
return Produced(output=retrieved.model_dump(mode="json"), fetched=True)
|
|
465
473
|
case ProbeStatus.FAILED:
|
|
466
474
|
failed = (
|
|
467
475
|
Failure.unknown(REMOTE_JOB_SAID, detail=probe.message)
|
|
@@ -532,6 +540,24 @@ class Engine:
|
|
|
532
540
|
return
|
|
533
541
|
attempt.remote_handle = handle.model_dump(mode="json")
|
|
534
542
|
|
|
543
|
+
async def _record_fetched(self, unit: ClaimedUnit, output: JsonMap) -> None:
|
|
544
|
+
"""Commit a fetched result on its own, before the outcome that settles it."""
|
|
545
|
+
await with_deadlock_retry(lambda: self._record_fetched_once(unit, output))
|
|
546
|
+
|
|
547
|
+
async def _record_fetched_once(self, unit: ClaimedUnit, output: JsonMap) -> None:
|
|
548
|
+
"""Run that transaction once; the caller runs it again after a deadlock.
|
|
549
|
+
|
|
550
|
+
It locks the run and then writes the attempt, the order every outcome uses and the
|
|
551
|
+
opposite of the claim's. Nothing of it survives a rollback, so it is safe to repeat.
|
|
552
|
+
"""
|
|
553
|
+
async with session_scope(self.sessions) as session:
|
|
554
|
+
await lock_run(session, unit.run_id)
|
|
555
|
+
attempt = await session.get(StepAttempt, unit.attempt_id)
|
|
556
|
+
if attempt is None or not self._holds_lease(unit, attempt):
|
|
557
|
+
self._log_lost_lease(unit, attempt, "fetched result")
|
|
558
|
+
return
|
|
559
|
+
attempt.fetched_output = output
|
|
560
|
+
|
|
535
561
|
async def _cancel_what_was_just_submitted(
|
|
536
562
|
self,
|
|
537
563
|
session: AsyncSession,
|
|
@@ -746,6 +772,7 @@ class Engine:
|
|
|
746
772
|
)
|
|
747
773
|
attempt.status = AttemptStatus.SUCCEEDED
|
|
748
774
|
attempt.finished_at = now
|
|
775
|
+
attempt.fetched_output = None
|
|
749
776
|
attempt.error = None
|
|
750
777
|
attempt.error_code = None
|
|
751
778
|
attempt.error_params = None
|
|
@@ -288,6 +288,10 @@ class StepAttempt(Entity):
|
|
|
288
288
|
|
|
289
289
|
remote_handle: Mapped[JsonMap | None] = mapped_column(JsonDocument)
|
|
290
290
|
|
|
291
|
+
fetched_output: Mapped[JsonMap | None] = mapped_column(JsonDocument)
|
|
292
|
+
"""The result a fetch returned, committed before the outcome so a worker that dies
|
|
293
|
+
between the two settles from it rather than fetching again."""
|
|
294
|
+
|
|
291
295
|
poke_cursor: Mapped[JsonMap | None] = mapped_column(JsonDocument)
|
|
292
296
|
"""How far a sensor's poke has read, handed to the next poke as ``ctx.cursor``."""
|
|
293
297
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|