-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Separate out entity models in SOFT and DLite (#74)
Create a "minimum set" of fields for the versioned SOFT models and then create the DLite implementations of these separately. Split DLite models to base and multi-inheritance.
- Loading branch information
Showing
14 changed files
with
330 additions
and
368 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
"""DLite model.""" | ||
|
||
from __future__ import annotations | ||
|
||
from typing import Annotated, Literal | ||
|
||
from pydantic import AliasChoices, BaseModel, Field | ||
from pydantic.networks import AnyHttpUrl | ||
|
||
|
||
class DLiteProperty(BaseModel): | ||
"""The defining metadata for a DLite Entity's property.""" | ||
|
||
ref: Annotated[ | ||
AnyHttpUrl | None, | ||
Field( | ||
validation_alias=AliasChoices("$ref", "ref"), | ||
serialization_alias="$ref", | ||
description=( | ||
"Formally a part of type. `$ref` is used together with the `ref` type, " | ||
"which is a special datatype for referring to other instances." | ||
), | ||
), | ||
] = None | ||
|
||
|
||
class DLiteEntity(BaseModel): | ||
"""A DLite Entity.""" | ||
|
||
meta: Annotated[ | ||
Literal["http://onto-ns.com/meta/0.3/EntitySchema"], | ||
Field( | ||
description=( | ||
"URI for the metadata entity. For all entities at onto-ns.com, the " | ||
"EntitySchema v0.3 is used." | ||
), | ||
), | ||
] = "http://onto-ns.com/meta/0.3/EntitySchema" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
"""DLite Entity model based on the SOFT5 Entity model.""" | ||
|
||
from __future__ import annotations | ||
|
||
from typing import Annotated | ||
|
||
from pydantic import Field | ||
|
||
from entities_service.models.dlite_soft import DLiteEntity, DLiteProperty | ||
from entities_service.models.soft5 import SOFT5Entity, SOFT5Property | ||
|
||
|
||
class DLiteSOFT5Property(SOFT5Property, DLiteProperty): | ||
"""The defining metadata for a (SOFT5-based) DLite Entity's property.""" | ||
|
||
|
||
class DLiteSOFT5Entity(SOFT5Entity, DLiteEntity): | ||
"""A (SOFT5-based) DLite Entity.""" | ||
|
||
properties: Annotated[ # type: ignore[assignment] | ||
list[DLiteSOFT5Property], Field(description="A list of properties.") | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
"""DLite Entity model based on the SOFT7 Entity model.""" | ||
|
||
from __future__ import annotations | ||
|
||
from typing import Annotated | ||
|
||
from pydantic import Field | ||
|
||
from entities_service.models.dlite_soft import DLiteEntity, DLiteProperty | ||
from entities_service.models.soft7 import SOFT7Entity, SOFT7Property | ||
|
||
|
||
class DLiteSOFT7Property(SOFT7Property, DLiteProperty): | ||
"""The defining metadata for a (SOFT7-based) DLite Entity's property.""" | ||
|
||
|
||
class DLiteSOFT7Entity(SOFT7Entity, DLiteEntity): | ||
"""A (SOFT7-based) DLite Entity.""" | ||
|
||
properties: Annotated[ # type: ignore[assignment] | ||
dict[str, DLiteSOFT7Property], | ||
Field( | ||
description=( | ||
"A dictionary of properties, mapping the property name to a dictionary " | ||
"of metadata defining the property." | ||
), | ||
), | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,149 @@ | ||
"""Base (minimum set) models for SOFT entities.""" | ||
|
||
from __future__ import annotations | ||
|
||
import difflib | ||
import re | ||
from typing import Annotated, Any | ||
|
||
from pydantic import ( | ||
AliasChoices, | ||
BaseModel, | ||
ConfigDict, | ||
Field, | ||
field_validator, | ||
model_validator, | ||
) | ||
from pydantic.networks import AnyHttpUrl | ||
|
||
from entities_service.service.config import CONFIG | ||
|
||
URI_REGEX = re.compile( | ||
r"^(?P<namespace>https?://.+)/(?P<version>\d(?:\.\d+){0,2})/(?P<name>[^/#?]+)$" | ||
) | ||
"""Regular expression to parse a SOFT entity URI.""" | ||
|
||
|
||
class SOFTProperty(BaseModel): | ||
"""The minimum set of defining metadata for a SOFT Entity's property.""" | ||
|
||
model_config = ConfigDict(extra="forbid") | ||
|
||
type_: Annotated[ | ||
str, | ||
Field( | ||
alias="type", | ||
description="The type of the described property, e.g., an integer.", | ||
), | ||
] | ||
shape: Annotated[ | ||
list[str] | None, | ||
Field( | ||
description=( | ||
"The dimension of multi-dimensional properties. This is a list of " | ||
"dimension expressions referring to the dimensions defined above. For " | ||
"instance, if an entity have dimensions with names `H`, `K`, and `L` " | ||
"and a property with shape `['K', 'H+1']`, the property of an instance " | ||
"of this entity with dimension values `H=2`, `K=2`, `L=6` will have " | ||
"shape `[2, 3]`." | ||
), | ||
validation_alias=AliasChoices("dims", "shape"), | ||
), | ||
] = None | ||
unit: Annotated[str | None, Field(description="The unit of the property.")] = None | ||
description: Annotated[ | ||
str, Field(description="A human-readable description of the property.") | ||
] | ||
|
||
|
||
class SOFTEntity(BaseModel): | ||
"""A minimum Field set SOFT Entity to be used in a versioned SOFT Entity model.""" | ||
|
||
model_config = ConfigDict(extra="forbid") | ||
|
||
name: Annotated[str | None, Field(description="The name of the entity.")] = None | ||
version: Annotated[str | None, Field(description="The version of the entity.")] = ( | ||
None | ||
) | ||
namespace: Annotated[ | ||
AnyHttpUrl | None, Field(description="The namespace of the entity.") | ||
] = None | ||
uri: Annotated[ | ||
AnyHttpUrl | None, | ||
Field( | ||
description=( | ||
"The universal identifier for the entity. This MUST start with the base" | ||
" URL." | ||
), | ||
), | ||
] = None | ||
description: Annotated[str, Field(description="Description of the entity.")] = "" | ||
|
||
@field_validator("uri", "namespace", mode="after") | ||
@classmethod | ||
def _validate_base_url(cls, value: AnyHttpUrl) -> AnyHttpUrl: | ||
"""Validate `uri` and `namespace` starts with the current base URL for the | ||
service.""" | ||
if not str(value).startswith(str(CONFIG.base_url)): | ||
error_message = ( | ||
"This service only works with SOFT entities at " f"{CONFIG.base_url}.\n" | ||
) | ||
raise ValueError(error_message) | ||
return value | ||
|
||
@field_validator("uri", mode="after") | ||
@classmethod | ||
def _validate_uri(cls, value: AnyHttpUrl) -> AnyHttpUrl: | ||
"""Validate `uri` is consistent with `name`, `version`, and `namespace`.""" | ||
if URI_REGEX.match(str(value)) is None: | ||
error_message = ( | ||
"The 'uri' is not a valid SOFT7 entity URI. It must be of the form " | ||
f"{str(CONFIG.base_url).rstrip('/')}/{{version}}/{{name}}.\n" | ||
) | ||
raise ValueError(error_message) | ||
return value | ||
|
||
@model_validator(mode="before") | ||
@classmethod | ||
def _check_cross_dependent_fields(cls, data: Any) -> Any: | ||
"""Check that `name`, `version`, and `namespace` are all set or all unset.""" | ||
if ( | ||
isinstance(data, dict) | ||
and any(data.get(_) is None for _ in ("name", "version", "namespace")) | ||
and not all(data.get(_) is None for _ in ("name", "version", "namespace")) | ||
): | ||
error_message = ( | ||
"Either all of `name`, `version`, and `namespace` must be set " | ||
"or all must be unset.\n" | ||
) | ||
raise ValueError(error_message) | ||
|
||
if ( | ||
isinstance(data, dict) | ||
and any(data.get(_) is None for _ in ("name", "version", "namespace")) | ||
and data.get("uri") is None | ||
): | ||
error_message = ( | ||
"Either `name`, `version`, and `namespace` or `uri` must be set.\n" | ||
) | ||
raise ValueError(error_message) | ||
|
||
if ( | ||
isinstance(data, dict) | ||
and all(data.get(_) is not None for _ in ("name", "version", "namespace")) | ||
and data.get("uri") is not None | ||
and data["uri"] != f"{data['namespace']}/{data['version']}/{data['name']}" | ||
): | ||
# Ensure that `uri` is consistent with `name`, `version`, and `namespace`. | ||
diff = "\n ".join( | ||
difflib.ndiff( | ||
[data["uri"]], | ||
[f"{data['namespace']}/{data['version']}/{data['name']}"], | ||
) | ||
) | ||
error_message = ( | ||
"The `uri` is not consistent with `name`, `version`, and " | ||
f"`namespace`:\n\n {diff}\n\n" | ||
) | ||
raise ValueError(error_message) | ||
return data |
Oops, something went wrong.