pydoptic 0.0.post1.dev38__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pydoptic/__init__.py +3 -0
- pydoptic/base_model.py +463 -0
- pydoptic/py.typed +0 -0
- pydoptic/selector.py +1351 -0
- pydoptic/validate_types.py +104 -0
- pydoptic-0.0.post1.dev38.dist-info/METADATA +847 -0
- pydoptic-0.0.post1.dev38.dist-info/RECORD +23 -0
- pydoptic-0.0.post1.dev38.dist-info/WHEEL +5 -0
- pydoptic-0.0.post1.dev38.dist-info/licenses/LICENSE +21 -0
- pydoptic-0.0.post1.dev38.dist-info/top_level.txt +3 -0
- pydoptic_elastic/__init__.py +3 -0
- pydoptic_elastic/elastic_model.py +88 -0
- pydoptic_elastic/elastic_query.py +154 -0
- pydoptic_elastic/elastic_service.py +73 -0
- pydoptic_elastic/py.typed +0 -0
- pydoptic_sql/__init__.py +6 -0
- pydoptic_sql/sql_computed.py +93 -0
- pydoptic_sql/sql_constraint.py +782 -0
- pydoptic_sql/sql_having.py +728 -0
- pydoptic_sql/sql_order.py +28 -0
- pydoptic_sql/sql_query.py +1144 -0
- pydoptic_sql/sql_service.py +335 -0
- pydoptic_sql/sql_table.py +188 -0
pydoptic/__init__.py
ADDED
pydoptic/base_model.py
ADDED
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from pydoptic.selector import Discrim, Param, Prop, Select, PropSelect, SelectVal, SelectOpt, SelectArr, SelectOptArr, ModelLike, SelectValue, A, B, Selectable
|
|
3
|
+
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from inspect import isclass
|
|
6
|
+
from typing import Any, Callable, Dict, Generic, List, Self, Set, Type, TypeVar, TypedDict, get_args, get_origin, \
|
|
7
|
+
get_type_hints, Mapping, Tuple
|
|
8
|
+
|
|
9
|
+
from pydoptic.validate_types import validate_type, Validator
|
|
10
|
+
|
|
11
|
+
X = TypeVar("X")
|
|
12
|
+
Y = TypeVar("Y")
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class SelectProxy(Generic[B]):
|
|
16
|
+
name: str | None
|
|
17
|
+
data: Dict[str, Any]
|
|
18
|
+
_field_name: str | None = field(default=None, compare=False, repr=False)
|
|
19
|
+
|
|
20
|
+
def __set_name__(self, owner: type, name: str) -> None:
|
|
21
|
+
object.__setattr__(self, '_field_name', name)
|
|
22
|
+
|
|
23
|
+
def __get__(self, instance: Any, owner: type) -> Any:
|
|
24
|
+
"""
|
|
25
|
+
A customized field (`x: Prop[...] = select(...)`) already has a real class-attribute value (this proxy)
|
|
26
|
+
from class-body execution, so plain attribute lookup on it would never "fail" and never reach
|
|
27
|
+
`BaseModelMeta.__getattr__`'s fallback trigger. Implementing the descriptor protocol here lets an
|
|
28
|
+
unresolved customized field trigger resolution on first access the same way -- since it's a *non-data*
|
|
29
|
+
descriptor (no `__set__`), it never interferes with normal instance attribute access, and once resolved
|
|
30
|
+
it's replaced in-place by the real `Prop`/`Discrim`, so this only ever runs once per class.
|
|
31
|
+
"""
|
|
32
|
+
_resolve_properties(owner)
|
|
33
|
+
assert self._field_name is not None, 'SelectProxy.__get__ called before __set_name__ resolved the field name'
|
|
34
|
+
return getattr(owner, self._field_name)
|
|
35
|
+
|
|
36
|
+
def select(name: str | None = None, **data) -> Any:
|
|
37
|
+
return SelectProxy(name=name, data=data)
|
|
38
|
+
|
|
39
|
+
_MISSING = object()
|
|
40
|
+
|
|
41
|
+
def _find_given_value(cls: Type[Any], name: str) -> Any:
|
|
42
|
+
"""
|
|
43
|
+
Look up a class-body-assigned value for `name` by walking the MRO directly via each class's own `__dict__`,
|
|
44
|
+
bypassing the attribute-access protocol (`getattr`/`hasattr`) entirely. This matters specifically because
|
|
45
|
+
`SelectProxy.__get__` triggers resolution on access: calling `getattr` here, from inside the resolution of
|
|
46
|
+
`cls` itself, would recurse right back into resolving the very field being looked up. Returns `_MISSING` if
|
|
47
|
+
`name` isn't set anywhere in the MRO (as opposed to being explicitly set to `None`).
|
|
48
|
+
"""
|
|
49
|
+
for base in cls.__mro__:
|
|
50
|
+
if name in base.__dict__:
|
|
51
|
+
return base.__dict__[name]
|
|
52
|
+
return _MISSING
|
|
53
|
+
|
|
54
|
+
def _selector_from_select_proxy(name: str, select_type: Type[Select[Any, Any]], origin: Type[Any], target: Type[Any], proxy: SelectProxy) -> PropSelect[Any, Any]:
|
|
55
|
+
if issubclass(select_type, SelectVal):
|
|
56
|
+
return PropSelect.val(proxy.name or name, origin, target, proxy.data) # type: ignore
|
|
57
|
+
if issubclass(select_type, SelectOpt):
|
|
58
|
+
return PropSelect.opt(proxy.name or name, origin, target, proxy.data) # type: ignore
|
|
59
|
+
if issubclass(select_type, SelectArr):
|
|
60
|
+
return PropSelect.arr(proxy.name or name, origin, target, proxy.data) # type: ignore
|
|
61
|
+
if issubclass(select_type, SelectOptArr):
|
|
62
|
+
return PropSelect.opt_arr(proxy.name or name, origin, target, proxy.data) # type: ignore
|
|
63
|
+
|
|
64
|
+
raise ValueError(f'Unsupported subtype of Select: {select_type.__name__}. Use Prop, PropOpt, PropArr, or PropOptArr')
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _resolve_properties(cls: Type[Any]) -> Dict[str, PropSelect[Any, Any] | Discrim[Any, Any]]:
|
|
68
|
+
"""
|
|
69
|
+
Build (and cache, in `BaseModel._properties`) the `Prop`/`Discrim` selectors for `cls`, based on its type hints.
|
|
70
|
+
|
|
71
|
+
Deferred until first use (rather than done in `BaseModelMeta.__new__`) because `get_type_hints` requires that
|
|
72
|
+
every type referenced in an annotation -- including forward references, e.g. a model that refers to another
|
|
73
|
+
model defined later in the same module, or two models that refer to each other -- already be resolvable. That's
|
|
74
|
+
only guaranteed once the whole defining module has finished executing, which first use is (definition never is).
|
|
75
|
+
|
|
76
|
+
Idempotent: safe to call repeatedly (e.g. from both `BaseModel.properties()` and `BaseModelMeta.__getattr__`);
|
|
77
|
+
a class is only ever resolved once.
|
|
78
|
+
"""
|
|
79
|
+
if cls in BaseModel._properties:
|
|
80
|
+
return BaseModel._properties[cls]
|
|
81
|
+
|
|
82
|
+
properties: Dict[str, PropSelect[Any, Any] | Discrim[Any, Any]] = {}
|
|
83
|
+
BaseModel._properties[cls] = properties
|
|
84
|
+
|
|
85
|
+
cls_name = cls.__name__
|
|
86
|
+
type_hints = get_type_hints(cls, include_extras=True)
|
|
87
|
+
for name, _type in type_hints.items():
|
|
88
|
+
origin = get_origin(_type)
|
|
89
|
+
type_params = get_args(_type)
|
|
90
|
+
if isclass(origin) and issubclass(origin, PropSelect):
|
|
91
|
+
assert len(type_params) > 1, f'Selector {name} on model {cls_name} is missing one or more of the three required type paramaters: {_type}'
|
|
92
|
+
assert len(type_params) == 2, f'Selector {name} on model {cls_name} has more than three type paramaters: {_type}'
|
|
93
|
+
assert issubclass(cls, type_params[0]), f'Selector {name} on model {cls_name} selects from {type_params[0]} instead of {cls_name}: {_type}'
|
|
94
|
+
target = type_params[1]
|
|
95
|
+
given_selector: Any = _find_given_value(cls, name)
|
|
96
|
+
if given_selector is not _MISSING:
|
|
97
|
+
if given_selector is not None:
|
|
98
|
+
if isinstance(given_selector, SelectProxy):
|
|
99
|
+
if given_selector.name is not None and given_selector.name != name:
|
|
100
|
+
if given_selector.name in type_hints:
|
|
101
|
+
attr_typ = type_hints[given_selector.name]
|
|
102
|
+
if not isclass(attr_typ) and issubclass(attr_typ, target):
|
|
103
|
+
raise ValueError(f'Attribute {given_selector.name} with type {attr_typ.__name__} does not correspond to property {name} with type {target}')
|
|
104
|
+
property = _selector_from_select_proxy(name, origin, cls, target, given_selector)
|
|
105
|
+
elif isinstance(given_selector, PropSelect):
|
|
106
|
+
property = given_selector
|
|
107
|
+
else:
|
|
108
|
+
raise ValueError(f'Invalid selector assigned to {name}: {given_selector}')
|
|
109
|
+
else:
|
|
110
|
+
raise ValueError(f'To configure a selector on {cls_name} use select(), select_opt(), select_arr(), or select_arr()')
|
|
111
|
+
else:
|
|
112
|
+
property = _selector_from_select_proxy(name, origin, cls, target, SelectProxy(None, {}))
|
|
113
|
+
setattr(cls, name, property)
|
|
114
|
+
properties[property.label] = property
|
|
115
|
+
elif isclass(origin) and issubclass(origin, Discrim):
|
|
116
|
+
assert len(type_params) > 1, f'Discriminator {name} on model {cls_name} is missing one or more of the three required type paramaters: {_type}'
|
|
117
|
+
assert len(type_params) == 2, f'Discriminator {name} on model {cls_name} has more than three type paramaters: {_type}'
|
|
118
|
+
assert issubclass(cls, type_params[0]), f'Selector {name} on model {cls_name} selects from {type_params[0]} instead of {cls_name}: {_type}'
|
|
119
|
+
super_class = type_params[0]
|
|
120
|
+
target = type_params[1]
|
|
121
|
+
given_selector = _find_given_value(cls, name)
|
|
122
|
+
if given_selector is not _MISSING:
|
|
123
|
+
if given_selector is not None:
|
|
124
|
+
if isinstance(given_selector, SelectProxy):
|
|
125
|
+
prop = _selector_from_select_proxy(name, Prop, cls, str, given_selector)
|
|
126
|
+
discrim = Discrim(super_class, target, prop, target.__name__)
|
|
127
|
+
elif isinstance(given_selector, Discrim):
|
|
128
|
+
discrim = given_selector
|
|
129
|
+
else:
|
|
130
|
+
raise ValueError(f'Invalid selector assigned to {name}: {given_selector}')
|
|
131
|
+
else:
|
|
132
|
+
raise ValueError(f'To configure a selector on {cls_name} use select(), select_opt(), select_arr(), or select_arr()')
|
|
133
|
+
else:
|
|
134
|
+
prop = _selector_from_select_proxy(name, Prop, cls, str, SelectProxy(None, {}))
|
|
135
|
+
discrim = Discrim(super_class, target, prop, target.__name__)
|
|
136
|
+
setattr(cls, name, discrim)
|
|
137
|
+
properties[discrim.property.label] = discrim
|
|
138
|
+
|
|
139
|
+
return properties
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class BaseModelMeta(type):
|
|
143
|
+
def __new__(mcls, class_name, bases, dct: Dict[str, Any]):
|
|
144
|
+
annos: Dict[str, Type[Any]] = dct.get('__annotations__', {})
|
|
145
|
+
slots: List[str] = []
|
|
146
|
+
dct['__slots__'] = slots
|
|
147
|
+
for name, tpe in annos.items():
|
|
148
|
+
if isclass(tpe) and issubclass(tpe, PropSelect):
|
|
149
|
+
if name in dct:
|
|
150
|
+
given_prop = dct[name]
|
|
151
|
+
if isinstance(given_prop, SelectProxy):
|
|
152
|
+
slots.append(given_prop.name or name)
|
|
153
|
+
elif isinstance(given_prop, PropSelect):
|
|
154
|
+
slots.append(given_prop.label)
|
|
155
|
+
else:
|
|
156
|
+
slots.append(name)
|
|
157
|
+
# Call the parent metaclass's __new__ to create the class
|
|
158
|
+
return super().__new__(mcls, class_name, bases, dct)
|
|
159
|
+
|
|
160
|
+
def __getattr__(cls, name_to_get: str):
|
|
161
|
+
"""
|
|
162
|
+
Fallback for a `Prop`/`Discrim`-annotated field accessed directly on the class (e.g. `Model.field`) before
|
|
163
|
+
`Model.properties()` has ever been called (which is the common trigger, via `__init__`). Only runs on a
|
|
164
|
+
failed lookup -- i.e. essentially only the first time, ever, for a given class -- since a resolved field
|
|
165
|
+
becomes a real class attribute findable by plain (fast, unintercepted) attribute lookup from then on.
|
|
166
|
+
"""
|
|
167
|
+
if cls is BaseModel:
|
|
168
|
+
raise AttributeError(name_to_get)
|
|
169
|
+
_resolve_properties(cls)
|
|
170
|
+
return type.__getattribute__(cls, name_to_get)
|
|
171
|
+
|
|
172
|
+
def _fully_validate(target: Type[M], value: Any, validators: Dict[Type[Any], Validator]) -> M:
|
|
173
|
+
if isinstance(value, target):
|
|
174
|
+
return value
|
|
175
|
+
elif isinstance(value, PartialModel):
|
|
176
|
+
if not issubclass(value.model, target):
|
|
177
|
+
raise ValueError(f'received partial model of {value.model} instead of expected model {target}')
|
|
178
|
+
return target(**value.as_dict(), _allow_extra_args=True, _validators=validators)
|
|
179
|
+
elif isinstance(value, dict):
|
|
180
|
+
return target(**value, _validators=validators)
|
|
181
|
+
else:
|
|
182
|
+
raise ValueError(f'expected type {target.__name__} (or dict) but received: {type(value).__name__}.')
|
|
183
|
+
|
|
184
|
+
class BaseModel(ModelLike, metaclass=BaseModelMeta):
|
|
185
|
+
"""
|
|
186
|
+
Base type for a Pydoptic model.
|
|
187
|
+
|
|
188
|
+
A Pydoptic model consists of `PropSelect` class attributes (`Prop`, `PropOpt`, `PropArr`, and `PropOptArr`) which
|
|
189
|
+
(1) determine which instance attributes are supported, and (2) provide a mechanism for accessing and manipulating data
|
|
190
|
+
both within instances and other, potentially incomplete data sources (e.g., `PartialModel` or `dict`)
|
|
191
|
+
|
|
192
|
+
Instances are fully validated.
|
|
193
|
+
|
|
194
|
+
Can include class attribute `validators` to specify how certain types should be validated.
|
|
195
|
+
"""
|
|
196
|
+
|
|
197
|
+
# Registry of resolved selectors, keyed by model class. Shared across all `BaseModel` subclasses; populated
|
|
198
|
+
# lazily (see `_resolve_properties`) since resolving a class's selectors requires its forward references to
|
|
199
|
+
# already be resolvable, which is only guaranteed once its defining module has finished executing.
|
|
200
|
+
_properties: Dict[Type[Any], Dict[str, PropSelect[Any, Any] | Discrim[Any, Any]]] = {}
|
|
201
|
+
|
|
202
|
+
# Table for looking up validation functions by type.
|
|
203
|
+
validators: Dict[Type[Any], Validator]
|
|
204
|
+
|
|
205
|
+
@classmethod
|
|
206
|
+
def properties(cls) -> Dict[str, PropSelect[Any, Any] | Discrim[Any, Any]]:
|
|
207
|
+
"""
|
|
208
|
+
All the selectors for this subclass
|
|
209
|
+
"""
|
|
210
|
+
if cls not in BaseModel._properties:
|
|
211
|
+
_resolve_properties(cls)
|
|
212
|
+
return BaseModel._properties[cls]
|
|
213
|
+
|
|
214
|
+
@classmethod
|
|
215
|
+
def partial(cls, **kwargs) -> PartialModel[Self]:
|
|
216
|
+
"""
|
|
217
|
+
Create a partial instance of this model with a subset of properties. Required properties can be
|
|
218
|
+
omitted, but must be otherwise valid. See `PartialModel`.
|
|
219
|
+
"""
|
|
220
|
+
return PartialModel(cls, **kwargs)
|
|
221
|
+
|
|
222
|
+
@classmethod
|
|
223
|
+
def construct_partial(cls, *values: Param[Self, Any], **kwargs) -> PartialModel[Self]:
|
|
224
|
+
"""
|
|
225
|
+
Create a partial instance of this model using selectors to specify fields. Required properties can be
|
|
226
|
+
omitted, but must be otherwise valid. See `PartialModel`.
|
|
227
|
+
"""
|
|
228
|
+
return PartialModel(cls, **{p.label: p.value for p in values}, **kwargs)
|
|
229
|
+
|
|
230
|
+
@classmethod
|
|
231
|
+
def construct(cls, *values: Param[Self, Any], **kwargs) -> Self:
|
|
232
|
+
"""
|
|
233
|
+
Create an instance of this model using selectors to specify fields instead of keyword arguments.
|
|
234
|
+
Required properties can be omitted, but must be otherwise valid. See `PartialModel`.
|
|
235
|
+
"""
|
|
236
|
+
return cls(**{p.label: p.value for p in values}, **kwargs)
|
|
237
|
+
|
|
238
|
+
def as_partial(self) -> PartialModel[Self]:
|
|
239
|
+
"""
|
|
240
|
+
Convert to a `PartialModel`
|
|
241
|
+
"""
|
|
242
|
+
return PartialModel(self.__class__, **self.as_dict())
|
|
243
|
+
|
|
244
|
+
def __init__(self, **kwargs):
|
|
245
|
+
"""
|
|
246
|
+
Construct a model instance, providing all required properties as keyword arguments. Two specialized arguments
|
|
247
|
+
can also be included:
|
|
248
|
+
_allow_extra_args: don't fail when unrecognized properties are provided (they will be excluded, however).
|
|
249
|
+
_validators: a dict instance to lookup validators by type. Will merge with (and overwrite) the validators class attribute
|
|
250
|
+
"""
|
|
251
|
+
allow_extra_args = kwargs.get('_allow_extra_args', False)
|
|
252
|
+
if '_allow_extra_args' in kwargs:
|
|
253
|
+
del kwargs['_allow_extra_args']
|
|
254
|
+
validators = getattr(self.__class__, 'validators', {})
|
|
255
|
+
if '_validators' in kwargs:
|
|
256
|
+
validators.update(kwargs['_validators'])
|
|
257
|
+
del kwargs['_validators']
|
|
258
|
+
for selector in self.__class__.properties().values():
|
|
259
|
+
if isinstance(selector, Discrim):
|
|
260
|
+
if selector.property.label in kwargs and kwargs[selector.property.label] != selector.value:
|
|
261
|
+
raise ValueError(f'Discriminator {selector.property.label} set to illegal value: {kwargs[selector.property.label]}. Must be {selector.value} (will be set automatically if omitted).')
|
|
262
|
+
setattr(self, selector.property.label, selector.value)
|
|
263
|
+
else:
|
|
264
|
+
if selector.label not in kwargs and not selector.is_opt:
|
|
265
|
+
raise ValueError(f'Missing required parameter {selector.label}')
|
|
266
|
+
if selector.label not in kwargs:
|
|
267
|
+
setattr(self, selector.label, None)
|
|
268
|
+
else:
|
|
269
|
+
value = kwargs[selector.label]
|
|
270
|
+
del kwargs[selector.label]
|
|
271
|
+
if value is None and not selector.is_opt:
|
|
272
|
+
raise ValueError(f'Received empty value for required parameter {selector.label}')
|
|
273
|
+
if value is None:
|
|
274
|
+
setattr(self, selector.label, None)
|
|
275
|
+
else:
|
|
276
|
+
if isclass(selector.target) and issubclass(selector.target, BaseModel):
|
|
277
|
+
if selector.is_arr:
|
|
278
|
+
if not isinstance(value, list):
|
|
279
|
+
raise ValueError(f'Received non-array value for array field {selector.label}')
|
|
280
|
+
updated_values = []
|
|
281
|
+
for i, v in enumerate(value):
|
|
282
|
+
try:
|
|
283
|
+
updated_values.append(_fully_validate(selector.target, v, validators))
|
|
284
|
+
except ValueError as ve:
|
|
285
|
+
raise ValueError(f'Property {selector.label} contains invalid element at index {i}: {ve}')
|
|
286
|
+
setattr(self, selector.label, updated_values)
|
|
287
|
+
else:
|
|
288
|
+
try:
|
|
289
|
+
valid_value = _fully_validate(selector.target, value, validators)
|
|
290
|
+
setattr(self, selector.label, valid_value)
|
|
291
|
+
except ValueError as ve:
|
|
292
|
+
raise ValueError(f'Invalid property {selector.label}: {ve}')
|
|
293
|
+
else:
|
|
294
|
+
error_message = validate_type(selector.is_opt, selector.is_arr, selector.target, value, validators)
|
|
295
|
+
if error_message is not None:
|
|
296
|
+
message = f'Invalid property {selector.label}: {error_message}'
|
|
297
|
+
raise ValueError(message)
|
|
298
|
+
setattr(self, selector.label, value)
|
|
299
|
+
|
|
300
|
+
if not allow_extra_args:
|
|
301
|
+
for k, v in kwargs.items():
|
|
302
|
+
if k not in self.__class__.properties():
|
|
303
|
+
raise ValueError(f'Unrecognized parameter {k} provided (value: {v})')
|
|
304
|
+
|
|
305
|
+
def __repr__(self):
|
|
306
|
+
return self.__class__.__name__ + '(' + ', '.join(f'{k}={v}' for k, v in self.as_dict().items()) + ')'
|
|
307
|
+
|
|
308
|
+
def __setattr__(self, name: str, value):
|
|
309
|
+
if name in self.properties():
|
|
310
|
+
prop = self.properties()[name]
|
|
311
|
+
if isinstance(prop, Discrim) and value != prop.value:
|
|
312
|
+
raise AttributeError(f'Setting discriminator value is forbidden')
|
|
313
|
+
|
|
314
|
+
super().__setattr__(name, value)
|
|
315
|
+
|
|
316
|
+
def as_dict(self) -> Mapping[str, Any]:
|
|
317
|
+
mapping: Dict[str, Any] = {}
|
|
318
|
+
for name in self.__class__.properties().keys():
|
|
319
|
+
try:
|
|
320
|
+
value = getattr(self, name)
|
|
321
|
+
mapping[name] = value
|
|
322
|
+
except AttributeError:
|
|
323
|
+
...
|
|
324
|
+
return mapping
|
|
325
|
+
|
|
326
|
+
def as_dict_full(self) -> Mapping[str, Any]:
|
|
327
|
+
mapping: Dict[str, Any] = {}
|
|
328
|
+
for name in self.__class__.properties().keys():
|
|
329
|
+
try:
|
|
330
|
+
value = getattr(self, name)
|
|
331
|
+
if isinstance(value, ModelLike):
|
|
332
|
+
mapping[name] = value.as_dict_full()
|
|
333
|
+
else:
|
|
334
|
+
mapping[name] = value
|
|
335
|
+
except AttributeError:
|
|
336
|
+
...
|
|
337
|
+
return mapping
|
|
338
|
+
|
|
339
|
+
def select_partial(self, *selectors: Select[Self, Any]) -> PartialModel[Self]:
|
|
340
|
+
"""
|
|
341
|
+
Generate a partial version of this instance, selecting the data to retain with one or more `Select` instances.
|
|
342
|
+
"""
|
|
343
|
+
data: Dict[str, Any] = {}
|
|
344
|
+
for selector in selectors:
|
|
345
|
+
selector.copy_to(self, data)
|
|
346
|
+
return self.__class__.partial(**data)
|
|
347
|
+
|
|
348
|
+
def _partly_validate(target: Type[M], value: Any, validators: Dict[Type[Any], Validator]) -> M | PartialModel[M]:
|
|
349
|
+
if isinstance(value, target):
|
|
350
|
+
return value
|
|
351
|
+
elif isinstance(value, PartialModel):
|
|
352
|
+
if not issubclass(value.model, target):
|
|
353
|
+
raise ValueError(f'received partial model of {value.model} instead of expected model {target}')
|
|
354
|
+
return value
|
|
355
|
+
elif isinstance(value, dict):
|
|
356
|
+
return target.partial(**value, _validators=validators)
|
|
357
|
+
else:
|
|
358
|
+
raise ValueError(f'expected type {target.__name__} (or dict) but received: {type(value).__name__}.')
|
|
359
|
+
|
|
360
|
+
M = TypeVar('M', bound=BaseModel)
|
|
361
|
+
|
|
362
|
+
class PartialModel(Generic[M], Selectable[M]):
|
|
363
|
+
"""
|
|
364
|
+
An incomplete version of model instances. Validates data without requiring that all required properties are present.
|
|
365
|
+
"""
|
|
366
|
+
__slots__ = ['_dict', 'model']
|
|
367
|
+
model: Type[M]
|
|
368
|
+
_dict: Dict[str, Any]
|
|
369
|
+
|
|
370
|
+
def as_model(self, **extra_args) -> M:
|
|
371
|
+
"""
|
|
372
|
+
Convert to a complete model, providing any missing properties as keyword arguments.
|
|
373
|
+
"""
|
|
374
|
+
return self.model(**self._dict, **extra_args)
|
|
375
|
+
|
|
376
|
+
def construct_as_model(self, values: Dict[PropSelect[Self, Any], Any], **extra_args) -> M:
|
|
377
|
+
"""
|
|
378
|
+
Convert to a complete model, providing any missing properties using `PropSelect` instances as lookups.
|
|
379
|
+
"""
|
|
380
|
+
return self.model(**{sel.label: val for sel, val in values.items()}, **self._dict, **extra_args)
|
|
381
|
+
|
|
382
|
+
def __init__(self, model: Type[M], **kwargs):
|
|
383
|
+
"""
|
|
384
|
+
Construct a partial model from a model class and any desired properties. Validates provided properties without enforcing
|
|
385
|
+
required property types. Allows unrecognized properties (but does not include them).
|
|
386
|
+
"""
|
|
387
|
+
object.__setattr__(self, '_dict', {})
|
|
388
|
+
object.__setattr__(self, 'model', model)
|
|
389
|
+
allow_extra_args = kwargs.get('_allow_extra_args', False)
|
|
390
|
+
if '_allow_extra_args' in kwargs:
|
|
391
|
+
del kwargs['_allow_extra_args']
|
|
392
|
+
validators = getattr(model, 'validators', {})
|
|
393
|
+
if '_validators' in kwargs:
|
|
394
|
+
validators.update(kwargs['_validators'])
|
|
395
|
+
del kwargs['_validators']
|
|
396
|
+
for selector in model.properties().values():
|
|
397
|
+
if isinstance(selector, Discrim):
|
|
398
|
+
if selector.property.label in kwargs and kwargs[selector.property.label] != selector.value:
|
|
399
|
+
raise ValueError(f'Discriminator {selector.property.label} set to illegal value: {kwargs[selector.property.label]}. Must be {selector.value} (will be set automatically if omitted).')
|
|
400
|
+
setattr(self, selector.property.label, selector.value)
|
|
401
|
+
else:
|
|
402
|
+
if selector.label in kwargs:
|
|
403
|
+
value = kwargs[selector.label]
|
|
404
|
+
if value is not None:
|
|
405
|
+
if isclass(selector.target) and issubclass(selector.target, BaseModel):
|
|
406
|
+
if selector.is_arr:
|
|
407
|
+
if not isinstance(value, list):
|
|
408
|
+
raise ValueError(f'Received non-array value for array field {selector.label}')
|
|
409
|
+
updated_values = []
|
|
410
|
+
for i, v in enumerate(value):
|
|
411
|
+
try:
|
|
412
|
+
updated_values.append(_partly_validate(selector.target, v, validators))
|
|
413
|
+
except ValueError as ve:
|
|
414
|
+
raise ValueError(f'Property {selector.label} contains invalid element at index {i}: {ve}')
|
|
415
|
+
self._dict[selector.label] = updated_values
|
|
416
|
+
else:
|
|
417
|
+
try:
|
|
418
|
+
valid_value = _partly_validate(selector.target, value, validators)
|
|
419
|
+
self._dict[selector.label] = valid_value
|
|
420
|
+
except ValueError as ve:
|
|
421
|
+
raise ValueError(f'Invalid property {selector.label}: {ve}')
|
|
422
|
+
else:
|
|
423
|
+
error_message = validate_type(selector.is_opt, selector.is_arr, selector.target, value, validators)
|
|
424
|
+
if error_message is not None:
|
|
425
|
+
message = f'Invalid property {selector.label}: {error_message}'
|
|
426
|
+
raise ValueError(message)
|
|
427
|
+
self._dict[selector.label] = value
|
|
428
|
+
|
|
429
|
+
if not allow_extra_args:
|
|
430
|
+
for k, v in kwargs.items():
|
|
431
|
+
if k not in model.properties():
|
|
432
|
+
raise ValueError(f'Unrecognized parameter {k} provided (value: {v})')
|
|
433
|
+
|
|
434
|
+
def __repr__(self):
|
|
435
|
+
return 'Partial' + self.model.__name__ + '(' + ', '.join(f'{k}={v}' for k, v in self._dict.items()) + ')'
|
|
436
|
+
|
|
437
|
+
def __getattr__(self, item):
|
|
438
|
+
try:
|
|
439
|
+
return object.__getattribute__(self, '_dict')[item]
|
|
440
|
+
except KeyError:
|
|
441
|
+
if item == 'model':
|
|
442
|
+
return object.__getattribute__(self, 'model')
|
|
443
|
+
raise AttributeError(item)
|
|
444
|
+
|
|
445
|
+
def __setattr__(self, key, value):
|
|
446
|
+
if key in self.model.properties():
|
|
447
|
+
prop = self.model.properties()[key]
|
|
448
|
+
if isinstance(prop, Discrim) and value != prop.value:
|
|
449
|
+
raise AttributeError(f'Setting discriminator value is forbidden')
|
|
450
|
+
|
|
451
|
+
object.__getattribute__(self, '_dict')[key] = value
|
|
452
|
+
|
|
453
|
+
def as_dict(self) -> Mapping[str, Any]:
|
|
454
|
+
return self._dict
|
|
455
|
+
|
|
456
|
+
def select_partial(self, *selectors: Select[M, Any]) -> PartialModel[M]:
|
|
457
|
+
"""
|
|
458
|
+
Generate another partial instance, selecting the data to retain with one or more `Select` instances.
|
|
459
|
+
"""
|
|
460
|
+
data: Dict[str, Any] = {}
|
|
461
|
+
for selector in selectors:
|
|
462
|
+
selector.copy_to_safe(self, data)
|
|
463
|
+
return self.model.partial(**data)
|
pydoptic/py.typed
ADDED
|
File without changes
|