v0.2.0rc1
Pre-release[0.2.0] - 2026-05-20
An rc1 - testing release note. Install for soak with: pip install --pre "odmlib==0.2.0rc1".
Added
Context Manager write_on_exit Opt-Out
odmlib/context.py: addedwrite_on_exit: bool = Trueparameter to
ODMContext,DefineContext,open_odm, andopen_define. Passing
write_on_exit=Falsesuppresses the auto-save on clean exit, enabling
read-only inspection through the context managers without modifying or
creating any file. The default (True) preserves the documented
in-place save behaviour ? additive change, no compat impact.tests/test_context_managers.py: 10 new tests covering the opt-out
(XML, JSON,open_odm,open_define, and the input-preservation
regression guard for the default-output-file footgun) plus explicit
default-still-writes guards.
ODM v2.0 Model/XSD Alignment (safe subset)
odmlib/data/valuesets.json: added 12 missingodm_2_0value-set keys
(ReturnValue.DataType,ItemRef.Core/Repeat/Other/IsNonStandard/HasNoData,
CodeListItem.Other,Telecom.TelecomType,ItemGroupDef.IsNonStandard/HasNoData,
CodeList.IsNonStandard,ODM.Context) bound to the ODM 2.0
ODM-enumerations.xsdvalue lists.tests/test_odm_2_0_model.py: new construction + XML/JSON round-trip suite
for the major ODM 2.0 classes.tests/test_odm_2_0_known_gaps.py: new strict-xfailmarkers pinning the
five deferred structural ODM 2.0 model/XSD gaps so CI documents the known
state and fails loudly if a gap is silently fixed or regressed. (Its
ItemDef test is now a passing regression guard ? see Changed below.)
Changed
-
ODM 2.0
TranslatedText.Typeis now required (odmlib/odm_2_0/model.py),
matching the XSD (use="required", free-text media type).ODMBuilder
now defaultsType="text/plain"for the ODM 2.0 model shape only via a new
_translated_text()helper. -
odmlib/valueset.py:ValueSet.value_set()no longer raises for an
unknown attribute ? it returns the newValueSet.UNKNOWN_ATTRIBUTE
sentinel sovalidate()returnsFalseand theSKIP_VALUESETpermissive
guard can bypass an unregistered value set. Unknown version still raises.
Behavioral note: in strict mode an unregistered value-set attribute now
raisesOdmlibTypeError(fromValidValues.__set__) instead of the former
OdmlibValidationError(fromvalue_set()). -
ODM v2.0
ItemDefaligned with the ODM 2.0 XSD
(odmlib/odm_2_0/model.py). Removed the XSD-rejected attributes
FractionDigits,DatasetVarName, andSDSVarName; added the
XSD-defined optional attributesDisplayFormatandVariableSet. Code
that set the removed attributes on anodm_2_0ItemDefshould migrate ?
those values were schema-invalid and are no longer serialized. (Closes
the ROADMAP v0.2.1 ItemDef gap /ODM20-MODEL-XSD-DIFFERENCES_PLAN.md
�3.7; the XSDItemDef/ValueListRefchild element remains deferred.)
Known Limitations
- ODM v2.0 structural model/XSD gaps deferred to v0.2.1. Five features
produce schema-invalid output if used undermodel_package="odm_2_0":
ConditionDef(no requiredMethodSignature), text-based
FormalExpression,Protocol.StudyEventRef(removed in the 2.0 schema),
MetaDataVersion.StudyTimingplacement, andStudyEventGroupDef(missing
required child group). The affectedODMBuilderhelpers carry docstring
caveats. See ROADMAP "v0.2.1 ? ODM v2.0 Model/XSD Alignment" and
ODM20-MODEL-XSD-DIFFERENCES_PLAN.md.
ODM v2.0 XSD Schema Validation
odmlib/schema_manager.py: registered("odm", "2.0") ? "ODM.xsd"
in_MAIN_SCHEMA.ODMSchemaValidator(standard="odm", version="2.0")
now resolves the bundledodmlib/schemas/odm/2.0/ODM.xsd(target
namespacehttp://www.cdisc.org/ns/odm/v2.0) and exposes the same
validate_tree()/validate_file()API used for ODM 1.3.2 and
Define-XML.- The v2.0 XSD set (
ODM.xsd+ODM-foundation.xsd+ 7 modular
includes + xlink/xml/xhtml) was already shipping in
odmlib/schemas/odm/2.0/via theschemas/**/*.xsdpackage-data
glob; the registry entry is the only missing wiring. tests/test_schema_manager.py: 4 new tests verifying
get_schema_dir("odm", "2.0"),get_schema_path("odm", "2.0"),
the resolved filename (ODM.xsd), and the integration
file-existence check.tests/test_odm_validator.py: newTestODMv20Validatorclass
validatingtests/data/odmv2_example.xmland
tests/data/cdash_demo_v20.xmlend to end, plus a regression test
that an ODM 1.3.2 document fails v2.0 validation. New
test_explicit_odm_v20_works()in
TestODMValidatorConstructorContract.docs/source/guides/validation.rst: rewrote the "Schema Validation"
section to documentODMSchemaValidator(the previous text
referenced a nonexistentSchemaManagerclass).
Permissive Loading Mode
- New
odmlib/mode.pymodule withValidationModeflag enum and
permissive()context manager for loading non-conformant ODM documents ValidationMode.STRICT(default) ? all validation enforced (existing
behavior, unchanged)ValidationMode.SKIP_REQUIRED? omit required-attribute checks during
construction and accessValidationMode.SKIP_TYPE? omit type checks (Typed, Integer, Float,
ODMObject, ODMListObject, Positive, NonNegative, and unknown-attribute
rejection)ValidationMode.SKIP_FORMAT? omit format validators (datetime, SAS
name/format, email, URL, filename, regex, sized string)ValidationMode.SKIP_VALUESET? omit ValidValues and
ExtendedValidValues enforcementValidationMode.PERMISSIVE? composite flag that skips all validation
categoriespermissive()context manager with automatic cleanup via
contextvars.ContextVar; supports graduated control via flag
combinationsopen_odm()andopen_define()context managers accept
permissive=Trueor a specificValidationModecombinationValidationMode,permissive,get_mode,set_modeexported from
odmlibpackage root- New how-to guide:
docs/source/guides/permissive_loading.rst tests/test_permissive_mode.py? 70 tests covering all validation
categories, context manager safety, integration with loaders, and the
load-fix-validate workflow
Error Reporting and Diagnostics
- New
odmlib/exceptions.pymodule with a structured exception hierarchy OdmlibError? base class for all odmlib exceptionsOdmlibValidationError? replaces bareValueErrorfor validation failures; includes
element_path,hint,attribute,element_type, andactual_valueattributesOdmlibRequiredAttributeError? raised when a required attribute is missing at constructionOdmlibOIDError? raised for OID uniqueness or ref/def integrity failuresOdmlibConformanceError? raised when Cerberus conformance validation fails; exposes
rawcerberus_errorsdict for programmatic inspectionOdmlibElementOrderError? raised when child elements violate ODM-spec orderingOdmlibTypeError? replaces bareTypeErrorfor type/enum validation failuresOdmlibParsingError? raised when an XML or JSON document cannot be parsedOdmlibLoaderStateError? raised when a loader method is called before the document is openedOdmlibSerializationError? raised when the model cannot be serialized to XML or JSONOdmlibNamespaceError? raised for namespace registration or lookup failuresOdmlibWarning,OdmlibDeprecationWarning,OdmlibInteroperabilityWarning? warning hierarchyErrorCollector? accumulates validation errors instead of raising on the first failure;
has_errors,add_error(),add_warning(),raise_if_errors()APIODMElement.validate()method ? unified validation entry point supporting both
fail-fast (default) and collect-all-errors (collect_errors=True) modes- All exceptions and
ErrorCollectorexported fromodmlibpackage root tests/test_exceptions.py? 47 tests covering hierarchy, formatting, backward compat, and model integrationtests/test_collect_errors.py? 10 tests covering fail-fast and collect-all-errors validation modes
Dataset-JSON v1.1 ODMElement Model
- New
odmlib/dataset_json_1_1/package ? spec-conformant Dataset-JSON v1.1 support using
the ODMElement/descriptor pattern (one dataset per file, matching the v1.1 specification)DatasetJSON? root element withto_json(),from_json(),write_json(),read_json(),
write_ndjson(),read_ndjson(),to_dict(),from_dict(),add_row(),add_column(),
column_namespropertyColumn? column metadata with validateddataTypeand optionaltargetDataType,
length,displayFormat,keySequenceSourceSystem? optional nested source system metadata objectDatasetJSONElementbase class disables XML serialization (to_xml()raises
NotImplementedError) and handles mixed list types into_dict()
- New
odmlib/dataset_json_1_1/define_flattener.py? converts Define-XML v2.1 metadata into
11 tabular Dataset-JSON datasets (study, standards, datasets, variables, value_level,
where_clauses, methods, comments, documents, codelists, codelist_terms)DefineFlattener.flatten_all()returns dict of dataset name ? DatasetJSONDefineFlattener.write_all(output_dir)writes individual JSON files- O(1) ItemDef and WhereClauseDef lookups via index
_safe_get()utility for traversing deeply nested optional attributes
- New
odmlib/dataset_json_1_1/converter.py? bidirectional Dataset-XML ? Dataset-JSON v1.1dataset_xml_to_dataset_json(odm_obj)returns dict[str, DatasetJSON] (one per ItemGroupOID)dataset_json_to_dataset_xml(dataset_json, model)converts back to Dataset-XML
- Updated
odmlib/dataframe.py? Pandas integration for the new modeldataset_json_to_dataframe()? DatasetJSON v1.1 ? DataFramedefine_metadata_to_dataframes(odm_root)? Define-XML v2.1 ? dict of DataFramesdataframe_to_dataset_json(df, name, label, oid)? DataFrame ? DatasetJSON v1.1
DatasetJSON,Column,SourceSystem,DefineFlattener,dataset_xml_to_dataset_json,
dataset_json_to_dataset_xmlexported fromodmlibpackage root- New how-to guide:
docs/source/guides/dataset_json.rst - New API reference page:
docs/source/odmlib.dataset_json_1_1.rst - New tests:
tests/test_dataset_json_1_1_model.py(68 tests),
tests/test_define_flattener.py(45 tests),
tests/test_dataset_json_1_1_converter.py(21 tests),
tests/test_dataframe_phase3.py(39 tests)
Interoperability and Format Support
- New
odmlib/dataframe.py? optional Pandas DataFrame integrationmetadata_to_dataframe(mdv, element_type, attributes=None)? exports odmlib metadata
elements (ItemDef, ItemGroupDef, CodeList, etc.) as a DataFrameclinical_data_to_dataframe(clinical_data, item_group_oid)? flattens ODM 1.3.2
hierarchical clinical data (SubjectData ? StudyEventData ? FormData ? ItemGroupData)
into a tabular DataFramedataset_to_dataframe(clinical_data)? flattens Dataset-XML 1.0.1 ClinicalData
(flat structure, no SubjectData) into a DataFramedataframe_to_items(df, model_module, element_type, column_mapping=None)? creates
odmlib element instances from a DataFrame (one row per element); skips invalid rows- Graceful degradation: importing the module always succeeds; calling any function
raisesImportErrorwith install hint when pandas is absent
- Optional dependency group
dataframeadded topyproject.toml:
pip install odmlib[dataframe]installs pandas ? 1.5 - pandas ? 1.5 added to the
devdependency group so the full test suite
runs against pandas in CI development environments - New
tests/test_dataframe.py? tests for DataFrame integration
(skipped automatically when pandas is not installed) - New how-to guide:
docs/source/guides/interoperability.rst - New API reference page:
odmlib.dataframeadded to Sphinxindex.rst
Packaging Modernization
pyproject.tomlfor modern Python packaging (replacessetup.pyas primary config)- GitHub Actions CI: automated testing on Python 3.9?3.13
- GitHub Actions: automated PyPI publishing on tagged releases
CHANGELOG.mdfor tracking changes going forward- Semantic versioning policy starting at 0.2.0
Changed
Exception Hierarchy: OdmlibSchemaValidationError
OdmlibSchemaValidationErrornow inherits fromOdmlibValidationError
(and therefore fromOdmlibError). Previously it inherited only from
Exceptionand was excluded from the unified hierarchy. A single
except OdmlibValidationErrornow catches both XSD violations raised by
ODMSchemaValidator.validate_file()and in-memory model validation
failures (required-attr, OID, conformance, element order).- The class has moved from
odmlib/odm_parser.pytoodmlib/exceptions.py.
from odmlib.odm_parser import OdmlibSchemaValidationErrorcontinues to
work via re-export ? no migration required for existing callers. - Also exported from the package root:
from odmlib import OdmlibSchemaValidationError. - Backward compatibility:
ex.args[0]still returns the wrapped
xmlschemaexception, so callers usingex.args[0].msgare unaffected.
The wrapped exception is also accessible via the newex.wrapped
attribute.
Minimum Python Version
- Bumped minimum supported Python version from 3.9 to 3.10. Python 3.9
reached end-of-life in October 2025 and is no longer tested in CI.
Users on 3.9 should upgrade; install requiresrequires-python = ">=3.10". - CI matrix now tests Python 3.10, 3.11, 3.12, and 3.13.
- Workflow hardening:
fail-fast: false(all matrix versions report
independently), action versions bumped to Node 24-compatible releases
(actions/checkout@v5,actions/setup-python@v6,
peaceiris/actions-gh-pages@v4), least-privilegeGITHUB_TOKEN
permissions, and a concurrency group so superseded runs on the same
ref are cancelled.
Permissive Loading Mode
odmlib/descriptor.py:Descriptor.__get__returnsNonefor unset
required attributes whenSKIP_REQUIREDmode is active (previously
always raisedOdmlibRequiredAttributeError)odmlib/odm_element.py:ODMElement.__init__and__setattr__
bypass unknown-attribute rejection and required-attribute enforcement
when appropriate mode flags are activeodmlib/typed.py: all 25__set__methods check the current
ValidationModebefore raising validation exceptionsodmlib/context.py:open_odm()andopen_define()accept a new
permissiveparameter;ODMContextandDefineContextmanage mode
lifecycle in__enter__/__exit__
Structured odmlib Exceptions
- All ~70
ValueError/TypeErrorraises across 16 files replaced with structured odmlib exceptions odmlib/odm_element.py:verify_order()now raisesOdmlibElementOrderErrorwith a hint to use
reorder_object();reorder_object()issues anOdmlibWarningbefore silently reorderingodmlib/odm_1_3_2,odmlib/define_2_0,odmlib/define_2_1conformance checkers now raise
OdmlibConformanceErrorwith structuredcerberus_errorsattribute instead ofValueError(dict)
Dynamic OID Ref/Def Generation
- New
odmlib/oid_generator.pymodule with fully dynamic OID ref/def checking derived
from model class introspection ? eliminates manual maintenance ofoid_ref.pyfiles DynamicOIDRefclass ? drop-in replacement for the manualOIDRefclasses with the
sameadd_oid(),add_oid_ref(),check_oid_refs(), andcheck_unreferenced_oids()APIcreate_oid_checker(model_package, extra_skip_attrs=None, extra_skip_elems=None)factory
function ? primary public API for creating OID checkers; exported fromodmlibpackage rootodmlib/oid_generator_config.py? per-model skip-attribute and skip-element configuration- ODM 2.0 OID checking now supported for the first time via
create_oid_checker("odm_2_0") - Manual
OIDRefclasses inrules/oid_ref.pyfor all three model packages
(odm_1_3_2,define_2_0,define_2_1) now emitOdmlibDeprecationWarningon
instantiation; they remain functional and will be removed in v0.3.0 tests/test_oid_generator.py? 57 new tests covering model introspection, mapping
correctness, DynamicOIDRef behavior, end-to-end validation, and deprecation warnings
ARM 1.0 Model Support
- New
odmlib/arm_1_0/package ? CDISC Analysis Results Metadata (ARM) v1.0 model using
the ODMElement/descriptor pattern- Supports ARM elements:
AnalysisResultDisplays,ResultDisplay,AnalysisResult,
AnalysisDatasets,AnalysisDataset,AnalysisVariable,ProgrammingCode,Code,
Documentation,AnalysisDocumentation, and supporting elements - Integrates with Define-XML 2.1 for analysis results metadata
- ARM namespace (
arm) registered automatically on import
- Supports ARM elements:
Valueset Regex Validation
odmlib/valueset.py:ValueSet.validate(value)? validates a value against the
valueset's allowed values, including regex pattern matching for string-type entriesodmlib/valueset.py:ValueSet.describe()? returns a human-readable description
of the valid values for error messagesodmlib/data/valuesets.json:MetaDataVersion.DefineVersionconverted from enumerated
list to regex pattern^2\.[01](\.\d+)?$for flexible version matchingtests/test_valueset.py? 30 tests for regex validation and describe functionality
Element Search Methods
ODMElement.find_all(element_type, attribute, value)? find all matching child elements
in a list attributeODMElement.find_by(**kwargs)? find a child element matching multiple attribute criteria
ODMBuilder Fluent API
- New
odmlib/builder.py?ODMBuilderclass providing a fluent/chained API for building
ODM documents programmatically withadd_study(),add_metadata_version(),
add_item_group_def(),add_item_def(),add_code_list(), andbuild()methods
Updated Packaging
- Unified version to
0.2.0acrosspyproject.toml,odmlib/__init__.py, anddocs/source/conf.py odmlib/__version__now read dynamically from installed package metadata viaimportlib.metadata- Installation:
pip install -e ".[dev]"replacespython setup.py develop - Installation:
pip install -e .replacespython setup.py install requirements.txtaligned to matchpyproject.tomldependency minimum versions
ODMSchemaValidator requires an explicit schema choice
odmlib/odm_parser.py:ODMSchemaValidator.__init__no longer silently
defaults tostandard="odm",version="1.3.2". The signature is now
(xsd_file=None, standard: Optional[str] = None, version: Optional[str] = None).
Callers must provide either anxsd_filepath, or bothstandardand
version; otherwise aValueErroris raised at construction time. The
silent ODM 1.3.2 fallback was a footgun ? for example, a Define-XML 2.1
document could be validated against the ODM 1.3.2 schema without any
warning. Existing call sites that already passstandard=andversion=
explicitly are unaffected. TheValueErrorhint explicitly points users
with custom or local schemas (anything not in
schema_manager._MAIN_SCHEMA) at thexsd_file=<path>escape hatch, so
they don't accidentally fall back to a packaged schema lookup that
doesn't apply to them.tests/test_odm_validator.py: newTestODMValidatorConstructorContract
class with 8 tests locking in the new error contract (no-args raises,
hint mentionsxsd_file=, partial-args raises, both-args works, xsd_file
works, xsd_file precedence, custom out-of-tree xsd works).
Schema-Ordered Child Serialization in to_xml()
odmlib/odm_element.py:ODMElement.to_xml()now emits child elements
in model declaration order (driven by_elems) instead of attribute
insertion order (self.__dict__). Previously, when a user assigned
child attributes in an order that diverged from the schema declaration
? for example mutatingigd.Description.TranslatedTextafter the
list-typedItemRefhad already been pre-populated by__init__, or
assigningigd.Classlast ? the saved XML put<Description>after
the<ItemRef>block and<def:Class>after<ItemRef>but before
<def:leaf>only by coincidence of the__dict__ordering. Define-XML
2.1 XSD validation rejected such files.- The fix matches what
verify_order()andreorder_object()already
trusted: declaration order from the class body, captured byODMMeta
into_elems. Users no longer need to callreorder_object()before
serializing ? assignment order is fully decoupled from emission order. tests/test_schema_ordered_serialization.py? 7 tests pinning the new
behaviour: ItemDef/ItemGroupDef children come out in schema order
regardless of assignment order; the Define-XML 2.1 ItemGroupDef pattern
fromnotebooks/first_define.ipynb(Description?ItemRef*?
def:Class?def:leaf) is locked in; unset optional children are
silently skipped; attribute serialization and_contentemission are
unchanged.
Fixed
XML Loader Namespace Handling
XMLDefineLoadernamespace mismatch: the default
ns_uriwas set to a Define-XML 2.0 default regardless ofmodel_package,
causingdef:-namespaced child ofMetaDataVersion
(Standard,CommentDef,ValueListDef,WhereClauseDef,leaf,
?) to be dropped when loading Define-XML 2.1 documents (when no ns_uri was provided).
Thens_uriargument is nowOptional[str]and, when omitted, derived
frommodel_package(define_2_0??/v2.0,define_2_1??/v2.1).
Explicit values still override the derived default.XMLODMLoaderns_uriparameter was dead code: the constructor
accepted anns_uriargument but_set_namespaceused the
default ODM 1.3 URI. The parameter is now stored on the loader and used by
_set_namespace; default is derived frommodel_package(odm_1_3_2
??/v1.3,odm_2_0??/v2.0).XMLArmLoader._set_registrydocumented in-place: ARM 1.0 is
intentionally paired with ODM 1.3 and Define-XML 2.1, no behavior
change.- New regression suite:
tests/test_loader_ns_defaults.py? 12 tests
covering Define 2.0/2.1 derivation, ODM 1.3.2/2.0 derivation, explicit
override behavior, fallback for unknownmodel_package, and the
end-to-end Define-XML 2.1 OID-index regression that reproduced the
original bug report. XMLODMLoader.__init__no longer mutates the global
NamespaceRegistryBorg singleton at construction time. Pre-fix,
__init__unconditionally called_set_namespace(None), which
registeredodm ? self.nos_uriimmediately. When users passed a
non-canonical wrapper URI (e.g.library-xml/v1.0for the CDISC
Library CDASH endpoint), the canonicalodm ? http://www.cdisc.org/ns/odm/v1.3
mapping was silently overwritten in the global Borg, breaking any code
in the same process that depended on it.XMLODMLoader.__init__now
mirrorsXMLDefineLoader.__init__: it storesself.ns_uribut assigns
an emptyNamespaceRegistry()view toself.nsrand defers the actual
prefix registration tocreate_document/
create_document_from_string.create_documentwas also updated to
call_set_namespace(namespace_registry)unconditionally so that the
deferred registration still happens when no caller-supplied registry
is provided. Thensr=constructor argument continues to be honored
immediately.tests/test_loader_ns_defaults.py: added
TestODMLoaderConstructionDoesNotMutateBorg(4 tests) covering the
no-mutation contract, the canonical-URI symmetry case, the explicit
nsr=constructor argument, and deferred-registration-at-parse-time;
updatedtest_odm_explicit_ns_uri_now_takes_effectto trigger the
deferred registration before asserting onloader.nsr.
Trailing Spaces Removed
- Trailing space bugs in
odmlib/odm_1_3_2/rules/oid_ref.py_init_def_ref():
"SignatureOID "corrected to"SignatureOID"and"ItemOID "corrected to"ItemOID";
these would have caused silent failures incheck_unreferenced_oids()
Packaging
MANIFEST.intypo:test/datacorrected totests/data- Version inconsistency: was 0.1.4 (
setup.py), 0.1.2 (__init__.py), 0.1.0 (docs/) - Missing
odmlib.odm_2_0package in build configuration (now auto-discovered viapackages.find)
Deprecation Notice
In v0.2.x, odmlib validation and type exceptions dual-inherit from ValueError/TypeError so that
all existing except ValueError and except TypeError clauses continue to work unchanged.
rc1 - testing release. Not promoted to default pip install odmlib.
Install for soak with: pip install --pre "odmlib==0.2.0rc1".