Skip to content

Model

model

Package containing models, the pythonic representation of feature classes.

They inherit from BaseFeature, which extends the Pydantic BaseModel with some utility, and are resolved via an Appschema instance. A feature collection is represented by the BaseCollection class.

Example

Load the BP_Plan model for XPlanung v6.0 and instantiate it with some data:

from xplan_tools.model.base import Appschema

plan = Appschema.from_prefix("xplan", "6.0").model_factory("BP_Plan")
instance = plan.model_validate(
    {
        "name": "Testplan",
        "gemeinde": [
            {
                "ags": "1234"
            }
        ],
        "raeumlicherGeltungsbereich": {
            "srid": 25832,
            "wkt": <WKT-String>
        }
    }
)

Appschema

Bases: BaseModel

Models a supported appschema.

Used to access metadata like name, version and description for an appschema.

During instantion, validation if an appschema is supported occurs, based on the modules in appschema subdirectory.

The instance stores a reference to the appschema's module, which is used in the model_factory method the retrieve appschema classes.

Usage example
# get a featuretype from appschema
appschema = Appschema.from_prefix(prefix="xplan", version="6.0")
plan = appschema.model_factory(name="BP_Plan")
# show list of supported appschemas
Appschema.supported_appschemas()
# get an enum of supported appschemas
SupportedAppschemas = Appschema.enum()
SupportedAppschemas.XPLAN_6_0.value # -> XPlanGML 6_0

code property

The enum code of the appschema.

top_level_featuretypes cached property

Names of the top-level owner feature types of this appschema.

A top-level owner existentially owns at least one part (an association whose dependent_part is set) but is itself never the part of another owner: the containment/deletion roots, e.g. BP_Plan. Derived from the appschema's association metadata as the owner-set minus the part-set.

Returns:

Type Description
list[str]

The owner feature type names, sorted.

version property

The version of the appschema in <major>.<minor> format.

enum() classmethod

Return an enumeration of supported appschemas.

Source code in xplan_tools/model/base.py
@classmethod
def enum(cls) -> type[StrEnum]:
    """Return an enumeration of supported appschemas."""
    if not cls._enum:

        def _new(
            cls,
            value,
            appschema,
        ):
            obj = str.__new__(cls, value)
            obj._value_ = value
            obj.appschema = appschema
            return obj

        cls._enum = StrEnum(
            "SupportedAppschemas",
            {
                f"{appschema.prefix.upper()}_{appschema.full_version.major}_{appschema.full_version.minor}": (
                    f"{appschema.prefix.upper()}_{appschema.version.replace('.', '_')}",
                    appschema,
                )
                for appschema in cls.supported_appschemas()
            }
            | {"__new__": _new, "__annotations__": {"appschema": Appschema}},
        )
    return cls._enum

from_enum(code) classmethod

Return an Appschema instance from enum code.

Parameters:

Name Type Description Default
code str

an appschema code, e.g. XPLAN_6_0

required
Source code in xplan_tools/model/base.py
@classmethod
def from_enum(cls, code: str) -> Appschema:
    """Return an Appschema instance from enum code.

    Args:
        code: an appschema code, e.g. `XPLAN_6_0`
    """
    if enum := getattr(cls.enum(), code, None):
        return enum.appschema
    else:
        raise NotImplementedError(f"no appschema with code {code!r}")

from_module(module_name) cached classmethod

Builds an Appschema instance from module name.

Parameters:

Name Type Description Default
module_name str

the fully qualified module name

required
Source code in xplan_tools/model/base.py
@classmethod
@functools.cache
def from_module(cls, module_name: str) -> Appschema:
    """Builds an Appschema instance from module name.

    Args:
        module_name: the fully qualified module name
    """
    try:
        module = import_module(module_name)
        model_cls: type[RootModel] = getattr(module, "Model")
    except (ImportError, AttributeError) as exc:
        raise RuntimeError(
            f"Unable to resolve appschema metadata for module {module_name!r}"
        ) from exc

    if not issubclass(model_cls, RootModel):
        raise ValueError(f"expected RootModel, got {model_cls!r}")

    metadata = model_cls.model_fields["root"]

    return cls.model_validate(
        metadata.json_schema_extra
        | {"description": metadata.description, "module": module}
    )

from_namespace(namespace) classmethod

Builds an Appschema instance from the appschema's namespace.

Might return a compatible appschema version if no exact match is found.

Parameters:

Name Type Description Default
namespace str

the namespace URI of the appschema

required
Source code in xplan_tools/model/base.py
@classmethod
def from_namespace(cls, namespace: str) -> Appschema:
    """Builds an Appschema instance from the appschema's namespace.

    Might return a compatible appschema version if no exact match is found.

    Args:
        namespace: the namespace URI of the appschema
    """
    uri = AnyUrl(namespace)
    candidates: list[Appschema] = []
    for appschema in cls.supported_appschemas():
        if appschema.namespace_uri == uri:
            return appschema
        elif str(appschema.namespace_uri)[:-1] == namespace[:-1]:
            candidates.append(appschema)
    if candidates:
        return sorted(candidates)[-1]
    raise NotImplementedError(f"no appschema with namespace {namespace!r}")

from_prefix(prefix, version) classmethod

Builds an Appschema instance from the appschema's prefix and version.

Might return a compatible appschema version if no exact match is found.

Parameters:

Name Type Description Default
prefix str

the namespace prefix of the appschema, e.g. xplan or xtrasse

required
version str

the version of the appschema; major and minor are required

required
Source code in xplan_tools/model/base.py
@classmethod
def from_prefix(cls, prefix: str, version: str) -> Appschema:
    """Builds an Appschema instance from the appschema's prefix and version.

    Might return a compatible appschema version if no exact match is found.

    Args:
        prefix: the namespace prefix of the appschema, e.g. `xplan` or `xtrasse`
        version: the version of the appschema; major and minor are required
    """
    sem_version = Version.parse(version, optional_minor_and_patch=True)
    versions = []
    candidates = []
    for appschema in filter(
        lambda appschema: appschema.prefix == prefix, cls.supported_appschemas()
    ):
        # if exact match, return
        if (
            sem_version.major == appschema.full_version.major
            and sem_version.minor == appschema.full_version.minor
        ):
            return appschema
        # if versions are compatible (modules minor version higher or equal), add to candidates
        elif sem_version.is_compatible(appschema.full_version):
            candidates.append(appschema)
        else:
            versions.append(
                f"{appschema.full_version.major}.{appschema.full_version.minor}"
            )
    # return appschema with newer minor version, if found
    # TODO: ensure minor version compatibility without version migration, e.g. via model_validator
    if candidates:
        return sorted(candidates)[-1]
    if versions:
        e = NotImplementedError(
            f"version {version!r} not supported for prefix {prefix!r}"
        )
        e.add_note(f"available versions: {', '.join(sorted(versions))}")
        raise e
    else:
        raise NotImplementedError(f"no appschema with prefix {prefix!r}")

model_factory(name)

Factory method for retrieving the corresponding pydantic model representation of a feature class.

Parameters:

Name Type Description Default
name str

name of the feature class or enumeration

required

Raises:

Type Description
ValueError

requested FeatureType not found

Returns:

Type Description
type[BaseFeature]

The concrete feature class inheriting from BaseFeature.

Source code in xplan_tools/model/base.py
def model_factory(self, name: str) -> type[BaseFeature]:
    """Factory method for retrieving the corresponding pydantic model representation of a feature class.

    Args:
        name: name of the feature class or enumeration

    Raises:
        ValueError: requested FeatureType not found

    Returns:
        The concrete feature class inheriting from BaseFeature.
    """
    try:
        cls: BaseFeature = getattr(self.module, name)
        return _ensure_subclass(cls, BaseFeature)
    except AttributeError:
        raise ValueError(f"featuretype {name!r} not found for appschema {self!r}")

supported_appschemas() classmethod

Return a list of currently supported appschemas.

Source code in xplan_tools/model/base.py
@classmethod
def supported_appschemas(cls) -> list[Appschema]:
    """Return a list of currently supported appschemas."""
    if not cls._supported_appschemas:
        cls._supported_appschemas = [
            cls.from_module(f"{__package__}.appschema.{module_name}")
            for module_name in APPSCHEMA_MODULE_NAMES
        ]
    return cls._supported_appschemas

BaseFeature

Bases: _ConfiguredBaseModel, GMLAdapter, CoretableAdapter, JsonFGAdapter

Base class for application schema classes.

It extends pydantic BaseModel with Feature-related helper methods as well as conversion capabilities from/to other formats via inheriting from respective adapter classes.

appschema() cached classmethod

Return metadata about the application schema for the feature class.

Source code in xplan_tools/model/base.py
@classmethod
@functools.cache
def appschema(cls) -> Appschema:
    """Return metadata about the application schema for the feature class."""
    return Appschema.from_module(cls.__module__)

get_associations() cached classmethod

Returns the classes association fields.

Source code in xplan_tools/model/base.py
@classmethod
@functools.cache
def get_associations(cls) -> list[str]:
    """Returns the classes association fields."""
    return [
        assoc
        for assoc in cls.model_fields.keys()
        if cls.get_property_info(assoc).stereotype == "Association"
    ]

get_geom_field() cached classmethod

Returns the classes geometry field name, if any.

Source code in xplan_tools/model/base.py
@classmethod
@functools.cache
def get_geom_field(cls) -> str | None:
    """Returns the classes geometry field name, if any."""
    geom_fields = [
        name
        for name in cls.model_fields
        if cls.get_property_info(name).stereotype == "Geometry"
    ]
    if len(geom_fields) > 1:
        raise ValueError(f"Multiple geometry fields declared: {geom_fields}")
    return geom_fields[0] if geom_fields else None

get_geom_srid()

Returns the object's geometry's SRID, if any.

Source code in xplan_tools/model/base.py
def get_geom_srid(self) -> int | None:
    """Returns the object's geometry's SRID, if any."""
    if geom_field := self.get_geom_field():
        if geom := getattr(self, geom_field, None):
            return geom.srid

get_geom_types() cached classmethod

Returns the types of the geometry attribute.

Source code in xplan_tools/model/base.py
@classmethod
@functools.cache
def get_geom_types(cls) -> list[type[GeometryType]] | None:
    """Returns the types of the geometry attribute."""
    if geom_field := cls.get_geom_field():
        geom_annotation = cls.model_fields[geom_field].annotation
        args = get_args(geom_annotation)
        if not args:
            geom_model = [geom_annotation]
        else:
            geom_model = [arg for arg in args if arg is not NoneType]
        return geom_model

get_geom_wkt()

Returns the object's eWKT geometry's WKT representation withouth SRID, if any.

Source code in xplan_tools/model/base.py
def get_geom_wkt(self) -> str | None:
    """Returns the object's eWKT geometry's WKT representation withouth SRID, if any."""
    if geom_field := self.get_geom_field():
        if geom := getattr(self, geom_field, None):
            return geom.wkt

get_name() cached classmethod

Returns the canonical name of the FeatureClass.

Source code in xplan_tools/model/base.py
@classmethod
@functools.cache
def get_name(cls) -> str:
    """Returns the canonical name of the FeatureClass."""
    return cls.__name__

get_property_info(name) cached classmethod

Property information.

Parameters:

Name Type Description Default
name str

The property's name.

required

Returns:

Type Description
PropertyInfo

A typed dataclass holding the information.

Raises:

Type Description
AttributeError

The name was not found in the model fields.

Source code in xplan_tools/model/base.py
@classmethod
@functools.cache
def get_property_info(cls, name: str) -> PropertyInfo:
    """Property information.

    Args:
        name: The property's name.

    Returns:
        A typed dataclass holding the information.

    Raises:
        AttributeError: The name was not found in the model fields.
    """
    try:
        field_info = cls.model_fields[name]
        extra_info = field_info.json_schema_extra or {}
    except KeyError:
        raise AttributeError(f"Unknown property: {name}")
    else:
        stereotype: Final = extra_info["stereotype"]
        typename: str | list[str] = extra_info["typename"]
        return PropertyInfo(
            stereotype=stereotype,
            typename=typename,
            list=get_origin(field_info.annotation) is list
            or list
            in [
                arg.__origin__
                for arg in get_args(field_info.annotation)
                if getattr(arg, "__origin__", None)
            ],
            nullable=NoneType in get_args(field_info.annotation),
            uom=extra_info.get("uom", None),
            enum=_ensure_subclass(
                getattr(cls.appschema().module, str(typename)), BaseEnum
            )
            if stereotype == "Enumeration"
            else None,
            assoc_info=None
            if stereotype != "Association"
            else AssocInfo(
                reverse=extra_info.get("reverseProperty"),
                source_or_target=extra_info.get("sourceOrTarget"),
                dependent_part=extra_info.get("dependent_part"),
            ),
        )

model_dump_coretable()

Dumps the model data to a coretable Feature object to store in a database.

Source code in xplan_tools/model/base.py
def model_dump_coretable(
    self,
) -> Feature:
    """Dumps the model data to a coretable Feature object to store in a database."""
    return self._to_coretable()

model_dump_coretable_bulk()

Dumps the model data to feature, refs and inverse refs dicts to bulk insert in a database.

Source code in xplan_tools/model/base.py
def model_dump_coretable_bulk(self) -> tuple[dict, list[dict], list[dict]]:
    """Dumps the model data to feature, refs and inverse refs dicts to bulk insert in a database."""
    return self._to_coretable(bulk_mode=True)

model_dump_gml(**kwargs)

Dumps the model data to a GML structure held in an etree.Element.

Source code in xplan_tools/model/base.py
def model_dump_gml(
    self,
    **kwargs,
) -> _Element:
    """Dumps the model data to a GML structure held in an etree.Element."""
    return self._to_etree(**kwargs)

model_dump_jsonfg(**kwargs)

Dumps the model data to a JSON-FG object.

Source code in xplan_tools/model/base.py
def model_dump_jsonfg(
    self,
    **kwargs,
) -> dict:
    """Dumps the model data to a JSON-FG object."""
    return self._to_jsonfg(**kwargs)

BaseCollection

Bases: BaseModel

Container for features that provides validation of references.

The features are stored in a dictionary with their ID as key and the feature instance as value.

add_style_properties(to_text=False, always_populate_schriftinhalt=False)

Add styling properties to presentational objects.

This method parses object (dientZurDarstellungVon) and property (art) references from presentational objects and derives styling information (stylesheetId, schriftinhalt) based on a set of defined rules.

Parameters:

Name Type Description Default
to_text bool

Whether to convert symbolic presentational objects to textual ones. Defaults to False.

False
always_populate_schriftinhalt bool

Populate schriftinhalt even if a rule has no text template.

False
Source code in xplan_tools/model/base.py
def add_style_properties(
    self, to_text: bool = False, always_populate_schriftinhalt: bool = False
) -> None:
    """Add styling properties to presentational objects.

    This method parses object (dientZurDarstellungVon) and property (art) references from
    presentational objects and derives styling information (stylesheetId, schriftinhalt)
    based on a set of defined rules.

    Args:
        to_text: Whether to convert symbolic presentational objects to textual ones. Defaults to False.
        always_populate_schriftinhalt: Populate `schriftinhalt` even if a rule has no text template.
    """
    logger.info("adding style properties to collection")

    for obj in filter(lambda x: hasattr(x, "stylesheetId"), self.get_features()):
        if not obj.dientZurDarstellungVon:
            logger.info(
                f"Feature {obj.id}: dientZurDarstellungVon not set, skipping"
            )
            continue
        elif len(obj.dientZurDarstellungVon) > 1:
            logger.warning(
                f"Feature {obj.id}: references to multiple objects '{obj.dientZurDarstellungVon}' not supported, skipping"
            )
            continue
        elif not obj.art:
            logger.info(f"Feature {obj.id}: art not set, skipping")
            continue
        ref_id = obj.dientZurDarstellungVon[0]
        try:
            ref_obj = self.features[ref_id]
        except KeyError:
            raise ValueError(
                f"Feature {obj.id}: dientZurDarstellungVon references unknown feature {ref_id}"
            )
        new_obj = add_style_properties_to_feature(
            obj, ref_obj, to_text, always_populate_schriftinhalt
        )
        self.features[obj.id] = new_obj

    logger.info("finished adding style properties to collection")

check_references_and_srs(info)

Checks if all objects referenced via UUID are part of the collection and if all features have the same SRS, version and appschema.

An association reference is only supported if it is a UUID naming a feature of a compatible type within the collection. An unresolvable UUID and an external URL both raise, unless the validation context sets DROP_INVALID_REFS: they are then removed from the feature and collected under the context's INVALID_REFS key, which is how the caller gets them back:

context = {DROP_INVALID_REFS: True}
collection = BaseCollection.model_validate(
    {"features": features, "srid": srid, "appschema": appschema},
    context=context,
)
dropped = context.get(INVALID_REFS, [])

A mandatory role is never emptied that way - a feature that has to carry the reference raises either way.

The whole collection is checked before anything is raised or removed, so one bad reference reports the rest with it and a failed check leaves the features as they were. Dropping edits the feature objects the caller passed in: pydantic does not copy BaseFeature instances when validating them into the collection, so they are the very same objects.

Source code in xplan_tools/model/base.py
@model_validator(mode="after")
def check_references_and_srs(self, info: ValidationInfo) -> Self:
    """Checks if all objects referenced via UUID are part of the collection and if all features have the same SRS, version and appschema.

    An association reference is only supported if it is a UUID naming a feature of a
    compatible type within the collection. An unresolvable UUID and an external URL
    both raise, unless the validation context sets `DROP_INVALID_REFS`: they are then
    removed from the feature and collected under the context's `INVALID_REFS` key,
    which is how the caller gets them back:

        context = {DROP_INVALID_REFS: True}
        collection = BaseCollection.model_validate(
            {"features": features, "srid": srid, "appschema": appschema},
            context=context,
        )
        dropped = context.get(INVALID_REFS, [])

    A mandatory role is never emptied that way - a feature that has to carry the
    reference raises either way.

    The whole collection is checked before anything is raised or removed, so one bad
    reference reports the rest with it and a failed check leaves the features as they
    were. Dropping edits the feature objects the caller passed in: pydantic does not copy
    `BaseFeature` instances when validating them into the collection, so they are the very
    same objects.
    """
    logger.debug("checking feature references")
    context = info.context if isinstance(info.context, dict) else None
    drop = bool(context and context.get(DROP_INVALID_REFS))
    invalid_refs: list[InvalidReference] = []
    errors: list[str] = []
    removals: list[tuple[BaseFeature, str, Any]] = []
    first_srid: int | None = None
    first_appschema: Appschema | None = None

    def _incompatible_type(
        feature: BaseFeature, name: str, value: Any
    ) -> str | None:
        """Returns the error for a reference resolving to the wrong type, or None.

        A reference to an incompatible feature type is a schema violation rather than a
        missing target, so it is reported straight away and never offered for dropping.
        """
        if not isinstance(value, UUID):
            return None
        if (ref_feature := self.features.get(value)) is None:
            return None
        if feature.get_property_info(name).is_type_ok(
            (ref_name := ref_feature.get_name())
        ):
            return None
        return f"association {name} in object {feature.id} references incompatible type {ref_name}"

    def _invalid_ref(
        feature: BaseFeature, name: str, value: Any
    ) -> InvalidReference | None:
        """Returns why a reference is unsupported, or None if it is fine.

        Values that are neither a UUID nor a URL are not references at all - a nilReason
        placeholder, say - and are left untouched.
        """
        if isinstance(value, UUID):
            if value in self.features:
                return None
            reason = "unresolvable"
        elif isinstance(value, AnyUrl):
            reason = "external"
        else:
            return None
        return InvalidReference(
            feature_id=feature.id,
            featuretype=feature.get_name(),
            field=name,
            value=str(value),
            reason=reason,
        )

    for feature in self.features.values():
        if srid := feature.get_geom_srid():
            if first_srid is None:
                first_srid = srid
            elif srid != first_srid:
                raise ValueError(
                    f"Multiple SRS within collection not supported: SRID {srid} != {first_srid} for feature {feature.id}"
                )
        appschema = feature.appschema()
        if first_appschema is None:
            first_appschema = appschema
        elif appschema != first_appschema:
            raise ValueError(
                f"Multiple appschemas within collection not supported: appschema {appschema} != {first_appschema} for feature {feature.id}"
            )

        for assoc in feature.get_associations():
            value = getattr(feature, assoc)
            if value is None:
                continue
            is_list = isinstance(value, list)
            kept: list[Any] = []
            dropped: list[InvalidReference] = []
            for item in value if is_list else [value]:
                if incompatible := _incompatible_type(feature, assoc, item):
                    errors.append(incompatible)
                    kept.append(item)
                elif (invalid_ref := _invalid_ref(feature, assoc, item)) is None:
                    kept.append(item)
                elif drop:
                    dropped.append(invalid_ref)
                else:
                    errors.append(str(invalid_ref))
                    kept.append(item)
            if not dropped:
                continue
            if not kept and not feature.get_property_info(assoc).nullable:
                errors.extend(
                    f"{invalid_ref} and cannot be dropped: {assoc} is mandatory"
                    for invalid_ref in dropped
                )
                continue
            invalid_refs.extend(dropped)
            removals.append((feature, assoc, (kept or None) if is_list else None))

    if errors:
        raise ValueError(
            f"{len(errors)} unsupported feature reference(s):\n"
            + "\n".join(f"- {error}" for error in errors)
        )
    for feature, assoc, remaining in removals:
        setattr(feature, assoc, remaining)
    for invalid_ref in invalid_refs:
        logger.warning(f"{invalid_ref}, dropped")
    if invalid_refs:
        context.setdefault(INVALID_REFS, []).extend(invalid_refs)
    logger.debug("all feature references resolvable")
    return self

from_features(features, srid, appschema, *, context=None) classmethod

Builds a collection, passing context to the reference check.

The one constructor every repository uses, so the read options a caller can set - DROP_INVALID_REFS above all - reach the validator the same way whichever format was read.

Parameters:

Name Type Description Default
features dict[UUID, BaseFeature] | list[BaseFeature]

The features, keyed by id or as a list.

required
srid int

The collection's spatial reference system identifier.

required
appschema Appschema

The appschema every feature belongs to.

required
context dict[str, Any] | None

Pydantic validation context; see check_references_and_srs.

None

Returns:

Type Description
BaseCollection

The validated collection.

Source code in xplan_tools/model/base.py
@classmethod
def from_features(
    cls,
    features: dict[UUID, BaseFeature] | list[BaseFeature],
    srid: int,
    appschema: Appschema,
    *,
    context: dict[str, Any] | None = None,
) -> BaseCollection:
    """Builds a collection, passing `context` to the reference check.

    The one constructor every repository uses, so the read options a caller can set -
    `DROP_INVALID_REFS` above all - reach the validator the same way whichever format
    was read.

    Args:
        features: The features, keyed by id or as a list.
        srid: The collection's spatial reference system identifier.
        appschema: The appschema every feature belongs to.
        context: Pydantic validation context; see `check_references_and_srs`.

    Returns:
        The validated collection.
    """
    return cls.model_validate(
        {"features": features, "srid": srid, "appschema": appschema},
        context=context,
    )

get_features()

Yields features stored in the collection.

Source code in xplan_tools/model/base.py
def get_features(self) -> Iterator["BaseFeature"]:
    """Yields features stored in the collection."""
    return (feature for feature in self.features.values())

get_single_plans(with_name=False)

Yields BaseCollection objects for every plan in the original collection.

A plan collection is the containment closure of a top-level feature type, derived from the appschema's dependent_part metadata rather than from hardcoded role names: a role marked target points at a part the feature owns, a role marked source points back at its owner. Both directions are followed, since a file may populate only one of them.

A plan is self-contained, so a reference leaving its collection is invalid data and raises when the yielded collection validates its references.

Raises:

Type Description
ValueError

If the appschema declares no dependent_part metadata - INSPIRE PLU is the only such appschema - so no containment closure can be derived.

Source code in xplan_tools/model/base.py
def get_single_plans(
    self, with_name: bool = False
) -> Iterator[BaseCollection] | Iterator[tuple[str, BaseCollection]]:
    """Yields BaseCollection objects for every plan in the original collection.

    A plan collection is the containment closure of a top-level feature type, derived
    from the appschema's `dependent_part` metadata rather than from hardcoded role
    names: a role marked `target` points at a part the feature owns, a role marked
    `source` points back at its owner. Both directions are followed, since a file may
    populate only one of them.

    A plan is self-contained, so a reference leaving its collection is invalid data and
    raises when the yielded collection validates its references.

    Raises:
        ValueError: If the appschema declares no `dependent_part` metadata - INSPIRE PLU
            is the only such appschema - so no containment closure can be derived.
    """
    if not (top_level_featuretypes := self.appschema.top_level_featuretypes):
        raise ValueError(
            f"appschema {self.appschema.full_name} declares no ownership metadata, "
            "single plans cannot be derived"
        )
    return self._single_plans(top_level_featuretypes, with_name)

list_to_dict(data) classmethod

Takes a list of BaseFeatures and returns a BaseCollection dict.

Source code in xplan_tools/model/base.py
@model_validator(mode="before")
@classmethod
def list_to_dict(cls, data: Any) -> Any:
    """Takes a list of BaseFeatures and returns a BaseCollection dict."""
    if isinstance(data, list):
        data_dict = {}
        for feature in data:
            if not isinstance(feature, BaseFeature):
                raise TypeError(
                    f"Object is not an instance of BaseFeature: {feature}"
                )
            data_dict[feature.id] = feature
        return data_dict
    return data

make_copy(with_id_map=False)

Return a copy of the collection with new IDs.

All feature IDs are renewed and respective references are updated.

Parameters:

Name Type Description Default
with_id_map bool

whether to additionally return a map of old IDs to new IDs

False
Source code in xplan_tools/model/base.py
def make_copy(
    self, with_id_map: bool = False
) -> BaseCollection | tuple[BaseCollection, dict]:
    """Return a copy of the collection with new IDs.

    All feature IDs are renewed and respective references are updated.

    Args:
        with_id_map: whether to additionally return a map of old IDs to new IDs
    """
    id_map = {key: uuid4() for key in self.features.keys()}
    new_features = {}
    for old_feature in self.features.values():
        new_id = id_map[old_feature.id]
        new_feature = old_feature.model_copy(deep=True)
        new_feature.id = new_id
        for assocation in old_feature.get_associations():
            old_value = getattr(old_feature, assocation)
            if isinstance(old_value, UUID):
                new_value = id_map[old_value]
                setattr(new_feature, assocation, new_value)
            elif isinstance(old_value, list):
                new_list = [
                    id_map[item] if isinstance(item, UUID) else item
                    for item in old_value
                ]
                setattr(new_feature, assocation, new_list)
        new_features[new_id] = new_feature

    new_collection = BaseCollection(
        features=new_features,
        srid=self.srid,
        appschema=self.appschema,
    )
    if with_id_map:
        return new_collection, id_map
    else:
        return new_collection