"""Mixin classes."""
from __future__ import annotations
from abc import ABC, abstractmethod
from collections.abc import Iterable
from typing import Generic, Literal, TypeVar, overload
from typing_extensions import Self
from .api import Converter
__all__ = [
"SemanticallyProcessable",
"SemanticallyStandardizable",
"process",
"process_many",
"standardize",
"standardize_many",
]
X = TypeVar("X")
[docs]
class SemanticallyProcessable(ABC, Generic[X]):
"""A class that can be processed with a converter.
The goal of this class is to standardize objects that come with unprocessed URIs
that can be processed into references with respect to a :class:`curies.Converter`.
For example, this is useful for :mod:`obographs` and :mod:`jskos`.
.. code-block:: python
from pydantic import BaseModel
from curies import SemanticallyProcessable
class ProcessedEntity(BaseModel):
reference: Reference
class RawEntity(BaseModel, SemanticallyProcessable[ProcessedEntity]):
uri: str
def process(self, converter: Converter) -> ProcessedEntity:
return ProcessedEntity(
reference=converter.parse_uri(self.uri, strict=True).to_pydantic()
)
:mod:`curies` provides a high-level interface for standardizing classes in
:func:`curies.process`.
.. code-block:: python
from curies import Converter
converter = Converter.from_prefix_map({"CHEBI": "http://purl.obolibrary.org/obo/CHEBI_"})
e1 = RawEntity(uri="http://purl.obolibrary.org/obo/CHEBI_1")
e2 = RawEntity(uri="http://purl.obolibrary.org/obo/CHEBI_2")
# can be used directly on an object
assert ProcessedEntity(reference=Reference.from_curie("CHEBI:1")) == curies.standardize(
e1, converter
)
# can also be used on an iterable/collection
assert [
ProcessedEntity(reference=Reference.from_curie("CHEBI:1")),
ProcessedEntity(reference=Reference.from_curie("CHEBI:2")),
] == curies.process((e1, e2), converter)
"""
[docs]
@abstractmethod
def process(self, converter: Converter) -> X:
"""Process this raw instance."""
raise NotImplementedError
# docstr-coverage:excused `overload`
@overload
def process(instances: None, converter: Converter, *, return_iterator: bool = ...) -> None: ...
# docstr-coverage:excused `overload`
@overload
def process(
instances: SemanticallyProcessable[X], converter: Converter, *, return_iterator: bool = ...
) -> X: ...
# docstr-coverage:excused `overload`
@overload
def process(
instances: Iterable[SemanticallyProcessable[X]],
converter: Converter,
*,
return_iterator: Literal[False] = ...,
) -> list[X]: ...
# docstr-coverage:excused `overload`
@overload
def process(
instances: Iterable[SemanticallyProcessable[X]],
converter: Converter,
*,
return_iterator: Literal[True] = ...,
) -> Iterable[X]: ...
[docs]
def process(
instances: SemanticallyProcessable[X] | Iterable[SemanticallyProcessable[X]] | None,
converter: Converter,
*,
return_iterator: bool = False,
) -> X | list[X] | Iterable[X] | None:
"""Process multiple semantically processable instances."""
if instances is None:
return None
elif isinstance(instances, Iterable):
if return_iterator:
return (instance.process(converter) for instance in instances)
else:
return [instance.process(converter) for instance in instances]
else:
return instances.process(converter)
process_many = process
[docs]
class SemanticallyStandardizable(ABC):
"""An object that can be standardized.
In the following example, a simple object is constructed:
.. code-block:: python
from typing_extensions import Self
from curies import Converter, Reference, SemanticallyStandardizable
class ReferenceHolder(SemanticallyStandardizable):
def __init__(self, reference):
self.reference = reference
def standardize(self, converter: Converter) -> Self:
return ReferenceHolder(converter.standardize_reference(self.reference, strict=True))
It's good form to make these operations return new objects, but there's no reason
you couldn't update the object in place like in :
.. code-block:: python
from typing_extensions import Self
from curies import Converter, Reference, SemanticallyStandardizable
class ReferenceHolder(SemanticallyStandardizable):
def __init__(self, reference):
self.reference = reference
def standardize(self, converter: Converter) -> Self:
self.reference = converter.standardize_reference(self.reference, strict=True)
return self
In the following example, the :meth:`pydantic.BaseModel.model_copy` is used to
automatically reuse all other fields that aren't updated, which creates a new
object.
.. code-block:: python
import datetime
from typing_extensions import Self
from curies import Converter, Reference, SemanticallyStandardizable
from pydantic import BaseModel
class Triple(BaseModel, SemanticallyStandardizable):
subject: Reference
predicate: Reference
object: Reference
date_asserted: datetime.date
def standardize(self, converter: Converter) -> Self:
return self.model_copy(
update={
"subject": converter.standardize_reference(self.subject, strict=True),
"predicate": converter.standardize_reference(self.predicate, strict=True),
"object": converter.standardize_reference(self.object, strict=True),
}
)
:mod:`curies` provides a high-level interface for standardizing classes in
:func:`curies.standardize`.
.. code-block:: python
from curies import Converter
converter = Converter()
converter.add_prefix("CHEBI", "http://purl.obolibrary.org/obo/CHEBI_")
converter.add_synonym("CHEBI", "chebi")
r1 = ReferenceHolder(Reference.from_curie("chebi:1"))
r2 = ReferenceHolder(Reference.from_curie("chebi:2"))
# can be used directly on an object
assert ReferenceHolder(Reference.from_curie("CHEBI:1")) == curies.standardize(r1, converter)
# can also be used on an iterable/collection
assert [
ReferenceHolder(Reference.from_curie("CHEBI:1")),
ReferenceHolder(Reference.from_curie("CHEBI:2")),
] == curies.standardize((r1, r2), converter)
"""
[docs]
@abstractmethod
def standardize(self, converter: Converter) -> Self:
"""Standardize all references in the object."""
raise NotImplementedError
SemanticallyStandardizableType = TypeVar(
"SemanticallyStandardizableType", bound=SemanticallyStandardizable
)
# docstr-coverage:excused `overload`
@overload
def standardize(instances: None, converter: Converter, *, return_iterator: bool = ...) -> None: ...
# docstr-coverage:excused `overload`
@overload
def standardize(
instances: SemanticallyStandardizableType, converter: Converter, *, return_iterator: bool = ...
) -> SemanticallyStandardizableType: ...
# docstr-coverage:excused `overload`
@overload
def standardize(
instances: Iterable[SemanticallyStandardizableType],
converter: Converter,
*,
return_iterator: Literal[True] = ...,
) -> Iterable[SemanticallyStandardizableType]: ...
# docstr-coverage:excused `overload`
@overload
def standardize(
instances: Iterable[SemanticallyStandardizableType],
converter: Converter,
*,
return_iterator: Literal[False] = ...,
) -> list[SemanticallyStandardizableType]: ...
[docs]
def standardize(
instances: SemanticallyStandardizableType | Iterable[SemanticallyStandardizableType] | None,
converter: Converter,
*,
return_iterator: bool = False,
) -> (
SemanticallyStandardizableType
| Iterable[SemanticallyStandardizableType]
| list[SemanticallyStandardizableType]
| None
):
"""Standardize an instance."""
if instances is None:
return None
elif isinstance(instances, Iterable):
if return_iterator:
return (instance.standardize(converter) for instance in instances)
else:
return [instance.standardize(converter) for instance in instances]
else:
return instances.standardize(converter)
standardize_many = standardize