pydantic-prism 0.1.0__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.
- pydantic_prism-0.1.0/LICENSE +21 -0
- pydantic_prism-0.1.0/PKG-INFO +138 -0
- pydantic_prism-0.1.0/README.md +98 -0
- pydantic_prism-0.1.0/pyproject.toml +151 -0
- pydantic_prism-0.1.0/src/pydantic_prism/__init__.py +73 -0
- pydantic_prism-0.1.0/src/pydantic_prism/__main__.py +5 -0
- pydantic_prism-0.1.0/src/pydantic_prism/_internal/__init__.py +7 -0
- pydantic_prism-0.1.0/src/pydantic_prism/_internal/codegen/__init__.py +44 -0
- pydantic_prism-0.1.0/src/pydantic_prism/_internal/codegen/cli.py +175 -0
- pydantic_prism-0.1.0/src/pydantic_prism/_internal/codegen/config.py +105 -0
- pydantic_prism-0.1.0/src/pydantic_prism/_internal/codegen/discover.py +124 -0
- pydantic_prism-0.1.0/src/pydantic_prism/_internal/codegen/generate.py +110 -0
- pydantic_prism-0.1.0/src/pydantic_prism/_internal/codegen/render.py +231 -0
- pydantic_prism-0.1.0/src/pydantic_prism/_internal/diagram.py +442 -0
- pydantic_prism-0.1.0/src/pydantic_prism/_internal/drift.py +62 -0
- pydantic_prism-0.1.0/src/pydantic_prism/_internal/flow.py +174 -0
- pydantic_prism-0.1.0/src/pydantic_prism/_internal/markers.py +155 -0
- pydantic_prism-0.1.0/src/pydantic_prism/_internal/model/__init__.py +24 -0
- pydantic_prism-0.1.0/src/pydantic_prism/_internal/model/bases.py +90 -0
- pydantic_prism-0.1.0/src/pydantic_prism/_internal/model/build.py +321 -0
- pydantic_prism-0.1.0/src/pydantic_prism/_internal/model/classes.py +411 -0
- pydantic_prism-0.1.0/src/pydantic_prism/_internal/model/collect.py +183 -0
- pydantic_prism-0.1.0/src/pydantic_prism/_internal/model/narrow.py +75 -0
- pydantic_prism-0.1.0/src/pydantic_prism/_internal/model/schema.py +100 -0
- pydantic_prism-0.1.0/src/pydantic_prism/_internal/readme.py +86 -0
- pydantic_prism-0.1.0/src/pydantic_prism/_internal/refs.py +362 -0
- pydantic_prism-0.1.0/src/pydantic_prism/_internal/scopes.py +397 -0
- pydantic_prism-0.1.0/src/pydantic_prism/_internal/validators.py +73 -0
- pydantic_prism-0.1.0/src/pydantic_prism/errors.py +69 -0
- pydantic_prism-0.1.0/src/pydantic_prism/py.typed +0 -0
- pydantic_prism-0.1.0/tests/__init__.py +0 -0
- pydantic_prism-0.1.0/tests/_codegen_fixtures.py +54 -0
- pydantic_prism-0.1.0/tests/_flow_fixtures.py +44 -0
- pydantic_prism-0.1.0/tests/test_bugsweep.py +274 -0
- pydantic_prism-0.1.0/tests/test_classification.py +171 -0
- pydantic_prism-0.1.0/tests/test_codegen.py +646 -0
- pydantic_prism-0.1.0/tests/test_custom_base.py +203 -0
- pydantic_prism-0.1.0/tests/test_default_scope.py +237 -0
- pydantic_prism-0.1.0/tests/test_diagram.py +294 -0
- pydantic_prism-0.1.0/tests/test_dict_refs.py +143 -0
- pydantic_prism-0.1.0/tests/test_docs.py +81 -0
- pydantic_prism-0.1.0/tests/test_embedded_refs.py +111 -0
- pydantic_prism-0.1.0/tests/test_ergonomics.py +86 -0
- pydantic_prism-0.1.0/tests/test_errors.py +104 -0
- pydantic_prism-0.1.0/tests/test_example_readmes.py +25 -0
- pydantic_prism-0.1.0/tests/test_fastapi.py +65 -0
- pydantic_prism-0.1.0/tests/test_from_canonical.py +107 -0
- pydantic_prism-0.1.0/tests/test_future_annotations.py +63 -0
- pydantic_prism-0.1.0/tests/test_internals.py +248 -0
- pydantic_prism-0.1.0/tests/test_name_template.py +72 -0
- pydantic_prism-0.1.0/tests/test_nested.py +97 -0
- pydantic_prism-0.1.0/tests/test_partial.py +156 -0
- pydantic_prism-0.1.0/tests/test_projection.py +201 -0
- pydantic_prism-0.1.0/tests/test_readme.py +374 -0
- pydantic_prism-0.1.0/tests/test_refinfo_kinds.py +98 -0
- pydantic_prism-0.1.0/tests/test_refs.py +160 -0
- pydantic_prism-0.1.0/tests/test_roundtrip.py +58 -0
- pydantic_prism-0.1.0/tests/test_scope_schema.py +158 -0
- pydantic_prism-0.1.0/tests/test_scoped_validator.py +185 -0
- pydantic_prism-0.1.0/tests/test_scopes.py +109 -0
- pydantic_prism-0.1.0/tests/test_sqlalchemy_bridge.py +115 -0
- pydantic_prism-0.1.0/tests/test_sqlmodel_bridge.py +176 -0
- pydantic_prism-0.1.0/tests/test_with_updates.py +144 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ilja Orlovs
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pydantic-prism
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: One canonical pydantic model, many scoped projections — with a relationship graph that survives them.
|
|
5
|
+
Keywords: pydantic,schema,projection,scopes,dto,serialization,validation,json-schema,fastapi,data-classification,pii,code-generation,type-safety
|
|
6
|
+
Author-Email: Ilja Orlovs <IljaOrlovs@users.noreply.github.com>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Classifier: Development Status :: 4 - Beta
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
16
|
+
Classifier: Topic :: Software Development :: Code Generators
|
|
17
|
+
Classifier: Framework :: Pydantic
|
|
18
|
+
Classifier: Framework :: Pydantic :: 2
|
|
19
|
+
Classifier: Framework :: FastAPI
|
|
20
|
+
Classifier: Typing :: Typed
|
|
21
|
+
Project-URL: Homepage, https://github.com/release-art/pydantic-prism
|
|
22
|
+
Project-URL: Documentation, https://github.com/release-art/pydantic-prism/blob/main/docs/README.md
|
|
23
|
+
Project-URL: Repository, https://github.com/release-art/pydantic-prism
|
|
24
|
+
Project-URL: Changelog, https://github.com/release-art/pydantic-prism/blob/main/CHANGELOG.md
|
|
25
|
+
Project-URL: Issues, https://github.com/release-art/pydantic-prism/issues
|
|
26
|
+
Requires-Python: >=3.12
|
|
27
|
+
Requires-Dist: pydantic>=2.12
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: pytest>=9.0.3; extra == "dev"
|
|
30
|
+
Requires-Dist: pytest-cov>=6.0; extra == "dev"
|
|
31
|
+
Requires-Dist: pytest-mock>=3.15.1; extra == "dev"
|
|
32
|
+
Requires-Dist: ruff>=0.15.16; extra == "dev"
|
|
33
|
+
Requires-Dist: pyright>=1.1.410; extra == "dev"
|
|
34
|
+
Requires-Dist: fastapi>=0.115; extra == "dev"
|
|
35
|
+
Requires-Dist: httpx>=0.27; extra == "dev"
|
|
36
|
+
Requires-Dist: pytest-local-badge>=1.3.0; extra == "dev"
|
|
37
|
+
Requires-Dist: sqlmodel>=0.0.22; extra == "dev"
|
|
38
|
+
Requires-Dist: sqlalchemy>=2.0; extra == "dev"
|
|
39
|
+
Description-Content-Type: text/markdown
|
|
40
|
+
|
|
41
|
+
# pydantic-prism
|
|
42
|
+
|
|
43
|
+

|
|
44
|
+

|
|
45
|
+

|
|
46
|
+

|
|
47
|
+

|
|
48
|
+

|
|
49
|
+

|
|
50
|
+
|
|
51
|
+
Project one canonical pydantic model along any axis — and keep the
|
|
52
|
+
relationship graph that survives the projection.
|
|
53
|
+
|
|
54
|
+
Tag a single model's fields with named **scopes**; derive real
|
|
55
|
+
`pydantic.BaseModel` subclasses per scope (API response, storage row, LLM
|
|
56
|
+
tool input, audit log) with working validation, serialization, and JSON
|
|
57
|
+
schema. Declare FK-style **references** in the same metadata and introspect
|
|
58
|
+
them through `__refs__` — the graph survives every projection. The
|
|
59
|
+
projection half has prior art; [combining it with an introspectable
|
|
60
|
+
relationship graph](docs/explanation/vs-prior-art.md) does not.
|
|
61
|
+
|
|
62
|
+
## 30 seconds
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
from typing import Annotated
|
|
66
|
+
from uuid import UUID
|
|
67
|
+
|
|
68
|
+
from pydantic_prism import Scope, ScopedModel, scoped
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class Public(Scope): ...
|
|
72
|
+
class Internal(Public): ... # Internal sees everything Public sees
|
|
73
|
+
class Storage(Internal): ... # Storage sees everything Internal sees
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class User(ScopedModel):
|
|
77
|
+
id: Annotated[UUID, scoped(Public)]
|
|
78
|
+
email: Annotated[str, scoped(Internal)]
|
|
79
|
+
password_hash: Annotated[str, scoped(Storage)]
|
|
80
|
+
display_name: Annotated[str, scoped(Public)]
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
UserPublic = User.scope(Public) # fields: id, display_name
|
|
84
|
+
UserInternal = User.scope(Internal) # fields: id, email, display_name
|
|
85
|
+
UserStorage = User.scope(Storage) # all four fields
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
`UserPublic` is a real, cached `BaseModel` subclass named `"UserPublic"` —
|
|
89
|
+
`User.scope(Public) is User.scope(Public)`, so FastAPI response models and
|
|
90
|
+
OpenAPI component schemas stay stable.
|
|
91
|
+
|
|
92
|
+
Scopes are classes; inheritance forms the scope graph, so the membership
|
|
93
|
+
rule is one line: a field tagged `T` is in projection `S` iff
|
|
94
|
+
`issubclass(S, T)`. Untagged fields belong to no scope and can never leak
|
|
95
|
+
into a projection. Scopes compose with set operators (`| & - ~`), both in
|
|
96
|
+
field tags and at the call site.
|
|
97
|
+
|
|
98
|
+
## Why not hand-write `UserIn` / `UserOut`?
|
|
99
|
+
|
|
100
|
+
Parallel classes drift from the canonical, lose constraints, and have no
|
|
101
|
+
idea your models reference each other. Prism derives every face from one
|
|
102
|
+
source of truth and keeps the references coherent across all of them.
|
|
103
|
+
See [projections, not inheritance](docs/explanation/projections-not-inheritance.md).
|
|
104
|
+
|
|
105
|
+
## Install
|
|
106
|
+
|
|
107
|
+
```sh
|
|
108
|
+
pip install pydantic-prism # pydantic >= 2.12, Python >= 3.12
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Documentation
|
|
112
|
+
|
|
113
|
+
The docs follow the [Diátaxis](https://diataxis.fr) framework — start where
|
|
114
|
+
your need fits:
|
|
115
|
+
|
|
116
|
+
- **[Documentation home](docs/README.md)** — the full table of contents.
|
|
117
|
+
- **[Tutorial: your first scoped model](docs/tutorial/first-scoped-model.md)** —
|
|
118
|
+
one hand-held lesson, one model to two projections.
|
|
119
|
+
- **[How-to guides](docs/how-to/README.md)** — short recipes: redact PII,
|
|
120
|
+
trace data flow, PATCH models, FastAPI, ORM bridge, editor stubs, diagrams.
|
|
121
|
+
- **[Reference](docs/reference/README.md)** — the [API](docs/reference/api.md),
|
|
122
|
+
the [`prism` CLI](docs/reference/cli.md), and the [error table](docs/reference/errors.md).
|
|
123
|
+
- **[Explanation](docs/explanation/README.md)** — the scope algebra, why
|
|
124
|
+
projections aren't inheritance, what `ref()` does and does not model.
|
|
125
|
+
|
|
126
|
+
[**ROADMAP.md**](ROADMAP.md) lists what is shipped, planned, and deliberately
|
|
127
|
+
out of scope.
|
|
128
|
+
|
|
129
|
+
## Develop
|
|
130
|
+
|
|
131
|
+
```sh
|
|
132
|
+
pdm install -G dev
|
|
133
|
+
bin/test.sh # pytest with coverage (100% gate)
|
|
134
|
+
bin/autoformat.sh # ruff format + ruff check --fix
|
|
135
|
+
pdm run pyright # strict, src/
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
MIT licensed. Built on the public pydantic API only — no `pydantic._internal`.
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# pydantic-prism
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+

|
|
5
|
+

|
|
6
|
+

|
|
7
|
+

|
|
8
|
+

|
|
9
|
+

|
|
10
|
+
|
|
11
|
+
Project one canonical pydantic model along any axis — and keep the
|
|
12
|
+
relationship graph that survives the projection.
|
|
13
|
+
|
|
14
|
+
Tag a single model's fields with named **scopes**; derive real
|
|
15
|
+
`pydantic.BaseModel` subclasses per scope (API response, storage row, LLM
|
|
16
|
+
tool input, audit log) with working validation, serialization, and JSON
|
|
17
|
+
schema. Declare FK-style **references** in the same metadata and introspect
|
|
18
|
+
them through `__refs__` — the graph survives every projection. The
|
|
19
|
+
projection half has prior art; [combining it with an introspectable
|
|
20
|
+
relationship graph](docs/explanation/vs-prior-art.md) does not.
|
|
21
|
+
|
|
22
|
+
## 30 seconds
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
from typing import Annotated
|
|
26
|
+
from uuid import UUID
|
|
27
|
+
|
|
28
|
+
from pydantic_prism import Scope, ScopedModel, scoped
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class Public(Scope): ...
|
|
32
|
+
class Internal(Public): ... # Internal sees everything Public sees
|
|
33
|
+
class Storage(Internal): ... # Storage sees everything Internal sees
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class User(ScopedModel):
|
|
37
|
+
id: Annotated[UUID, scoped(Public)]
|
|
38
|
+
email: Annotated[str, scoped(Internal)]
|
|
39
|
+
password_hash: Annotated[str, scoped(Storage)]
|
|
40
|
+
display_name: Annotated[str, scoped(Public)]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
UserPublic = User.scope(Public) # fields: id, display_name
|
|
44
|
+
UserInternal = User.scope(Internal) # fields: id, email, display_name
|
|
45
|
+
UserStorage = User.scope(Storage) # all four fields
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
`UserPublic` is a real, cached `BaseModel` subclass named `"UserPublic"` —
|
|
49
|
+
`User.scope(Public) is User.scope(Public)`, so FastAPI response models and
|
|
50
|
+
OpenAPI component schemas stay stable.
|
|
51
|
+
|
|
52
|
+
Scopes are classes; inheritance forms the scope graph, so the membership
|
|
53
|
+
rule is one line: a field tagged `T` is in projection `S` iff
|
|
54
|
+
`issubclass(S, T)`. Untagged fields belong to no scope and can never leak
|
|
55
|
+
into a projection. Scopes compose with set operators (`| & - ~`), both in
|
|
56
|
+
field tags and at the call site.
|
|
57
|
+
|
|
58
|
+
## Why not hand-write `UserIn` / `UserOut`?
|
|
59
|
+
|
|
60
|
+
Parallel classes drift from the canonical, lose constraints, and have no
|
|
61
|
+
idea your models reference each other. Prism derives every face from one
|
|
62
|
+
source of truth and keeps the references coherent across all of them.
|
|
63
|
+
See [projections, not inheritance](docs/explanation/projections-not-inheritance.md).
|
|
64
|
+
|
|
65
|
+
## Install
|
|
66
|
+
|
|
67
|
+
```sh
|
|
68
|
+
pip install pydantic-prism # pydantic >= 2.12, Python >= 3.12
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Documentation
|
|
72
|
+
|
|
73
|
+
The docs follow the [Diátaxis](https://diataxis.fr) framework — start where
|
|
74
|
+
your need fits:
|
|
75
|
+
|
|
76
|
+
- **[Documentation home](docs/README.md)** — the full table of contents.
|
|
77
|
+
- **[Tutorial: your first scoped model](docs/tutorial/first-scoped-model.md)** —
|
|
78
|
+
one hand-held lesson, one model to two projections.
|
|
79
|
+
- **[How-to guides](docs/how-to/README.md)** — short recipes: redact PII,
|
|
80
|
+
trace data flow, PATCH models, FastAPI, ORM bridge, editor stubs, diagrams.
|
|
81
|
+
- **[Reference](docs/reference/README.md)** — the [API](docs/reference/api.md),
|
|
82
|
+
the [`prism` CLI](docs/reference/cli.md), and the [error table](docs/reference/errors.md).
|
|
83
|
+
- **[Explanation](docs/explanation/README.md)** — the scope algebra, why
|
|
84
|
+
projections aren't inheritance, what `ref()` does and does not model.
|
|
85
|
+
|
|
86
|
+
[**ROADMAP.md**](ROADMAP.md) lists what is shipped, planned, and deliberately
|
|
87
|
+
out of scope.
|
|
88
|
+
|
|
89
|
+
## Develop
|
|
90
|
+
|
|
91
|
+
```sh
|
|
92
|
+
pdm install -G dev
|
|
93
|
+
bin/test.sh # pytest with coverage (100% gate)
|
|
94
|
+
bin/autoformat.sh # ruff format + ruff check --fix
|
|
95
|
+
pdm run pyright # strict, src/
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
MIT licensed. Built on the public pydantic API only — no `pydantic._internal`.
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "pydantic-prism"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "One canonical pydantic model, many scoped projections — with a relationship graph that survives them."
|
|
5
|
+
authors = [
|
|
6
|
+
{ name = "Ilja Orlovs", email = "IljaOrlovs@users.noreply.github.com" },
|
|
7
|
+
]
|
|
8
|
+
dependencies = [
|
|
9
|
+
"pydantic>=2.12",
|
|
10
|
+
]
|
|
11
|
+
requires-python = ">=3.12"
|
|
12
|
+
readme = "README.md"
|
|
13
|
+
license = "MIT"
|
|
14
|
+
keywords = [
|
|
15
|
+
"pydantic",
|
|
16
|
+
"schema",
|
|
17
|
+
"projection",
|
|
18
|
+
"scopes",
|
|
19
|
+
"dto",
|
|
20
|
+
"serialization",
|
|
21
|
+
"validation",
|
|
22
|
+
"json-schema",
|
|
23
|
+
"fastapi",
|
|
24
|
+
"data-classification",
|
|
25
|
+
"pii",
|
|
26
|
+
"code-generation",
|
|
27
|
+
"type-safety",
|
|
28
|
+
]
|
|
29
|
+
classifiers = [
|
|
30
|
+
"Development Status :: 4 - Beta",
|
|
31
|
+
"Intended Audience :: Developers",
|
|
32
|
+
"Operating System :: OS Independent",
|
|
33
|
+
"Programming Language :: Python :: 3",
|
|
34
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
35
|
+
"Programming Language :: Python :: 3.12",
|
|
36
|
+
"Programming Language :: Python :: 3.13",
|
|
37
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
38
|
+
"Topic :: Software Development :: Code Generators",
|
|
39
|
+
"Framework :: Pydantic",
|
|
40
|
+
"Framework :: Pydantic :: 2",
|
|
41
|
+
"Framework :: FastAPI",
|
|
42
|
+
"Typing :: Typed",
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
[project.urls]
|
|
46
|
+
Homepage = "https://github.com/release-art/pydantic-prism"
|
|
47
|
+
Documentation = "https://github.com/release-art/pydantic-prism/blob/main/docs/README.md"
|
|
48
|
+
Repository = "https://github.com/release-art/pydantic-prism"
|
|
49
|
+
Changelog = "https://github.com/release-art/pydantic-prism/blob/main/CHANGELOG.md"
|
|
50
|
+
Issues = "https://github.com/release-art/pydantic-prism/issues"
|
|
51
|
+
|
|
52
|
+
[project.scripts]
|
|
53
|
+
prism = "pydantic_prism._internal.codegen:main"
|
|
54
|
+
|
|
55
|
+
[project.optional-dependencies]
|
|
56
|
+
dev = [
|
|
57
|
+
"pytest>=9.0.3",
|
|
58
|
+
"pytest-cov>=6.0",
|
|
59
|
+
"pytest-mock>=3.15.1",
|
|
60
|
+
"ruff>=0.15.16",
|
|
61
|
+
"pyright>=1.1.410",
|
|
62
|
+
"fastapi>=0.115",
|
|
63
|
+
"httpx>=0.27",
|
|
64
|
+
"pytest-local-badge>=1.3.0",
|
|
65
|
+
"sqlmodel>=0.0.22",
|
|
66
|
+
"sqlalchemy>=2.0",
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
[build-system]
|
|
70
|
+
requires = [
|
|
71
|
+
"pdm-backend",
|
|
72
|
+
]
|
|
73
|
+
build-backend = "pdm.backend"
|
|
74
|
+
|
|
75
|
+
[tool.pdm]
|
|
76
|
+
distribution = true
|
|
77
|
+
|
|
78
|
+
[tool.ruff]
|
|
79
|
+
target-version = "py312"
|
|
80
|
+
line-length = 88
|
|
81
|
+
|
|
82
|
+
[tool.ruff.lint]
|
|
83
|
+
select = [
|
|
84
|
+
"A",
|
|
85
|
+
"B",
|
|
86
|
+
"C",
|
|
87
|
+
"E",
|
|
88
|
+
"F",
|
|
89
|
+
"I",
|
|
90
|
+
"W",
|
|
91
|
+
"N",
|
|
92
|
+
"C4",
|
|
93
|
+
"T20",
|
|
94
|
+
"PTH",
|
|
95
|
+
]
|
|
96
|
+
ignore = [
|
|
97
|
+
"N802",
|
|
98
|
+
]
|
|
99
|
+
|
|
100
|
+
[tool.ruff.lint.per-file-ignores]
|
|
101
|
+
"__init__.py" = [
|
|
102
|
+
"F401",
|
|
103
|
+
"E402",
|
|
104
|
+
]
|
|
105
|
+
"src/pydantic_prism/_internal/codegen/cli.py" = [
|
|
106
|
+
"T201",
|
|
107
|
+
]
|
|
108
|
+
"bin/*.py" = [
|
|
109
|
+
"T201",
|
|
110
|
+
]
|
|
111
|
+
"tests/**" = [
|
|
112
|
+
"N806",
|
|
113
|
+
]
|
|
114
|
+
"examples/**" = [
|
|
115
|
+
"N806",
|
|
116
|
+
"T201",
|
|
117
|
+
]
|
|
118
|
+
|
|
119
|
+
[tool.ruff.lint.isort]
|
|
120
|
+
order-by-type = true
|
|
121
|
+
known-first-party = [
|
|
122
|
+
"pydantic_prism",
|
|
123
|
+
]
|
|
124
|
+
forced-separate = [
|
|
125
|
+
"tests",
|
|
126
|
+
]
|
|
127
|
+
|
|
128
|
+
[tool.ruff.format]
|
|
129
|
+
quote-style = "double"
|
|
130
|
+
|
|
131
|
+
[tool.pyright]
|
|
132
|
+
include = [
|
|
133
|
+
"src",
|
|
134
|
+
]
|
|
135
|
+
typeCheckingMode = "strict"
|
|
136
|
+
pythonVersion = "3.12"
|
|
137
|
+
venvPath = "."
|
|
138
|
+
venv = ".venv"
|
|
139
|
+
|
|
140
|
+
[tool.coverage.report]
|
|
141
|
+
exclude_also = [
|
|
142
|
+
"raise NotImplementedError",
|
|
143
|
+
"if TYPE_CHECKING:",
|
|
144
|
+
]
|
|
145
|
+
|
|
146
|
+
[tool.pytest.ini_options]
|
|
147
|
+
minversion = "8.4"
|
|
148
|
+
testpaths = [
|
|
149
|
+
"tests",
|
|
150
|
+
]
|
|
151
|
+
addopts = " --cov --no-cov-on-fail\n -v -l --color=yes\n --local-badge-output-dir docs/badges/\n --cov-fail-under=100\n --local-badge-package=pydantic_prism\n"
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""pydantic-prism: one canonical pydantic model, many scoped projections.
|
|
2
|
+
|
|
3
|
+
Tag fields on a single model with named scopes via ``Annotated`` metadata,
|
|
4
|
+
derive real pydantic model subclasses per scope, and keep FK-style
|
|
5
|
+
relationships introspectable across projections.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from pydantic.experimental.missing_sentinel import MISSING
|
|
9
|
+
|
|
10
|
+
from ._internal.diagram import Diagram, projection_diagram, scope_diagram
|
|
11
|
+
from ._internal.flow import (
|
|
12
|
+
ClassifiedField,
|
|
13
|
+
FlowEdge,
|
|
14
|
+
FlowNode,
|
|
15
|
+
FlowReport,
|
|
16
|
+
build_flow_report,
|
|
17
|
+
)
|
|
18
|
+
from ._internal.markers import BackRef, Ref, Scoped, backref, ref, scoped
|
|
19
|
+
from ._internal.model import Projection, ScopedModel
|
|
20
|
+
from ._internal.refs import (
|
|
21
|
+
BackRefInfo,
|
|
22
|
+
EmbeddedRefInfo,
|
|
23
|
+
IdRefInfo,
|
|
24
|
+
RefGraph,
|
|
25
|
+
RefInfo,
|
|
26
|
+
RefShape,
|
|
27
|
+
)
|
|
28
|
+
from ._internal.scopes import Classification, Scope, ScopeExpr
|
|
29
|
+
from ._internal.validators import scoped_validator
|
|
30
|
+
from .errors import (
|
|
31
|
+
EmptyProjectionError,
|
|
32
|
+
PrismError,
|
|
33
|
+
ProjectionBaseError,
|
|
34
|
+
ProjectionNameError,
|
|
35
|
+
RefResolutionError,
|
|
36
|
+
StaleProjectionStubError,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
__all__ = [
|
|
40
|
+
"MISSING",
|
|
41
|
+
"BackRef",
|
|
42
|
+
"BackRefInfo",
|
|
43
|
+
"Classification",
|
|
44
|
+
"ClassifiedField",
|
|
45
|
+
"Diagram",
|
|
46
|
+
"EmbeddedRefInfo",
|
|
47
|
+
"EmptyProjectionError",
|
|
48
|
+
"FlowEdge",
|
|
49
|
+
"FlowNode",
|
|
50
|
+
"FlowReport",
|
|
51
|
+
"IdRefInfo",
|
|
52
|
+
"PrismError",
|
|
53
|
+
"Projection",
|
|
54
|
+
"ProjectionBaseError",
|
|
55
|
+
"ProjectionNameError",
|
|
56
|
+
"Ref",
|
|
57
|
+
"RefGraph",
|
|
58
|
+
"RefInfo",
|
|
59
|
+
"RefResolutionError",
|
|
60
|
+
"RefShape",
|
|
61
|
+
"Scope",
|
|
62
|
+
"ScopeExpr",
|
|
63
|
+
"Scoped",
|
|
64
|
+
"ScopedModel",
|
|
65
|
+
"StaleProjectionStubError",
|
|
66
|
+
"backref",
|
|
67
|
+
"build_flow_report",
|
|
68
|
+
"projection_diagram",
|
|
69
|
+
"ref",
|
|
70
|
+
"scope_diagram",
|
|
71
|
+
"scoped",
|
|
72
|
+
"scoped_validator",
|
|
73
|
+
]
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"""Private implementation package for pydantic-prism.
|
|
2
|
+
|
|
3
|
+
Everything here is internal: the public API is re-exported from
|
|
4
|
+
:mod:`pydantic_prism`'s top-level ``__init__``. Import paths under
|
|
5
|
+
``pydantic_prism._internal`` are not part of the supported surface and may change
|
|
6
|
+
between releases without notice.
|
|
7
|
+
"""
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""``prism gen`` / ``prism check`` — generate static-typing stubs for projections.
|
|
2
|
+
|
|
3
|
+
Static type checkers (pyright/Pylance — and thus VSCode — plus mypy) cannot see
|
|
4
|
+
the fields of ``Model.scope(...)``: scope membership lives in ``Annotated``
|
|
5
|
+
metadata and the selection runs the scope algebra at runtime. Pyright has no
|
|
6
|
+
third-party plugin API, so the only *universal* fix is ordinary type
|
|
7
|
+
declarations every tool already reads. This package generates them.
|
|
8
|
+
|
|
9
|
+
For each projection it emits, into one module::
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
class ScreenshotRef(Projection): # the checker reads this
|
|
13
|
+
id: UUID
|
|
14
|
+
timestamp: datetime
|
|
15
|
+
else:
|
|
16
|
+
ScreenshotRef = Screenshot.scope(Ref) # the genuine cached projection
|
|
17
|
+
|
|
18
|
+
assert_fresh(ScreenshotRef, "<signature>") # startup drift guard
|
|
19
|
+
|
|
20
|
+
Layout: :mod:`config` (load `[tool.pydantic-prism]`), :mod:`discover` (find
|
|
21
|
+
the projection workset), :mod:`render` (annotations/scopes/defaults → source),
|
|
22
|
+
:mod:`generate` (assemble the stub + README), :mod:`cli` (the ``prism``
|
|
23
|
+
command). Names re-exported here (with ``as`` aliases) keep the
|
|
24
|
+
``pydantic_prism._internal.codegen.X`` import paths stable.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from .cli import main as main
|
|
28
|
+
from .config import CodegenError as CodegenError
|
|
29
|
+
from .config import Config as Config
|
|
30
|
+
from .config import ProjectionSpec as ProjectionSpec
|
|
31
|
+
from .config import load_config as load_config
|
|
32
|
+
from .discover import _projections_in as _projections_in
|
|
33
|
+
from .discover import _reject_name_clashes as _reject_name_clashes
|
|
34
|
+
from .generate import generate as generate
|
|
35
|
+
from .generate import generate_readme as generate_readme
|
|
36
|
+
from .render import _field_suffix as _field_suffix
|
|
37
|
+
from .render import _import_lines as _import_lines
|
|
38
|
+
from .render import _Imports as _Imports
|
|
39
|
+
from .render import _render_annotation as _render_annotation
|
|
40
|
+
from .render import _render_bare as _render_bare
|
|
41
|
+
from .render import _render_literal as _render_literal
|
|
42
|
+
from .render import _render_scope_expr as _render_scope_expr
|
|
43
|
+
|
|
44
|
+
__all__ = ["CodegenError", "Config", "ProjectionSpec", "generate", "main"]
|