pydoptic 0.0.2.post1.dev1__tar.gz → 0.0.2.post1.dev2__tar.gz
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-0.0.2.post1.dev1/src/pydoptic.egg-info → pydoptic-0.0.2.post1.dev2}/PKG-INFO +11 -11
- {pydoptic-0.0.2.post1.dev1 → pydoptic-0.0.2.post1.dev2}/README.md +10 -10
- {pydoptic-0.0.2.post1.dev1 → pydoptic-0.0.2.post1.dev2}/src/pydoptic/__init__.py +2 -2
- {pydoptic-0.0.2.post1.dev1 → pydoptic-0.0.2.post1.dev2}/src/pydoptic/base_model.py +17 -17
- {pydoptic-0.0.2.post1.dev1 → pydoptic-0.0.2.post1.dev2/src/pydoptic.egg-info}/PKG-INFO +11 -11
- {pydoptic-0.0.2.post1.dev1 → pydoptic-0.0.2.post1.dev2}/src/pydoptic.egg-info/SOURCES.txt +1 -1
- {pydoptic-0.0.2.post1.dev1 → pydoptic-0.0.2.post1.dev2}/src/pydoptic.egg-info/scm_file_list.json +1 -1
- pydoptic-0.0.2.post1.dev2/src/pydoptic.egg-info/scm_version.json +8 -0
- pydoptic-0.0.2.post1.dev1/test/test_partial_model.py → pydoptic-0.0.2.post1.dev2/test/test_partial.py +27 -27
- pydoptic-0.0.2.post1.dev1/src/pydoptic.egg-info/scm_version.json +0 -8
- {pydoptic-0.0.2.post1.dev1 → pydoptic-0.0.2.post1.dev2}/LICENSE +0 -0
- {pydoptic-0.0.2.post1.dev1 → pydoptic-0.0.2.post1.dev2}/pyproject.toml +0 -0
- {pydoptic-0.0.2.post1.dev1 → pydoptic-0.0.2.post1.dev2}/setup.cfg +0 -0
- {pydoptic-0.0.2.post1.dev1 → pydoptic-0.0.2.post1.dev2}/src/pydoptic/py.typed +0 -0
- {pydoptic-0.0.2.post1.dev1 → pydoptic-0.0.2.post1.dev2}/src/pydoptic/selector.py +0 -0
- {pydoptic-0.0.2.post1.dev1 → pydoptic-0.0.2.post1.dev2}/src/pydoptic/validate_types.py +0 -0
- {pydoptic-0.0.2.post1.dev1 → pydoptic-0.0.2.post1.dev2}/src/pydoptic.egg-info/dependency_links.txt +0 -0
- {pydoptic-0.0.2.post1.dev1 → pydoptic-0.0.2.post1.dev2}/src/pydoptic.egg-info/requires.txt +0 -0
- {pydoptic-0.0.2.post1.dev1 → pydoptic-0.0.2.post1.dev2}/src/pydoptic.egg-info/top_level.txt +0 -0
- {pydoptic-0.0.2.post1.dev1 → pydoptic-0.0.2.post1.dev2}/test/test_base_model.py +0 -0
- {pydoptic-0.0.2.post1.dev1 → pydoptic-0.0.2.post1.dev2}/test/test_selector.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pydoptic
|
|
3
|
-
Version: 0.0.2.post1.
|
|
3
|
+
Version: 0.0.2.post1.dev2
|
|
4
4
|
Summary: Model data using reified optics
|
|
5
5
|
Author: John Hungerford
|
|
6
6
|
License-Expression: MIT
|
|
@@ -35,7 +35,7 @@ The examples below assume this import, unless a different one is shown:
|
|
|
35
35
|
|
|
36
36
|
```python3
|
|
37
37
|
from pydoptic import (
|
|
38
|
-
BaseModel,
|
|
38
|
+
BaseModel, Partial, select,
|
|
39
39
|
Discrim, Prop, PropArr, PropOpt, PropOptArr, PropSelect,
|
|
40
40
|
Select, SelectArr, SelectOpt, SelectOptArr, SelectVal, SelectValue,
|
|
41
41
|
)
|
|
@@ -87,8 +87,8 @@ model_1_kw: Model2 = Model2(prop_one=42, prop_two="hello")
|
|
|
87
87
|
model_2_pr: Model2 = Model2.construct(Model1.prop_1.param(42), Model1.prop_2.param("hello"))
|
|
88
88
|
|
|
89
89
|
# Partial model
|
|
90
|
-
model_1_partial_kw:
|
|
91
|
-
model_1_partial_pr:
|
|
90
|
+
model_1_partial_kw: Partial[Model2] = Model2.partial(prop_one=42)
|
|
91
|
+
model_1_partial_pr: Partial[Model2] = Model2.construct_partial(
|
|
92
92
|
Model.prop_2.param("hello"),
|
|
93
93
|
)
|
|
94
94
|
|
|
@@ -221,7 +221,7 @@ model: MyModel = MyModel(prop_1='value', prop_2=23, ...)
|
|
|
221
221
|
dict_value: Dict[str, Any] = model.as_dict_full()
|
|
222
222
|
str_value: str = json.dumps(dict_value)
|
|
223
223
|
|
|
224
|
-
partial_model:
|
|
224
|
+
partial_model: Partial[MyModel] = MyModel.partial(prop_1='value', prop_2=23, ...)
|
|
225
225
|
partial_dict: Dict[str, Any] = partial_model.as_dict_full()
|
|
226
226
|
partial_str: str = json.dumps(partial_dict)
|
|
227
227
|
```
|
|
@@ -237,7 +237,7 @@ model: MyModel = MyModel(**dict_value)
|
|
|
237
237
|
|
|
238
238
|
partial_str: str = ???
|
|
239
239
|
partial_dict: Dict[str, Any] = json.loads(partial_str)
|
|
240
|
-
partial_model:
|
|
240
|
+
partial_model: Partial[MyModel] = MyModel.partial(**partial_dict)
|
|
241
241
|
```
|
|
242
242
|
|
|
243
243
|
Note that given how data can be read from and written to objects, there is no necessity that you validate to models or even partial models at all. My own preferred workflow is to use models for ingestion only -- that is, to consume external data and convert them to my domain in the form of Pydoptic models, and then feed these into the data backend. Once valid data is persisted in a backend, it can then retrieved and manipulated in the form of `dict`s, without ever having to generate a full model. This is a matter of preference, however.
|
|
@@ -305,9 +305,9 @@ Conversely, if you only wanted to contact one POC (and you don't care whether th
|
|
|
305
305
|
|
|
306
306
|
In the above case, if there are many `Person`s selected byte `poc_select`, only the first one will be returned. If none are, `poc` will be `None`.
|
|
307
307
|
|
|
308
|
-
To retrieve values from an incomplete representation of the data, such as a `
|
|
308
|
+
To retrieve values from an incomplete representation of the data, such as a `Partial` or a `dict`, there are two variants of `get` that you can use. `get_unsafe` will try to retrieve the selected value and fail with a `ValueError` if it's inaccessible due to invalid data (if its inaccessible due to a valid optional property being missing, it will generate an empty `SelectValue` as expected). `get_safe`, on the other hand, will always fail silently, generating empty `SelectValue` when data is missing.
|
|
309
309
|
|
|
310
|
-
Note that the type signature of `get_unsafe` and `get_safe` are a bit misleading. They will provide accurate types for non-model values (e.g., primitive properties like `int` or `str`), but they will treat nested models as though they are fully typed. When you retrieve a nested model from a `dict` or `
|
|
310
|
+
Note that the type signature of `get_unsafe` and `get_safe` are a bit misleading. They will provide accurate types for non-model values (e.g., primitive properties like `int` or `str`), but they will treat nested models as though they are fully typed. When you retrieve a nested model from a `dict` or `Partial`, however, in all likelihood it is going to be another `dict` or `Partial`.
|
|
311
311
|
|
|
312
312
|
```python3
|
|
313
313
|
class Inner(BaseModel):
|
|
@@ -328,7 +328,7 @@ inner_result_full: SelectValue[Inner] = Outer.inner.get(full)
|
|
|
328
328
|
partial_val = Outer.partial(inner=Inner.partial(prop=23))
|
|
329
329
|
# Accurately typed
|
|
330
330
|
prop_result_partial: SelectValue[int] = prop_select.get_unsafe(partial_val)
|
|
331
|
-
# Inaccurate type! Should be SelectValue[
|
|
331
|
+
# Inaccurate type! Should be SelectValue[Partial[Inner]]
|
|
332
332
|
inner_result_partial: SelectValue[Inner] = Outer.inner.get_unsafe(partial_val)
|
|
333
333
|
|
|
334
334
|
dict_val = {'inner': {'prop': 23}}
|
|
@@ -403,7 +403,7 @@ for arr in prop_arr_select.get(value).as_list:
|
|
|
403
403
|
arr.append(len(arr))
|
|
404
404
|
```
|
|
405
405
|
|
|
406
|
-
To update incomplete data, use `update_unsafe` and `update_safe`. `update_unsafe` will fail with an exception if it's unable to access the data to be updated due to invalid data, whereas `update_safe` will simply ignore (and therefore skip) invalid cases. Like `get`, you need to be careful when updating nested models with the partial variants of `update`. The types will appear to be complete models when they are most likely `
|
|
406
|
+
To update incomplete data, use `update_unsafe` and `update_safe`. `update_unsafe` will fail with an exception if it's unable to access the data to be updated due to invalid data, whereas `update_safe` will simply ignore (and therefore skip) invalid cases. Like `get`, you need to be careful when updating nested models with the partial variants of `update`. The types will appear to be complete models when they are most likely `Partial`s or `dict`s.
|
|
407
407
|
|
|
408
408
|
|
|
409
409
|
#### `set`, `set_unsafe`, `set_safe`
|
|
@@ -478,7 +478,7 @@ print(value)
|
|
|
478
478
|
|
|
479
479
|
In the above example, while `sel_opt` is able to clear the `opt` property, `sel_req` cannot clear the selected `req` property so instead it clears the next clearable property, setting `c` to an empty array.
|
|
480
480
|
|
|
481
|
-
Clearing partial data is even more complicated, since partial data (`
|
|
481
|
+
Clearing partial data is even more complicated, since partial data (`Partial`s and `dict`s) *can* be cleared even when its required in the model. For this reason, the partial variants of `clear` are distinguished into "strict" and "non-strict" variants. `clear_unsafe_strict` works just like `clear`: it will only clear clearable (optional/array) properties, and will try to clear selected property and work its way backward across the property chain until it finds a clearable property. `clear_unsafe`, on the other hand, will clear the selected property whether or not it's a required field. `clear_safe_strict` and `clear_safe` work the same way, but does not raise exceptions when they encounter invalid data, but simply skip it and move on.
|
|
482
482
|
|
|
483
483
|
#### Precise `Select`s
|
|
484
484
|
|
|
@@ -17,7 +17,7 @@ The examples below assume this import, unless a different one is shown:
|
|
|
17
17
|
|
|
18
18
|
```python3
|
|
19
19
|
from pydoptic import (
|
|
20
|
-
BaseModel,
|
|
20
|
+
BaseModel, Partial, select,
|
|
21
21
|
Discrim, Prop, PropArr, PropOpt, PropOptArr, PropSelect,
|
|
22
22
|
Select, SelectArr, SelectOpt, SelectOptArr, SelectVal, SelectValue,
|
|
23
23
|
)
|
|
@@ -69,8 +69,8 @@ model_1_kw: Model2 = Model2(prop_one=42, prop_two="hello")
|
|
|
69
69
|
model_2_pr: Model2 = Model2.construct(Model1.prop_1.param(42), Model1.prop_2.param("hello"))
|
|
70
70
|
|
|
71
71
|
# Partial model
|
|
72
|
-
model_1_partial_kw:
|
|
73
|
-
model_1_partial_pr:
|
|
72
|
+
model_1_partial_kw: Partial[Model2] = Model2.partial(prop_one=42)
|
|
73
|
+
model_1_partial_pr: Partial[Model2] = Model2.construct_partial(
|
|
74
74
|
Model.prop_2.param("hello"),
|
|
75
75
|
)
|
|
76
76
|
|
|
@@ -203,7 +203,7 @@ model: MyModel = MyModel(prop_1='value', prop_2=23, ...)
|
|
|
203
203
|
dict_value: Dict[str, Any] = model.as_dict_full()
|
|
204
204
|
str_value: str = json.dumps(dict_value)
|
|
205
205
|
|
|
206
|
-
partial_model:
|
|
206
|
+
partial_model: Partial[MyModel] = MyModel.partial(prop_1='value', prop_2=23, ...)
|
|
207
207
|
partial_dict: Dict[str, Any] = partial_model.as_dict_full()
|
|
208
208
|
partial_str: str = json.dumps(partial_dict)
|
|
209
209
|
```
|
|
@@ -219,7 +219,7 @@ model: MyModel = MyModel(**dict_value)
|
|
|
219
219
|
|
|
220
220
|
partial_str: str = ???
|
|
221
221
|
partial_dict: Dict[str, Any] = json.loads(partial_str)
|
|
222
|
-
partial_model:
|
|
222
|
+
partial_model: Partial[MyModel] = MyModel.partial(**partial_dict)
|
|
223
223
|
```
|
|
224
224
|
|
|
225
225
|
Note that given how data can be read from and written to objects, there is no necessity that you validate to models or even partial models at all. My own preferred workflow is to use models for ingestion only -- that is, to consume external data and convert them to my domain in the form of Pydoptic models, and then feed these into the data backend. Once valid data is persisted in a backend, it can then retrieved and manipulated in the form of `dict`s, without ever having to generate a full model. This is a matter of preference, however.
|
|
@@ -287,9 +287,9 @@ Conversely, if you only wanted to contact one POC (and you don't care whether th
|
|
|
287
287
|
|
|
288
288
|
In the above case, if there are many `Person`s selected byte `poc_select`, only the first one will be returned. If none are, `poc` will be `None`.
|
|
289
289
|
|
|
290
|
-
To retrieve values from an incomplete representation of the data, such as a `
|
|
290
|
+
To retrieve values from an incomplete representation of the data, such as a `Partial` or a `dict`, there are two variants of `get` that you can use. `get_unsafe` will try to retrieve the selected value and fail with a `ValueError` if it's inaccessible due to invalid data (if its inaccessible due to a valid optional property being missing, it will generate an empty `SelectValue` as expected). `get_safe`, on the other hand, will always fail silently, generating empty `SelectValue` when data is missing.
|
|
291
291
|
|
|
292
|
-
Note that the type signature of `get_unsafe` and `get_safe` are a bit misleading. They will provide accurate types for non-model values (e.g., primitive properties like `int` or `str`), but they will treat nested models as though they are fully typed. When you retrieve a nested model from a `dict` or `
|
|
292
|
+
Note that the type signature of `get_unsafe` and `get_safe` are a bit misleading. They will provide accurate types for non-model values (e.g., primitive properties like `int` or `str`), but they will treat nested models as though they are fully typed. When you retrieve a nested model from a `dict` or `Partial`, however, in all likelihood it is going to be another `dict` or `Partial`.
|
|
293
293
|
|
|
294
294
|
```python3
|
|
295
295
|
class Inner(BaseModel):
|
|
@@ -310,7 +310,7 @@ inner_result_full: SelectValue[Inner] = Outer.inner.get(full)
|
|
|
310
310
|
partial_val = Outer.partial(inner=Inner.partial(prop=23))
|
|
311
311
|
# Accurately typed
|
|
312
312
|
prop_result_partial: SelectValue[int] = prop_select.get_unsafe(partial_val)
|
|
313
|
-
# Inaccurate type! Should be SelectValue[
|
|
313
|
+
# Inaccurate type! Should be SelectValue[Partial[Inner]]
|
|
314
314
|
inner_result_partial: SelectValue[Inner] = Outer.inner.get_unsafe(partial_val)
|
|
315
315
|
|
|
316
316
|
dict_val = {'inner': {'prop': 23}}
|
|
@@ -385,7 +385,7 @@ for arr in prop_arr_select.get(value).as_list:
|
|
|
385
385
|
arr.append(len(arr))
|
|
386
386
|
```
|
|
387
387
|
|
|
388
|
-
To update incomplete data, use `update_unsafe` and `update_safe`. `update_unsafe` will fail with an exception if it's unable to access the data to be updated due to invalid data, whereas `update_safe` will simply ignore (and therefore skip) invalid cases. Like `get`, you need to be careful when updating nested models with the partial variants of `update`. The types will appear to be complete models when they are most likely `
|
|
388
|
+
To update incomplete data, use `update_unsafe` and `update_safe`. `update_unsafe` will fail with an exception if it's unable to access the data to be updated due to invalid data, whereas `update_safe` will simply ignore (and therefore skip) invalid cases. Like `get`, you need to be careful when updating nested models with the partial variants of `update`. The types will appear to be complete models when they are most likely `Partial`s or `dict`s.
|
|
389
389
|
|
|
390
390
|
|
|
391
391
|
#### `set`, `set_unsafe`, `set_safe`
|
|
@@ -460,7 +460,7 @@ print(value)
|
|
|
460
460
|
|
|
461
461
|
In the above example, while `sel_opt` is able to clear the `opt` property, `sel_req` cannot clear the selected `req` property so instead it clears the next clearable property, setting `c` to an empty array.
|
|
462
462
|
|
|
463
|
-
Clearing partial data is even more complicated, since partial data (`
|
|
463
|
+
Clearing partial data is even more complicated, since partial data (`Partial`s and `dict`s) *can* be cleared even when its required in the model. For this reason, the partial variants of `clear` are distinguished into "strict" and "non-strict" variants. `clear_unsafe_strict` works just like `clear`: it will only clear clearable (optional/array) properties, and will try to clear selected property and work its way backward across the property chain until it finds a clearable property. `clear_unsafe`, on the other hand, will clear the selected property whether or not it's a required field. `clear_safe_strict` and `clear_safe` work the same way, but does not raise exceptions when they encounter invalid data, but simply skip it and move on.
|
|
464
464
|
|
|
465
465
|
#### Precise `Select`s
|
|
466
466
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from pydoptic.base_model import BaseModel,
|
|
1
|
+
from pydoptic.base_model import BaseModel, Partial, select
|
|
2
2
|
from pydoptic.selector import (
|
|
3
3
|
Discrim,
|
|
4
4
|
LinkedSelect,
|
|
@@ -25,7 +25,7 @@ from pydoptic.selector import (
|
|
|
25
25
|
from pydoptic.validate_types import Validator
|
|
26
26
|
|
|
27
27
|
__all__ = [
|
|
28
|
-
'BaseModel', '
|
|
28
|
+
'BaseModel', 'Partial', 'select',
|
|
29
29
|
'Discrim', 'LinkedSelect', 'LinkedSelectArr', 'LinkedSelectOpt', 'LinkedSelectOptArr', 'LinkedSelectVal',
|
|
30
30
|
'MatchSelect', 'ModelLike', 'Param', 'Prop', 'PropArr', 'PropOpt', 'PropOptArr', 'PropSelect',
|
|
31
31
|
'Select', 'Selectable', 'SelectArr', 'SelectOpt', 'SelectOptArr', 'SelectVal', 'SelectValue',
|
|
@@ -172,7 +172,7 @@ class BaseModelMeta(type):
|
|
|
172
172
|
def _fully_validate(target: Type[M], value: Any, validators: Dict[Type[Any], Validator]) -> M:
|
|
173
173
|
if isinstance(value, target):
|
|
174
174
|
return value
|
|
175
|
-
elif isinstance(value,
|
|
175
|
+
elif isinstance(value, Partial):
|
|
176
176
|
if not issubclass(value.model, target):
|
|
177
177
|
raise ValueError(f'received partial model of {value.model} instead of expected model {target}')
|
|
178
178
|
return target(**value.as_dict(), _allow_extra_args=True, _validators=validators)
|
|
@@ -187,7 +187,7 @@ class BaseModel(ModelLike, metaclass=BaseModelMeta):
|
|
|
187
187
|
|
|
188
188
|
A Pydoptic model consists of `PropSelect` class attributes (`Prop`, `PropOpt`, `PropArr`, and `PropOptArr`) which
|
|
189
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., `
|
|
190
|
+
both within instances and other, potentially incomplete data sources (e.g., `Partial` or `dict`)
|
|
191
191
|
|
|
192
192
|
Instances are fully validated.
|
|
193
193
|
|
|
@@ -212,34 +212,34 @@ class BaseModel(ModelLike, metaclass=BaseModelMeta):
|
|
|
212
212
|
return BaseModel._properties[cls]
|
|
213
213
|
|
|
214
214
|
@classmethod
|
|
215
|
-
def partial(cls, **kwargs) ->
|
|
215
|
+
def partial(cls, **kwargs) -> Partial[Self]:
|
|
216
216
|
"""
|
|
217
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 `
|
|
218
|
+
omitted, but must be otherwise valid. See `Partial`.
|
|
219
219
|
"""
|
|
220
|
-
return
|
|
220
|
+
return Partial(cls, **kwargs)
|
|
221
221
|
|
|
222
222
|
@classmethod
|
|
223
|
-
def construct_partial(cls, *values: Param[Self, Any], **kwargs) ->
|
|
223
|
+
def construct_partial(cls, *values: Param[Self, Any], **kwargs) -> Partial[Self]:
|
|
224
224
|
"""
|
|
225
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 `
|
|
226
|
+
omitted, but must be otherwise valid. See `Partial`.
|
|
227
227
|
"""
|
|
228
|
-
return
|
|
228
|
+
return Partial(cls, **{p.label: p.value for p in values}, **kwargs)
|
|
229
229
|
|
|
230
230
|
@classmethod
|
|
231
231
|
def construct(cls, *values: Param[Self, Any], **kwargs) -> Self:
|
|
232
232
|
"""
|
|
233
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 `
|
|
234
|
+
Required properties can be omitted, but must be otherwise valid. See `Partial`.
|
|
235
235
|
"""
|
|
236
236
|
return cls(**{p.label: p.value for p in values}, **kwargs)
|
|
237
237
|
|
|
238
|
-
def as_partial(self) ->
|
|
238
|
+
def as_partial(self) -> Partial[Self]:
|
|
239
239
|
"""
|
|
240
|
-
Convert to a `
|
|
240
|
+
Convert to a `Partial`
|
|
241
241
|
"""
|
|
242
|
-
return
|
|
242
|
+
return Partial(self.__class__, **self.as_dict())
|
|
243
243
|
|
|
244
244
|
def __init__(self, **kwargs):
|
|
245
245
|
"""
|
|
@@ -336,7 +336,7 @@ class BaseModel(ModelLike, metaclass=BaseModelMeta):
|
|
|
336
336
|
...
|
|
337
337
|
return mapping
|
|
338
338
|
|
|
339
|
-
def select_partial(self, *selectors: Select[Self, Any]) ->
|
|
339
|
+
def select_partial(self, *selectors: Select[Self, Any]) -> Partial[Self]:
|
|
340
340
|
"""
|
|
341
341
|
Generate a partial version of this instance, selecting the data to retain with one or more `Select` instances.
|
|
342
342
|
"""
|
|
@@ -345,10 +345,10 @@ class BaseModel(ModelLike, metaclass=BaseModelMeta):
|
|
|
345
345
|
selector.copy_to(self, data)
|
|
346
346
|
return self.__class__.partial(**data)
|
|
347
347
|
|
|
348
|
-
def _partly_validate(target: Type[M], value: Any, validators: Dict[Type[Any], Validator]) -> M |
|
|
348
|
+
def _partly_validate(target: Type[M], value: Any, validators: Dict[Type[Any], Validator]) -> M | Partial[M]:
|
|
349
349
|
if isinstance(value, target):
|
|
350
350
|
return value
|
|
351
|
-
elif isinstance(value,
|
|
351
|
+
elif isinstance(value, Partial):
|
|
352
352
|
if not issubclass(value.model, target):
|
|
353
353
|
raise ValueError(f'received partial model of {value.model} instead of expected model {target}')
|
|
354
354
|
return value
|
|
@@ -359,7 +359,7 @@ def _partly_validate(target: Type[M], value: Any, validators: Dict[Type[Any], Va
|
|
|
359
359
|
|
|
360
360
|
M = TypeVar('M', bound=BaseModel)
|
|
361
361
|
|
|
362
|
-
class
|
|
362
|
+
class Partial(Generic[M], Selectable[M]):
|
|
363
363
|
"""
|
|
364
364
|
An incomplete version of model instances. Validates data without requiring that all required properties are present.
|
|
365
365
|
"""
|
|
@@ -453,7 +453,7 @@ class PartialModel(Generic[M], Selectable[M]):
|
|
|
453
453
|
def as_dict(self) -> Mapping[str, Any]:
|
|
454
454
|
return self._dict
|
|
455
455
|
|
|
456
|
-
def select_partial(self, *selectors: Select[M, Any]) ->
|
|
456
|
+
def select_partial(self, *selectors: Select[M, Any]) -> Partial[M]:
|
|
457
457
|
"""
|
|
458
458
|
Generate another partial instance, selecting the data to retain with one or more `Select` instances.
|
|
459
459
|
"""
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pydoptic
|
|
3
|
-
Version: 0.0.2.post1.
|
|
3
|
+
Version: 0.0.2.post1.dev2
|
|
4
4
|
Summary: Model data using reified optics
|
|
5
5
|
Author: John Hungerford
|
|
6
6
|
License-Expression: MIT
|
|
@@ -35,7 +35,7 @@ The examples below assume this import, unless a different one is shown:
|
|
|
35
35
|
|
|
36
36
|
```python3
|
|
37
37
|
from pydoptic import (
|
|
38
|
-
BaseModel,
|
|
38
|
+
BaseModel, Partial, select,
|
|
39
39
|
Discrim, Prop, PropArr, PropOpt, PropOptArr, PropSelect,
|
|
40
40
|
Select, SelectArr, SelectOpt, SelectOptArr, SelectVal, SelectValue,
|
|
41
41
|
)
|
|
@@ -87,8 +87,8 @@ model_1_kw: Model2 = Model2(prop_one=42, prop_two="hello")
|
|
|
87
87
|
model_2_pr: Model2 = Model2.construct(Model1.prop_1.param(42), Model1.prop_2.param("hello"))
|
|
88
88
|
|
|
89
89
|
# Partial model
|
|
90
|
-
model_1_partial_kw:
|
|
91
|
-
model_1_partial_pr:
|
|
90
|
+
model_1_partial_kw: Partial[Model2] = Model2.partial(prop_one=42)
|
|
91
|
+
model_1_partial_pr: Partial[Model2] = Model2.construct_partial(
|
|
92
92
|
Model.prop_2.param("hello"),
|
|
93
93
|
)
|
|
94
94
|
|
|
@@ -221,7 +221,7 @@ model: MyModel = MyModel(prop_1='value', prop_2=23, ...)
|
|
|
221
221
|
dict_value: Dict[str, Any] = model.as_dict_full()
|
|
222
222
|
str_value: str = json.dumps(dict_value)
|
|
223
223
|
|
|
224
|
-
partial_model:
|
|
224
|
+
partial_model: Partial[MyModel] = MyModel.partial(prop_1='value', prop_2=23, ...)
|
|
225
225
|
partial_dict: Dict[str, Any] = partial_model.as_dict_full()
|
|
226
226
|
partial_str: str = json.dumps(partial_dict)
|
|
227
227
|
```
|
|
@@ -237,7 +237,7 @@ model: MyModel = MyModel(**dict_value)
|
|
|
237
237
|
|
|
238
238
|
partial_str: str = ???
|
|
239
239
|
partial_dict: Dict[str, Any] = json.loads(partial_str)
|
|
240
|
-
partial_model:
|
|
240
|
+
partial_model: Partial[MyModel] = MyModel.partial(**partial_dict)
|
|
241
241
|
```
|
|
242
242
|
|
|
243
243
|
Note that given how data can be read from and written to objects, there is no necessity that you validate to models or even partial models at all. My own preferred workflow is to use models for ingestion only -- that is, to consume external data and convert them to my domain in the form of Pydoptic models, and then feed these into the data backend. Once valid data is persisted in a backend, it can then retrieved and manipulated in the form of `dict`s, without ever having to generate a full model. This is a matter of preference, however.
|
|
@@ -305,9 +305,9 @@ Conversely, if you only wanted to contact one POC (and you don't care whether th
|
|
|
305
305
|
|
|
306
306
|
In the above case, if there are many `Person`s selected byte `poc_select`, only the first one will be returned. If none are, `poc` will be `None`.
|
|
307
307
|
|
|
308
|
-
To retrieve values from an incomplete representation of the data, such as a `
|
|
308
|
+
To retrieve values from an incomplete representation of the data, such as a `Partial` or a `dict`, there are two variants of `get` that you can use. `get_unsafe` will try to retrieve the selected value and fail with a `ValueError` if it's inaccessible due to invalid data (if its inaccessible due to a valid optional property being missing, it will generate an empty `SelectValue` as expected). `get_safe`, on the other hand, will always fail silently, generating empty `SelectValue` when data is missing.
|
|
309
309
|
|
|
310
|
-
Note that the type signature of `get_unsafe` and `get_safe` are a bit misleading. They will provide accurate types for non-model values (e.g., primitive properties like `int` or `str`), but they will treat nested models as though they are fully typed. When you retrieve a nested model from a `dict` or `
|
|
310
|
+
Note that the type signature of `get_unsafe` and `get_safe` are a bit misleading. They will provide accurate types for non-model values (e.g., primitive properties like `int` or `str`), but they will treat nested models as though they are fully typed. When you retrieve a nested model from a `dict` or `Partial`, however, in all likelihood it is going to be another `dict` or `Partial`.
|
|
311
311
|
|
|
312
312
|
```python3
|
|
313
313
|
class Inner(BaseModel):
|
|
@@ -328,7 +328,7 @@ inner_result_full: SelectValue[Inner] = Outer.inner.get(full)
|
|
|
328
328
|
partial_val = Outer.partial(inner=Inner.partial(prop=23))
|
|
329
329
|
# Accurately typed
|
|
330
330
|
prop_result_partial: SelectValue[int] = prop_select.get_unsafe(partial_val)
|
|
331
|
-
# Inaccurate type! Should be SelectValue[
|
|
331
|
+
# Inaccurate type! Should be SelectValue[Partial[Inner]]
|
|
332
332
|
inner_result_partial: SelectValue[Inner] = Outer.inner.get_unsafe(partial_val)
|
|
333
333
|
|
|
334
334
|
dict_val = {'inner': {'prop': 23}}
|
|
@@ -403,7 +403,7 @@ for arr in prop_arr_select.get(value).as_list:
|
|
|
403
403
|
arr.append(len(arr))
|
|
404
404
|
```
|
|
405
405
|
|
|
406
|
-
To update incomplete data, use `update_unsafe` and `update_safe`. `update_unsafe` will fail with an exception if it's unable to access the data to be updated due to invalid data, whereas `update_safe` will simply ignore (and therefore skip) invalid cases. Like `get`, you need to be careful when updating nested models with the partial variants of `update`. The types will appear to be complete models when they are most likely `
|
|
406
|
+
To update incomplete data, use `update_unsafe` and `update_safe`. `update_unsafe` will fail with an exception if it's unable to access the data to be updated due to invalid data, whereas `update_safe` will simply ignore (and therefore skip) invalid cases. Like `get`, you need to be careful when updating nested models with the partial variants of `update`. The types will appear to be complete models when they are most likely `Partial`s or `dict`s.
|
|
407
407
|
|
|
408
408
|
|
|
409
409
|
#### `set`, `set_unsafe`, `set_safe`
|
|
@@ -478,7 +478,7 @@ print(value)
|
|
|
478
478
|
|
|
479
479
|
In the above example, while `sel_opt` is able to clear the `opt` property, `sel_req` cannot clear the selected `req` property so instead it clears the next clearable property, setting `c` to an empty array.
|
|
480
480
|
|
|
481
|
-
Clearing partial data is even more complicated, since partial data (`
|
|
481
|
+
Clearing partial data is even more complicated, since partial data (`Partial`s and `dict`s) *can* be cleared even when its required in the model. For this reason, the partial variants of `clear` are distinguished into "strict" and "non-strict" variants. `clear_unsafe_strict` works just like `clear`: it will only clear clearable (optional/array) properties, and will try to clear selected property and work its way backward across the property chain until it finds a clearable property. `clear_unsafe`, on the other hand, will clear the selected property whether or not it's a required field. `clear_safe_strict` and `clear_safe` work the same way, but does not raise exceptions when they encounter invalid data, but simply skip it and move on.
|
|
482
482
|
|
|
483
483
|
#### Precise `Select`s
|
|
484
484
|
|
|
@@ -24,7 +24,7 @@ class Other(BaseModel):
|
|
|
24
24
|
class Another(BaseModel):
|
|
25
25
|
value: Prop['Another', float]
|
|
26
26
|
|
|
27
|
-
def
|
|
27
|
+
def test_partial_constructed_from_full_model():
|
|
28
28
|
model_value = TestModel(
|
|
29
29
|
annotation="str",
|
|
30
30
|
select_val_empty=1,
|
|
@@ -55,7 +55,7 @@ def test_partial_model_constructed_from_full_model():
|
|
|
55
55
|
assert model_value.other.value == True # type: ignore[attr-defined]
|
|
56
56
|
assert model_value.other.another.value == 0.2 # type: ignore[attr-defined]
|
|
57
57
|
|
|
58
|
-
def
|
|
58
|
+
def test_partial_should_accept_all_valid_values():
|
|
59
59
|
model_value = TestModel.partial(
|
|
60
60
|
annotation="str",
|
|
61
61
|
select_val_empty=1,
|
|
@@ -86,7 +86,7 @@ def test_partial_model_should_accept_all_valid_values():
|
|
|
86
86
|
assert model_value.other.value == True # type: ignore[attr-defined]
|
|
87
87
|
assert model_value.other.another.value == 0.2 # type: ignore[attr-defined]
|
|
88
88
|
|
|
89
|
-
def
|
|
89
|
+
def test_complete_partial_should_become_full_model():
|
|
90
90
|
model_value = TestModel.partial(
|
|
91
91
|
annotation="str",
|
|
92
92
|
select_val_empty=1,
|
|
@@ -117,7 +117,7 @@ def test_complete_partial_model_should_become_full_model():
|
|
|
117
117
|
assert model_value.other.value == True # type: ignore[attr-defined]
|
|
118
118
|
assert model_value.other.another.value == 0.2 # type: ignore[attr-defined]
|
|
119
119
|
|
|
120
|
-
def
|
|
120
|
+
def test_partial_should_accept_valid_missing_arguments():
|
|
121
121
|
model_value = TestModel.partial(
|
|
122
122
|
annotation="str",
|
|
123
123
|
select_val_empty=1,
|
|
@@ -146,7 +146,7 @@ def test_partial_model_should_accept_valid_missing_arguments():
|
|
|
146
146
|
with pytest.raises(AttributeError):
|
|
147
147
|
model_value.select_opt_arr_renamed # type: ignore[attr-defined]
|
|
148
148
|
|
|
149
|
-
def
|
|
149
|
+
def test_partial_should_accept_partial_for_nested_models_if_complete():
|
|
150
150
|
model_value = TestModel.partial(
|
|
151
151
|
annotation="str",
|
|
152
152
|
select_val_empty=1,
|
|
@@ -161,14 +161,14 @@ def test_partial_model_should_accept_partial_model_for_nested_models_if_complete
|
|
|
161
161
|
),
|
|
162
162
|
)
|
|
163
163
|
|
|
164
|
-
assert isinstance(model_value.other,
|
|
164
|
+
assert isinstance(model_value.other, Partial) # type: ignore[attr-defined]
|
|
165
165
|
assert model_value.other.model is Other # type: ignore[attr-defined]
|
|
166
166
|
assert model_value.other.value == True # type: ignore[attr-defined]
|
|
167
|
-
assert isinstance(model_value.other.another,
|
|
167
|
+
assert isinstance(model_value.other.another, Partial) # type: ignore[attr-defined]
|
|
168
168
|
assert model_value.other.another.model is Another # type: ignore[attr-defined]
|
|
169
169
|
assert model_value.other.another.value == 0.2 # type: ignore[attr-defined]
|
|
170
170
|
|
|
171
|
-
def
|
|
171
|
+
def test_partial_should_accept_dict_for_nested_models_if_complete():
|
|
172
172
|
model_value = TestModel.partial(
|
|
173
173
|
annotation="str",
|
|
174
174
|
select_val_empty=1,
|
|
@@ -183,14 +183,14 @@ def test_partial_model_should_accept_dict_for_nested_models_if_complete():
|
|
|
183
183
|
},
|
|
184
184
|
)
|
|
185
185
|
|
|
186
|
-
assert isinstance(model_value.other,
|
|
186
|
+
assert isinstance(model_value.other, Partial) # type: ignore[attr-defined]
|
|
187
187
|
assert model_value.other.model is Other # type: ignore[attr-defined]
|
|
188
188
|
assert model_value.other.value == True # type: ignore[attr-defined]
|
|
189
|
-
assert isinstance(model_value.other.another,
|
|
189
|
+
assert isinstance(model_value.other.another, Partial) # type: ignore[attr-defined]
|
|
190
190
|
assert model_value.other.another.model is Another # type: ignore[attr-defined]
|
|
191
191
|
assert model_value.other.another.value == 0.2 # type: ignore[attr-defined]
|
|
192
192
|
|
|
193
|
-
def
|
|
193
|
+
def test_partial_init_should_succeed_if_required_param_is_missing():
|
|
194
194
|
model_value = TestModel.partial(
|
|
195
195
|
select_val_empty=1,
|
|
196
196
|
select_val_renamed=1,
|
|
@@ -219,7 +219,7 @@ def test_partial_model_init_should_succeed_if_required_param_is_missing():
|
|
|
219
219
|
assert model_value.select_opt_arr_empty == [1,2,3] # type: ignore[attr-defined]
|
|
220
220
|
assert model_value.select_opt_arr_renamed == [1,2,3] # type: ignore[attr-defined]
|
|
221
221
|
|
|
222
|
-
def
|
|
222
|
+
def test_incomplete_partial_should_fail_to_become_full_model_if_missing_params_are_not_provided():
|
|
223
223
|
passed = False
|
|
224
224
|
try:
|
|
225
225
|
TestModel.partial(
|
|
@@ -244,7 +244,7 @@ def test_incomplete_partial_model_should_fail_to_become_full_model_if_missing_pa
|
|
|
244
244
|
assert passed
|
|
245
245
|
|
|
246
246
|
|
|
247
|
-
def
|
|
247
|
+
def test_incomplete_partial_should_become_full_model_if_missing_params_are_provided():
|
|
248
248
|
model_value = TestModel.partial(
|
|
249
249
|
annotation="str",
|
|
250
250
|
select_val_renamed=1,
|
|
@@ -276,7 +276,7 @@ def test_incomplete_partial_model_should_become_full_model_if_missing_params_are
|
|
|
276
276
|
assert model_value.other.value == True # type: ignore[attr-defined]
|
|
277
277
|
assert model_value.other.another.value == 0.2 # type: ignore[attr-defined]
|
|
278
278
|
|
|
279
|
-
def
|
|
279
|
+
def test_partial_init_should_fail_if_primitive_type_is_wrong():
|
|
280
280
|
passed = True
|
|
281
281
|
try:
|
|
282
282
|
TestModel.partial(
|
|
@@ -302,7 +302,7 @@ def test_partial_model_init_should_fail_if_primitive_type_is_wrong():
|
|
|
302
302
|
|
|
303
303
|
assert passed is False
|
|
304
304
|
|
|
305
|
-
def
|
|
305
|
+
def test_partial_init_should_fail_if_primitive_array_param_receives_non_array():
|
|
306
306
|
passed = True
|
|
307
307
|
try:
|
|
308
308
|
TestModel.partial(
|
|
@@ -328,7 +328,7 @@ def test_partial_model_init_should_fail_if_primitive_array_param_receives_non_ar
|
|
|
328
328
|
|
|
329
329
|
assert passed is False
|
|
330
330
|
|
|
331
|
-
def
|
|
331
|
+
def test_partial_init_should_fail_if_primitive_array_param_contains_invalid_element():
|
|
332
332
|
passed = True
|
|
333
333
|
try:
|
|
334
334
|
TestModel(
|
|
@@ -355,7 +355,7 @@ def test_partial_model_init_should_fail_if_primitive_array_param_contains_invali
|
|
|
355
355
|
|
|
356
356
|
assert passed is False
|
|
357
357
|
|
|
358
|
-
def
|
|
358
|
+
def test_partial_init_should_fail_if_wrong_model_is_passed():
|
|
359
359
|
passed = True
|
|
360
360
|
try:
|
|
361
361
|
TestModel(
|
|
@@ -378,7 +378,7 @@ def test_partial_model_init_should_fail_if_wrong_model_is_passed():
|
|
|
378
378
|
|
|
379
379
|
assert passed is False
|
|
380
380
|
|
|
381
|
-
def
|
|
381
|
+
def test_partial_init_should_fail_if_nested_model_is_invalid_dict():
|
|
382
382
|
passed = True
|
|
383
383
|
try:
|
|
384
384
|
TestModel(
|
|
@@ -405,7 +405,7 @@ def test_partial_model_init_should_fail_if_nested_model_is_invalid_dict():
|
|
|
405
405
|
|
|
406
406
|
assert passed is False
|
|
407
407
|
|
|
408
|
-
def
|
|
408
|
+
def test_partial_init_should_accept_incomplete_nested_partial():
|
|
409
409
|
TestModel.partial(
|
|
410
410
|
annotation="str",
|
|
411
411
|
select_val_empty=1,
|
|
@@ -426,14 +426,14 @@ def test_partial_model_init_should_accept_incomplete_nested_partial_model():
|
|
|
426
426
|
class NestedModelExample(BaseModel):
|
|
427
427
|
nested_model: PropArr['NestedModelExample', Another] = select()
|
|
428
428
|
|
|
429
|
-
def
|
|
429
|
+
def test_partial_init_should_accept_valid_nested_model_array():
|
|
430
430
|
nested_vals = [Another(value=2.0), {'value': 0.2}, Another.partial(value=23.042)]
|
|
431
431
|
result = NestedModelExample.partial(
|
|
432
432
|
nested_model=nested_vals,
|
|
433
433
|
)
|
|
434
434
|
assert [v.value for v in result.nested_model] == [2.0, 0.2, 23.042] # type: ignore[attr-defined]
|
|
435
435
|
|
|
436
|
-
def
|
|
436
|
+
def test_partial_init_should_fail_if_nested_model_array_is_not_array():
|
|
437
437
|
passed = True
|
|
438
438
|
try:
|
|
439
439
|
NestedModelExample.partial(
|
|
@@ -445,7 +445,7 @@ def test_partial_model_init_should_fail_if_nested_model_array_is_not_array():
|
|
|
445
445
|
|
|
446
446
|
assert passed
|
|
447
447
|
|
|
448
|
-
def
|
|
448
|
+
def test_partial_init_should_fail_if_nested_model_array_contains_invalid_model():
|
|
449
449
|
passed = True
|
|
450
450
|
try:
|
|
451
451
|
NestedModelExample.partial(
|
|
@@ -460,7 +460,7 @@ def test_patial_model_init_should_fail_if_nested_model_array_contains_invald_mod
|
|
|
460
460
|
|
|
461
461
|
assert passed
|
|
462
462
|
|
|
463
|
-
def
|
|
463
|
+
def test_partial_as_mapping_full_should_be_reversable():
|
|
464
464
|
model_value = TestModel.partial(
|
|
465
465
|
annotation="str",
|
|
466
466
|
select_val_empty=1,
|
|
@@ -480,7 +480,7 @@ def test_partial_model_as_mapping_full_should_be_reversable():
|
|
|
480
480
|
|
|
481
481
|
assert model_value == model_value_2
|
|
482
482
|
|
|
483
|
-
def
|
|
483
|
+
def test_base_model_select_parts_should_generate_partial_from_selectors_p():
|
|
484
484
|
model_value = TestModel.partial(
|
|
485
485
|
annotation="str",
|
|
486
486
|
select_val_renamed=1,
|
|
@@ -526,21 +526,21 @@ class Sub2(Base):
|
|
|
526
526
|
class Root(BaseModel):
|
|
527
527
|
next: Prop['Root', Base]
|
|
528
528
|
|
|
529
|
-
def
|
|
529
|
+
def test_partial_should_construct_with_discrim():
|
|
530
530
|
value_1 = Root.partial(next=Sub1.partial(v='1', s1v=23))
|
|
531
531
|
value_2 = Root.partial(next=Sub2.partial(v='2', s2v=True))
|
|
532
532
|
|
|
533
533
|
assert value_1.next.d == 'Sub1' # type: ignore
|
|
534
534
|
assert value_2.next.d == 'Sub2' # type: ignore
|
|
535
535
|
|
|
536
|
-
def
|
|
536
|
+
def test_partial_should_forbid_constructing_with_invalid_discrim():
|
|
537
537
|
value = Root.partial(next=Sub1.partial(d='Sub1', v='1', s1v=23))
|
|
538
538
|
assert value.next.d == 'Sub1' # type: ignore
|
|
539
539
|
|
|
540
540
|
with pytest.raises(ValueError):
|
|
541
541
|
Root.partial(next=Sub2.partial(d='wrong!', v='2', s2v=True))
|
|
542
542
|
|
|
543
|
-
def
|
|
543
|
+
def test_partial_should_forbid_setting_invalid_discrim():
|
|
544
544
|
value = Root.partial(next=Sub1.partial(d='Sub1', v='1', s1v=23))
|
|
545
545
|
assert value.next.d == 'Sub1' # type: ignore
|
|
546
546
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pydoptic-0.0.2.post1.dev1 → pydoptic-0.0.2.post1.dev2}/src/pydoptic.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|