Typed Pydantic v2 models for OSDU data payloads — an opt-in companion to
osdu-python-client,
mirroring the C# osdu-csharp-schemas
library.
Scope. Covers all
work-product-component,master-dataanddatasetentity types in the pinned OSDU snapshot — 194 entity types across 551 schema versions (93 work-product-component + 73 master-data + 28 dataset), plus 128 sharedabstractmodules pulled in on demand. The generator is data-driven: the scope is theSCOPE_GROUPSlist intools/generate.py, and every type and version is discovered from the snapshot automatically, so a snapshot bump or adding a group needs no other code change.abstractschemas are not listed explicitly — only those reachable from a selected entity's$refclosure are generated.
osdu-python-client leaves the record data block free-form (a dict), matching
the canonical os-core-common Map<String, Object> — the right call, because OSDU
data is schema-on-read and versioned by kind. But consumers building ingestion
or validation logic for a specific kind + version still want types: autocomplete,
runtime validation, and self-documenting payloads. This package provides exactly
that, without touching the client.
from osdu_models.workproductcomponent.well_log.v1_5_0 import Data, Curve
# Typed authoring — autocomplete + validation
data = Data(
WellboreID="namespace:master-data--Wellbore:abc:",
TopMeasuredDepth=1234.5,
Curves=[Curve(Mnemonic="GR")],
)
# Bridge into the client's free-form `data` — just model_dump(), no client changes
record = {"kind": "osdu:wks:work-product-component--WellLog:1.5.0",
"acl": ..., "legal": ...,
"data": data.model_dump(by_alias=True, exclude_none=True)}Reading is the mirror image: Data.model_validate(record["data"]).
pip install osdu-python-modelsRequires Python 3.10–3.14.
or with uv:
uv add osdu-python-modelsModels are shipped pre-generated in the published distributions — no codegen step needed to consume them. To build from source instead, see Build & test.
Each tagged version is also attached to its GitHub Release as
.whl/.tar.gzassets.
- Types only the
datablock. The client owns the envelope (id/kind/acl/legal); this package ownsdata. The two compose viamodel_dump()/model_validate(). - Side-by-side versions. Each published version is its own module
(
well_log/v1_4_0.py,v1_5_0.py) — no "latest wins", explicit per-kind. - Shared abstract modules. OSDU
abstract/*building blocks are generated once underosdu_models/abstract/<type>/v<ver>.py; each entity model imports them rather than inlining a private copy. This removes the ~96 % class duplication of a per-entity bundling approach (551 models: ~65,000 → 2,071 class defs) and is what makes scaling to the full schema set viable. Same idea as the C# library'sExternalReferenceCodeabstract sharing. - String-only constraints stripped off non-string nodes. A few OSDU schemas
attach
pattern/formattoarray/integerfields (e.g.AbstractColumnBasedTable.IntegerColumn); the generator drops them so Pydantic v2 doesn't reject otherwise-valid payloads. extra='allow'everywhere. Unknown / forward-compatible fields round-trip untouched (the Pydantic equivalent of C#'s[JsonExtensionData]).- String
formatdropped → plainstr. This library typesdatafor lossless round-tripping, not semantic validation. Honouringformatwould make codegen emit validating/normalising types that defeat that or pull optional deps:date/date-time/time(OSDU payloads carry non-conformant variants a strict parser rejects),email(EmailStr, needsemail-validator),uri(AnyUrl, normalises the value). Keeping plainstrpreserves the input. Same pragmatic choice the C# library makes. - Pinned snapshot.
schemas/2026.05.22/is a frozen copy of the OSDUdata-definitionsGenerated/schemas (shared with the C# library). Bumping it is an explicit, reviewable change.
osdu-python-models/
├── schemas/2026.05.22/ # pinned data-definitions snapshot (abstract + entities)
├── tools/generate.py # restructures the `data` sub-schemas, runs datamodel-codegen
├── src/osdu_models/ # generated Pydantic models (gitignored, regenerable)
│ ├── abstract/<type>/v<ver>.py # shared abstract building blocks (generated once)
│ ├── workproductcomponent/<type>/v<ver>.py
│ ├── masterdata/<type>/v<ver>.py # → class Data, importing the shared abstracts
│ └── dataset/<type>/v<ver>.py # e.g. dataset/file_generic/v1_1_0.py
├── tests/test_roundtrip.py # round-trip + typed-access tests vs real OSDU examples (all versions)
└── samples/author_welllog.py # end-to-end authoring demo (no network)
uv venv && uv pip install -e ".[dev]"
uv run python tools/generate.py # generate models from the pinned snapshot
uv run ruff check tools tests samples # lint hand-written sources (not generated)
uv run pytest # round-trip tests against OSDU example payloads
uv run python samples/author_welllog.pyOSDU record schemas put the payload under properties.data as an allOf of
abstract building blocks (../abstract/*.json) plus inline fields. tools/generate.py
uses datamodel-codegen directory mode, which turns cross-file $refs into
Python imports:
- lifts each entity's
datasub-schema (titledData) and transitively collects the sharedabstract/*files it references, - lays both out in a temp tree mirroring the output package structure (every
file at the same depth,
<pkg>/<snake>/v<ver>.json), cleaning schema quirks and rewriting every$refto a canonical root-relative path (../../<pkg>/ <snake>/v<ver>.json) so it resolves identically regardless of which file the resolver treats as the base — sidestepping adatamodel-codegenquirk that otherwise resolves a shared schema's nested$refs against the referencing entity's directory, - runs
datamodel-codegenonce over that tree — emitting each abstract as a shared module and each entity as aDatamodel that imports them.
Generated code is gitignored — regenerable from the pinned snapshot, never hand-edited.
Contributions are welcome — see CONTRIBUTING.md for
development setup, the pull-request process, and commit conventions.
To report a security vulnerability, follow the process in
SECURITY.md. Do not open a public issue.
Licensed under the Apache License 2.0.