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

1"""Helpers to create pydantic models that parse into and (de-)serialize to JSON-LD.""" 

2from __future__ import annotations 

3 

4from collections import ChainMap 

5from typing import Any, Dict, Mapping, Optional, TypeVar, Union 

6 

7from pydantic import Extra, Field 

8from typing_extensions import Annotated, TypeAlias 

9 

10from .core import MetadataSchema 

11from .decorators import add_const_fields 

12from .types import NonEmptyStr 

13 

14 

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} 

18 

19 

20def ld_decorator(**presets): 

21 """Return LD schema decorator with pre-set fields, e.g. `@context`. 

22 

23 The returned decorator will attach the passed fields to a schema. 

24 

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. 

27 

28 Note that the pre-set fields will ALWAYS override existing fields, 

29 regardless of the state of the override flag. 

30 

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. 

35 

36 By default, will silently override any inherited 

37 constant fields that already exist in the schema. 

38 """ 

39 

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) 

44 

45 return decorator 

46 

47 

48ld = ld_decorator() 

49"""Decorator to add constant JSON-LD fields equal for all instances of a schema.""" 

50 

51 

52class LDSchema(MetadataSchema): 

53 """Semantically enriched schema for JSON-LD.""" 

54 

55 id_: Annotated[Optional[NonEmptyStr], Field(alias="@id")] 

56 

57 def ref(self) -> LDIdRef: 

58 """Return LDIdRef, i.e. a pure @id reference for object. 

59 

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_) 

65 

66 @property 

67 def is_ld_ref(self): 

68 return False 

69 

70 

71class LDIdRef(LDSchema): 

72 """Object with just an @id reference (more info is given elsewhere).""" 

73 

74 class Config: 

75 extra = Extra.forbid 

76 

77 id_: Annotated[NonEmptyStr, Field(alias="@id")] 

78 

79 def ref(self) -> LDIdRef: 

80 return self 

81 

82 @property 

83 def is_ld_ref(self): 

84 return True 

85 

86 

87T = TypeVar("T", bound=LDSchema) 

88 

89LDOrRef: TypeAlias = Union[LDIdRef, T] 

90"""LDOrRef[T] is either an object of LD Schema T, or a reference to an object. 

91 

92An LD reference is just an object with an @id. 

93""" 

94 

95LDRef: TypeAlias = LDIdRef # Annotated[LDIdRef, T] # <- does not work :( 

96"""LDRef[T] is a reference to an object of type T. 

97 

98An LD reference is just an object with an @id. 

99"""