didactic 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.
Files changed (49) hide show
  1. didactic-0.1.0/.gitignore +48 -0
  2. didactic-0.1.0/PKG-INFO +68 -0
  3. didactic-0.1.0/README.md +48 -0
  4. didactic-0.1.0/pyproject.toml +40 -0
  5. didactic-0.1.0/src/didactic/_self_describing.py +206 -0
  6. didactic-0.1.0/src/didactic/api.py +126 -0
  7. didactic-0.1.0/src/didactic/axioms/__init__.py +11 -0
  8. didactic-0.1.0/src/didactic/axioms/_axiom_enforcement.py +236 -0
  9. didactic-0.1.0/src/didactic/axioms/_axioms.py +137 -0
  10. didactic-0.1.0/src/didactic/cli/__init__.py +7 -0
  11. didactic-0.1.0/src/didactic/cli/_cli.py +241 -0
  12. didactic-0.1.0/src/didactic/codegen/__init__.py +54 -0
  13. didactic-0.1.0/src/didactic/codegen/_emitter.py +286 -0
  14. didactic-0.1.0/src/didactic/codegen/_json_schema.py +215 -0
  15. didactic-0.1.0/src/didactic/codegen/_write.py +128 -0
  16. didactic-0.1.0/src/didactic/codegen/io.py +278 -0
  17. didactic-0.1.0/src/didactic/codegen/source.py +198 -0
  18. didactic-0.1.0/src/didactic/fields/__init__.py +28 -0
  19. didactic-0.1.0/src/didactic/fields/_computed.py +149 -0
  20. didactic-0.1.0/src/didactic/fields/_derived.py +142 -0
  21. didactic-0.1.0/src/didactic/fields/_fields.py +543 -0
  22. didactic-0.1.0/src/didactic/fields/_refs.py +246 -0
  23. didactic-0.1.0/src/didactic/fields/_unions.py +255 -0
  24. didactic-0.1.0/src/didactic/fields/_validators.py +164 -0
  25. didactic-0.1.0/src/didactic/lenses/__init__.py +16 -0
  26. didactic-0.1.0/src/didactic/lenses/_dependent_lens.py +315 -0
  27. didactic-0.1.0/src/didactic/lenses/_lens.py +444 -0
  28. didactic-0.1.0/src/didactic/lenses/_testing.py +302 -0
  29. didactic-0.1.0/src/didactic/migrations/__init__.py +38 -0
  30. didactic-0.1.0/src/didactic/migrations/_diff.py +128 -0
  31. didactic-0.1.0/src/didactic/migrations/_fingerprint.py +220 -0
  32. didactic-0.1.0/src/didactic/migrations/_migrations.py +499 -0
  33. didactic-0.1.0/src/didactic/migrations/_synthesis.py +138 -0
  34. didactic-0.1.0/src/didactic/models/__init__.py +19 -0
  35. didactic-0.1.0/src/didactic/models/_config.py +104 -0
  36. didactic-0.1.0/src/didactic/models/_meta.py +469 -0
  37. didactic-0.1.0/src/didactic/models/_model.py +790 -0
  38. didactic-0.1.0/src/didactic/models/_root.py +124 -0
  39. didactic-0.1.0/src/didactic/models/_storage.py +151 -0
  40. didactic-0.1.0/src/didactic/py.typed +0 -0
  41. didactic-0.1.0/src/didactic/theory/__init__.py +8 -0
  42. didactic-0.1.0/src/didactic/theory/_theory.py +477 -0
  43. didactic-0.1.0/src/didactic/types/__init__.py +16 -0
  44. didactic-0.1.0/src/didactic/types/_types.py +866 -0
  45. didactic-0.1.0/src/didactic/types/_types_lib.py +147 -0
  46. didactic-0.1.0/src/didactic/types/_typing.py +136 -0
  47. didactic-0.1.0/src/didactic/vcs/__init__.py +10 -0
  48. didactic-0.1.0/src/didactic/vcs/_backref.py +232 -0
  49. didactic-0.1.0/src/didactic/vcs/_repo.py +328 -0
@@ -0,0 +1,48 @@
1
+ # dev-only working notes, design drafts, scratch
2
+ notes/
3
+
4
+ # python
5
+ __pycache__/
6
+ *.py[cod]
7
+ *$py.class
8
+ *.so
9
+ .Python
10
+ build/
11
+ dist/
12
+ *.egg-info/
13
+ .eggs/
14
+ *.egg
15
+
16
+ # virtualenvs
17
+ .venv/
18
+ venv/
19
+ env/
20
+
21
+ # uv
22
+ .uv/
23
+
24
+ # testing / coverage
25
+ .pytest_cache/
26
+ .coverage
27
+ .coverage.*
28
+ htmlcov/
29
+ .tox/
30
+ .nox/
31
+
32
+ # type-checkers / linters
33
+ .mypy_cache/
34
+ .ruff_cache/
35
+ .pyright/
36
+
37
+ # mkdocs build output
38
+ site/
39
+
40
+ # editors
41
+ .vscode/
42
+ .idea/
43
+ *.swp
44
+ *.swo
45
+
46
+ # os
47
+ .DS_Store
48
+ Thumbs.db
@@ -0,0 +1,68 @@
1
+ Metadata-Version: 2.4
2
+ Name: didactic
3
+ Version: 0.1.0
4
+ Summary: Pydantic-class API on top of panproto: GATs, lenses, and VCS.
5
+ Project-URL: Homepage, https://github.com/panproto/didactic
6
+ Project-URL: Repository, https://github.com/panproto/didactic
7
+ Author-email: Aaron Steven White <aaronstevenwhite@gmail.com>
8
+ License-Expression: MIT
9
+ Keywords: gat,lenses,models,panproto,schema,validation
10
+ Classifier: Development Status :: 2 - Pre-Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.14
15
+ Classifier: Typing :: Typed
16
+ Requires-Python: >=3.14
17
+ Requires-Dist: annotated-types>=0.7
18
+ Requires-Dist: panproto>=0.43
19
+ Description-Content-Type: text/markdown
20
+
21
+ # didactic
22
+
23
+ A typed-data library for Python that uses
24
+ [panproto](https://github.com/panproto/panproto) as its substrate.
25
+ Authoring is class-based and looks like Pydantic. Underneath, every
26
+ Model corresponds to a panproto `Theory`, every value to a panproto
27
+ `Schema`, and every transformation between Models to a panproto
28
+ `Lens`.
29
+
30
+ ```python
31
+ import didactic.api as dx
32
+
33
+
34
+ class User(dx.Model):
35
+ """A user record."""
36
+
37
+ id: str
38
+ email: str
39
+ display_name: str = ""
40
+
41
+
42
+ u = User(id="u1", email="a@b.c")
43
+ u2 = u.with_(display_name="Alice")
44
+ ```
45
+
46
+ This is the core distribution. Three sibling distributions
47
+ (`didactic-pydantic`, `didactic-settings`, `didactic-fastapi`)
48
+ contribute submodules under `didactic.<name>`.
49
+
50
+ ## Install
51
+
52
+ didactic targets Python 3.14 and panproto 0.42+.
53
+
54
+ ```sh
55
+ pip install didactic
56
+ ```
57
+
58
+ ## Documentation
59
+
60
+ The full documentation site is at
61
+ [https://panproto.dev/didactic/](https://panproto.dev/didactic/)
62
+ and includes a tutorial, task-oriented guides, conceptual background,
63
+ and per-symbol API reference. Source is in the workspace `docs/`
64
+ directory.
65
+
66
+ ## License
67
+
68
+ MIT.
@@ -0,0 +1,48 @@
1
+ # didactic
2
+
3
+ A typed-data library for Python that uses
4
+ [panproto](https://github.com/panproto/panproto) as its substrate.
5
+ Authoring is class-based and looks like Pydantic. Underneath, every
6
+ Model corresponds to a panproto `Theory`, every value to a panproto
7
+ `Schema`, and every transformation between Models to a panproto
8
+ `Lens`.
9
+
10
+ ```python
11
+ import didactic.api as dx
12
+
13
+
14
+ class User(dx.Model):
15
+ """A user record."""
16
+
17
+ id: str
18
+ email: str
19
+ display_name: str = ""
20
+
21
+
22
+ u = User(id="u1", email="a@b.c")
23
+ u2 = u.with_(display_name="Alice")
24
+ ```
25
+
26
+ This is the core distribution. Three sibling distributions
27
+ (`didactic-pydantic`, `didactic-settings`, `didactic-fastapi`)
28
+ contribute submodules under `didactic.<name>`.
29
+
30
+ ## Install
31
+
32
+ didactic targets Python 3.14 and panproto 0.42+.
33
+
34
+ ```sh
35
+ pip install didactic
36
+ ```
37
+
38
+ ## Documentation
39
+
40
+ The full documentation site is at
41
+ [https://panproto.dev/didactic/](https://panproto.dev/didactic/)
42
+ and includes a tutorial, task-oriented guides, conceptual background,
43
+ and per-symbol API reference. Source is in the workspace `docs/`
44
+ directory.
45
+
46
+ ## License
47
+
48
+ MIT.
@@ -0,0 +1,40 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.27"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "didactic"
7
+ version = "0.1.0"
8
+ description = "Pydantic-class API on top of panproto: GATs, lenses, and VCS."
9
+ readme = "README.md"
10
+ requires-python = ">=3.14"
11
+ license = "MIT"
12
+ authors = [
13
+ { name = "Aaron Steven White", email = "aaronstevenwhite@gmail.com" },
14
+ ]
15
+ keywords = ["schema", "models", "validation", "lenses", "gat", "panproto"]
16
+ classifiers = [
17
+ "Development Status :: 2 - Pre-Alpha",
18
+ "Intended Audience :: Developers",
19
+ "License :: OSI Approved :: MIT License",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3.14",
22
+ "Typing :: Typed",
23
+ ]
24
+ dependencies = [
25
+ "panproto>=0.43",
26
+ "annotated-types>=0.7",
27
+ ]
28
+
29
+ [project.urls]
30
+ Homepage = "https://github.com/panproto/didactic"
31
+ Repository = "https://github.com/panproto/didactic"
32
+
33
+ [project.scripts]
34
+ didactic = "didactic.cli._cli:main"
35
+
36
+ [tool.hatch.build.targets.sdist]
37
+ include = ["src/didactic"]
38
+
39
+ [tool.hatch.build.targets.wheel]
40
+ packages = ["src/didactic"]
@@ -0,0 +1,206 @@
1
+ """Self-describing JSON via fingerprint URIs.
2
+
3
+ A panproto-native form of self-describing data: an emitted JSON
4
+ payload carries a ``$schema`` URI of the form
5
+ ``didactic://v1/<structural-fingerprint>``. A consumer that holds a
6
+ registry of known Theories (by fingerprint) can validate an unknown
7
+ payload by looking the URI up.
8
+
9
+ This is something Pydantic structurally cannot do, because Pydantic
10
+ has no content-addressed schema identity. didactic's structural
11
+ fingerprint already gives every Model a stable address; this module
12
+ just plumbs it through to JSON.
13
+
14
+ Examples
15
+ --------
16
+ >>> import didactic.api as dx
17
+ >>>
18
+ >>> class User(dx.Model):
19
+ ... id: str
20
+ ... email: str
21
+ >>>
22
+ >>> u = User(id="u1", email="ada@example.org")
23
+ >>> payload = dx.embed_schema_uri(u)
24
+ >>> payload["$schema"].startswith("didactic://v1/")
25
+ True
26
+
27
+ See Also
28
+ --------
29
+ didactic.migrations._fingerprint.structural_fingerprint : the underlying address.
30
+ """
31
+
32
+ # Cross-translation between ``FieldValue`` and ``JsonValue`` shapes
33
+ # in the schema-URI registry layer.
34
+ # Tracked in panproto/didactic#1.
35
+ # pyright: reportArgumentType=false, reportReturnType=false
36
+
37
+ from __future__ import annotations
38
+
39
+ from typing import TYPE_CHECKING
40
+
41
+ if TYPE_CHECKING:
42
+ from didactic.models._model import Model
43
+ from didactic.types._typing import JsonObject
44
+
45
+ #: URI scheme prefix didactic uses for self-describing payloads.
46
+ URI_PREFIX = "didactic://v1/"
47
+
48
+
49
+ def schema_uri(cls: type[Model]) -> str:
50
+ """Return the canonical schema URI for a Model class.
51
+
52
+ Parameters
53
+ ----------
54
+ cls
55
+ A [Model][didactic.api.Model] subclass.
56
+
57
+ Returns
58
+ -------
59
+ str
60
+ ``"didactic://v1/<fingerprint>"`` where ``<fingerprint>`` is
61
+ the Model's structural fingerprint.
62
+ """
63
+ from didactic.migrations._fingerprint import structural_fingerprint # noqa: PLC0415
64
+ from didactic.theory._theory import build_theory_spec # noqa: PLC0415
65
+
66
+ return f"{URI_PREFIX}{structural_fingerprint(build_theory_spec(cls))}"
67
+
68
+
69
+ def embed_schema_uri(instance: Model) -> JsonObject:
70
+ """Return ``instance.model_dump()`` with a ``$schema`` URI prepended.
71
+
72
+ Parameters
73
+ ----------
74
+ instance
75
+ A Model instance.
76
+
77
+ Returns
78
+ -------
79
+ dict
80
+ The dump dict, with ``"$schema"`` set to the Model's
81
+ canonical URI as the first key.
82
+
83
+ Notes
84
+ -----
85
+ A consumer that knows how to resolve ``didactic://v1/<fp>`` URIs
86
+ can fetch the Theory by fingerprint and validate the payload
87
+ without knowing the original Python class.
88
+ """
89
+ payload = instance.model_dump()
90
+ return {"$schema": schema_uri(type(instance)), **payload}
91
+
92
+
93
+ class FingerprintRegistry:
94
+ """An in-memory mapping of structural fingerprint to Model class.
95
+
96
+ Use as the lookup side of a self-describing JSON pipeline:
97
+ register every Model your application understands, then
98
+ [validate_with_uri_lookup][didactic.api.validate_with_uri_lookup]
99
+ can resolve an unknown payload's ``$schema`` URI back to a class.
100
+
101
+ Examples
102
+ --------
103
+ >>> import didactic.api as dx
104
+ >>> class User(dx.Model):
105
+ ... id: str
106
+ >>>
107
+ >>> reg = dx.FingerprintRegistry()
108
+ >>> reg.register(User)
109
+ >>>
110
+ >>> u = User(id="u1")
111
+ >>> payload = dx.embed_schema_uri(u)
112
+ >>> back = dx.validate_with_uri_lookup(payload, reg)
113
+ >>> back == u
114
+ True
115
+ """
116
+
117
+ __slots__ = ("_by_fingerprint",)
118
+
119
+ def __init__(self) -> None:
120
+ self._by_fingerprint: dict[str, type[Model]] = {}
121
+
122
+ def register[M: Model](self, cls: type[M]) -> type[M]:
123
+ """Register ``cls`` under its structural fingerprint."""
124
+ from didactic.migrations._fingerprint import ( # noqa: PLC0415
125
+ structural_fingerprint,
126
+ )
127
+ from didactic.theory._theory import build_theory_spec # noqa: PLC0415
128
+
129
+ fp = structural_fingerprint(build_theory_spec(cls))
130
+ self._by_fingerprint[fp] = cls
131
+ return cls
132
+
133
+ def lookup(self, uri: str) -> type[Model] | None:
134
+ """Resolve a ``didactic://v1/<fp>`` URI to a registered class."""
135
+ if not uri.startswith(URI_PREFIX):
136
+ return None
137
+ fp = uri[len(URI_PREFIX) :]
138
+ return self._by_fingerprint.get(fp)
139
+
140
+ def __contains__(self, cls_or_uri: object) -> bool:
141
+ if isinstance(cls_or_uri, str):
142
+ return self.lookup(cls_or_uri) is not None
143
+ if isinstance(cls_or_uri, type):
144
+ from didactic.migrations._fingerprint import ( # noqa: PLC0415
145
+ structural_fingerprint,
146
+ )
147
+ from didactic.theory._theory import build_theory_spec # noqa: PLC0415
148
+
149
+ return (
150
+ structural_fingerprint(build_theory_spec(cls_or_uri))
151
+ in self._by_fingerprint
152
+ )
153
+ return False
154
+
155
+ def __len__(self) -> int:
156
+ return len(self._by_fingerprint)
157
+
158
+
159
+ def validate_with_uri_lookup(
160
+ payload: JsonObject,
161
+ registry: FingerprintRegistry,
162
+ ) -> Model:
163
+ """Validate ``payload`` against the Model named by its ``$schema`` URI.
164
+
165
+ Parameters
166
+ ----------
167
+ payload
168
+ A dict that includes a ``$schema`` key.
169
+ registry
170
+ A [FingerprintRegistry][didactic.api.FingerprintRegistry] mapping
171
+ URIs to Model classes.
172
+
173
+ Returns
174
+ -------
175
+ Model
176
+ A validated Model instance whose class came from the
177
+ registry.
178
+
179
+ Raises
180
+ ------
181
+ LookupError
182
+ If the URI is not registered.
183
+ KeyError
184
+ If the payload has no ``$schema`` key.
185
+ """
186
+ if "$schema" not in payload:
187
+ msg = "validate_with_uri_lookup: payload has no $schema key"
188
+ raise KeyError(msg)
189
+
190
+ uri = payload["$schema"]
191
+ cls = registry.lookup(uri)
192
+ if cls is None:
193
+ msg = f"validate_with_uri_lookup: no registered model for URI {uri!r}"
194
+ raise LookupError(msg)
195
+
196
+ body = {k: v for k, v in payload.items() if k != "$schema"}
197
+ return cls.model_validate(body)
198
+
199
+
200
+ __all__ = [
201
+ "URI_PREFIX",
202
+ "FingerprintRegistry",
203
+ "embed_schema_uri",
204
+ "schema_uri",
205
+ "validate_with_uri_lookup",
206
+ ]
@@ -0,0 +1,126 @@
1
+ """Aggregator module for the didactic API.
2
+
3
+ `didactic` lets you author class-based, declarative data models the way
4
+ [Pydantic][pydantic] does, while the underlying values are
5
+ [panproto][panproto] [Theory][panproto.Theory] and
6
+ [Schema][panproto.Schema] instances rather than ad hoc Python objects with
7
+ bolt-on validation. The selling point is everything you get *for free*
8
+ once your data is panproto-native: lenses, dependent optics, theory
9
+ colimits, schema migrations as data, vertex-level VCS, and cross-language
10
+ semantic export.
11
+
12
+ [pydantic]: https://docs.pydantic.dev/
13
+ [panproto]: https://github.com/panproto/panproto
14
+
15
+ Notes
16
+ -----
17
+ The conventional alias for this module is ``dx``::
18
+
19
+ import didactic.api as dx
20
+
21
+
22
+ class User(dx.Model):
23
+ id: str
24
+ email: str
25
+
26
+ The ``didactic`` namespace itself is a PEP 420 implicit namespace
27
+ package; the four distributions (``didactic`` / ``didactic-pydantic`` /
28
+ ``didactic-settings`` / ``didactic-fastapi``) each contribute a
29
+ sub-package (``didactic.api``, ``didactic.pydantic``,
30
+ ``didactic.settings``, ``didactic.fastapi``) without an
31
+ ``__init__.py`` at the namespace root. This lets static type checkers
32
+ resolve cross-distribution imports without a ``pkgutil`` workaround.
33
+ """
34
+
35
+ from didactic import codegen
36
+ from didactic._self_describing import (
37
+ FingerprintRegistry,
38
+ embed_schema_uri,
39
+ schema_uri,
40
+ validate_with_uri_lookup,
41
+ )
42
+ from didactic.axioms._axioms import Axiom, axiom
43
+ from didactic.fields._computed import computed
44
+ from didactic.fields._derived import derived
45
+ from didactic.fields._fields import Field, FieldSpec, field
46
+ from didactic.fields._refs import Backref, Embed, Ref
47
+ from didactic.fields._unions import TaggedUnion
48
+ from didactic.fields._validators import (
49
+ ValidationError,
50
+ ValidationErrorEntry,
51
+ validates,
52
+ )
53
+ from didactic.lenses import _testing as testing
54
+ from didactic.lenses._dependent_lens import DependentLens
55
+ from didactic.lenses._lens import Iso, Lens, Mapping, lens
56
+ from didactic.migrations._diff import classify_change, diff, is_breaking_change
57
+ from didactic.migrations._migrations import (
58
+ load_registry,
59
+ migrate,
60
+ register_migration,
61
+ save_registry,
62
+ )
63
+ from didactic.migrations._synthesis import SynthesisResult, synthesise_migration
64
+ from didactic.models._config import DEFAULT_CONFIG, ExtraPolicy, ModelConfig
65
+ from didactic.models._model import BaseModel, Model
66
+ from didactic.models._root import RootModel, TypeAdapter
67
+ from didactic.types import _types_lib as types
68
+ from didactic.vcs._backref import ModelPool, resolve_backrefs
69
+ from didactic.vcs._repo import Repository
70
+
71
+ __version__ = "0.1.0"
72
+
73
+ #: Conventional namespace for lens utilities (`dx.lens.identity(...)`,
74
+ #: `dx.lens.Lens`, etc.). The ``lens`` name doubles as a decorator
75
+ #: (``@dx.lens(A, B)``) and as a module-style namespace. The attributes
76
+ #: are bound by ``_LensNamespace.__init__`` in :mod:`didactic.lenses._lens`.
77
+
78
+
79
+ __all__ = [
80
+ "DEFAULT_CONFIG",
81
+ "Axiom",
82
+ "Backref",
83
+ "BaseModel",
84
+ "DependentLens",
85
+ "Embed",
86
+ "ExtraPolicy",
87
+ "Field",
88
+ "FieldSpec",
89
+ "FingerprintRegistry",
90
+ "Iso",
91
+ "Lens",
92
+ "Mapping",
93
+ "Model",
94
+ "ModelConfig",
95
+ "ModelPool",
96
+ "Ref",
97
+ "Repository",
98
+ "RootModel",
99
+ "SynthesisResult",
100
+ "TaggedUnion",
101
+ "TypeAdapter",
102
+ "ValidationError",
103
+ "ValidationErrorEntry",
104
+ "__version__",
105
+ "axiom",
106
+ "classify_change",
107
+ "codegen",
108
+ "computed",
109
+ "derived",
110
+ "diff",
111
+ "embed_schema_uri",
112
+ "field",
113
+ "is_breaking_change",
114
+ "lens",
115
+ "load_registry",
116
+ "migrate",
117
+ "register_migration",
118
+ "resolve_backrefs",
119
+ "save_registry",
120
+ "schema_uri",
121
+ "synthesise_migration",
122
+ "testing",
123
+ "types",
124
+ "validate_with_uri_lookup",
125
+ "validates",
126
+ ]
@@ -0,0 +1,11 @@
1
+ """Class-level axioms and their construction-time enforcement."""
2
+
3
+ from didactic.axioms._axiom_enforcement import check_class_axioms
4
+ from didactic.axioms._axioms import Axiom, axiom, collect_class_axioms
5
+
6
+ __all__ = [
7
+ "Axiom",
8
+ "axiom",
9
+ "check_class_axioms",
10
+ "collect_class_axioms",
11
+ ]