"""Facade loader for ODM, Define-XML, and ARM documents.
This module provides :class:`ODMLoader`, a unified interface that wraps
any specialized loader (:class:`~odmlib.odm_loader.XMLODMLoader`,
:class:`~odmlib.odm_loader.JSONODMLoader`,
:class:`~odmlib.define_loader.XMLDefineLoader`,
:class:`~odmlib.define_loader.JSONDefineLoader`,
:class:`~odmlib.arm_loader.XMLArmLoader`, or
:class:`~odmlib.arm_loader.JSONArmLoader`) and delegates all
document loading operations to it.
"""
from __future__ import annotations
from typing import Any, Optional
import odmlib.document_loader as DL
import odmlib.ns_registry as NS
from odmlib.exceptions import OdmlibTypeError
[docs]
class ODMLoader:
"""Facade providing a unified interface for loading ODM documents.
Wraps a specialized loader (:class:`~odmlib.odm_loader.XMLODMLoader`,
:class:`~odmlib.odm_loader.JSONODMLoader`,
:class:`~odmlib.define_loader.XMLDefineLoader`,
:class:`~odmlib.define_loader.JSONDefineLoader`,
:class:`~odmlib.arm_loader.XMLArmLoader`, or
:class:`~odmlib.arm_loader.JSONArmLoader`) and delegates
all operations to it.
Args:
odm_loader (DocumentLoader): A specialized loader instance.
Raises:
OdmlibTypeError: If ``odm_loader`` is not a DocumentLoader instance.
Example::
import odmlib.odm_loader as OL
import odmlib.loader as LD
loader = LD.ODMLoader(OL.XMLODMLoader(model_package="odm_1_3_2"))
loader.open_odm_document("study.xml")
odm = loader.root()
mdv = loader.MetaDataVersion()
"""
def __init__(self, odm_loader: DL.DocumentLoader) -> None:
if not isinstance(odm_loader, DL.DocumentLoader):
raise OdmlibTypeError(
"odm_loader argument must implement DocumentLoader",
expected_type="DocumentLoader",
actual_value=type(odm_loader).__name__,
hint="Pass an instance of XMLODMLoader, JSONODMLoader, XMLDefineLoader, JSONDefineLoader, XMLArmLoader, or JSONArmLoader",
)
self.loader = odm_loader
self._ns_snapshot: Optional[dict] = None
def _bind_namespaces(self, odm_obj: Any) -> Any:
"""Attach the document-open-time namespace snapshot to *odm_obj*.
The NamespaceRegistry is shared process-wide, so opening another
document later can change it; binding a snapshot lets write_xml /
to_xml_string serialize this document with the namespaces it was
actually loaded under.
Bound recursively so that serializing a nested element reached by walking
the tree behaves like serializing its root. Elements constructed *after*
the load and grafted in are not covered - bind those explicitly with
``NS.bind_document_namespaces(new_elem, NS.get_document_namespaces(root))``.
"""
if odm_obj is not None:
NS.bind_document_namespaces(odm_obj, snapshot=self._ns_snapshot, recursive=True)
return odm_obj
[docs]
def create_odmlib(self, odm_doc: Any, odm_key: Optional[str] = None) -> Any:
"""Load an odmlib object from an existing document dict or element.
Delegates to the wrapped loader's ``load_document`` method.
Args:
odm_doc: The document dict (JSON loaders) or XML Element
(XML loaders) to load from.
odm_key: The element key/class name to use when loading from
a dict (JSON loaders only).
Returns:
An odmlib element object populated from ``odm_doc``.
"""
odm_obj = self.loader.load_document(odm_doc, odm_key)
return self._bind_namespaces(odm_obj)
[docs]
def open_odm_document(self, filename: str) -> Any:
"""Parse an ODM document file and prepare for loading.
Args:
filename (str): Path to the ODM XML or JSON file.
Returns:
The parsed root element or dict, depending on the loader type.
"""
root = self.loader.create_document(filename)
self._ns_snapshot = NS.NamespaceRegistry().snapshot()
return root
[docs]
def load_odm_string(self, odm_string: str) -> Any:
"""Parse an ODM document from a string and prepare for loading.
Args:
odm_string (str): XML or JSON string containing the ODM document.
Returns:
The parsed root element or dict, depending on the loader type.
"""
root = self.loader.create_document_from_string(odm_string)
self._ns_snapshot = NS.NamespaceRegistry().snapshot()
return root
[docs]
def root(self) -> Any:
"""Return the root ODM element.
Must be called after :meth:`open_odm_document` or
:meth:`load_odm_string`.
Returns:
The root odmlib ODM object (e.g., an ``ODM`` instance).
"""
odm = self.loader.load_odm()
return self._bind_namespaces(odm)
[docs]
def Study(self, idx: int = 0) -> Any:
"""Return the Study at the specified index.
Must be called after :meth:`open_odm_document` or
:meth:`load_odm_string`.
Args:
idx (int): Zero-based index of the Study to return
(default: 0, the first one).
Returns:
A ``Study`` odmlib object.
"""
study = self.loader.load_study(idx)
return self._bind_namespaces(study)
def __getattr__(self, attr: str) -> Any:
"""Delegate unknown method calls to the wrapped loader.
Allows calling loader-specific methods (e.g., ``load_odm_dataset``)
directly on the facade without explicitly exposing them.
Args:
attr (str): Name of the attribute or method to look up.
Returns:
The callable attribute from the wrapped loader.
Raises:
AttributeError: If the attribute does not exist on the loader,
or exists but is not callable.
"""
if hasattr(self.loader, attr):
load_func = getattr(self.loader, attr, None)
if callable(load_func):
return load_func
else:
raise AttributeError(f"{attr} is not callable in the loader {self.loader.__class__.__name__}")
else:
raise AttributeError(f"The {attr} method does not exist in the loader {self.loader.__class__.__name__}")