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
« 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.)
3Not to be confused with JSON Schema (<https://json-schema.org/specification.html>)
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.
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.
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
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
35from pydantic import BaseModel
36from pydantic import root_validator
39T = TypeVar("T")
42class Schema(BaseModel, Generic[T]):
43 """A schema for (de)serializing data (JSON, YAML, etc.)"""
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]]
50 class Config:
51 extra = "allow"
53 @classmethod
54 def from_data(cls, data: T) -> Schema[T]:
55 """Create a schema from data"""
56 return cls(data=data)
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
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
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]
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
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}")
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}")
99 values["module"] = module.__name__
100 cls._parse_data(values)
101 return values
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