Skip to content

Add kumo.job() for scheduled job scripts - #70

Closed
Vedaant-J wants to merge 123 commits into
vedaant/mlops-mlflow-modelfrom
vedaant/mlops-cli
Closed

Vedaant-J wants to merge 123 commits into
vedaant/mlops-mlflow-modelfrom
vedaant/mlops-cli

Conversation

@Vedaant-J

@Vedaant-J Vedaant-J commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Series: kumo-engine MLOps series: every snapshot, training table, model bundle and prediction run records where it came from; snapshots and models move between machines and registries; fits and prediction runs are tracked in MLflow or Snowflake; plain Python job scripts run on a scheduler through a small job helper; prediction runs are monitored for drift and realized performance.
Stack: Scheduling: #70 → #71
Merge order: one linear stack; merge bottom to top (#53 first)
This PR: Adds kumo.job(), a context manager that turns a plain Python script into a scheduled job: one JSON result line on stdout however the script ends, a 0 / non-zero exit code with the failure's retry class in the summary, and the scheduler's job ids recorded in every manifest and tracked run the script writes.

Summary

This PR adds kumo.job(), a context manager that turns a plain Python script into a scheduled job: one JSON result line on stdout however the script ends, a 0 / non-zero exit code with the failure's retry class in the summary, and the scheduler's job ids recorded in every manifest and tracked run the script writes.

Problem. Teams putting Kumo on a scheduler (Databricks Jobs, SageMaker, Snowflake ML Jobs, Airflow) need answers like these:

  • Did last night's run fail because a source was briefly unavailable (retry), or because a table is missing (page someone)?
  • Which bundle, snapshot and outputs did it produce, without scraping logs?
  • How do the scheduler's job and run ids end up in the snapshot, training-table and bundle records and on the tracked run?

Cause. A job script uses the normal Python API. Progress lines, Lightning output, native-library output and warnings all share stdout, so a scheduler cannot tell results from log noise. A failure is a traceback with exit code 1, whatever its cause. Scheduler ids reach the manifests only if the script exports KUMO_JOB_ID/KUMO_JOB_RUN_ID itself.

Change.

from kumo.engine import PredictiveQuery, Trainer, job

# graph and plan come from your job script.
with job() as run:                                  # or job(job_id=..., job_run_id=...)
    snapshot = graph.snapshot()
    query = PredictiveQuery(graph=graph, query="PREDICT users.churned FOR EACH users.user_id")
    result = Trainer(plan).fit(query.generate_training_table(snapshot), snapshot=snapshot)
    run.record(bundle_id=result.bundle_id, snapshot_id=snapshot.id,
               outputs={"bundle": str(result.bundle_dir)})
  • job(job_id=None, job_run_id=None) sets KUMO_JOB_ID/KUMO_JOB_RUN_ID when given; otherwise the existing variables or the detected platform (Record provenance for every artifact under one MLOps artifact root #50's scheduler context) apply. Every manifest the body writes (snapshot, training table, provenance.json) and every tracked fit run (kumo.scheduler, kumo.job_id, kumo.job_run_id, Log, register and serve fitted models through MLflow pyfunc #65) carries them.
  • Logging goes to stderr (logging.basicConfig(level=INFO, stream=stderr)).
  • run.record(bundle_id=, snapshot_id=, run_id=, outputs=) adds to the summary; None leaves a field as it was.
  • On exit, however the body ends (success, exception, Ctrl-C, sys.exit(), SIGTERM), the environment and warnings.showwarning are restored and one JSON line goes to stdout (both in a finally); then a failed body exits the process.
  • Warnings still reach the previous showwarning. When another library wraps kumo.job()'s handler after it is installed (MLflow autologging's warnings controller does this inside a Databricks trial), the re-entered call prints the warning directly instead of recursing; the test simulates that wrapper.
How the body ends Exit code error.kind
succeeded 0 none
PermanentSourceError in the cause chain (e.g. a missing table or no permission) 1 permanent: do not retry
TransientSourceError, ConnectionError or TimeoutError in the cause chain 1 transient: worth a retry
any other exception 1 unknown
SIGTERM 143 unknown
Ctrl-C re-raised (130 in the summary) unknown
sys.exit(n) in the body n passes through when n is non-zero

Databricks, SageMaker and Snowflake retry policies only see zero vs non-zero, so the retry class lives in the summary line rather than in the exit code.

Real stdout of a job script that builds and exports a snapshot of a synthetic users table, then of the same script failing on a missing table, with KUMO_JOB_ID=job-7 KUMO_JOB_RUN_ID=run-7 (hashes shortened; stderr held the logs):

$ python job.py ok; echo exit=$?
{"event": "kumo.engine.job", "status": "succeeded", "exit_code": 0, "scheduler": {"platform": "other", "job_id": "job-7", "job_run_id": "run-7"}, "bundle_id": null, "snapshot_id": "190b5d6c…@db819f3c5a154468", "run_id": null, "outputs": {"snapshot": "<store>/snapshots/190b5d6c…/db819f3c5a154468"}, "warnings": []}
exit=0

$ python job.py missing; echo exit=$?
{"event": "kumo.engine.job", "status": "failed", "exit_code": 1, "scheduler": {"platform": "other", "job_id": "job-7", "job_run_id": "run-7"}, "bundle_id": null, "snapshot_id": null, "run_id": null, "outputs": {}, "warnings": [], "error": {"type": "SourceNotFoundError", "kind": "permanent", "message": "no table users in the source"}}
exit=1

Summary fields: event (always kumo.engine.job), status, exit_code, scheduler (platform, job_id, job_run_id, or null when there is none), bundle_id, snapshot_id, run_id, outputs (what the body recorded; record full URIs or table names, since a scheduler cannot resolve connector-relative paths), warnings (the first 20 distinct warnings raised in the body, 300 characters each; deprecation and future warnings, mostly from third-party libraries, are left out and still go to stderr), and on failure error (type, kind, message cut to 500 characters; the traceback goes to stderr).

This replaces the kumo-engine console script and its JSON config that earlier revisions of this PR proposed: a job script already expresses graphs, connectors, tracking and registry options in the normal API, so a second config schema was only another surface to keep in sync. The job templates in #71 are plain Python scripts under kumo.job().

Back-compat

New API only.

Situation Behaviour
Scripts that do not use job() Unchanged.
KUMO_JOB_ID/KUMO_JOB_RUN_ID already set Kept unless job() is given ids.
Body raises One summary line, traceback on stderr, SystemExit(1).
Ctrl-C or sys.exit() in the body The summary line is still printed and the environment restored; the exception propagates unchanged.
job() entered off the main thread Works; SIGTERM is not intercepted there.
Previous SIGTERM handler and warnings.showwarning Restored on exit.

Known limitations

  • Inside a Databricks notebook job task the job and run ids are not visible to the process; pass them with job(job_id=..., job_run_id=...) from task parameters, or only platform: databricks is recorded.

Testing Done

  • test_job.py (CPU): the summary line with recorded ids, outputs and the scheduler context, and the same scheduler block in a manifest written inside the body; warnings deduplicated, capped at 20 and 300 characters, deprecations left out; each failure class gives exit 1 with error.kind transient (in the cause chain, connection reset), permanent (missing source) or unknown, and a long message cut to 500 characters; Ctrl-C, sys.exit(3) and sys.exit(0) still print the summary with the right code and restore the job-id environment; SIGTERM prints the summary, exits 143 and restores the previous handler.
  • Re-run on the current head: test_job.py + test_optional_imports.py: 56 passed. flake8 and isort clean.
  • The output above is from an earlier head, with the exit code and error.kind updated to what this head prints.

Checklist

  • No breaking changes (new API).
  • README updated ("Scheduled jobs").
  • Unit tests added.

🤖 Generated with Claude Code

@Vedaant-J

Vedaant-J commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor Author

Change map

File Function / scope What changed Why
packages/kumo-engine/README.md "Scheduled jobs" new section: kumo.job(), summary fields, 0/1/143 exit codes and error.kind, warning cap, where scheduler ids are recorded user docs
packages/kumo-engine/kumo/engine/__init__.py exports job, JobRun public API
packages/kumo-engine/kumo/engine/_job.py new: job, JobRun.record, _print_summary, _exit_code, _error_kind, Terminated, _sigterm_raises sets KUMO_JOB_ID/KUMO_JOB_RUN_ID, logs to stderr, collects up to 20 distinct non-deprecation warnings (300 chars each); in finally restores the environment and prints one JSON summary line however the body ends; exits 0 / 1 / 143 (SIGTERM), a body's own sys.exit code passes through; error.kind transient/permanent/unknown from the cause chain schedulers get a result line on every exit and a retry hint
packages/kumo-engine/test/test_job.py new tests summary with ids, outputs, capped warnings and scheduler (also in a manifest); each error kind; Ctrl-C and sys.exit still print the summary; SIGTERM coverage

Requirement: a scheduled script gives the scheduler a machine-readable result on every exit, a 0/non-zero exit code and a retry hint, and its job ids reach every record. Existing: the Python API, #50's scheduler context read from KUMO_JOB_*, #65's scheduler tags on tracked runs. Smallest: one context manager over the existing API, no config schema. Stopping condition: success, each failure class, Ctrl-C and sys.exit produce one summary line and the right exit code; ids appear in manifests.

@copy-pr-bot

copy-pr-bot Bot commented Oct 21, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@Vedaant-J
Vedaant-J temporarily deployed to gha-kumo-workflow-tests October 21, 2026 01:00 — with GitHub Actions Inactive
@coderabbitai

coderabbitai Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

@Vedaant-J
Vedaant-J force-pushed the vedaant/mlops-mlflow-model branch from 63da7fe to 1bea937 Compare October 21, 2026 01:56
@Vedaant-J
Vedaant-J temporarily deployed to gha-kumo-workflow-tests October 21, 2026 01:56 — with GitHub Actions Inactive
@Vedaant-J
Vedaant-J force-pushed the vedaant/mlops-mlflow-model branch from 1bea937 to eae8b9a Compare October 21, 2026 07:18
@Vedaant-J
Vedaant-J temporarily deployed to gha-kumo-workflow-tests October 21, 2026 07:18 — with GitHub Actions Inactive
@Vedaant-J
Vedaant-J force-pushed the vedaant/mlops-mlflow-model branch from eae8b9a to 71256af Compare October 21, 2026 07:57
@Vedaant-J
Vedaant-J temporarily deployed to gha-kumo-workflow-tests October 21, 2026 07:57 — with GitHub Actions Inactive
@Vedaant-J
Vedaant-J force-pushed the vedaant/mlops-mlflow-model branch from 71256af to 1119fef Compare October 21, 2026 08:58
@Vedaant-J
Vedaant-J temporarily deployed to gha-kumo-workflow-tests October 21, 2026 08:59 — with GitHub Actions Inactive
@Vedaant-J
Vedaant-J force-pushed the vedaant/mlops-mlflow-model branch from 1119fef to 2bee8ea Compare October 21, 2026 09:01
@Vedaant-J
Vedaant-J temporarily deployed to gha-kumo-workflow-tests October 21, 2026 09:02 — with GitHub Actions Inactive
@Vedaant-J
Vedaant-J force-pushed the vedaant/mlops-mlflow-model branch from 2bee8ea to db65780 Compare October 21, 2026 10:06
@Vedaant-J
Vedaant-J temporarily deployed to gha-kumo-workflow-tests October 21, 2026 10:06 — with GitHub Actions Inactive
@Vedaant-J
Vedaant-J force-pushed the vedaant/mlops-mlflow-model branch from db65780 to 8ce09a0 Compare October 21, 2026 16:10
@Vedaant-J
Vedaant-J temporarily deployed to gha-kumo-workflow-tests October 21, 2026 16:10 — with GitHub Actions Inactive
@Vedaant-J
Vedaant-J force-pushed the vedaant/mlops-mlflow-model branch from 8ce09a0 to da0ca24 Compare October 21, 2026 16:38
@Vedaant-J
Vedaant-J had a problem deploying to gha-kumo-workflow-tests October 21, 2026 16:41 — with GitHub Actions Error
@Vedaant-J
Vedaant-J temporarily deployed to gha-kumo-workflow-tests October 21, 2026 16:41 — with GitHub Actions Inactive
Vedaant-J and others added 23 commits October 22, 2026 18:51
fit_inputs parsed the snapshot and training-table manifest files by hand.
It now reads them with read_manifest as SnapshotManifest and
TrainingTableManifest, so an unreadable snapshot manifest no longer drops
a custom training table's source.

Verified with test_mlflow_tracking.py.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Rebuilt on the reworked tracking session (one tracker, no Capabilities,
no per-trial Lightning logger); the earlier ten commits are squashed.

SnowflakeTracking(session, experiment_name=...) logs the fit as one
Snowflake run: model-plan params, kumo.* tags as params, trial attempts
as trial-<n>-attempt-<m>/ params and per-epoch metrics, final metrics,
query.pql / model_plan.json / provenance.json artifacts, and FAILED on a
failed fit. A taken run_name gets a suffix instead of resuming a run; an
ExperimentTracking already in use in the process is left alone;
native_model_lineage keeps the run open as a lineage context.

Review changes: the adapter no longer runs ALTER SESSION on the user's
Snowpark session (snowflake-ml-python takes no per-statement parameters,
and a stuck call already cannot hold the fit past the 30 s drain), and
snowflake-ml-python is capped below 3 because the adapter reads
ExperimentTracking internals (checked present in 2.0.0 and 2.6.0).

Verified with test_snowflake_tracking (incl. no statements issued on the
session), test_tracking_contract for MLflow and Snowflake,
test_mlflow_tracking and test_optional_imports; uv lock --check passes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Job scripts read the tracker's run_id for their summary line, but
SnowflakeTracking had none, so scheduled fits reported run_id null.

SnowflakeTracking.run_id is the latest fit run's name (Snowflake
identifies runs by name), set when the run starts and kept after the
fit returns.

Verified: test_snowflake_tracking checks run_id is the (suffixed) run
name; 20 passed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A binary threshold is chosen after looking at the holdout, but a bundle
had nowhere to keep it, so every scheduled predict had to pass it again
and a fitted or loaded trainer could only be published from fit().

ScoringDefaults(binary_classification_threshold, num_classes_to_return)
is exported from kumo.engine. Trainer.with_scoring_defaults validates the
defaults against the bundle's task type, copies the bundle (hardlinking
unchanged files) to a new scratch directory, writes scoring-defaults.json
(role scoring_defaults, part of the bundle_id; empty defaults remove it via
a new kumo-core remove_bundle_file), records parents.derived_from =
<old bundle_id> in provenance.json (a reconstructed record when the parent
has none) and returns a trainer loaded from the copy. Trainer.
scoring_defaults exposes them. predict uses the threshold default when the
argument is omitted and the count default only when the argument is
omitted and the query has no TOP k. A loaded bundle reads the file only
when the manifest lists it and warns and ignores it when unreadable or
invalid for the task, so older bundles predict as before. Trainer.export
publishes a fitted or loaded trainer through the publish step fit() used.

Verified with new tests for default vs explicit thresholds, a multilabel
count default vs explicit count and TOP k, parent bundle untouched with
its id, child id and provenance link, empty defaults restoring the
no-defaults id, a parent without provenance, export and connector reload,
invalid defaults, and bundles with a stray, unreadable or invalid defaults
file; plus remove_bundle_file and the registry, provenance and
output-column suites.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: with_scoring_defaults refused a threshold default for
multilabel classification although predict accepts one there, wrote 1
and 1.0 as different bytes (so different bundle ids) and rejected numpy
scalars, and replaced an unreadable or newer provenance.json with a
reconstructed record.

Change: defaults are validated and normalised (threshold float, class
count int) before the bundle is copied; the threshold applies to binary
and multilabel classification. record_derivation leaves a listed but
unreadable provenance.json untouched and warns. README documents
with_scoring_defaults and Trainer.export. The kumo-core
remove_bundle_file unit test is dropped; the empty-defaults bundle id
test covers it.

Verification: test_scoring_defaults.py, test_trainer_registry.py and
test_bundle_provenance.py (47) and kumo-core test_artifacts.py (10) pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Three readers each re-parsed the publication manifest to find one
role's file, the tests split one derived-bundle scenario over several
tests and ran a second real fit, and the README repeated the
docstrings.

Add one listed_file helper for provenance and scoring-defaults reads,
merge the derived-bundle, provenance-fallback and back-compat tests,
drop the second real fit and the private validator matrix, and cut
README and docstrings to usage and limits. Behaviour is unchanged.

Verified: test_scoring_defaults, test_bundle_provenance,
test_trainer_registry and kumo-core test_artifacts pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The bundle's task type was read from prediction-schema.json in three
places (scoring defaults, the trainer's warm-start check, kumo-core's
warm-start resolver), and scoring-defaults.json had its own JSON writer.
Add kumo_core.artifacts.bundle_task_type next to the bundle layout and
use it everywhere; write the defaults with the engine manifest writer.

Verified: test_scoring_defaults, test_bundle_provenance, test_warm_start,
kumo-core test_artifacts and warm-start tests pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
with_scoring_defaults copied the parent's provenance.json into the
derived bundle, so its id named the parent. Set it to the derived
bundle's own id (computed from the publication manifest, which excludes
provenance.json).

Verified with test_scoring_defaults.py and test_bundle_provenance.py.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A derived bundle's parent now goes in BundleParents.derived_from, and a
derived bundle whose parent had no provenance gets a reconstructed
BundleProvenance that still writes only the fields it knows. Bundle
fields absent from such a record are optional. write_manifest also
takes the plain dict scoring-defaults.json is written from.

Verified with test_scoring_defaults.py and test_bundle_provenance.py.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Unity Catalog Files API lists directories as '.../1/model/', so the
immediate-child check in _download rejected every real model bundle and
Trainer.load_from_connector(DatabricksConnector, location=...) raised
'Invalid model artifact path returned by Databricks.'

Strip the trailing slash before the check and the recursive download.
The fake Files API in the registry tests now lists directories the same
way, so every fetch test covers it.

Verified: kumo-databricks test_registry and test_uc_registry pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A version published to Snowflake or Databricks could not be loaded by a
stable name, there was no way to promote one, and nothing on the version
said which bundle it held.

ModelRegistryLocation gains alias (exclusive with version): an export
publishes, then points the alias at the new version (a failed alias step
warns and returns the published model); a fetch loads the version the
alias points at. kumo.engine.set_model_alias promotes explicitly through
a new ModelAliasingSink protocol. Each version records the kumo_engine
marker and kumo_bundle_id only (Snowflake version metrics, which keep the
finite training metrics; Databricks version tags); provenance.json in the
bundle holds the lineage. list_models and fetch_model return
kumo_bundle_id, and load_from_connector warns when the fetched bundle
differs from it. kumoapi.plugin shares the marker, check_model_alias and
registry_lineage between the registries. Snowflake fetch accepts
DEFAULT/FIRST/LAST; Trainer.export of a derived bundle publishes the
child's bundle id.

Verified: kumo-api test_plugin_registry, kumo-snowflake and
kumo-databricks registry tests, test_trainer_registry and
test_scoring_defaults pass. No live Snowflake or Databricks calls.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A Snowflake alias move unset the alias on the old version before setting
it on the new one, so a failure in between left no version holding it.
Restore it on the old version and log a warning. Export aliases were only
checked after publishing; check them in fit()/export() before any work,
with one syntax every registry accepts (letters, digits, underscores;
DEFAULT/FIRST/LAST/LATEST/v<digits> reserved).

Snowflake versions no longer record every training metric: registry
versions carry only the kumo_engine marker and kumo_bundle_id on both
Snowflake and Databricks; metrics stay in the bundle and the tracked run.

Verified with the registry, plugin and trainer registry tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An MLflow model signature needs the columns a scored frame carries for
each task type, and a served model needs a connector for a snapshot URI.
format_prediction kept its long-format task set inline, and nothing
mapped an s3://, gs://, file:// or local root to a connector.

kumo_core.train.formatting.prediction_output_columns(task_type) returns
the prediction columns after the entity column, sharing the task sets
format_prediction uses; kumo.engine.connector.connector_for_root(uri)
builds the S3, GCS or file connector for a root.

Verified: kumo-core test_prediction_output_columns and kumo-engine
test_connector_for_root pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A fitted model could be published to a connector registry, but not to
MLflow, where Databricks Unity Catalog and SageMaker managed MLflow
expect models; and a tracked fit run did not say which bundle it made
or which scheduler job ran it.

kumo.engine.integrations.mlflow gains log_model(trainer, snapshot=...)
(a pyfunc model wrapping the bundle, scored with params={'snapshot':
uri} or a serving_snapshot loaded once per replica at startup; metadata
kumo_bundle_id only; provenance.json as an artifact; pip requirement
kumo-engine[s3,gcs]==<version>) and load_model(uri). MlflowTracking(
log_model=, registered_model_name=, aliases=, serving_snapshot=) logs,
registers and aliases the fit's model on the fit run through an
optional log_model(result, snapshot) tracker member. Trainer.fit runs
it on the tracking thread and waits for it without the 30 s finish
limit, so the model is registered (or the run ends FAILED with a
warning) before fit returns.

Fit runs gain the pointer tags kumo.bundle_id and, under a scheduler,
kumo.scheduler, kumo.job_id and kumo.job_run_id; a tracked fit's
provenance.json records "tracking": {"run_id": <kumo run id>}.

Verified: test_mlflow_model, test_mlflow_tracking, test_tracking_contract,
test_snowflake_tracking, test_bundle_provenance, test_trainer_registry,
test_scoring_defaults, test_fit_events and test_optional_imports pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The pyfunc model's predict deleted every directory that appeared under
the shared prediction_tables/ and the trainer's runs/ during the call,
so concurrent calls (other threads, or other serving processes on the
same disk) lost directories they were still using; and it never removed
<trainer>/predict_output/<run>, which holds the scored rows. The README
said kumo-engine needs MLflow 3.11 while the extra said 3.0.

- kumo.engine._scratch gains scoped_scratch_dir(path): a context
  variable that get_scratch_dir() reads before the configured root.
- predict runs inside a TemporaryDirectory under the trainer's scratch
  dir with that scope, so everything the call creates (prediction
  table, run dir, predict output) is removed with it, on success or
  error, and nothing else is touched.
- README: the mlflow extra needs MLflow 3.13 or later (DiskGraph pins
  pyarrow 24; older MLflow releases exclude it), stated once.

Verified: test_mlflow_model passes; its predict test now also checks a
directory another call creates meanwhile survives and the trainer's
scratch dir is left empty (fails on the previous cleanup).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fit waited for the tracker's model logging with no limit, so a stuck
upload or a dead tracking thread hung the fit; and the fit's 30 s MLflow
HTTP timeout also applied to the model upload.

- TrackingSession.log_model waits while the tracking thread is alive, up
  to MODEL_SECONDS (1 h). If the thread stops or the hour passes it
  warns, ends the tracker's run as failed from the fit thread and
  returns the error; fit still returns its result.
- MlflowTracking.log_model raises MLFLOW_HTTP_REQUEST_TIMEOUT to 600 s
  for the upload only, and only when the fit set the 30 s limit (the
  user had not set it), then puts the limit back. A model whose upload
  finishes after its run was ended is not registered or aliased
  (warning).

Verified: test_mlflow_model (slow upload and dead thread both return
within the bound and end the run; the upload sees 600 s, the run 30 s,
nothing is left set and a late upload is not registered),
test_mlflow_tracking, test_tracking_contract and test_snowflake_tracking
pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
log_model's dataset inputs now come from BundleProvenance's source tables
and training table record, and the scheduler tags from SchedulerContext.
The tracked run id is BundleProvenance.tracking (a TrackingRef); the
provenance.json an MLflow model carries is unchanged.

Verified with test_mlflow_model.py and test_bundle_provenance.py.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A scheduler runs a Python script and needs two things back: one
machine-readable result line, and an exit code that says whether a retry
can help. A plain kumo-engine script prints logs, progress and warnings
to stdout and exits 1 for every failure, and its scheduler job ids reach
no manifest unless the script exports them.

kumo.job() is a context manager for the script body. It records
job_id/job_run_id (else KUMO_JOB_ID/KUMO_JOB_RUN_ID or the detected
platform) so manifests and tracked runs carry them, sends logs to
stderr, and on exit prints one JSON line to stdout: status, exit_code,
the scheduler context, the ids and outputs passed to run.record(),
warnings raised, and error on failure. A failed body exits 10 (a source
refused the request), 20 (transient source, connection or timeout
error), 30 (anything else) or 143 (SIGTERM).

Verified: test_job covers the summary line, every exit-code class,
SIGTERM, and the scheduler ids reaching a manifest.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
kumo.job() restored the environment and printed its summary only after
an Exception or SIGTERM, so Ctrl-C or sys.exit() inside the body left no
summary line and the job ids set in the environment. Do both in finally,
for every way the body ends.

Exit codes are now 0 on success, 1 on failure, 143 on SIGTERM (a body's
own sys.exit code passes through). The transient/permanent split moves
from the 10/20/30 codes to the summary's error.kind.

Verified with test_job.py.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The summary line collected every Python warning in full, so a run that
hit pandas FutureWarnings printed kilobytes of repeated array dumps in
one line. Keep the first 20 distinct messages, 300 characters each.

Verified with test_job.py.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Third-party DeprecationWarning and FutureWarning messages (optuna,
lightning, pandas) filled the summary's warnings. Record only the other
categories; deprecations still reach stderr.

Verified with test_job.py.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
scheduler_context() now returns a SchedulerContext, so the job summary
line writes its dict; the printed JSON is unchanged.

Verified with test_job.py.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Vedaant-J
Vedaant-J force-pushed the vedaant/mlops-mlflow-model branch from fdf8531 to 5255ad9 Compare October 22, 2026 19:16
@Vedaant-J
Vedaant-J temporarily deployed to gha-kumo-workflow-tests October 22, 2026 19:16 — with GitHub Actions Inactive
@Vedaant-J
Vedaant-J force-pushed the vedaant/mlops-mlflow-model branch 2 times, most recently from 09c3701 to 19982c1 Compare October 24, 2026 18:38
@Vedaant-J

Copy link
Copy Markdown
Contributor Author

Closing this PR: its pieces either moved or were dropped.

@Vedaant-J Vedaant-J closed this Oct 24, 2026

This branch was previously deployed

1 inactive deployment
gha-kumo-workflow-tests — 2c180514 Deployed Oct 22, 2026 by Vedaant-J via import #631
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant