pydoptic 0.0.post1.dev38__tar.gz → 0.0.1.post1.dev1__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.post1.dev38 → pydoptic-0.0.1.post1.dev1}/PKG-INFO +37 -11
- {pydoptic-0.0.post1.dev38 → pydoptic-0.0.1.post1.dev1}/README.md +36 -10
- pydoptic-0.0.1.post1.dev1/src/pydoptic/__init__.py +33 -0
- {pydoptic-0.0.post1.dev38 → pydoptic-0.0.1.post1.dev1}/src/pydoptic.egg-info/PKG-INFO +37 -11
- pydoptic-0.0.1.post1.dev1/src/pydoptic.egg-info/scm_version.json +8 -0
- pydoptic-0.0.1.post1.dev1/src/pydoptic_elastic/__init__.py +9 -0
- pydoptic-0.0.1.post1.dev1/src/pydoptic_sql/__init__.py +49 -0
- pydoptic-0.0.post1.dev38/src/pydoptic/__init__.py +0 -3
- pydoptic-0.0.post1.dev38/src/pydoptic.egg-info/scm_version.json +0 -8
- pydoptic-0.0.post1.dev38/src/pydoptic_elastic/__init__.py +0 -3
- pydoptic-0.0.post1.dev38/src/pydoptic_sql/__init__.py +0 -6
- {pydoptic-0.0.post1.dev38 → pydoptic-0.0.1.post1.dev1}/.github/workflows/integration-tests.yml +0 -0
- {pydoptic-0.0.post1.dev38 → pydoptic-0.0.1.post1.dev1}/.github/workflows/release.yml +0 -0
- {pydoptic-0.0.post1.dev38 → pydoptic-0.0.1.post1.dev1}/.github/workflows/snapshot.yml +0 -0
- {pydoptic-0.0.post1.dev38 → pydoptic-0.0.1.post1.dev1}/.github/workflows/unit-tests.yml +0 -0
- {pydoptic-0.0.post1.dev38 → pydoptic-0.0.1.post1.dev1}/.gitignore +0 -0
- {pydoptic-0.0.post1.dev38 → pydoptic-0.0.1.post1.dev1}/CONTEXT.md +0 -0
- {pydoptic-0.0.post1.dev38 → pydoptic-0.0.1.post1.dev1}/LICENSE +0 -0
- {pydoptic-0.0.post1.dev38 → pydoptic-0.0.1.post1.dev1}/benchmarks/bench_model.py +0 -0
- {pydoptic-0.0.post1.dev38 → pydoptic-0.0.1.post1.dev1}/docker-compose.yml +0 -0
- {pydoptic-0.0.post1.dev38 → pydoptic-0.0.1.post1.dev1}/mypy.ini +0 -0
- {pydoptic-0.0.post1.dev38 → pydoptic-0.0.1.post1.dev1}/pyproject.toml +0 -0
- {pydoptic-0.0.post1.dev38 → pydoptic-0.0.1.post1.dev1}/setup.cfg +0 -0
- {pydoptic-0.0.post1.dev38 → pydoptic-0.0.1.post1.dev1}/src/pydoptic/base_model.py +0 -0
- {pydoptic-0.0.post1.dev38 → pydoptic-0.0.1.post1.dev1}/src/pydoptic/py.typed +0 -0
- {pydoptic-0.0.post1.dev38 → pydoptic-0.0.1.post1.dev1}/src/pydoptic/selector.py +0 -0
- {pydoptic-0.0.post1.dev38 → pydoptic-0.0.1.post1.dev1}/src/pydoptic/validate_types.py +0 -0
- {pydoptic-0.0.post1.dev38 → pydoptic-0.0.1.post1.dev1}/src/pydoptic.egg-info/SOURCES.txt +0 -0
- {pydoptic-0.0.post1.dev38 → pydoptic-0.0.1.post1.dev1}/src/pydoptic.egg-info/dependency_links.txt +0 -0
- {pydoptic-0.0.post1.dev38 → pydoptic-0.0.1.post1.dev1}/src/pydoptic.egg-info/requires.txt +0 -0
- {pydoptic-0.0.post1.dev38 → pydoptic-0.0.1.post1.dev1}/src/pydoptic.egg-info/scm_file_list.json +0 -0
- {pydoptic-0.0.post1.dev38 → pydoptic-0.0.1.post1.dev1}/src/pydoptic.egg-info/top_level.txt +0 -0
- {pydoptic-0.0.post1.dev38 → pydoptic-0.0.1.post1.dev1}/src/pydoptic_elastic/elastic_model.py +0 -0
- {pydoptic-0.0.post1.dev38 → pydoptic-0.0.1.post1.dev1}/src/pydoptic_elastic/elastic_query.py +0 -0
- {pydoptic-0.0.post1.dev38 → pydoptic-0.0.1.post1.dev1}/src/pydoptic_elastic/elastic_service.py +0 -0
- {pydoptic-0.0.post1.dev38 → pydoptic-0.0.1.post1.dev1}/src/pydoptic_elastic/py.typed +0 -0
- {pydoptic-0.0.post1.dev38 → pydoptic-0.0.1.post1.dev1}/src/pydoptic_sql/sql_computed.py +0 -0
- {pydoptic-0.0.post1.dev38 → pydoptic-0.0.1.post1.dev1}/src/pydoptic_sql/sql_constraint.py +0 -0
- {pydoptic-0.0.post1.dev38 → pydoptic-0.0.1.post1.dev1}/src/pydoptic_sql/sql_having.py +0 -0
- {pydoptic-0.0.post1.dev38 → pydoptic-0.0.1.post1.dev1}/src/pydoptic_sql/sql_order.py +0 -0
- {pydoptic-0.0.post1.dev38 → pydoptic-0.0.1.post1.dev1}/src/pydoptic_sql/sql_query.py +0 -0
- {pydoptic-0.0.post1.dev38 → pydoptic-0.0.1.post1.dev1}/src/pydoptic_sql/sql_service.py +0 -0
- {pydoptic-0.0.post1.dev38 → pydoptic-0.0.1.post1.dev1}/src/pydoptic_sql/sql_table.py +0 -0
- {pydoptic-0.0.post1.dev38 → pydoptic-0.0.1.post1.dev1}/test/pydoptic/test_base_model.py +0 -0
- {pydoptic-0.0.post1.dev38 → pydoptic-0.0.1.post1.dev1}/test/pydoptic/test_partial_model.py +0 -0
- {pydoptic-0.0.post1.dev38 → pydoptic-0.0.1.post1.dev1}/test/pydoptic/test_selector.py +0 -0
- {pydoptic-0.0.post1.dev38 → pydoptic-0.0.1.post1.dev1}/test/pydoptic_elastic/test_elastic_model.py +0 -0
- {pydoptic-0.0.post1.dev38 → pydoptic-0.0.1.post1.dev1}/test/pydoptic_elastic/test_elastic_service.py +0 -0
- {pydoptic-0.0.post1.dev38 → pydoptic-0.0.1.post1.dev1}/test/pydoptic_sql/test_sql_computed.py +0 -0
- {pydoptic-0.0.post1.dev38 → pydoptic-0.0.1.post1.dev1}/test/pydoptic_sql/test_sql_constraint.py +0 -0
- {pydoptic-0.0.post1.dev38 → pydoptic-0.0.1.post1.dev1}/test/pydoptic_sql/test_sql_having.py +0 -0
- {pydoptic-0.0.post1.dev38 → pydoptic-0.0.1.post1.dev1}/test/pydoptic_sql/test_sql_params.py +0 -0
- {pydoptic-0.0.post1.dev38 → pydoptic-0.0.1.post1.dev1}/test/pydoptic_sql/test_sql_query.py +0 -0
- {pydoptic-0.0.post1.dev38 → pydoptic-0.0.1.post1.dev1}/test/pydoptic_sql/test_sql_service.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pydoptic
|
|
3
|
-
Version: 0.0.post1.
|
|
3
|
+
Version: 0.0.1.post1.dev1
|
|
4
4
|
Summary: Model data using reified optics
|
|
5
5
|
Author: John Hungerford
|
|
6
6
|
License-Expression: MIT
|
|
@@ -29,6 +29,20 @@ A good alternative to Pydantic when one or more of the following is true:
|
|
|
29
29
|
2. You frequently need to retrieve and manipulate deeply nested values
|
|
30
30
|
3. You need a type-safe way to refer to properties when querying remote data
|
|
31
31
|
|
|
32
|
+
## Installation
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install pydoptic
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
This single package includes all three libraries in this repository:
|
|
39
|
+
- `pydoptic` -- the core data modeling library (see below)
|
|
40
|
+
- `pydoptic_sql` -- a type-safe SQL query builder (see [`src/pydoptic_sql/`](src/pydoptic_sql/))
|
|
41
|
+
- `pydoptic_elastic` -- a reference Elasticsearch integration (see [`src/pydoptic_elastic/`](src/pydoptic_elastic/))
|
|
42
|
+
|
|
43
|
+
`pydoptic_sql`'s Postgres client (`psycopg`) and `pydoptic_elastic`'s Elasticsearch client are regular
|
|
44
|
+
dependencies, so no extras are needed to use either.
|
|
45
|
+
|
|
32
46
|
## Why?
|
|
33
47
|
|
|
34
48
|
In every programming language, data is modeled as the type of container used to store it in memory. In Python, this generally looks something like the following:
|
|
@@ -85,7 +99,7 @@ Whereas traditional data models represent data as *containers* of properties, Py
|
|
|
85
99
|
Here is what a Pydoptic version of the data model above looks like:
|
|
86
100
|
|
|
87
101
|
```python3
|
|
88
|
-
from pydoptic import
|
|
102
|
+
from pydoptic import BaseModel, PartialModel, Prop, PropOpt, PropOptArr
|
|
89
103
|
from datetime import date
|
|
90
104
|
|
|
91
105
|
class Address(BaseModel):
|
|
@@ -152,19 +166,19 @@ In this case, Pydoptic's model is substantially *less* verbose. We can reproduce
|
|
|
152
166
|
```python3
|
|
153
167
|
select_related_statuses = Person.organizations(Organization.members)(Person.is_active)
|
|
154
168
|
|
|
155
|
-
statuses =
|
|
169
|
+
statuses = select_related_statuses.get(person).as_list
|
|
156
170
|
print(statuses)
|
|
157
171
|
# [True, False, False, True, ...]
|
|
158
172
|
|
|
159
173
|
# All the updating is done here:
|
|
160
|
-
|
|
174
|
+
select_related_statuses.update(person, lambda status: !status)
|
|
161
175
|
|
|
162
|
-
statuses =
|
|
176
|
+
statuses = select_related_statuses.get(person).as_list
|
|
163
177
|
print(statuses)
|
|
164
178
|
# [False, True, True, False, ...]
|
|
165
179
|
```
|
|
166
180
|
|
|
167
|
-
As your type checker should indicate, `
|
|
181
|
+
As your type checker should indicate, `select_related_statuses` is a `Select[Person, bool]` which is composed from the `Prop`s `Person.organizations`, `Organization.members`, and `Person.is_active`. This particular chain of props can compose because each prop's *target* type is the same as the *origin* type of the prop chained to it. When this is not the case (meaning the chaining is *invalid*), the type checker will complain. The resulting `Select` can then be used to retrieve and update all `Person.is_active` properties (the final `Prop` in the chain) nested within the original `person` via the path `organizations` -> `members` -> `is_active`. Since `Person.organizations` and `Organization.members` are optional properties, ordinarily retrieving and updating these values would require checking for `None` in multiple places.
|
|
168
182
|
|
|
169
183
|
### Incomplete data
|
|
170
184
|
|
|
@@ -193,6 +207,7 @@ Since `Prop`s contain all the information about the properties they reference, t
|
|
|
193
207
|
from pydoptic import Prop, PropOptArr, PartialModel
|
|
194
208
|
from pydoptic_elastic import ElasticModel, Query, ElasticService, elastic_prop, ESMapping
|
|
195
209
|
from datetime import date
|
|
210
|
+
from typing import List
|
|
196
211
|
from elasticsearch import Elasticsearch
|
|
197
212
|
|
|
198
213
|
class Address(ElasticModel):
|
|
@@ -246,6 +261,17 @@ This concept of reified optics comes from the Scala project [ZIO schema](https:/
|
|
|
246
261
|
|
|
247
262
|
## User guide
|
|
248
263
|
|
|
264
|
+
The examples below assume this import, unless a different one is shown:
|
|
265
|
+
|
|
266
|
+
```python3
|
|
267
|
+
from pydoptic import (
|
|
268
|
+
BaseModel, PartialModel, select,
|
|
269
|
+
Discrim, Prop, PropArr, PropOpt, PropOptArr, PropSelect,
|
|
270
|
+
Select, SelectArr, SelectOpt, SelectOptArr, SelectVal, SelectValue,
|
|
271
|
+
)
|
|
272
|
+
from typing import Any, Dict, List
|
|
273
|
+
```
|
|
274
|
+
|
|
249
275
|
### Defining and constructing models
|
|
250
276
|
|
|
251
277
|
There are two basic units of data in Pydoptic: `BaseModel` and `PropSelect[A, B]`, where `A` is some subtype of `BaseModel`. Models of type `BaseModel` are defined by their `PropSelect` class attributes, which are class attributes describing what values can be stored in or retrieved from the model:
|
|
@@ -271,8 +297,8 @@ Any subclass of `BaseModel` will automatically construct the appropriate values
|
|
|
271
297
|
|
|
272
298
|
```python3
|
|
273
299
|
class Model2(BaseModel):
|
|
274
|
-
prop_1: Prop['Model2', int] =
|
|
275
|
-
prop_2: Prop['Model2', str] =
|
|
300
|
+
prop_1: Prop['Model2', int] = select('prop_one', sql_metadata='PRIMARY KEY')
|
|
301
|
+
prop_2: Prop['Model2', str] = select('prop_two', sql_metadata='FOREIGN KEY')
|
|
276
302
|
|
|
277
303
|
print(f'{prop_1.label}: {prop_1.origin} -> {prop_1.target})
|
|
278
304
|
print(f'Property data: {prop_1.data})
|
|
@@ -281,7 +307,7 @@ print(f'Property data: {prop_1.data})
|
|
|
281
307
|
# Property data: {'sql_metadata': 'PRIMARY_KEY'}
|
|
282
308
|
```
|
|
283
309
|
|
|
284
|
-
In the above example, the `
|
|
310
|
+
In the above example, the `select` function is used to customize `prop_1` and `prop_2`, providing different `label`s ("prop_one" and "prop_two") and adding some metadata. As we can see when we log the `data` attribute of the resulting `Prop`, the keyword arguments we provide to the `select` function are captured in the `Prop` in the form of a `dict`. The metadata in the example above could be used to generate a SQL table from `MyModel`, or interpret `join` queries using `prop_2`.
|
|
285
311
|
|
|
286
312
|
Once you define a model, there are kinds of instances we can construct:
|
|
287
313
|
|
|
@@ -811,7 +837,7 @@ print(value)
|
|
|
811
837
|
# Root(super=[Sub1(prop_1=24), Sub2(prop_2="hello")])
|
|
812
838
|
```
|
|
813
839
|
|
|
814
|
-
The `
|
|
840
|
+
The `Discrim[A, B]` type is a `SelectOpt` that selects down from some value of supertype `B` to a subtype `A`, filtering out cases where it is not that subtype. Note that `Discrim` property is treated differently by `BaseModel` than regular properties. It generates a special string attribute `subtype` (or whatever the property name is) that is automatically set to the class name of the subtype when a model is constructed. The `Discrim` `Select` checks whether this string value matches the class name to conclude it is the correct subtype.
|
|
815
841
|
|
|
816
842
|
You can see the discriminator value if you convert a model to a `dict`:
|
|
817
843
|
|
|
@@ -833,7 +859,7 @@ class Model(BaseModel):
|
|
|
833
859
|
prop_1: Prop['Model', int]
|
|
834
860
|
prop_2: Prop['Model', str]
|
|
835
861
|
|
|
836
|
-
all_props: List[PropSelect[Any, Any] |
|
|
862
|
+
all_props: List[PropSelect[Any, Any] | Discrim[Any, Any]] = Model.properties()
|
|
837
863
|
|
|
838
864
|
assert len(all_props) == 2
|
|
839
865
|
assert all_props[0] = Model.prop_1
|
|
@@ -7,6 +7,20 @@ A good alternative to Pydantic when one or more of the following is true:
|
|
|
7
7
|
2. You frequently need to retrieve and manipulate deeply nested values
|
|
8
8
|
3. You need a type-safe way to refer to properties when querying remote data
|
|
9
9
|
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
pip install pydoptic
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
This single package includes all three libraries in this repository:
|
|
17
|
+
- `pydoptic` -- the core data modeling library (see below)
|
|
18
|
+
- `pydoptic_sql` -- a type-safe SQL query builder (see [`src/pydoptic_sql/`](src/pydoptic_sql/))
|
|
19
|
+
- `pydoptic_elastic` -- a reference Elasticsearch integration (see [`src/pydoptic_elastic/`](src/pydoptic_elastic/))
|
|
20
|
+
|
|
21
|
+
`pydoptic_sql`'s Postgres client (`psycopg`) and `pydoptic_elastic`'s Elasticsearch client are regular
|
|
22
|
+
dependencies, so no extras are needed to use either.
|
|
23
|
+
|
|
10
24
|
## Why?
|
|
11
25
|
|
|
12
26
|
In every programming language, data is modeled as the type of container used to store it in memory. In Python, this generally looks something like the following:
|
|
@@ -63,7 +77,7 @@ Whereas traditional data models represent data as *containers* of properties, Py
|
|
|
63
77
|
Here is what a Pydoptic version of the data model above looks like:
|
|
64
78
|
|
|
65
79
|
```python3
|
|
66
|
-
from pydoptic import
|
|
80
|
+
from pydoptic import BaseModel, PartialModel, Prop, PropOpt, PropOptArr
|
|
67
81
|
from datetime import date
|
|
68
82
|
|
|
69
83
|
class Address(BaseModel):
|
|
@@ -130,19 +144,19 @@ In this case, Pydoptic's model is substantially *less* verbose. We can reproduce
|
|
|
130
144
|
```python3
|
|
131
145
|
select_related_statuses = Person.organizations(Organization.members)(Person.is_active)
|
|
132
146
|
|
|
133
|
-
statuses =
|
|
147
|
+
statuses = select_related_statuses.get(person).as_list
|
|
134
148
|
print(statuses)
|
|
135
149
|
# [True, False, False, True, ...]
|
|
136
150
|
|
|
137
151
|
# All the updating is done here:
|
|
138
|
-
|
|
152
|
+
select_related_statuses.update(person, lambda status: !status)
|
|
139
153
|
|
|
140
|
-
statuses =
|
|
154
|
+
statuses = select_related_statuses.get(person).as_list
|
|
141
155
|
print(statuses)
|
|
142
156
|
# [False, True, True, False, ...]
|
|
143
157
|
```
|
|
144
158
|
|
|
145
|
-
As your type checker should indicate, `
|
|
159
|
+
As your type checker should indicate, `select_related_statuses` is a `Select[Person, bool]` which is composed from the `Prop`s `Person.organizations`, `Organization.members`, and `Person.is_active`. This particular chain of props can compose because each prop's *target* type is the same as the *origin* type of the prop chained to it. When this is not the case (meaning the chaining is *invalid*), the type checker will complain. The resulting `Select` can then be used to retrieve and update all `Person.is_active` properties (the final `Prop` in the chain) nested within the original `person` via the path `organizations` -> `members` -> `is_active`. Since `Person.organizations` and `Organization.members` are optional properties, ordinarily retrieving and updating these values would require checking for `None` in multiple places.
|
|
146
160
|
|
|
147
161
|
### Incomplete data
|
|
148
162
|
|
|
@@ -171,6 +185,7 @@ Since `Prop`s contain all the information about the properties they reference, t
|
|
|
171
185
|
from pydoptic import Prop, PropOptArr, PartialModel
|
|
172
186
|
from pydoptic_elastic import ElasticModel, Query, ElasticService, elastic_prop, ESMapping
|
|
173
187
|
from datetime import date
|
|
188
|
+
from typing import List
|
|
174
189
|
from elasticsearch import Elasticsearch
|
|
175
190
|
|
|
176
191
|
class Address(ElasticModel):
|
|
@@ -224,6 +239,17 @@ This concept of reified optics comes from the Scala project [ZIO schema](https:/
|
|
|
224
239
|
|
|
225
240
|
## User guide
|
|
226
241
|
|
|
242
|
+
The examples below assume this import, unless a different one is shown:
|
|
243
|
+
|
|
244
|
+
```python3
|
|
245
|
+
from pydoptic import (
|
|
246
|
+
BaseModel, PartialModel, select,
|
|
247
|
+
Discrim, Prop, PropArr, PropOpt, PropOptArr, PropSelect,
|
|
248
|
+
Select, SelectArr, SelectOpt, SelectOptArr, SelectVal, SelectValue,
|
|
249
|
+
)
|
|
250
|
+
from typing import Any, Dict, List
|
|
251
|
+
```
|
|
252
|
+
|
|
227
253
|
### Defining and constructing models
|
|
228
254
|
|
|
229
255
|
There are two basic units of data in Pydoptic: `BaseModel` and `PropSelect[A, B]`, where `A` is some subtype of `BaseModel`. Models of type `BaseModel` are defined by their `PropSelect` class attributes, which are class attributes describing what values can be stored in or retrieved from the model:
|
|
@@ -249,8 +275,8 @@ Any subclass of `BaseModel` will automatically construct the appropriate values
|
|
|
249
275
|
|
|
250
276
|
```python3
|
|
251
277
|
class Model2(BaseModel):
|
|
252
|
-
prop_1: Prop['Model2', int] =
|
|
253
|
-
prop_2: Prop['Model2', str] =
|
|
278
|
+
prop_1: Prop['Model2', int] = select('prop_one', sql_metadata='PRIMARY KEY')
|
|
279
|
+
prop_2: Prop['Model2', str] = select('prop_two', sql_metadata='FOREIGN KEY')
|
|
254
280
|
|
|
255
281
|
print(f'{prop_1.label}: {prop_1.origin} -> {prop_1.target})
|
|
256
282
|
print(f'Property data: {prop_1.data})
|
|
@@ -259,7 +285,7 @@ print(f'Property data: {prop_1.data})
|
|
|
259
285
|
# Property data: {'sql_metadata': 'PRIMARY_KEY'}
|
|
260
286
|
```
|
|
261
287
|
|
|
262
|
-
In the above example, the `
|
|
288
|
+
In the above example, the `select` function is used to customize `prop_1` and `prop_2`, providing different `label`s ("prop_one" and "prop_two") and adding some metadata. As we can see when we log the `data` attribute of the resulting `Prop`, the keyword arguments we provide to the `select` function are captured in the `Prop` in the form of a `dict`. The metadata in the example above could be used to generate a SQL table from `MyModel`, or interpret `join` queries using `prop_2`.
|
|
263
289
|
|
|
264
290
|
Once you define a model, there are kinds of instances we can construct:
|
|
265
291
|
|
|
@@ -789,7 +815,7 @@ print(value)
|
|
|
789
815
|
# Root(super=[Sub1(prop_1=24), Sub2(prop_2="hello")])
|
|
790
816
|
```
|
|
791
817
|
|
|
792
|
-
The `
|
|
818
|
+
The `Discrim[A, B]` type is a `SelectOpt` that selects down from some value of supertype `B` to a subtype `A`, filtering out cases where it is not that subtype. Note that `Discrim` property is treated differently by `BaseModel` than regular properties. It generates a special string attribute `subtype` (or whatever the property name is) that is automatically set to the class name of the subtype when a model is constructed. The `Discrim` `Select` checks whether this string value matches the class name to conclude it is the correct subtype.
|
|
793
819
|
|
|
794
820
|
You can see the discriminator value if you convert a model to a `dict`:
|
|
795
821
|
|
|
@@ -811,7 +837,7 @@ class Model(BaseModel):
|
|
|
811
837
|
prop_1: Prop['Model', int]
|
|
812
838
|
prop_2: Prop['Model', str]
|
|
813
839
|
|
|
814
|
-
all_props: List[PropSelect[Any, Any] |
|
|
840
|
+
all_props: List[PropSelect[Any, Any] | Discrim[Any, Any]] = Model.properties()
|
|
815
841
|
|
|
816
842
|
assert len(all_props) == 2
|
|
817
843
|
assert all_props[0] = Model.prop_1
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from pydoptic.base_model import BaseModel, PartialModel, select
|
|
2
|
+
from pydoptic.selector import (
|
|
3
|
+
Discrim,
|
|
4
|
+
LinkedSelect,
|
|
5
|
+
LinkedSelectArr,
|
|
6
|
+
LinkedSelectOpt,
|
|
7
|
+
LinkedSelectOptArr,
|
|
8
|
+
LinkedSelectVal,
|
|
9
|
+
MatchSelect,
|
|
10
|
+
ModelLike,
|
|
11
|
+
Param,
|
|
12
|
+
Prop,
|
|
13
|
+
PropArr,
|
|
14
|
+
PropOpt,
|
|
15
|
+
PropOptArr,
|
|
16
|
+
PropSelect,
|
|
17
|
+
Select,
|
|
18
|
+
Selectable,
|
|
19
|
+
SelectArr,
|
|
20
|
+
SelectOpt,
|
|
21
|
+
SelectOptArr,
|
|
22
|
+
SelectVal,
|
|
23
|
+
SelectValue,
|
|
24
|
+
)
|
|
25
|
+
from pydoptic.validate_types import Validator
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
'BaseModel', 'PartialModel', 'select',
|
|
29
|
+
'Discrim', 'LinkedSelect', 'LinkedSelectArr', 'LinkedSelectOpt', 'LinkedSelectOptArr', 'LinkedSelectVal',
|
|
30
|
+
'MatchSelect', 'ModelLike', 'Param', 'Prop', 'PropArr', 'PropOpt', 'PropOptArr', 'PropSelect',
|
|
31
|
+
'Select', 'Selectable', 'SelectArr', 'SelectOpt', 'SelectOptArr', 'SelectVal', 'SelectValue',
|
|
32
|
+
'Validator',
|
|
33
|
+
]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pydoptic
|
|
3
|
-
Version: 0.0.post1.
|
|
3
|
+
Version: 0.0.1.post1.dev1
|
|
4
4
|
Summary: Model data using reified optics
|
|
5
5
|
Author: John Hungerford
|
|
6
6
|
License-Expression: MIT
|
|
@@ -29,6 +29,20 @@ A good alternative to Pydantic when one or more of the following is true:
|
|
|
29
29
|
2. You frequently need to retrieve and manipulate deeply nested values
|
|
30
30
|
3. You need a type-safe way to refer to properties when querying remote data
|
|
31
31
|
|
|
32
|
+
## Installation
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install pydoptic
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
This single package includes all three libraries in this repository:
|
|
39
|
+
- `pydoptic` -- the core data modeling library (see below)
|
|
40
|
+
- `pydoptic_sql` -- a type-safe SQL query builder (see [`src/pydoptic_sql/`](src/pydoptic_sql/))
|
|
41
|
+
- `pydoptic_elastic` -- a reference Elasticsearch integration (see [`src/pydoptic_elastic/`](src/pydoptic_elastic/))
|
|
42
|
+
|
|
43
|
+
`pydoptic_sql`'s Postgres client (`psycopg`) and `pydoptic_elastic`'s Elasticsearch client are regular
|
|
44
|
+
dependencies, so no extras are needed to use either.
|
|
45
|
+
|
|
32
46
|
## Why?
|
|
33
47
|
|
|
34
48
|
In every programming language, data is modeled as the type of container used to store it in memory. In Python, this generally looks something like the following:
|
|
@@ -85,7 +99,7 @@ Whereas traditional data models represent data as *containers* of properties, Py
|
|
|
85
99
|
Here is what a Pydoptic version of the data model above looks like:
|
|
86
100
|
|
|
87
101
|
```python3
|
|
88
|
-
from pydoptic import
|
|
102
|
+
from pydoptic import BaseModel, PartialModel, Prop, PropOpt, PropOptArr
|
|
89
103
|
from datetime import date
|
|
90
104
|
|
|
91
105
|
class Address(BaseModel):
|
|
@@ -152,19 +166,19 @@ In this case, Pydoptic's model is substantially *less* verbose. We can reproduce
|
|
|
152
166
|
```python3
|
|
153
167
|
select_related_statuses = Person.organizations(Organization.members)(Person.is_active)
|
|
154
168
|
|
|
155
|
-
statuses =
|
|
169
|
+
statuses = select_related_statuses.get(person).as_list
|
|
156
170
|
print(statuses)
|
|
157
171
|
# [True, False, False, True, ...]
|
|
158
172
|
|
|
159
173
|
# All the updating is done here:
|
|
160
|
-
|
|
174
|
+
select_related_statuses.update(person, lambda status: !status)
|
|
161
175
|
|
|
162
|
-
statuses =
|
|
176
|
+
statuses = select_related_statuses.get(person).as_list
|
|
163
177
|
print(statuses)
|
|
164
178
|
# [False, True, True, False, ...]
|
|
165
179
|
```
|
|
166
180
|
|
|
167
|
-
As your type checker should indicate, `
|
|
181
|
+
As your type checker should indicate, `select_related_statuses` is a `Select[Person, bool]` which is composed from the `Prop`s `Person.organizations`, `Organization.members`, and `Person.is_active`. This particular chain of props can compose because each prop's *target* type is the same as the *origin* type of the prop chained to it. When this is not the case (meaning the chaining is *invalid*), the type checker will complain. The resulting `Select` can then be used to retrieve and update all `Person.is_active` properties (the final `Prop` in the chain) nested within the original `person` via the path `organizations` -> `members` -> `is_active`. Since `Person.organizations` and `Organization.members` are optional properties, ordinarily retrieving and updating these values would require checking for `None` in multiple places.
|
|
168
182
|
|
|
169
183
|
### Incomplete data
|
|
170
184
|
|
|
@@ -193,6 +207,7 @@ Since `Prop`s contain all the information about the properties they reference, t
|
|
|
193
207
|
from pydoptic import Prop, PropOptArr, PartialModel
|
|
194
208
|
from pydoptic_elastic import ElasticModel, Query, ElasticService, elastic_prop, ESMapping
|
|
195
209
|
from datetime import date
|
|
210
|
+
from typing import List
|
|
196
211
|
from elasticsearch import Elasticsearch
|
|
197
212
|
|
|
198
213
|
class Address(ElasticModel):
|
|
@@ -246,6 +261,17 @@ This concept of reified optics comes from the Scala project [ZIO schema](https:/
|
|
|
246
261
|
|
|
247
262
|
## User guide
|
|
248
263
|
|
|
264
|
+
The examples below assume this import, unless a different one is shown:
|
|
265
|
+
|
|
266
|
+
```python3
|
|
267
|
+
from pydoptic import (
|
|
268
|
+
BaseModel, PartialModel, select,
|
|
269
|
+
Discrim, Prop, PropArr, PropOpt, PropOptArr, PropSelect,
|
|
270
|
+
Select, SelectArr, SelectOpt, SelectOptArr, SelectVal, SelectValue,
|
|
271
|
+
)
|
|
272
|
+
from typing import Any, Dict, List
|
|
273
|
+
```
|
|
274
|
+
|
|
249
275
|
### Defining and constructing models
|
|
250
276
|
|
|
251
277
|
There are two basic units of data in Pydoptic: `BaseModel` and `PropSelect[A, B]`, where `A` is some subtype of `BaseModel`. Models of type `BaseModel` are defined by their `PropSelect` class attributes, which are class attributes describing what values can be stored in or retrieved from the model:
|
|
@@ -271,8 +297,8 @@ Any subclass of `BaseModel` will automatically construct the appropriate values
|
|
|
271
297
|
|
|
272
298
|
```python3
|
|
273
299
|
class Model2(BaseModel):
|
|
274
|
-
prop_1: Prop['Model2', int] =
|
|
275
|
-
prop_2: Prop['Model2', str] =
|
|
300
|
+
prop_1: Prop['Model2', int] = select('prop_one', sql_metadata='PRIMARY KEY')
|
|
301
|
+
prop_2: Prop['Model2', str] = select('prop_two', sql_metadata='FOREIGN KEY')
|
|
276
302
|
|
|
277
303
|
print(f'{prop_1.label}: {prop_1.origin} -> {prop_1.target})
|
|
278
304
|
print(f'Property data: {prop_1.data})
|
|
@@ -281,7 +307,7 @@ print(f'Property data: {prop_1.data})
|
|
|
281
307
|
# Property data: {'sql_metadata': 'PRIMARY_KEY'}
|
|
282
308
|
```
|
|
283
309
|
|
|
284
|
-
In the above example, the `
|
|
310
|
+
In the above example, the `select` function is used to customize `prop_1` and `prop_2`, providing different `label`s ("prop_one" and "prop_two") and adding some metadata. As we can see when we log the `data` attribute of the resulting `Prop`, the keyword arguments we provide to the `select` function are captured in the `Prop` in the form of a `dict`. The metadata in the example above could be used to generate a SQL table from `MyModel`, or interpret `join` queries using `prop_2`.
|
|
285
311
|
|
|
286
312
|
Once you define a model, there are kinds of instances we can construct:
|
|
287
313
|
|
|
@@ -811,7 +837,7 @@ print(value)
|
|
|
811
837
|
# Root(super=[Sub1(prop_1=24), Sub2(prop_2="hello")])
|
|
812
838
|
```
|
|
813
839
|
|
|
814
|
-
The `
|
|
840
|
+
The `Discrim[A, B]` type is a `SelectOpt` that selects down from some value of supertype `B` to a subtype `A`, filtering out cases where it is not that subtype. Note that `Discrim` property is treated differently by `BaseModel` than regular properties. It generates a special string attribute `subtype` (or whatever the property name is) that is automatically set to the class name of the subtype when a model is constructed. The `Discrim` `Select` checks whether this string value matches the class name to conclude it is the correct subtype.
|
|
815
841
|
|
|
816
842
|
You can see the discriminator value if you convert a model to a `dict`:
|
|
817
843
|
|
|
@@ -833,7 +859,7 @@ class Model(BaseModel):
|
|
|
833
859
|
prop_1: Prop['Model', int]
|
|
834
860
|
prop_2: Prop['Model', str]
|
|
835
861
|
|
|
836
|
-
all_props: List[PropSelect[Any, Any] |
|
|
862
|
+
all_props: List[PropSelect[Any, Any] | Discrim[Any, Any]] = Model.properties()
|
|
837
863
|
|
|
838
864
|
assert len(all_props) == 2
|
|
839
865
|
assert all_props[0] = Model.prop_1
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
from pydoptic_elastic.elastic_model import ESFieldData, ESMapping, ElasticModel, elastic_prop
|
|
2
|
+
from pydoptic_elastic.elastic_service import ElasticService
|
|
3
|
+
from pydoptic_elastic.elastic_query import Query
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
'ESFieldData', 'ESMapping', 'ElasticModel', 'elastic_prop',
|
|
7
|
+
'ElasticService',
|
|
8
|
+
'Query',
|
|
9
|
+
]
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
from pydoptic_sql.sql_table import (
|
|
2
|
+
AutoIncrement,
|
|
3
|
+
Check,
|
|
4
|
+
ColumnConstraint,
|
|
5
|
+
ColumnInfo,
|
|
6
|
+
ColumnType,
|
|
7
|
+
Default,
|
|
8
|
+
ForeignKey,
|
|
9
|
+
ManualColumnConstraint,
|
|
10
|
+
PrimaryKey,
|
|
11
|
+
SqlTable,
|
|
12
|
+
Unique,
|
|
13
|
+
column,
|
|
14
|
+
)
|
|
15
|
+
from pydoptic_sql.sql_constraint import Comparison, Constraint, Constraint2, Constraint3, Constraint4
|
|
16
|
+
from pydoptic_sql.sql_order import Direction, OrderBy
|
|
17
|
+
from pydoptic_sql.sql_computed import AggregateFunction, Computed, ComputedResult
|
|
18
|
+
from pydoptic_sql.sql_having import HavingConstraint, HavingConstraint2, HavingConstraint3, HavingConstraint4
|
|
19
|
+
from pydoptic_sql.sql_query import (
|
|
20
|
+
ComputedQuery1,
|
|
21
|
+
ComputedQuery2,
|
|
22
|
+
ComputedQuery3,
|
|
23
|
+
ComputedQuery4,
|
|
24
|
+
CreateQuery,
|
|
25
|
+
DeleteQuery,
|
|
26
|
+
DropQuery,
|
|
27
|
+
InsertQuery,
|
|
28
|
+
JoinType,
|
|
29
|
+
Query1,
|
|
30
|
+
Query2,
|
|
31
|
+
Query3,
|
|
32
|
+
Query4,
|
|
33
|
+
SqlQuery,
|
|
34
|
+
UpdateQuery,
|
|
35
|
+
)
|
|
36
|
+
from pydoptic_sql.sql_service import PsycoPgSqlClient, SqlClient, SqlResponse, SqlTransaction
|
|
37
|
+
|
|
38
|
+
__all__ = [
|
|
39
|
+
'AutoIncrement', 'Check', 'ColumnConstraint', 'ColumnInfo', 'ColumnType', 'Default', 'ForeignKey',
|
|
40
|
+
'ManualColumnConstraint', 'PrimaryKey', 'SqlTable', 'Unique', 'column',
|
|
41
|
+
'Comparison', 'Constraint', 'Constraint2', 'Constraint3', 'Constraint4',
|
|
42
|
+
'Direction', 'OrderBy',
|
|
43
|
+
'AggregateFunction', 'Computed', 'ComputedResult',
|
|
44
|
+
'HavingConstraint', 'HavingConstraint2', 'HavingConstraint3', 'HavingConstraint4',
|
|
45
|
+
'ComputedQuery1', 'ComputedQuery2', 'ComputedQuery3', 'ComputedQuery4',
|
|
46
|
+
'CreateQuery', 'DeleteQuery', 'DropQuery', 'InsertQuery', 'JoinType',
|
|
47
|
+
'Query1', 'Query2', 'Query3', 'Query4', 'SqlQuery', 'UpdateQuery',
|
|
48
|
+
'PsycoPgSqlClient', 'SqlClient', 'SqlResponse', 'SqlTransaction',
|
|
49
|
+
]
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
from pydoptic_sql.sql_table import SqlTable, ColumnConstraint, ColumnType, column, PrimaryKey, Unique, AutoIncrement, ForeignKey
|
|
2
|
-
from pydoptic_sql.sql_constraint import Constraint, Constraint2, Constraint3, Constraint4
|
|
3
|
-
from pydoptic_sql.sql_order import Direction, OrderBy
|
|
4
|
-
from pydoptic_sql.sql_computed import AggregateFunction, Computed, ComputedResult
|
|
5
|
-
from pydoptic_sql.sql_having import HavingConstraint, HavingConstraint2, HavingConstraint3, HavingConstraint4
|
|
6
|
-
from pydoptic_sql.sql_query import SqlQuery
|
{pydoptic-0.0.post1.dev38 → pydoptic-0.0.1.post1.dev1}/.github/workflows/integration-tests.yml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pydoptic-0.0.post1.dev38 → pydoptic-0.0.1.post1.dev1}/src/pydoptic.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
{pydoptic-0.0.post1.dev38 → pydoptic-0.0.1.post1.dev1}/src/pydoptic.egg-info/scm_file_list.json
RENAMED
|
File without changes
|
|
File without changes
|
{pydoptic-0.0.post1.dev38 → pydoptic-0.0.1.post1.dev1}/src/pydoptic_elastic/elastic_model.py
RENAMED
|
File without changes
|
{pydoptic-0.0.post1.dev38 → pydoptic-0.0.1.post1.dev1}/src/pydoptic_elastic/elastic_query.py
RENAMED
|
File without changes
|
{pydoptic-0.0.post1.dev38 → pydoptic-0.0.1.post1.dev1}/src/pydoptic_elastic/elastic_service.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pydoptic-0.0.post1.dev38 → pydoptic-0.0.1.post1.dev1}/test/pydoptic_elastic/test_elastic_model.py
RENAMED
|
File without changes
|
{pydoptic-0.0.post1.dev38 → pydoptic-0.0.1.post1.dev1}/test/pydoptic_elastic/test_elastic_service.py
RENAMED
|
File without changes
|
{pydoptic-0.0.post1.dev38 → pydoptic-0.0.1.post1.dev1}/test/pydoptic_sql/test_sql_computed.py
RENAMED
|
File without changes
|
{pydoptic-0.0.post1.dev38 → pydoptic-0.0.1.post1.dev1}/test/pydoptic_sql/test_sql_constraint.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pydoptic-0.0.post1.dev38 → pydoptic-0.0.1.post1.dev1}/test/pydoptic_sql/test_sql_service.py
RENAMED
|
File without changes
|