optio-core 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.
- optio_core/__init__.py +50 -0
- optio_core/_engine_service.py +161 -0
- optio_core/_force_cancel.py +73 -0
- optio_core/_generated/optio_engine.py +399 -0
- optio_core/_launch_block_store.py +81 -0
- optio_core/context.py +698 -0
- optio_core/exceptions.py +25 -0
- optio_core/executor.py +448 -0
- optio_core/lifecycle.py +1089 -0
- optio_core/migrations/__init__.py +11 -0
- optio_core/migrations/m001_status_subdocument.py +64 -0
- optio_core/migrations/m002_backfill_child_metadata.py +33 -0
- optio_core/migrations/m003_backfill_has_saved_state.py +22 -0
- optio_core/migrations/m004_create_expire_at_ttl_index.py +31 -0
- optio_core/models.py +205 -0
- optio_core/progress_helpers.py +76 -0
- optio_core/scheduler.py +97 -0
- optio_core/state_machine.py +29 -0
- optio_core/store.py +432 -0
- optio_core-0.1.0.dist-info/METADATA +674 -0
- optio_core-0.1.0.dist-info/RECORD +24 -0
- optio_core-0.1.0.dist-info/WHEEL +5 -0
- optio_core-0.1.0.dist-info/licenses/LICENSE +201 -0
- optio_core-0.1.0.dist-info/top_level.txt +1 -0
optio_core/__init__.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Optio — reusable async process management library."""
|
|
2
|
+
|
|
3
|
+
from optio_core.models import (
|
|
4
|
+
TaskInstance, TaskInstanceCore, ChildResult, LaunchBlocked,
|
|
5
|
+
LaunchOutcome, CancelOutcome, DismissOutcome,
|
|
6
|
+
)
|
|
7
|
+
from optio_core.lifecycle import Optio
|
|
8
|
+
|
|
9
|
+
_instance = Optio()
|
|
10
|
+
|
|
11
|
+
init = _instance.init
|
|
12
|
+
run = _instance.run
|
|
13
|
+
shutdown = _instance.shutdown
|
|
14
|
+
adhoc_define = _instance.adhoc_define
|
|
15
|
+
adhoc_delete = _instance.adhoc_delete
|
|
16
|
+
launch = _instance.launch
|
|
17
|
+
launch_and_wait = _instance.launch_and_wait
|
|
18
|
+
cancel = _instance.cancel
|
|
19
|
+
dismiss = _instance.dismiss
|
|
20
|
+
resync = _instance.resync
|
|
21
|
+
get_process = _instance.get_process
|
|
22
|
+
list_processes = _instance.list_processes
|
|
23
|
+
block_launches = _instance.block_launches
|
|
24
|
+
unblock_launches = _instance.unblock_launches
|
|
25
|
+
group_cancel = _instance.group_cancel
|
|
26
|
+
group_cancel_and_wait = _instance.group_cancel_and_wait
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
"TaskInstance", "TaskInstanceCore", "ChildResult", "LaunchBlocked",
|
|
30
|
+
"LaunchOutcome", "CancelOutcome", "DismissOutcome",
|
|
31
|
+
"init", "run", "shutdown",
|
|
32
|
+
"adhoc_define", "adhoc_delete",
|
|
33
|
+
"launch", "launch_and_wait", "cancel", "dismiss", "resync",
|
|
34
|
+
"get_process", "list_processes",
|
|
35
|
+
"block_launches", "unblock_launches",
|
|
36
|
+
"group_cancel", "group_cancel_and_wait",
|
|
37
|
+
"rpc_server",
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def __getattr__(name: str):
|
|
42
|
+
"""Module-level attribute lookup for runtime-populated attributes.
|
|
43
|
+
|
|
44
|
+
`rpc_server` is set on the singleton _instance during init(); a normal
|
|
45
|
+
`rpc_server = _instance.rpc_server` binding at module import time would
|
|
46
|
+
capture None forever. PEP 562 __getattr__ forwards reads on access.
|
|
47
|
+
"""
|
|
48
|
+
if name == "rpc_server":
|
|
49
|
+
return _instance.rpc_server
|
|
50
|
+
raise AttributeError(f"module 'optio_core' has no attribute {name!r}")
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""Clamator RPC implementation for the optio-engine contract."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import datetime
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from bson import ObjectId
|
|
9
|
+
|
|
10
|
+
from optio_core._generated.optio_engine import (
|
|
11
|
+
OptioEngineService as OptioEngineServiceBase,
|
|
12
|
+
LaunchParams, LaunchResult,
|
|
13
|
+
CancelParams, CancelResult,
|
|
14
|
+
DismissParams, DismissResult,
|
|
15
|
+
GroupCancelParams, GroupCancelResult,
|
|
16
|
+
GroupCancelAndWaitParams, GroupCancelAndWaitResult,
|
|
17
|
+
BlockLaunchesParams, BlockLaunchesResult,
|
|
18
|
+
UnblockLaunchesParams, UnblockLaunchesResult,
|
|
19
|
+
ResyncParams,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from optio_core.lifecycle import Optio
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
_UTC = datetime.timezone.utc
|
|
27
|
+
|
|
28
|
+
# Allowed top-level keys in the Process wire model (by-alias names).
|
|
29
|
+
_PROCESS_WIRE_KEYS = frozenset({
|
|
30
|
+
"_id", "processId", "name", "params", "metadata", "parentId", "rootId",
|
|
31
|
+
"depth", "order", "cancellable", "special", "warning", "description",
|
|
32
|
+
"status", "progress", "log", "uiWidget", "widgetData", "supportsResume",
|
|
33
|
+
"hasSavedState", "createdAt",
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _fix_value(v: object) -> object:
|
|
38
|
+
"""Recursively normalize a Mongo value to a wire-safe value."""
|
|
39
|
+
if isinstance(v, ObjectId):
|
|
40
|
+
return str(v)
|
|
41
|
+
if isinstance(v, datetime.datetime) and v.tzinfo is None:
|
|
42
|
+
return v.replace(tzinfo=_UTC)
|
|
43
|
+
if isinstance(v, dict):
|
|
44
|
+
return {k2: _fix_value(v2) for k2, v2 in v.items()}
|
|
45
|
+
if isinstance(v, list):
|
|
46
|
+
return [_fix_value(item) for item in v]
|
|
47
|
+
return v
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _to_process_dict(doc: dict) -> dict:
|
|
51
|
+
"""Render a Mongo process doc as the wire-shape Process payload.
|
|
52
|
+
|
|
53
|
+
Returns a dict that LaunchResult1.process / CancelResult1.process /
|
|
54
|
+
DismissResult1.process etc. can validate. Generated Process model uses
|
|
55
|
+
by-alias field names (e.g. _id, processId, supportsResume).
|
|
56
|
+
|
|
57
|
+
Strips fields unknown to the contract (e.g. adhoc, ephemeral, ttlSeconds
|
|
58
|
+
stored by the scheduler), stringifies ObjectIds, and makes naive datetimes
|
|
59
|
+
UTC-aware so Pydantic AwareDatetime validation passes.
|
|
60
|
+
"""
|
|
61
|
+
out = {
|
|
62
|
+
k: _fix_value(v)
|
|
63
|
+
for k, v in doc.items()
|
|
64
|
+
if k in _PROCESS_WIRE_KEYS
|
|
65
|
+
}
|
|
66
|
+
return out
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class OptioEngineService(OptioEngineServiceBase):
|
|
70
|
+
"""Concrete OptioEngineService backing the clamator optio-engine contract."""
|
|
71
|
+
|
|
72
|
+
def __init__(self, optio: "Optio") -> None:
|
|
73
|
+
self._optio = optio
|
|
74
|
+
|
|
75
|
+
# --------------------------------------------------------------- launch
|
|
76
|
+
async def launch(self, params: LaunchParams) -> LaunchResult:
|
|
77
|
+
outcome = await self._optio.launch(
|
|
78
|
+
params.process_id, resume=bool(params.resume),
|
|
79
|
+
)
|
|
80
|
+
if not outcome.ok:
|
|
81
|
+
return LaunchResult.model_validate(
|
|
82
|
+
{"ok": False, "reason": outcome.reason}
|
|
83
|
+
)
|
|
84
|
+
return LaunchResult.model_validate(
|
|
85
|
+
{"ok": True, "process": _to_process_dict(outcome.proc)}
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
# --------------------------------------------------------------- cancel
|
|
89
|
+
async def cancel(self, params: CancelParams) -> CancelResult:
|
|
90
|
+
outcome = await self._optio.cancel(params.process_id)
|
|
91
|
+
if not outcome.ok:
|
|
92
|
+
return CancelResult.model_validate(
|
|
93
|
+
{"ok": False, "reason": outcome.reason}
|
|
94
|
+
)
|
|
95
|
+
return CancelResult.model_validate(
|
|
96
|
+
{"ok": True, "process": _to_process_dict(outcome.proc)}
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
# --------------------------------------------------------------- dismiss
|
|
100
|
+
async def dismiss(self, params: DismissParams) -> DismissResult:
|
|
101
|
+
outcome = await self._optio.dismiss(params.process_id)
|
|
102
|
+
if not outcome.ok:
|
|
103
|
+
return DismissResult.model_validate(
|
|
104
|
+
{"ok": False, "reason": outcome.reason}
|
|
105
|
+
)
|
|
106
|
+
return DismissResult.model_validate(
|
|
107
|
+
{"ok": True, "process": _to_process_dict(outcome.proc)}
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
# --------------------------------------------------------------- resync
|
|
111
|
+
async def resync(self, params: ResyncParams) -> None:
|
|
112
|
+
await self._optio.resync(
|
|
113
|
+
clean=bool(params.clean),
|
|
114
|
+
metadata_filter=params.metadata_filter,
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
# --------------------------------------------------------------- group_cancel / group_cancel_and_wait
|
|
118
|
+
async def group_cancel(self, params: GroupCancelParams) -> GroupCancelResult:
|
|
119
|
+
if params.persist and not params.block_new_launches:
|
|
120
|
+
return GroupCancelResult.model_validate(
|
|
121
|
+
{"ok": False, "reason": "invalid-persist-without-block"}
|
|
122
|
+
)
|
|
123
|
+
count = await self._optio.group_cancel(
|
|
124
|
+
metadata_filter=params.metadata_filter,
|
|
125
|
+
block_new_launches=bool(params.block_new_launches),
|
|
126
|
+
persist=bool(params.persist),
|
|
127
|
+
reason=params.reason,
|
|
128
|
+
)
|
|
129
|
+
return GroupCancelResult.model_validate({"ok": True, "cancelledCount": count})
|
|
130
|
+
|
|
131
|
+
async def group_cancel_and_wait(
|
|
132
|
+
self, params: GroupCancelAndWaitParams
|
|
133
|
+
) -> GroupCancelAndWaitResult:
|
|
134
|
+
if params.persist and not params.block_new_launches:
|
|
135
|
+
return GroupCancelAndWaitResult.model_validate(
|
|
136
|
+
{"ok": False, "reason": "invalid-persist-without-block"}
|
|
137
|
+
)
|
|
138
|
+
count = await self._optio.group_cancel_and_wait(
|
|
139
|
+
metadata_filter=params.metadata_filter,
|
|
140
|
+
block_new_launches=bool(params.block_new_launches),
|
|
141
|
+
persist=bool(params.persist),
|
|
142
|
+
reason=params.reason,
|
|
143
|
+
)
|
|
144
|
+
return GroupCancelAndWaitResult.model_validate({"ok": True, "cancelledCount": count})
|
|
145
|
+
|
|
146
|
+
# --------------------------------------------------------------- block_launches / unblock_launches
|
|
147
|
+
async def block_launches(self, params: BlockLaunchesParams) -> BlockLaunchesResult:
|
|
148
|
+
from optio_core import _launch_block_store as _lb_store
|
|
149
|
+
coll = _lb_store.collection(
|
|
150
|
+
self._optio._config.mongo_db,
|
|
151
|
+
self._optio._config.prefix,
|
|
152
|
+
)
|
|
153
|
+
await _lb_store.upsert_block(coll, params.launch_filter, params.reason)
|
|
154
|
+
await self._optio._load_persisted_blocks()
|
|
155
|
+
return BlockLaunchesResult.model_validate({"ok": True})
|
|
156
|
+
|
|
157
|
+
async def unblock_launches(
|
|
158
|
+
self, params: UnblockLaunchesParams
|
|
159
|
+
) -> UnblockLaunchesResult:
|
|
160
|
+
removed = await self._optio.unblock_launches(params.launch_filter)
|
|
161
|
+
return UnblockLaunchesResult(removed=removed)
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Shared helper for writing the canonical 'force-cancelled' terminal state.
|
|
2
|
+
|
|
3
|
+
Imported by both Executor.force_cancel and Optio.shutdown. Kept in its own
|
|
4
|
+
module to avoid a circular import between executor.py and lifecycle.py.
|
|
5
|
+
|
|
6
|
+
Spec: docs/2026-04-29-deadline-driven-cancel-design.md
|
|
7
|
+
"""
|
|
8
|
+
import logging as _logging
|
|
9
|
+
import os as _os
|
|
10
|
+
import time as _time
|
|
11
|
+
from datetime import datetime, timezone
|
|
12
|
+
|
|
13
|
+
from bson import ObjectId
|
|
14
|
+
from motor.motor_asyncio import AsyncIOMotorDatabase
|
|
15
|
+
|
|
16
|
+
from optio_core.models import ProcessStatus
|
|
17
|
+
from optio_core.state_machine import ACTIVE_STATES
|
|
18
|
+
from optio_core.store import append_log, compute_expire_at
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
_trace_logger = _logging.getLogger("optio_core.cancel_trace")
|
|
22
|
+
_CANCEL_TRACE = _os.environ.get("OPTIO_CANCEL_TRACE", "0").lower() in ("1", "true", "yes")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _trace(fmt: str, *args: object) -> None:
|
|
26
|
+
if _CANCEL_TRACE:
|
|
27
|
+
_trace_logger.warning("[%.3f] _force_cancel " + fmt, _time.monotonic(), *args)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
FORCE_CANCEL_ERROR = "Task did not unwind within cancellation grace period"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
async def _write_force_cancelled_state(
|
|
34
|
+
db: AsyncIOMotorDatabase, prefix: str, oid: ObjectId,
|
|
35
|
+
) -> bool:
|
|
36
|
+
"""Conditionally flip an active process to terminal 'failed' state.
|
|
37
|
+
|
|
38
|
+
Only updates rows whose current state is in ACTIVE_STATES. A task that
|
|
39
|
+
won the race to a terminal state owns its own transition and is left
|
|
40
|
+
alone. Returns True if the row was updated, False otherwise.
|
|
41
|
+
|
|
42
|
+
If the row carries a `ttlSeconds` field, also $set `expireAt = now + ttl`
|
|
43
|
+
so the TTL index evicts it at the same point a cooperative-cancel
|
|
44
|
+
record would have been evicted (B2 invariant: every terminal-state
|
|
45
|
+
writer honours TTL).
|
|
46
|
+
"""
|
|
47
|
+
coll = db[f"{prefix}_processes"]
|
|
48
|
+
now = datetime.now(timezone.utc)
|
|
49
|
+
status_doc = ProcessStatus(
|
|
50
|
+
state="failed", error=FORCE_CANCEL_ERROR, failed_at=now,
|
|
51
|
+
).to_dict()
|
|
52
|
+
|
|
53
|
+
# Read ttlSeconds so we can compute expireAt for the TTL index.
|
|
54
|
+
ttl_doc = await coll.find_one({"_id": oid}, {"ttlSeconds": 1})
|
|
55
|
+
expire_at = compute_expire_at((ttl_doc or {}).get("ttlSeconds"), now=now)
|
|
56
|
+
set_doc: dict = {"status": status_doc, "widgetUpstream": None}
|
|
57
|
+
if expire_at is not None:
|
|
58
|
+
set_doc["expireAt"] = expire_at
|
|
59
|
+
|
|
60
|
+
result = await coll.update_one(
|
|
61
|
+
{"_id": oid, "status.state": {"$in": list(ACTIVE_STATES)}},
|
|
62
|
+
{"$set": set_doc},
|
|
63
|
+
)
|
|
64
|
+
if result.modified_count:
|
|
65
|
+
_trace("oid=%s WROTE state=failed reason=grace-exceeded", oid)
|
|
66
|
+
await append_log(
|
|
67
|
+
db, prefix, oid,
|
|
68
|
+
"event",
|
|
69
|
+
"State forced: running -> failed (cancellation grace period exceeded)",
|
|
70
|
+
)
|
|
71
|
+
return True
|
|
72
|
+
_trace("oid=%s no-op: row already in terminal state (lost race)", oid)
|
|
73
|
+
return False
|
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
# AUTO-GENERATED by @clamator/codegen v0.1.9 from optio-engine-to-api.ts.
|
|
2
|
+
# DO NOT EDIT. Re-run codegen to update.
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
from abc import ABC, abstractmethod
|
|
5
|
+
from clamator_protocol import ClamatorClient, Contract, MethodEntry
|
|
6
|
+
|
|
7
|
+
from typing import Any, Literal
|
|
8
|
+
|
|
9
|
+
from pydantic import AwareDatetime, BaseModel, ConfigDict, Field, RootModel
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class BlockLaunchesParams(BaseModel):
|
|
13
|
+
model_config = ConfigDict(
|
|
14
|
+
extra="forbid",
|
|
15
|
+
)
|
|
16
|
+
launch_filter: dict[str, Any] = Field(..., alias="launchFilter")
|
|
17
|
+
reason: str | None = None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class BlockLaunchesResult1(BaseModel):
|
|
21
|
+
model_config = ConfigDict(
|
|
22
|
+
extra="forbid",
|
|
23
|
+
)
|
|
24
|
+
ok: Literal[True]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class BlockLaunchesResult2(BaseModel):
|
|
28
|
+
model_config = ConfigDict(
|
|
29
|
+
extra="forbid",
|
|
30
|
+
)
|
|
31
|
+
ok: Literal[False]
|
|
32
|
+
reason: Literal["invalid-filter"]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class BlockLaunchesResult(RootModel[BlockLaunchesResult1 | BlockLaunchesResult2]):
|
|
36
|
+
root: BlockLaunchesResult1 | BlockLaunchesResult2
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class CancelParams(BaseModel):
|
|
40
|
+
model_config = ConfigDict(
|
|
41
|
+
extra="forbid",
|
|
42
|
+
)
|
|
43
|
+
process_id: str = Field(..., alias="processId", min_length=1)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class Status(BaseModel):
|
|
47
|
+
model_config = ConfigDict(
|
|
48
|
+
extra="forbid",
|
|
49
|
+
)
|
|
50
|
+
state: Literal[
|
|
51
|
+
"idle",
|
|
52
|
+
"scheduled",
|
|
53
|
+
"running",
|
|
54
|
+
"done",
|
|
55
|
+
"failed",
|
|
56
|
+
"cancel_requested",
|
|
57
|
+
"cancelling",
|
|
58
|
+
"cancelled",
|
|
59
|
+
]
|
|
60
|
+
error: str | None = None
|
|
61
|
+
running_since: AwareDatetime | None = Field(None, alias="runningSince")
|
|
62
|
+
done_at: AwareDatetime | None = Field(None, alias="doneAt")
|
|
63
|
+
duration: float | None = None
|
|
64
|
+
failed_at: AwareDatetime | None = Field(None, alias="failedAt")
|
|
65
|
+
stopped_at: AwareDatetime | None = Field(None, alias="stoppedAt")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class Percent(RootModel[float]):
|
|
69
|
+
root: float = Field(..., ge=0.0, le=100.0)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class Progress(BaseModel):
|
|
73
|
+
model_config = ConfigDict(
|
|
74
|
+
extra="forbid",
|
|
75
|
+
)
|
|
76
|
+
percent: Percent | None
|
|
77
|
+
message: str | None = None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class LogItem(BaseModel):
|
|
81
|
+
model_config = ConfigDict(
|
|
82
|
+
extra="forbid",
|
|
83
|
+
)
|
|
84
|
+
timestamp: AwareDatetime
|
|
85
|
+
level: Literal["event", "info", "debug", "warning", "error"]
|
|
86
|
+
message: str
|
|
87
|
+
data: dict[str, Any] | None = None
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class Process(BaseModel):
|
|
91
|
+
model_config = ConfigDict(
|
|
92
|
+
extra="forbid",
|
|
93
|
+
)
|
|
94
|
+
field_id: str = Field(..., alias="_id", pattern="^[a-f\\d]{24}$")
|
|
95
|
+
process_id: str = Field(..., alias="processId")
|
|
96
|
+
name: str
|
|
97
|
+
params: dict[str, Any] | None = None
|
|
98
|
+
metadata: dict[str, Any] | None = None
|
|
99
|
+
parent_id: str | None = Field(None, alias="parentId", pattern="^[a-f\\d]{24}$")
|
|
100
|
+
root_id: str = Field(..., alias="rootId", pattern="^[a-f\\d]{24}$")
|
|
101
|
+
depth: int = Field(..., ge=0)
|
|
102
|
+
order: int = Field(..., ge=0)
|
|
103
|
+
cancellable: bool
|
|
104
|
+
special: bool | None = None
|
|
105
|
+
warning: str | None = None
|
|
106
|
+
description: str | None = None
|
|
107
|
+
status: Status
|
|
108
|
+
progress: Progress
|
|
109
|
+
log: list[LogItem]
|
|
110
|
+
ui_widget: str | None = Field(None, alias="uiWidget")
|
|
111
|
+
widget_data: Any | None = Field(None, alias="widgetData")
|
|
112
|
+
supports_resume: bool | None = Field(None, alias="supportsResume")
|
|
113
|
+
has_saved_state: bool | None = Field(None, alias="hasSavedState")
|
|
114
|
+
created_at: AwareDatetime = Field(..., alias="createdAt")
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class CancelResult1(BaseModel):
|
|
118
|
+
model_config = ConfigDict(
|
|
119
|
+
extra="forbid",
|
|
120
|
+
)
|
|
121
|
+
ok: Literal[True]
|
|
122
|
+
process: Process
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class CancelResult2(BaseModel):
|
|
126
|
+
model_config = ConfigDict(
|
|
127
|
+
extra="forbid",
|
|
128
|
+
)
|
|
129
|
+
ok: Literal[False]
|
|
130
|
+
reason: Literal["not-found", "not-cancellable"]
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class CancelResult(RootModel[CancelResult1 | CancelResult2]):
|
|
134
|
+
root: CancelResult1 | CancelResult2
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class DismissParams(CancelParams):
|
|
138
|
+
pass
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class Progress1(Progress):
|
|
142
|
+
pass
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class Process1(BaseModel):
|
|
146
|
+
model_config = ConfigDict(
|
|
147
|
+
extra="forbid",
|
|
148
|
+
)
|
|
149
|
+
field_id: str = Field(..., alias="_id", pattern="^[a-f\\d]{24}$")
|
|
150
|
+
process_id: str = Field(..., alias="processId")
|
|
151
|
+
name: str
|
|
152
|
+
params: dict[str, Any] | None = None
|
|
153
|
+
metadata: dict[str, Any] | None = None
|
|
154
|
+
parent_id: str | None = Field(None, alias="parentId", pattern="^[a-f\\d]{24}$")
|
|
155
|
+
root_id: str = Field(..., alias="rootId", pattern="^[a-f\\d]{24}$")
|
|
156
|
+
depth: int = Field(..., ge=0)
|
|
157
|
+
order: int = Field(..., ge=0)
|
|
158
|
+
cancellable: bool
|
|
159
|
+
special: bool | None = None
|
|
160
|
+
warning: str | None = None
|
|
161
|
+
description: str | None = None
|
|
162
|
+
status: Status
|
|
163
|
+
progress: Progress1
|
|
164
|
+
log: list[LogItem]
|
|
165
|
+
ui_widget: str | None = Field(None, alias="uiWidget")
|
|
166
|
+
widget_data: Any | None = Field(None, alias="widgetData")
|
|
167
|
+
supports_resume: bool | None = Field(None, alias="supportsResume")
|
|
168
|
+
has_saved_state: bool | None = Field(None, alias="hasSavedState")
|
|
169
|
+
created_at: AwareDatetime = Field(..., alias="createdAt")
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class DismissResult1(BaseModel):
|
|
173
|
+
model_config = ConfigDict(
|
|
174
|
+
extra="forbid",
|
|
175
|
+
)
|
|
176
|
+
ok: Literal[True]
|
|
177
|
+
process: Process1
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class DismissResult2(BaseModel):
|
|
181
|
+
model_config = ConfigDict(
|
|
182
|
+
extra="forbid",
|
|
183
|
+
)
|
|
184
|
+
ok: Literal[False]
|
|
185
|
+
reason: Literal["not-found", "not-dismissable"]
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
class DismissResult(RootModel[DismissResult1 | DismissResult2]):
|
|
189
|
+
root: DismissResult1 | DismissResult2
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class GroupCancelParams(BaseModel):
|
|
193
|
+
model_config = ConfigDict(
|
|
194
|
+
extra="forbid",
|
|
195
|
+
)
|
|
196
|
+
metadata_filter: dict[str, Any] = Field(..., alias="metadataFilter")
|
|
197
|
+
block_new_launches: bool | None = Field(None, alias="blockNewLaunches")
|
|
198
|
+
persist: bool | None = None
|
|
199
|
+
reason: str | None = None
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
class GroupCancelResult1(BaseModel):
|
|
203
|
+
model_config = ConfigDict(
|
|
204
|
+
extra="forbid",
|
|
205
|
+
)
|
|
206
|
+
ok: Literal[True]
|
|
207
|
+
cancelled_count: int = Field(..., alias="cancelledCount", ge=0)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
class GroupCancelResult2(BaseModel):
|
|
211
|
+
model_config = ConfigDict(
|
|
212
|
+
extra="forbid",
|
|
213
|
+
)
|
|
214
|
+
ok: Literal[False]
|
|
215
|
+
reason: Literal["invalid-persist-without-block"]
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
class GroupCancelResult(RootModel[GroupCancelResult1 | GroupCancelResult2]):
|
|
219
|
+
root: GroupCancelResult1 | GroupCancelResult2
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
class GroupCancelAndWaitParams(GroupCancelParams):
|
|
223
|
+
pass
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
class GroupCancelAndWaitResult1(GroupCancelResult1):
|
|
227
|
+
pass
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
class GroupCancelAndWaitResult2(GroupCancelResult2):
|
|
231
|
+
pass
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
class GroupCancelAndWaitResult(
|
|
235
|
+
RootModel[GroupCancelAndWaitResult1 | GroupCancelAndWaitResult2]
|
|
236
|
+
):
|
|
237
|
+
root: GroupCancelAndWaitResult1 | GroupCancelAndWaitResult2
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
class LaunchParams(BaseModel):
|
|
241
|
+
model_config = ConfigDict(
|
|
242
|
+
extra="forbid",
|
|
243
|
+
)
|
|
244
|
+
process_id: str = Field(..., alias="processId", min_length=1)
|
|
245
|
+
resume: bool | None = None
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
class Progress2(Progress):
|
|
249
|
+
pass
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
class Process2(BaseModel):
|
|
253
|
+
model_config = ConfigDict(
|
|
254
|
+
extra="forbid",
|
|
255
|
+
)
|
|
256
|
+
field_id: str = Field(..., alias="_id", pattern="^[a-f\\d]{24}$")
|
|
257
|
+
process_id: str = Field(..., alias="processId")
|
|
258
|
+
name: str
|
|
259
|
+
params: dict[str, Any] | None = None
|
|
260
|
+
metadata: dict[str, Any] | None = None
|
|
261
|
+
parent_id: str | None = Field(None, alias="parentId", pattern="^[a-f\\d]{24}$")
|
|
262
|
+
root_id: str = Field(..., alias="rootId", pattern="^[a-f\\d]{24}$")
|
|
263
|
+
depth: int = Field(..., ge=0)
|
|
264
|
+
order: int = Field(..., ge=0)
|
|
265
|
+
cancellable: bool
|
|
266
|
+
special: bool | None = None
|
|
267
|
+
warning: str | None = None
|
|
268
|
+
description: str | None = None
|
|
269
|
+
status: Status
|
|
270
|
+
progress: Progress2
|
|
271
|
+
log: list[LogItem]
|
|
272
|
+
ui_widget: str | None = Field(None, alias="uiWidget")
|
|
273
|
+
widget_data: Any | None = Field(None, alias="widgetData")
|
|
274
|
+
supports_resume: bool | None = Field(None, alias="supportsResume")
|
|
275
|
+
has_saved_state: bool | None = Field(None, alias="hasSavedState")
|
|
276
|
+
created_at: AwareDatetime = Field(..., alias="createdAt")
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
class LaunchResult1(BaseModel):
|
|
280
|
+
model_config = ConfigDict(
|
|
281
|
+
extra="forbid",
|
|
282
|
+
)
|
|
283
|
+
ok: Literal[True]
|
|
284
|
+
process: Process2
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
class LaunchResult2(BaseModel):
|
|
288
|
+
model_config = ConfigDict(
|
|
289
|
+
extra="forbid",
|
|
290
|
+
)
|
|
291
|
+
ok: Literal[False]
|
|
292
|
+
reason: Literal[
|
|
293
|
+
"not-found", "not-launchable", "no-resume-support", "launch-blocked"
|
|
294
|
+
]
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
class LaunchResult(RootModel[LaunchResult1 | LaunchResult2]):
|
|
298
|
+
root: LaunchResult1 | LaunchResult2
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
class ResyncParams(BaseModel):
|
|
302
|
+
model_config = ConfigDict(
|
|
303
|
+
extra="forbid",
|
|
304
|
+
)
|
|
305
|
+
clean: bool | None = None
|
|
306
|
+
metadata_filter: dict[str, Any] | None = Field(None, alias="metadataFilter")
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
class UnblockLaunchesParams(BaseModel):
|
|
310
|
+
model_config = ConfigDict(
|
|
311
|
+
extra="forbid",
|
|
312
|
+
)
|
|
313
|
+
launch_filter: dict[str, Any] = Field(..., alias="launchFilter")
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
class UnblockLaunchesResult(BaseModel):
|
|
317
|
+
model_config = ConfigDict(
|
|
318
|
+
extra="forbid",
|
|
319
|
+
)
|
|
320
|
+
removed: int = Field(..., ge=0)
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
class OptioEngineClient:
|
|
324
|
+
def __init__(self, client: ClamatorClient) -> None:
|
|
325
|
+
self._client = client
|
|
326
|
+
|
|
327
|
+
async def block_launches(self, params: BlockLaunchesParams, *, timeout_ms: int | None = None) -> BlockLaunchesResult:
|
|
328
|
+
raw = await self._client.call("optio-engine", "blockLaunches", params.model_dump(mode='json', by_alias=True), timeout_ms=timeout_ms)
|
|
329
|
+
return BlockLaunchesResult.model_validate(raw)
|
|
330
|
+
|
|
331
|
+
async def cancel(self, params: CancelParams, *, timeout_ms: int | None = None) -> CancelResult:
|
|
332
|
+
raw = await self._client.call("optio-engine", "cancel", params.model_dump(mode='json', by_alias=True), timeout_ms=timeout_ms)
|
|
333
|
+
return CancelResult.model_validate(raw)
|
|
334
|
+
|
|
335
|
+
async def dismiss(self, params: DismissParams, *, timeout_ms: int | None = None) -> DismissResult:
|
|
336
|
+
raw = await self._client.call("optio-engine", "dismiss", params.model_dump(mode='json', by_alias=True), timeout_ms=timeout_ms)
|
|
337
|
+
return DismissResult.model_validate(raw)
|
|
338
|
+
|
|
339
|
+
async def group_cancel(self, params: GroupCancelParams, *, timeout_ms: int | None = None) -> GroupCancelResult:
|
|
340
|
+
raw = await self._client.call("optio-engine", "groupCancel", params.model_dump(mode='json', by_alias=True), timeout_ms=timeout_ms)
|
|
341
|
+
return GroupCancelResult.model_validate(raw)
|
|
342
|
+
|
|
343
|
+
async def group_cancel_and_wait(self, params: GroupCancelAndWaitParams, *, timeout_ms: int | None = None) -> GroupCancelAndWaitResult:
|
|
344
|
+
raw = await self._client.call("optio-engine", "groupCancelAndWait", params.model_dump(mode='json', by_alias=True), timeout_ms=timeout_ms)
|
|
345
|
+
return GroupCancelAndWaitResult.model_validate(raw)
|
|
346
|
+
|
|
347
|
+
async def launch(self, params: LaunchParams, *, timeout_ms: int | None = None) -> LaunchResult:
|
|
348
|
+
raw = await self._client.call("optio-engine", "launch", params.model_dump(mode='json', by_alias=True), timeout_ms=timeout_ms)
|
|
349
|
+
return LaunchResult.model_validate(raw)
|
|
350
|
+
|
|
351
|
+
async def resync(self, params: ResyncParams) -> None:
|
|
352
|
+
await self._client.notify("optio-engine", "resync", params.model_dump(mode='json', by_alias=True))
|
|
353
|
+
|
|
354
|
+
async def unblock_launches(self, params: UnblockLaunchesParams, *, timeout_ms: int | None = None) -> UnblockLaunchesResult:
|
|
355
|
+
raw = await self._client.call("optio-engine", "unblockLaunches", params.model_dump(mode='json', by_alias=True), timeout_ms=timeout_ms)
|
|
356
|
+
return UnblockLaunchesResult.model_validate(raw)
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
class OptioEngineService(ABC):
|
|
360
|
+
@abstractmethod
|
|
361
|
+
async def block_launches(self, params: BlockLaunchesParams) -> BlockLaunchesResult: ...
|
|
362
|
+
|
|
363
|
+
@abstractmethod
|
|
364
|
+
async def cancel(self, params: CancelParams) -> CancelResult: ...
|
|
365
|
+
|
|
366
|
+
@abstractmethod
|
|
367
|
+
async def dismiss(self, params: DismissParams) -> DismissResult: ...
|
|
368
|
+
|
|
369
|
+
@abstractmethod
|
|
370
|
+
async def group_cancel(self, params: GroupCancelParams) -> GroupCancelResult: ...
|
|
371
|
+
|
|
372
|
+
@abstractmethod
|
|
373
|
+
async def group_cancel_and_wait(self, params: GroupCancelAndWaitParams) -> GroupCancelAndWaitResult: ...
|
|
374
|
+
|
|
375
|
+
@abstractmethod
|
|
376
|
+
async def launch(self, params: LaunchParams) -> LaunchResult: ...
|
|
377
|
+
|
|
378
|
+
@abstractmethod
|
|
379
|
+
async def resync(self, params: ResyncParams) -> None: ...
|
|
380
|
+
|
|
381
|
+
@abstractmethod
|
|
382
|
+
async def unblock_launches(self, params: UnblockLaunchesParams) -> UnblockLaunchesResult: ...
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
METHODS = {
|
|
386
|
+
"blockLaunches": MethodEntry(params_model=BlockLaunchesParams, result_model=BlockLaunchesResult, handler_attr="block_launches"),
|
|
387
|
+
"cancel": MethodEntry(params_model=CancelParams, result_model=CancelResult, handler_attr="cancel"),
|
|
388
|
+
"dismiss": MethodEntry(params_model=DismissParams, result_model=DismissResult, handler_attr="dismiss"),
|
|
389
|
+
"groupCancel": MethodEntry(params_model=GroupCancelParams, result_model=GroupCancelResult, handler_attr="group_cancel"),
|
|
390
|
+
"groupCancelAndWait": MethodEntry(params_model=GroupCancelAndWaitParams, result_model=GroupCancelAndWaitResult, handler_attr="group_cancel_and_wait"),
|
|
391
|
+
"launch": MethodEntry(params_model=LaunchParams, result_model=LaunchResult, handler_attr="launch"),
|
|
392
|
+
"resync": MethodEntry(params_model=ResyncParams, result_model=None, handler_attr="resync"),
|
|
393
|
+
"unblockLaunches": MethodEntry(params_model=UnblockLaunchesParams, result_model=UnblockLaunchesResult, handler_attr="unblock_launches"),
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
optio_engine_contract = Contract(
|
|
397
|
+
service="optio-engine",
|
|
398
|
+
methods=METHODS,
|
|
399
|
+
)
|