Skip to main content

Pydantic schemas bridging PureLMS (AGPL) and its simulation backends (MIT)

Project description

purelms-shared

Pydantic schemas that bridge PureLMS (AGPL-3.0-or-later) and the simulation backend containers (MIT, in purelms-interactive-tasks — formerly purelms-backends).

What's in here

  • purelms_shared.envelopesSimulationInputEnvelope and SimulationOutputEnvelope, the JSON contract for run input + output. Plus the helper types: InputFile, ResourceFile, OutputArtifact, Message, ExecutionContext.
  • purelms_shared.callbacksProgressCallback and CompleteCallback, the request bodies a backend POSTs to the worker during a run.
  • purelms_shared.evidenceEvidenceManifest and EvidenceReference, the immutable record of a completed run that credential issuance reads later.
  • purelms_shared.constantsRunStatus, OutputStatus, MessageLevel, BackendCallbackEvent, InputFileRole enums, plus the purelms.{input,output,evidence}.v1 schema-version literals.

What's not in here

  • No Django.
  • No business logic.
  • No dependencies beyond pydantic.

If you find yourself reaching for any of those while writing code here, you've probably wandered into the LMS's purelms.simulations Django app or a specific backend's runner — not this package.

Why it's MIT

PureLMS is AGPL-3.0-or-later (matching Validibot's Community Edition). Simulation backends are MIT so educator-contributors aren't burdened with copyleft for the per-backend image they ship.

The only artifact that crosses the AGPL/MIT boundary is this schema package — so it's MIT too. Neither side imports the other's code; they share a data contract.

Versioning

Schema versions are dotted-package strings, not integers:

  • purelms.input.v1 — input envelope shape v1
  • purelms.output.v1 — output envelope shape v1
  • purelms.evidence.v1 — evidence manifest shape v1

Additive changes (new optional field with a default) ship in v1. Incompatible changes bump to v2 with a new class living alongside the old one — running backends pin specific versions of this package, and we never break old envelopes.

"Additive within v1" is NOT backward compatible at the wire level. Pydantic's ConfigDict(extra="forbid") on every envelope class means a consumer pinning an older purelms-shared will REJECT messages from a newer producer that include the new field. Bump the package version and update every consumer's floor pin in lockstep — this was the discipline unit_block_id followed when it was added in 0.4.0.

Updating the wire format — lockstep consumer updates

When you add a field to an envelope or a value to a shared enum, update these consumer files in the SAME PR:

Python consumers (each pins a purelms-shared>=X.Y.Z floor):

  • purelms/pyproject.toml (and re-resolve the lockfile)
  • purelms-interactive-tasks/pyproject.toml
  • purelms-interactive-tasks/echo/backend/pyproject.toml
  • purelms-interactive-tasks/energyplus_single_zone/backend/pyproject.toml
  • purelms-interactive-tasks/_template/backend/pyproject.toml
  • Any other per-InteractiveTask backend/pyproject.toml

TypeScript consumers (vendor the wire types — extra="forbid" doesn't apply here, but unions out of sync silently break bundles branching on the missing case):

  • purelms/purelms/static/src/ts/sims/api/types.ts — the LMS-side dispatcher's authoritative TS types
  • purelms-interactive-tasks/echo/frontend/src/echo.ts — echo's vendored inline types
  • purelms-interactive-tasks/energyplus_single_zone/frontend/src/types.ts — EnergyPlus's vendored types
  • Any other per-InteractiveTask frontend/src/types.ts

The wire-format-sync tests in purelms/purelms/simulations/tests/test_wire_format_sync.py parametrize over each enum value and grep the LMS-side TS file for it — they fail loudly if a Python enum gains a value that doesn't appear in the matching TS union. Run them in CI on every push.

Why this matters: Slice 3d's post-shipping review round 2 caught a real drift — the TS RunStatus union was missing failed_simulation, failed_runtime, and timed_out entirely. Bundles branching on status would have silently failed to render any FAILED_* UI. The lockstep procedure above + the sync tests close that failure mode.

On extra="forbid" — why strictness over flexibility

Every envelope class in this package sets ConfigDict(extra="forbid", frozen=True). That's the choice driving the lockstep-update tax above: a consumer pinning purelms-shared<X will reject a message from a producer at >=X that includes a new field, even when the new field is purely additive and the consumer wouldn't have read it anyway.

We considered the looser alternative — extra="ignore" — and rejected it. The reasoning, recorded here so future contributors don't re-litigate:

  • Wire schemas are a credential surface, not just a transport. The output envelope is what evidence manifests and credentials are computed from. A field the producer thinks is meaningful but the consumer silently ignores is the classic shape of evidence drift. extra="forbid" turns "the consumer is one version behind" from a silent mis-credential into a loud rejection at deserialization time.
  • The lockstep cost is bounded and visible. Six pyproject.toml floor-pins plus a handful of TS type files — all enumerated in the section above, all enforced by the test_wire_format_sync.py parametrized tests. A grep-and-bump operation, not an open-ended migration. The cost is real but pays for itself the first time it catches a drift like Slice 3d round 2's RunStatus mismatch.
  • The alternative failure mode is worse. With extra="ignore", a backend author adding provenance_hash to the output envelope and an LMS one version behind would silently strip it on read. The credential issued from that run would lack the field. The bug surfaces months later in audit — if at all. Strict mode surfaces it on the first run.

The escape hatch. If the lockstep tax becomes painful in practice, the relaxation is mechanical: change extra="forbid" to extra="ignore" on the relevant envelope class (or all of them) and bump to v2. Old consumers keep working; new producers can add fields without coordinating updates. The decision is reversible — we'd lose the drift-catching property in exchange for looser coupling. We didn't bake the strictness into the wire format itself; it's a per-class Pydantic setting.

Three triggers that would prompt revisiting:

  1. A third external InteractiveTask author appears (i.e. someone outside the PureLMS core team writing a backend). At that point the shared schema + lockstep-floor-pin workflow becomes a contributor-onboarding tax worth paying for — likely by extracting the TS types into an npm-published @purelms/shared-types package so frontend bundles can npm install instead of vendoring.
  2. Backend authors complain that the lockstep update is blocking them — e.g. "I want to add simulation_seed to my outputs but I can't ship until the LMS bumps its purelms-shared floor." That's the signal the strictness cost has crossed the line from disciplined to obstructive.
  3. A second implementation language wants to write backends (Go, Rust, Node). At that point the right answer probably isn't "port Pydantic" but "generate JSON Schema from these classes and let each language consume it." The strictness invariant survives the language change; the implementation does not.

None of those triggers are tripped today. If they trip in the future, this section is the breadcrumb back to the decision.

Install

uv add purelms-shared
# or
pip install purelms-shared

Quick usage:

from purelms_shared import (
    SimulationInputEnvelope,
    SimulationOutputEnvelope,
    OutputStatus,
    Message,
    MessageLevel,
)

# Backends construct the output envelope at completion:
out = SimulationOutputEnvelope(
    run_id=run_id,
    status=OutputStatus.SUCCESS,
    outputs={"annual_eui_kbtu_ft2": 47.3},
    messages=[
        Message(level=MessageLevel.INFO, code="EPLUS.OK", text="Annual run complete"),
    ],
    runtime_seconds=23.4,
)

Architecture context

See the PureLMS simulation backend contract and simulation runtime protocol for the full surrounding design.

Legacy

purelms_shared.energyplus carries an older EnergyPlus-specific result schema used by the (soon-to-be-retired) Modal runtime in purelms-modal. It will be removed when that runtime is phased out per ADR-0002. New code should use the generic envelope schemas above.

License

MIT — see LICENSE.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

purelms_shared-0.4.0.tar.gz (29.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

purelms_shared-0.4.0-py3-none-any.whl (22.4 kB view details)

Uploaded Python 3

File details

Details for the file purelms_shared-0.4.0.tar.gz.

File metadata

  • Download URL: purelms_shared-0.4.0.tar.gz
  • Upload date:
  • Size: 29.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.13

File hashes

Hashes for purelms_shared-0.4.0.tar.gz
Algorithm Hash digest
SHA256 cfbb6deb31272e563f3f07b28c98f2415f575fbabd52a8914844f5d5736c6f2c
MD5 15c31a23aece899583e56ef3c9ba4c1a
BLAKE2b-256 1f7f9346e766c1ffe24a35a527c65c70cd4f0150a5b7cb6be381cc954dbf3c77

See more details on using hashes here.

Provenance

The following attestation bundles were made for purelms_shared-0.4.0.tar.gz:

Publisher: publish.yml on danielmcquillen/purelms-shared

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file purelms_shared-0.4.0-py3-none-any.whl.

File metadata

  • Download URL: purelms_shared-0.4.0-py3-none-any.whl
  • Upload date:
  • Size: 22.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.13

File hashes

Hashes for purelms_shared-0.4.0-py3-none-any.whl
Algorithm Hash digest
SHA256 878be9e83d5b25eff01938ab7d1c2566c0581346c24c49a10570bf814121425f
MD5 13b633999288da69dcbedd1f96ee0e31
BLAKE2b-256 1fb629d6b5577f4177eeceb8bd12ac956a6a478d8d18a389617d144a170b62da

See more details on using hashes here.

Provenance

The following attestation bundles were made for purelms_shared-0.4.0-py3-none-any.whl:

Publisher: publish.yml on danielmcquillen/purelms-shared

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page