Coverage for src/metador_core/schema/ld.py: 100%
41 statements
« prev ^ index » next coverage.py v7.3.2, created at 2023-11-02 09:33 +0000
« prev ^ index » next coverage.py v7.3.2, created at 2023-11-02 09:33 +0000
1"""Helpers to create pydantic models that parse into and (de-)serialize to JSON-LD."""
2from __future__ import annotations
4from collections import ChainMap
5from typing import Any, Dict, Mapping, Optional, TypeVar, Union
7from pydantic import Extra, Field
8from typing_extensions import Annotated, TypeAlias
10from .core import MetadataSchema
11from .decorators import add_const_fields
12from .types import NonEmptyStr
15def with_key_prefix(prefix: str, dct: Mapping[str, Any]) -> Dict[str, Any]:
16 """Return new dict with all keys prefixed by `prefix`."""
17 return {f"{prefix}{k}": v for k, v in dct.items() if v is not None}
20def ld_decorator(**presets):
21 """Return LD schema decorator with pre-set fields, e.g. `@context`.
23 The returned decorator will attach the passed fields to a schema.
25 All additional fields passed to the decorator will also be added,
26 if not present, or override the default that is passed to this function.
28 Note that the pre-set fields will ALWAYS override existing fields,
29 regardless of the state of the override flag.
31 Example usage:
32 Pass your `@context` as `context` to this decorator factory.
33 Use the returned decorator with `type` in order to
34 set both the `@context` and `@type` for a schema.
36 By default, will silently override any inherited
37 constant fields that already exist in the schema.
38 """
40 def decorator(schema=None, *, override: bool = True, **kwargs):
41 fields = with_key_prefix("@", ChainMap(kwargs, presets))
42 dec = add_const_fields(fields, override=override)
43 return dec if schema is None else dec(schema)
45 return decorator
48ld = ld_decorator()
49"""Decorator to add constant JSON-LD fields equal for all instances of a schema."""
52class LDSchema(MetadataSchema):
53 """Semantically enriched schema for JSON-LD."""
55 id_: Annotated[Optional[NonEmptyStr], Field(alias="@id")]
57 def ref(self) -> LDIdRef:
58 """Return LDIdRef, i.e. a pure @id reference for object.
60 Throws an exception if no @id is found.
61 """
62 if self.id_ is None:
63 raise ValueError("Object has no @id attribute!")
64 return LDIdRef(id_=self.id_)
66 @property
67 def is_ld_ref(self):
68 return False
71class LDIdRef(LDSchema):
72 """Object with just an @id reference (more info is given elsewhere)."""
74 class Config:
75 extra = Extra.forbid
77 id_: Annotated[NonEmptyStr, Field(alias="@id")]
79 def ref(self) -> LDIdRef:
80 return self
82 @property
83 def is_ld_ref(self):
84 return True
87T = TypeVar("T", bound=LDSchema)
89LDOrRef: TypeAlias = Union[LDIdRef, T]
90"""LDOrRef[T] is either an object of LD Schema T, or a reference to an object.
92An LD reference is just an object with an @id.
93"""
95LDRef: TypeAlias = LDIdRef # Annotated[LDIdRef, T] # <- does not work :(
96"""LDRef[T] is a reference to an object of type T.
98An LD reference is just an object with an @id.
99"""