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 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
+ )