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.
Files changed (54) hide show
  1. {pydoptic-0.0.post1.dev38 → pydoptic-0.0.1.post1.dev1}/PKG-INFO +37 -11
  2. {pydoptic-0.0.post1.dev38 → pydoptic-0.0.1.post1.dev1}/README.md +36 -10
  3. pydoptic-0.0.1.post1.dev1/src/pydoptic/__init__.py +33 -0
  4. {pydoptic-0.0.post1.dev38 → pydoptic-0.0.1.post1.dev1}/src/pydoptic.egg-info/PKG-INFO +37 -11
  5. pydoptic-0.0.1.post1.dev1/src/pydoptic.egg-info/scm_version.json +8 -0
  6. pydoptic-0.0.1.post1.dev1/src/pydoptic_elastic/__init__.py +9 -0
  7. pydoptic-0.0.1.post1.dev1/src/pydoptic_sql/__init__.py +49 -0
  8. pydoptic-0.0.post1.dev38/src/pydoptic/__init__.py +0 -3
  9. pydoptic-0.0.post1.dev38/src/pydoptic.egg-info/scm_version.json +0 -8
  10. pydoptic-0.0.post1.dev38/src/pydoptic_elastic/__init__.py +0 -3
  11. pydoptic-0.0.post1.dev38/src/pydoptic_sql/__init__.py +0 -6
  12. {pydoptic-0.0.post1.dev38 → pydoptic-0.0.1.post1.dev1}/.github/workflows/integration-tests.yml +0 -0
  13. {pydoptic-0.0.post1.dev38 → pydoptic-0.0.1.post1.dev1}/.github/workflows/release.yml +0 -0
  14. {pydoptic-0.0.post1.dev38 → pydoptic-0.0.1.post1.dev1}/.github/workflows/snapshot.yml +0 -0
  15. {pydoptic-0.0.post1.dev38 → pydoptic-0.0.1.post1.dev1}/.github/workflows/unit-tests.yml +0 -0
  16. {pydoptic-0.0.post1.dev38 → pydoptic-0.0.1.post1.dev1}/.gitignore +0 -0
  17. {pydoptic-0.0.post1.dev38 → pydoptic-0.0.1.post1.dev1}/CONTEXT.md +0 -0
  18. {pydoptic-0.0.post1.dev38 → pydoptic-0.0.1.post1.dev1}/LICENSE +0 -0
  19. {pydoptic-0.0.post1.dev38 → pydoptic-0.0.1.post1.dev1}/benchmarks/bench_model.py +0 -0
  20. {pydoptic-0.0.post1.dev38 → pydoptic-0.0.1.post1.dev1}/docker-compose.yml +0 -0
  21. {pydoptic-0.0.post1.dev38 → pydoptic-0.0.1.post1.dev1}/mypy.ini +0 -0
  22. {pydoptic-0.0.post1.dev38 → pydoptic-0.0.1.post1.dev1}/pyproject.toml +0 -0
  23. {pydoptic-0.0.post1.dev38 → pydoptic-0.0.1.post1.dev1}/setup.cfg +0 -0
  24. {pydoptic-0.0.post1.dev38 → pydoptic-0.0.1.post1.dev1}/src/pydoptic/base_model.py +0 -0
  25. {pydoptic-0.0.post1.dev38 → pydoptic-0.0.1.post1.dev1}/src/pydoptic/py.typed +0 -0
  26. {pydoptic-0.0.post1.dev38 → pydoptic-0.0.1.post1.dev1}/src/pydoptic/selector.py +0 -0
  27. {pydoptic-0.0.post1.dev38 → pydoptic-0.0.1.post1.dev1}/src/pydoptic/validate_types.py +0 -0
  28. {pydoptic-0.0.post1.dev38 → pydoptic-0.0.1.post1.dev1}/src/pydoptic.egg-info/SOURCES.txt +0 -0
  29. {pydoptic-0.0.post1.dev38 → pydoptic-0.0.1.post1.dev1}/src/pydoptic.egg-info/dependency_links.txt +0 -0
  30. {pydoptic-0.0.post1.dev38 → pydoptic-0.0.1.post1.dev1}/src/pydoptic.egg-info/requires.txt +0 -0
  31. {pydoptic-0.0.post1.dev38 → pydoptic-0.0.1.post1.dev1}/src/pydoptic.egg-info/scm_file_list.json +0 -0
  32. {pydoptic-0.0.post1.dev38 → pydoptic-0.0.1.post1.dev1}/src/pydoptic.egg-info/top_level.txt +0 -0
  33. {pydoptic-0.0.post1.dev38 → pydoptic-0.0.1.post1.dev1}/src/pydoptic_elastic/elastic_model.py +0 -0
  34. {pydoptic-0.0.post1.dev38 → pydoptic-0.0.1.post1.dev1}/src/pydoptic_elastic/elastic_query.py +0 -0
  35. {pydoptic-0.0.post1.dev38 → pydoptic-0.0.1.post1.dev1}/src/pydoptic_elastic/elastic_service.py +0 -0
  36. {pydoptic-0.0.post1.dev38 → pydoptic-0.0.1.post1.dev1}/src/pydoptic_elastic/py.typed +0 -0
  37. {pydoptic-0.0.post1.dev38 → pydoptic-0.0.1.post1.dev1}/src/pydoptic_sql/sql_computed.py +0 -0
  38. {pydoptic-0.0.post1.dev38 → pydoptic-0.0.1.post1.dev1}/src/pydoptic_sql/sql_constraint.py +0 -0
  39. {pydoptic-0.0.post1.dev38 → pydoptic-0.0.1.post1.dev1}/src/pydoptic_sql/sql_having.py +0 -0
  40. {pydoptic-0.0.post1.dev38 → pydoptic-0.0.1.post1.dev1}/src/pydoptic_sql/sql_order.py +0 -0
  41. {pydoptic-0.0.post1.dev38 → pydoptic-0.0.1.post1.dev1}/src/pydoptic_sql/sql_query.py +0 -0
  42. {pydoptic-0.0.post1.dev38 → pydoptic-0.0.1.post1.dev1}/src/pydoptic_sql/sql_service.py +0 -0
  43. {pydoptic-0.0.post1.dev38 → pydoptic-0.0.1.post1.dev1}/src/pydoptic_sql/sql_table.py +0 -0
  44. {pydoptic-0.0.post1.dev38 → pydoptic-0.0.1.post1.dev1}/test/pydoptic/test_base_model.py +0 -0
  45. {pydoptic-0.0.post1.dev38 → pydoptic-0.0.1.post1.dev1}/test/pydoptic/test_partial_model.py +0 -0
  46. {pydoptic-0.0.post1.dev38 → pydoptic-0.0.1.post1.dev1}/test/pydoptic/test_selector.py +0 -0
  47. {pydoptic-0.0.post1.dev38 → pydoptic-0.0.1.post1.dev1}/test/pydoptic_elastic/test_elastic_model.py +0 -0
  48. {pydoptic-0.0.post1.dev38 → pydoptic-0.0.1.post1.dev1}/test/pydoptic_elastic/test_elastic_service.py +0 -0
  49. {pydoptic-0.0.post1.dev38 → pydoptic-0.0.1.post1.dev1}/test/pydoptic_sql/test_sql_computed.py +0 -0
  50. {pydoptic-0.0.post1.dev38 → pydoptic-0.0.1.post1.dev1}/test/pydoptic_sql/test_sql_constraint.py +0 -0
  51. {pydoptic-0.0.post1.dev38 → pydoptic-0.0.1.post1.dev1}/test/pydoptic_sql/test_sql_having.py +0 -0
  52. {pydoptic-0.0.post1.dev38 → pydoptic-0.0.1.post1.dev1}/test/pydoptic_sql/test_sql_params.py +0 -0
  53. {pydoptic-0.0.post1.dev38 → pydoptic-0.0.1.post1.dev1}/test/pydoptic_sql/test_sql_query.py +0 -0
  54. {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.dev38
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 = related_status_select.get(person).as_list
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
- related_status_select.update(person, lambda status: !status)
174
+ select_related_statuses.update(person, lambda status: !status)
161
175
 
162
- statuses = related_status_select.get(person).as_list
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, `select_related_status` 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.
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] = property('prop_one', sql_metadata='PRIMARY KEY')
275
- prop_2: Prop['Model2', str] = property('prop_two', sql_metadata='FOREIGN KEY')
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 `property` 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 `property` 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`.
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 `Discr[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 `Discr` 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 `Discr` `Select` checks whether this string value matches the class name to conclude it is the correct subtype.
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] | Discr[Any, Any]] = Model.properties()
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 = related_status_select.get(person).as_list
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
- related_status_select.update(person, lambda status: !status)
152
+ select_related_statuses.update(person, lambda status: !status)
139
153
 
140
- statuses = related_status_select.get(person).as_list
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, `select_related_status` 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.
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] = property('prop_one', sql_metadata='PRIMARY KEY')
253
- prop_2: Prop['Model2', str] = property('prop_two', sql_metadata='FOREIGN KEY')
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 `property` 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 `property` 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`.
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 `Discr[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 `Discr` 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 `Discr` `Select` checks whether this string value matches the class name to conclude it is the correct subtype.
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] | Discr[Any, Any]] = Model.properties()
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.dev38
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 = related_status_select.get(person).as_list
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
- related_status_select.update(person, lambda status: !status)
174
+ select_related_statuses.update(person, lambda status: !status)
161
175
 
162
- statuses = related_status_select.get(person).as_list
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, `select_related_status` 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.
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] = property('prop_one', sql_metadata='PRIMARY KEY')
275
- prop_2: Prop['Model2', str] = property('prop_two', sql_metadata='FOREIGN KEY')
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 `property` 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 `property` 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`.
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 `Discr[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 `Discr` 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 `Discr` `Select` checks whether this string value matches the class name to conclude it is the correct subtype.
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] | Discr[Any, Any]] = Model.properties()
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,8 @@
1
+ {
2
+ "tag": "0.0.1",
3
+ "distance": 1,
4
+ "node": "gb41e03ecd15e1c898f91a988d95c9f29270a4b5f",
5
+ "dirty": false,
6
+ "branch": "main",
7
+ "node_date": "2026-08-18"
8
+ }
@@ -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,3 +0,0 @@
1
- from pydoptic.base_model import BaseModel, PartialModel, select
2
- from pydoptic.selector import Select, SelectVal, SelectOpt, SelectArr, SelectOptArr, SelectValue, Prop, PropOpt, PropArr, PropOptArr
3
- from pydoptic.validate_types import Validator
@@ -1,8 +0,0 @@
1
- {
2
- "tag": "0.0",
3
- "distance": 38,
4
- "node": "g0f93999978115c3051abd02d6f8fbf3c1de9200e",
5
- "dirty": false,
6
- "branch": "main",
7
- "node_date": "2026-08-17"
8
- }
@@ -1,3 +0,0 @@
1
- from pydoptic_elastic.elastic_model import ElasticModel, ESMapping, elastic_prop
2
- from pydoptic_elastic.elastic_service import ElasticService
3
- from pydoptic_elastic.elastic_query import Query
@@ -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