Skip to main content

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)

Source distribution for samco-job-lifecycle 0.2.0
File Size Uploaded
samco_job_lifecycle-0.2.0.tar.gz 1.5 MB Details

Built distribution (wheel)

Table of built distributions (wheels) for samco-job-lifecycle 0.2.0
File Interpreter ABI Platform
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

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 release files

0.1.1

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page