Coverage for harbor_cli/output/schema.py: 83%

67 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2023-01-26 09:12 +0100

1"""Somewhat experimental schema for (de)serializing data (JSON, YAML, etc.) 

2 

3Not to be confused with JSON Schema (<https://json-schema.org/specification.html>) 

4 

5The aim is to be able to serialize a Pydantic model to JSON, YAML, etc. and 

6include metadata about the model in the output. This metadata can 

7then be used to deserialize the data back into the correct Pydantic model. 

8 

9The benefit of this is that we can more easily print the data as tables 

10using the harborapi.models.BaseModel's as_table() method, and we can also use the 

11Pydantic models' custom validation, methods and properties. Furthermore, 

12we avoid the problem of forwards-compatibility of pickling data, 

13because we can always deserialize the data back into the most up-to-date 

14version of the data type. 

15 

16The difference between this and the built-in schema funtionality of Pydantic 

17is that we are not interested in actually exporting the full schema of all 

18the models, but rather just enough information to dynamically load the correct 

19model from the correct location when deserializing the data. 

20""" 

21from __future__ import annotations 

22 

23import importlib 

24import inspect 

25from pathlib import Path 

26from typing import Any 

27from typing import Dict 

28from typing import Generic 

29from typing import List 

30from typing import Optional 

31from typing import Sequence 

32from typing import TypeVar 

33from typing import Union 

34 

35from pydantic import BaseModel 

36from pydantic import root_validator 

37 

38 

39T = TypeVar("T") 

40 

41 

42class Schema(BaseModel, Generic[T]): 

43 """A schema for (de)serializing data (JSON, YAML, etc.)""" 

44 

45 version: str = "1.0.0" # TODO: use harborapi.models.SemVer? 

46 type: Optional[str] = None # should only be None if empty list 

47 module: Optional[str] = None 

48 data: Union[T, List[T]] 

49 

50 class Config: 

51 extra = "allow" 

52 

53 @classmethod 

54 def from_data(cls, data: T) -> Schema[T]: 

55 """Create a schema from data""" 

56 return cls(data=data) 

57 

58 @classmethod 

59 def from_file(cls, path: Path) -> Schema[T]: 

60 """Load a schema from a file""" 

61 if path.suffix == ".json": 61 ↛ 64line 61 didn't jump to line 64, because the condition on line 61 was never false

62 obj = cls.parse_file(path) 

63 else: 

64 raise ValueError(f"Unsupported file type {path.suffix}") 

65 return obj 

66 

67 @root_validator 

68 def set_type(cls, values: Dict[str, Any]) -> Dict[str, Any]: 

69 # If schema has type and module, we are loading from a file 

70 if values.get("type") is not None and values.get("module") is not None: 

71 cls._parse_data(values) 

72 return values 

73 

74 data = values.get("data") 

75 if isinstance(data, Sequence): 

76 if not data: 76 ↛ 77line 76 didn't jump to line 77, because the condition on line 76 was never true

77 return values 

78 data = data[0] 

79 

80 typ = type(data) 

81 if typ is None: 81 ↛ 82line 81 didn't jump to line 82, because the condition on line 81 was never true

82 return values # no validation to perform 

83 

84 module = inspect.getmodule(typ) 

85 if not module: 85 ↛ 86line 85 didn't jump to line 86, because the condition on line 85 was never true

86 raise ValueError(f"Unknown data type: {typ}") 

87 

88 # Determine the correct type name 

89 for n in [typ.__qualname__, typ.__name__]: 89 ↛ 97line 89 didn't jump to line 97, because the loop on line 89 didn't complete

90 try: 

91 typ = getattr(module, n) # check if we can getattr the type 

92 values["type"] = n 

93 break 

94 except AttributeError: 

95 pass 

96 else: 

97 raise ValueError(f"Unknown data type: {typ}") 

98 

99 values["module"] = module.__name__ 

100 cls._parse_data(values) 

101 return values 

102 

103 @classmethod 

104 def _parse_data(cls, values: Dict[str, Any]) -> None: 

105 """Parses the value of data into the correct type if possible.""" 

106 module = importlib.import_module(values["module"]) 

107 typ = getattr(module, values["type"]) 

108 try: 

109 if issubclass(typ, BaseModel): 

110 values["data"] = typ.parse_obj(values["data"]) 

111 except TypeError: 

112 pass