Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add models for per-entry & per-property metadata #2168

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 17 additions & 4 deletions openapi/index_openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -477,6 +477,19 @@
"title": "BaseRelationshipResource",
"description": "Minimum requirements to represent a relationship resource"
},
"EntryMetadata": {
"properties": {
"property_metadata": {
"type": "object",
"title": "Property Metadata",
"description": "An object containing per-entry and per-property metadata. The keys are the names of the fields in attributes for which metadata is available. The values belonging to these keys are dictionaries containing the relevant metadata fields. See also [Metadata properties](https://github.com/Materials-Consortia/OPTIMADE/blob/develop/optimade.rst#metadata-properties)"
}
},
"additionalProperties": true,
"type": "object",
"title": "EntryMetadata",
"description": "Contains the metadata for the attributes of an entry"
},
"EntryRelationships": {
"properties": {
"references": {
Expand Down Expand Up @@ -536,13 +549,13 @@
"meta": {
"anyOf": [
{
"$ref": "#/components/schemas/Meta"
"$ref": "#/components/schemas/EntryMetadata"
},
{
"type": "null"
}
],
"description": "a meta object containing non-standard meta-information about a resource that can not be represented as an attribute or relationship."
"description": "A [JSON API meta object](https://jsonapi.org/format/1.1/#document-meta) that is used to communicate metadata."
},
"attributes": {
"$ref": "#/components/schemas/EntryResourceAttributes",
Expand Down Expand Up @@ -1283,13 +1296,13 @@
"meta": {
"anyOf": [
{
"$ref": "#/components/schemas/Meta"
"$ref": "#/components/schemas/EntryMetadata"
},
{
"type": "null"
}
],
"description": "a meta object containing non-standard meta-information about a resource that can not be represented as an attribute or relationship."
"description": "A [JSON API meta object](https://jsonapi.org/format/1.1/#document-meta) that is used to communicate metadata."
},
"attributes": {
"$ref": "#/components/schemas/LinksResourceAttributes",
Expand Down
29 changes: 21 additions & 8 deletions openapi/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -1797,6 +1797,19 @@
],
"title": "EntryInfoResponse"
},
"EntryMetadata": {
"properties": {
"property_metadata": {
"type": "object",
"title": "Property Metadata",
"description": "An object containing per-entry and per-property metadata. The keys are the names of the fields in attributes for which metadata is available. The values belonging to these keys are dictionaries containing the relevant metadata fields. See also [Metadata properties](https://github.com/Materials-Consortia/OPTIMADE/blob/develop/optimade.rst#metadata-properties)"
}
},
"additionalProperties": true,
"type": "object",
"title": "EntryMetadata",
"description": "Contains the metadata for the attributes of an entry"
},
"EntryRelationships": {
"properties": {
"references": {
Expand Down Expand Up @@ -1856,13 +1869,13 @@
"meta": {
"anyOf": [
{
"$ref": "#/components/schemas/Meta"
"$ref": "#/components/schemas/EntryMetadata"
},
{
"type": "null"
}
],
"description": "a meta object containing non-standard meta-information about a resource that can not be represented as an attribute or relationship."
"description": "A [JSON API meta object](https://jsonapi.org/format/1.1/#document-meta) that is used to communicate metadata."
},
"attributes": {
"$ref": "#/components/schemas/EntryResourceAttributes",
Expand Down Expand Up @@ -2443,13 +2456,13 @@
"meta": {
"anyOf": [
{
"$ref": "#/components/schemas/Meta"
"$ref": "#/components/schemas/EntryMetadata"
},
{
"type": "null"
}
],
"description": "a meta object containing non-standard meta-information about a resource that can not be represented as an attribute or relationship."
"description": "A [JSON API meta object](https://jsonapi.org/format/1.1/#document-meta) that is used to communicate metadata."
},
"attributes": {
"$ref": "#/components/schemas/LinksResourceAttributes",
Expand Down Expand Up @@ -2943,13 +2956,13 @@
"meta": {
"anyOf": [
{
"$ref": "#/components/schemas/Meta"
"$ref": "#/components/schemas/EntryMetadata"
},
{
"type": "null"
}
],
"description": "a meta object containing non-standard meta-information about a resource that can not be represented as an attribute or relationship."
"description": "A [JSON API meta object](https://jsonapi.org/format/1.1/#document-meta) that is used to communicate metadata."
},
"attributes": {
"$ref": "#/components/schemas/ReferenceResourceAttributes"
Expand Down Expand Up @@ -4070,13 +4083,13 @@
"meta": {
"anyOf": [
{
"$ref": "#/components/schemas/Meta"
"$ref": "#/components/schemas/EntryMetadata"
},
{
"type": "null"
}
],
"description": "a meta object containing non-standard meta-information about a resource that can not be represented as an attribute or relationship."
"description": "A [JSON API meta object](https://jsonapi.org/format/1.1/#document-meta) that is used to communicate metadata."
},
"attributes": {
"$ref": "#/components/schemas/StructureResourceAttributes"
Expand Down
83 changes: 81 additions & 2 deletions optimade/models/entries.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
from datetime import datetime
from typing import Annotated, Any, ClassVar, Literal

from pydantic import BaseModel, field_validator
from pydantic import BaseModel, field_validator, model_validator

from optimade.models.jsonapi import Attributes, Relationships, Resource
from optimade.models.jsonapi import Attributes, Meta, Relationships, Resource
from optimade.models.optimade_json import (
BaseRelationshipResource,
DataType,
Expand Down Expand Up @@ -117,6 +117,36 @@ def cast_immutable_id_to_str(cls, value: Any) -> str:
return value


class EntryMetadata(Meta):
"""Contains the metadata for the attributes of an entry"""

property_metadata: dict = StrictField(
None,
description="""An object containing per-entry and per-property metadata. The keys are the names of the fields in attributes for which metadata is available. The values belonging to these keys are dictionaries containing the relevant metadata fields. See also [Metadata properties](https://github.com/Materials-Consortia/OPTIMADE/blob/develop/optimade.rst#metadata-properties)""",
)

@field_validator("property_metadata", mode="before")
def check_property_metadata_subfields(cls, value: Any) -> dict:
"""Loop through any per-property metadata field and check that
the subfields are prefixed correctly.

"""
error_fields: list[str] = []

if value is not None and isinstance(value, dict):
for field in value:
if property_metadata := value.get(field):
for subfield in property_metadata:
if not subfield.startswith("_"):
error_fields.append(subfield)

if error_fields:
raise ValueError(
f"The keys under the field `property_metadata` need to be prefixed. The field(s) {error_fields} are not prefixed."
)
return value


class EntryResource(Resource):
"""The base model for an entry resource."""

Expand Down Expand Up @@ -171,6 +201,14 @@ class EntryResource(Resource):
),
]

meta: Annotated[
EntryMetadata | None,
StrictField(
None,
description="""A [JSON API meta object](https://jsonapi.org/format/1.1/#document-meta) that is used to communicate metadata.""",
),
] = None

relationships: Annotated[
EntryRelationships | None,
StrictField(
Expand All @@ -179,6 +217,47 @@ class EntryResource(Resource):
),
] = None

@model_validator(mode="before")
def check_meta(cls, data: Any) -> dict | None:
"""Validator to check whether the per-entry `meta` field is valid,
including stripping out any per-property metadata for properties that
do not otherwise appear in the model.

"""

meta = data.get("meta")
if not meta:
return data

if property_metadata := meta.pop("property_metadata", None):
# check that all the fields under property metadata are in attributes
attributes = data.get("attributes", {})
property_error_fields: list[str] = []
for subfield in property_metadata:
if subfield not in attributes:
property_error_fields.append(subfield)

if property_error_fields:
raise ValueError(
f"The keys under the field `property_metadata` need to match with the field names in attributes. The field(s) {property_error_fields} are however not present in attributes {attributes}"
)

meta["property_metadata"] = property_metadata

meta_error_fields: list[str] = []
for field in meta:
if field not in EntryMetadata.model_fields:
if not field.startswith("_"):
meta_error_fields.append(field)

if meta_error_fields:
raise ValueError(
f"The keys under the field `meta` need to be prefixed if not otherwise defined. The field(s) {meta_error_fields} are not defined for per-entry `meta`."
)

data["meta"] = meta
return data


class EntryInfoProperty(BaseModel):
description: Annotated[
Expand Down
58 changes: 58 additions & 0 deletions optimade/server/data/test_structures.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@
"_id": {
"$oid": "5cfb441f053b174410700d02"
},
"meta": {
"property_metadata": {
"elements_ratios": {
"_exmpl_originates_from_project": "Pure Metals"
}
}
},
"assemblies": null,
"chemsys": "Ac",
"cartesian_site_positions": [
Expand Down Expand Up @@ -80,6 +87,13 @@
"_id": {
"$oid": "5cfb441f053b174410700d03"
},
"meta": {
"property_metadata": {
"elements_ratios": {
"_exmpl_originates_from_project": "Actinides_Alloys"
}
}
},
"assemblies": null,
"chemsys": "Ac-Ag-Ir",
"cartesian_site_positions": [
Expand Down Expand Up @@ -197,6 +211,13 @@
"_id": {
"$oid": "5cfb441f053b174410700d04"
},
"meta": {
"property_metadata": {
"elements_ratios": {
"_exmpl_originates_from_project": "Actinides_Alloys"
}
}
},
"assemblies": null,
"chemsys": "Ac-Ag-Pb",
"cartesian_site_positions": [
Expand Down Expand Up @@ -323,6 +344,13 @@
"_id": {
"$oid": "5cfb441f053b174410700d18"
},
"meta": {
"property_metadata": {
"elements_ratios": {
"_exmpl_originates_from_project": "Actinides_Alloys"
}
}
},
"assemblies": null,
"chemsys": "Ac-Mg",
"cartesian_site_positions": [
Expand Down Expand Up @@ -413,6 +441,13 @@
"_id": {
"$oid": "5cfb441f053b174410700d1f"
},
"meta": {
"property_metadata": {
"elements_ratios": {
"_exmpl_originates_from_project": null
}
}
},
"assemblies": null,
"chemsys": "Ac-O",
"cartesian_site_positions": [
Expand Down Expand Up @@ -515,6 +550,11 @@
"_id": {
"$oid": "5cfb441f053b174410700d6f"
},
"meta": {
"property_metadata": {
"elements_ratios": {}
}
},
"assemblies": null,
"chemsys": "Ac-Cu-F-O",
"cartesian_site_positions": [
Expand Down Expand Up @@ -639,6 +679,13 @@
"_id": {
"$oid": "5cfb441f053b174410700dc9"
},
"meta": {
"property_metadata": {
"elements_ratios": {
"_exmpl_originates_from_project": "Pure Metals"
}
}
},
"assemblies": null,
"chemsys": "Ag",
"cartesian_site_positions": [
Expand Down Expand Up @@ -706,6 +753,11 @@
"_id": {
"$oid": "5cfb441f053b174410700ddd"
},
"meta": {
"property_metadata": {
"elements_ratios": null
}
},
"assemblies": null,
"chemsys": "Ag-Br-Cl-Te",
"cartesian_site_positions": [
Expand Down Expand Up @@ -896,6 +948,9 @@
"_id": {
"$oid": "5cfb441f053b174410700e04"
},
"meta": {
"property_metadata": {}
},
"assemblies": null,
"chemsys": "Ag-C-Cl-N-O-S",
"cartesian_site_positions": [
Expand Down Expand Up @@ -1072,6 +1127,9 @@
"_id": {
"$oid": "5cfb441f053b174410700e11"
},
"meta": {
"property_metadata": null
},
"assemblies": null,
"chemsys": "Ag-C-Cl-H-N",
"cartesian_site_positions": [
Expand Down
Loading
Loading