samco-job-lifecycle
One job lifecycle, shared by every use case that runs work as a Kubernetes Job.
claim due work -> build execution -> start Job -> record -> observe -> close
Before this package the loop above existed twice — in screener-schedule-engine
and in realtime-signal-engine — and the two had drifted. This package keeps
the shared lifecycle in one place.
Version 0.2.0 replaces the unrelated 0.1.x starter API. It does not expose the
old Job class; use AbstractLifecycleEngine and the models below.
Install
pip install samco-job-lifecycle==0.2.0
To use KubernetesJobRuntime, install the optional Kubernetes client:
pip install 'samco-job-lifecycle[kubernetes]==0.2.0'
What it does and does not know
| Owns | Never owns |
|---|---|
| the lifecycle sequence and legal phase changes | screener cadence, trading or signal rules |
| the concurrency cap and fail-closed counting | MySQL or MongoDB schema |
| idempotent Job creation, 409 handling | a project or source name switch |
| observing Jobs and classifying failures | an untyped dict of "project inputs" |
| cycle reports and metrics | what a run computed |
SUCCEEDED here means the process exited zero. What a screen matched or which
signal was emitted is the runner's to record, and stays the runner's.
Using it
This subclass is a sketch: each method shown with ... must be implemented
against your application's store before calling schedule_once().
from samco_job_lifecycle import (AbstractLifecycleEngine, ExecutionSpec,
KubernetesJobRuntime, LabelSelector,
LifecycleLimits)
class PaperTradeLifecycle(AbstractLifecycleEngine):
def claim_due(self, now, limit): ... # your store, your atomic claim
def build_execution(self, work): ... # -> ExecutionSpec
def mark_launched(self, work, handle): ...
def mark_launch_failed(self, work, failure): ...
def active_executions(self): ...
def apply_observation(self, execution, observation): ...
engine = PaperTradeLifecycle(
KubernetesJobRuntime("strategies"),
LifecycleLimits(max_active=40, per_cycle=20),
LabelSelector({"app": "strategy-runner"}),
)
engine.schedule_once()
Main API
| Component | Public methods or functions | Purpose |
|---|---|---|
AbstractLifecycleEngine |
schedule_once, claim_due, build_execution, mark_launched, mark_launch_failed, mark_launch_ambiguous, active_executions, apply_observation |
Run one scheduling cycle; subclasses provide store-specific operations. |
KubernetesJobRuntime / JobRuntime |
active_count, start, observe |
Count, create and observe Kubernetes Jobs. |
Reconciler |
reconcile_once |
Close finished executions and flag missing Jobs. |
| State helpers | can_transition, check_transition; states.is_terminal |
Validate lifecycle phase changes. |
| Manifest helpers | render_manifest, observation_from_job |
Convert execution specs and Kubernetes Jobs. |
FakeJobRuntime |
active_count, start, observe, finish, forget |
Exercise the lifecycle without a cluster. |
LifecycleContract |
make_engine, seed_due, seed_malformed |
Hooks for the shared subclass test suite. |
CycleReport / ReconcileReport |
summary, ok |
Read cycle and reconciliation outcomes. |
Metrics |
count, gauge |
Emit lifecycle counters and gauges. |
The package also exports LifecyclePhase, TERMINAL, LifecycleLimits,
ExecutionSpec, EnvVar, EnvSource, ResourceSpec, LabelSelector,
WorkRef, JobHandle, JobObservation, TrackedExecution, and failure types.
Do not override schedule_once. It is the template method, and overriding it
opts out of every guarantee the contract suite checks.
The two cap policies
Both consumers were right and they disagreed, so the difference is configuration rather than a subclass replacing the cycle:
LifecycleLimits(max_active=20, wait_for_slot=True) # screener: run everything, in waves
LifecycleLimits(max_active=40, per_cycle=20) # signals: trim the cycle, retry next minute
The contract suite
Every subclass must pass the same suite, or it is not substitutable:
from samco_job_lifecycle.testing import LifecycleContract
class TestPaperContract(LifecycleContract):
def make_engine(self, runtime):
return PaperTradeLifecycle(runtime, LIMITS, SELECTOR, store=FakeStore())
def seed_due(self, engine, count):
engine.store.add(count)
It checks the properties that were incidents waiting to happen: an atomic claim, a 409 read as success, one item's failure not stopping the rest, an ambiguous create keeping its claim, and monotonic idempotent phase writes.
Consuming a local checkout
For development, a path dependency can override the published package:
dependencies = ["samco-job-lifecycle"]
[tool.uv.sources]
samco-job-lifecycle = { path = "../samco-job-lifecycle" }
It is not editable, so an edit here needs
uv sync --reinstall-package samco-job-lifecycle in each consumer and a
restart of anything running.
Python floor is 3.10, not 3.12: a shared package cannot be newer than its
oldest consumer. The Kubernetes client is an extra ([kubernetes]) — testing
against FakeJobRuntime needs neither a client nor a cluster.
Layout
src/samco_job_lifecycle/
engine.py AbstractLifecycleEngine — the template method
models.py frozen value objects crossing the boundary
states.py phases and legal transitions
errors.py definite vs ambiguous failure
runtime.py JobRuntime contract
kubernetes.py KubernetesJobRuntime — the only file importing the client
reconciler.py closing runs a pod could not close
report.py CycleReport / ReconcileReport
metrics.py counters, no domain labels
testing/ FakeJobRuntime and the contract suite
Run the tests with uv run pytest -q and the linter with uv run ruff check ..
Metadata
Release files for samco-job-lifecycle 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| samco_job_lifecycle-0.2.0.tar.gz | 1.5 MB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| samco_job_lifecycle-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 1.5 MB
Release files / samco_job_lifecycle-0.2.0.tar.gz
| Download URL | samco_job_lifecycle-0.2.0.tar.gz |
|---|---|
| Size | 1.5 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
62b091f60e7f7d338e4a2a8f13d068315cbc7a44c8e00ab43c1e1b0a9b14f7b5
|
|
BLAKE2b-256 checksum How to use checksums |
9bd8fa060e732d1affe1da764653e8e1f5c8beadd8353412f2133c9bf2db6775
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.10.10
|
Release files / samco_job_lifecycle-0.2.0-py3-none-any.whl
| Download URL | samco_job_lifecycle-0.2.0-py3-none-any.whl |
|---|---|
| Size | 29.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
acc956f35dda65dc37db9eb5008f040888cf7e2df9b6594ae90c85cf749f8668
|
|
BLAKE2b-256 checksum How to use checksums |
33c6b36d4262a4f4083c12e6e810e19dfd1baaa657f2d30889dec9bd89e6e82f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.10.10
|