mdmodels 0.2.10

A tool to generate models, code and schemas from markdown files
Documentation
"""
This file contains Pydantic model definitions for data validation.

Pydantic is a data validation library that uses Python type annotations.
It allows you to define data models with type hints that are validated
at runtime while providing static type checking.

Usage example:
```python
from my_model import MyModel

# Validates data at runtime
my_model = MyModel(name="John", age=30)

# Type-safe - my_model has correct type hints
print(my_model.name)

# Will raise error if validation fails
try:
    MyModel(name="", age=30)
except ValidationError as e:
    print(e)
```

For more information see:
https://docs.pydantic.dev/

WARNING: This is an auto-generated file.
Do not edit directly - any changes will be overwritten.
"""

{% import "python-macros.jinja" as utils %}
## This is a generated file. Do not modify it manually!

from __future__ import annotations
from pydantic import BaseModel, Field, ConfigDict
from typing import Optional, Generic, TypeVar, Union
from enum import Enum
from uuid import uuid4
from datetime import date, datetime
{%- if config.astropy %}
from mdmodels.units.annotation import UnitDefinitionAnnot
{%- endif %}

# Filter Wrapper definition used to filter a list of objects
# based on their attributes
Cls = TypeVar("Cls")

class FilterWrapper(Generic[Cls]):
    """Wrapper class to filter a list of objects based on their attributes"""

    def __init__(self, collection: list[Cls], **kwargs):
        self.collection = collection
        self.kwargs = kwargs

    def filter(self) -> list[Cls]:
        for key, value in self.kwargs.items():
            self.collection = [
                item for item in self.collection if self._fetch_attr(key, item) == value
            ]
        return self.collection

    def _fetch_attr(self, name: str, item: Cls):
        try:
            return getattr(item, name)
        except AttributeError:
            raise AttributeError(f"{item} does not have attribute {name}")


# JSON-LD Helper Functions
def add_namespace(obj, prefix: str | None, iri: str | None):
    """Adds a namespace to the JSON-LD context

    Args:
        prefix (str): The prefix to add
        iri (str): The IRI to add
    """
    if prefix is None and iri is None:
        return
    elif prefix and iri is None:
        raise ValueError("If prefix is provided, iri must also be provided")
    elif iri and prefix is None:
        raise ValueError("If iri is provided, prefix must also be provided")

    obj.ld_context[prefix] = iri # type: ignore

def validate_prefix(term: str | dict, prefix: str):
    """Validates that a term is prefixed with a given prefix

    Args:
        term (str): The term to validate
        prefix (str): The prefix to validate against

    Returns:
        bool: True if the term is prefixed with the prefix, False otherwise
    """

    if isinstance(term, dict) and not term["@id"].startswith(prefix + ":"):
        raise ValueError(f"Term {term} is not prefixed with {prefix}")
    elif isinstance(term, str) and not term.startswith(prefix + ":"):
        raise ValueError(f"Term {term} is not prefixed with {prefix}")

# Model Definitions
{% for object in objects %}
{%- if not (astropy and object.name in ["UnitDefinition", "BaseUnit"]) %}
class {{ object.name }}(BaseModel):

    model_config: ConfigDict = ConfigDict( # type: ignore
        validate_assignment = True,
        populate_by_name = True,
    ) # type: ignore
    {% for attribute in object.attributes %}
    {%- if attribute.multiple is true %}
    {{ attribute.name | to_identifier }}: {{ utils.get_type(attribute) }} = Field(
        default_factory=list,
        {%- if (attribute.name | to_identifier) != attribute.name %}
        alias="{{ attribute.name }}",
        {%- endif %}
        description="""{{ wrap(attribute.docstring, 50, "", "        ", None ) }}""",
    )
    {%- elif 'default' in attribute%}
    {{ attribute.name | to_identifier }}: {{ utils.get_type(attribute) }} = Field(
        default{{ utils.get_default(attribute) }},
        {%- if (attribute.name | to_identifier) != attribute.name %}
        alias="{{ attribute.name }}",
        {%- endif %}
        description="""{{ wrap(attribute.docstring, 50, "", "        ", None ) }}""",
    )
    {%- elif attribute.required is true %}
    {{ attribute.name | to_identifier }}: {{ utils.get_type(attribute) }} = Field(
        default=...,
        {%- if (attribute.name | to_identifier) != attribute.name %}
        alias="{{ attribute.name }}",
        {%- endif %}
        description="""{{ wrap(attribute.docstring, 50, "", "        ", None ) }}""",
    )
    {%- else %}
    {{ attribute.name | to_identifier }}: {{ utils.get_type(attribute) }} = Field(
        default=None,
        {%- if (attribute.name | to_identifier) != attribute.name %}
        alias="{{ attribute.name }}",
        {%- endif %}
        description="""{{ wrap(attribute.docstring, 50, "", "        ", None ) }}""",
    )
    {%- endif %}
    {%- endfor %}

    # JSON-LD fields
    ld_id: str = Field(
        serialization_alias="@id",
        default_factory=lambda: "{{ prefix }}:{{ object.name }}/" + str(uuid4())
    )
    ld_type: list[str] = Field(
        serialization_alias="@type",
        default_factory = lambda: [
            "{{ prefix }}:{{ object.name }}",
            {%- if object.term -%}"{{ object.term }}"{%- endif %}
        ],
    )
    ld_context: dict[str, str | dict] = Field(
        serialization_alias="@context",
        default_factory = lambda: {
            "{{ prefix }}": "{{ repo }}",
            {%- for prefix, address in prefixes %}
            "{{ prefix }}": "{{ address }}",
            {%- endfor %}
            {%- for attribute in object.attributes %}
            {%- if attribute.dtypes[0] in object_names %}
            "{{ attribute.dtypes[0] }}": "{{ repo }}{%- if repo[-1] != "/" -%}#{%- endif -%}{{ attribute.dtypes[0] }}/",
            {%- elif attribute.dtypes[0] in enum_names %}
            "{{ attribute.dtypes[0] }}": "{{ repo }}{%- if repo[-1] != "/" -%}#{%- endif -%}{{ attribute.dtypes[0] }}/",
            {%- endif %}
            {%- endfor %}
            {%- for attribute in object.attributes %}
            {%- if attribute.is_id %}
            "{{ attribute.name }}": {
                {%- if attribute.term %}
                "@id": "{{ attribute.term }}",
                {%- endif %}
                "@type": "@id",
            },
            {%- elif attribute.term %}
            "{{ attribute.name }}": "{{ attribute.term }}",
            {%- endif -%}
            {%- endfor %}
        }
    )
    {% for attr in object.attributes -%}
    {%- if attr.multiple is true and attr.dtypes[0] in object_names %}
    def filter_{{ attr.name | to_identifier }}(self, **kwargs) -> list[{{ attr.dtypes[0] }}]:
        """Filters the {{ attr.name }} attribute based on the given kwargs

        Args:
            **kwargs: The attributes to filter by.

        Returns:
            list[{{ attr.dtypes[0] }}]: The filtered list of {{ attr.dtypes[0] }} objects
        """

        return FilterWrapper[{{ attr.dtypes[0] }}](self.{{ attr.name | to_identifier }}, **kwargs).filter()
    {% endif %}
    {%- endfor %}

    def set_attr_term(
        self,
        attr: str,
        term: str | dict,
        prefix: str | None = None,
        iri: str | None = None
    ):
        """Sets the term for a given attribute in the JSON-LD object

        Example:
            # Using an IRI term
            >> obj.set_attr_term("name", "http://schema.org/givenName")

            # Using a prefix and term
            >> obj.set_attr_term("name", "schema:givenName", "schema", "http://schema.org")

            # Usinng a dictionary term
            >> obj.set_attr_term("name", {"@id": "http://schema.org/givenName", "@type": "@id"})

        Args:
            attr (str): The attribute to set the term for
            term (str | dict): The term to set for the attribute

        Raises:
            AssertionError: If the attribute is not found in the model
        """

        assert attr in self.model_fields, f"Attribute {attr} not found in {self.__class__.__name__}"

        if prefix:
            validate_prefix(term, prefix)

        add_namespace(self, prefix, iri)
        self.ld_context[attr] = term

    def add_type_term(
        self,
        term: str,
        prefix: str | None = None,
        iri: str | None = None
    ):
        """Adds a term to the @type field of the JSON-LD object

        Example:
            # Using a term
            >> obj.add_type_term("https://schema.org/Person")

            # Using a prefixed term
            >> obj.add_type_term("schema:Person", "schema", "https://schema.org/Person")

        Args:
            term (str): The term to add to the @type field
            prefix (str, optional): The prefix to use for the term. Defaults to None.
            iri (str, optional): The IRI to use for the term prefix. Defaults to None.

        Raises:
            ValueError: If prefix is provided but iri is not
            ValueError: If iri is provided but prefix is not
        """

        if prefix:
            validate_prefix(term, prefix)

        add_namespace(self, prefix, iri)
        self.ld_type.append(term)

    {% for attr in object.attributes %}
    {% for dtype in attr.dtypes %}
    {%- if dtype in object_names and attr.multiple is true %}
    def add_to_{{ attr.name | to_identifier }}(
        {{ utils.signature(objects, dtype) }}
    ):
        params = { {{ utils.params(objects, dtype) }}
        }

        if "id" in kwargs:
            params["id"] = kwargs["id"]

        self.{{ attr.name | to_identifier }}.append(
            {{ dtype }}(**params)
        )

        return self.{{ attr.name | to_identifier }}[-1]

    {%- endif %}
    {%- endfor %}
    {% endfor %}
{%- endif %}
{%- endfor %}

{%- for enum in enums %}
class {{ enum.name }}(Enum):
    {%- for key, value in enum.mappings | dictsort %}
    {{ key }} = "{{ value }}"
    {%- endfor %}
{% endfor %}