yaddd-pydantic 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.
@@ -0,0 +1,165 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # PyInstaller
30
+ # Usually these files are written by a python script from a template
31
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
+ *.manifest
33
+ *.spec
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+
39
+ # Unit test / coverage reports
40
+ htmlcov/
41
+ .tox/
42
+ .nox/
43
+ .coverage
44
+ .coverage.*
45
+ .cache
46
+ nosetests.xml
47
+ coverage.xml
48
+ *.cover
49
+ *.py,cover
50
+ .hypothesis/
51
+ .pytest_cache/
52
+ cover/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+ db.sqlite3-journal
63
+
64
+ # Flask stuff:
65
+ instance/
66
+ .webassets-cache
67
+
68
+ # Scrapy stuff:
69
+ .scrapy
70
+
71
+ # Sphinx documentation
72
+ docs/_build/
73
+
74
+ # PyBuilder
75
+ .pybuilder/
76
+ target/
77
+
78
+ # Jupyter Notebook
79
+ .ipynb_checkpoints
80
+
81
+ # IPython
82
+ profile_default/
83
+ ipython_config.py
84
+
85
+ # pyenv
86
+ # For a library or package, you might want to ignore these files since the code is
87
+ # intended to run in multiple environments; otherwise, check them in:
88
+ # .python-version
89
+
90
+ # pipenv
91
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
93
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
94
+ # install all needed dependencies.
95
+ #Pipfile.lock
96
+
97
+ # poetry
98
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
99
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
100
+ # commonly ignored for libraries.
101
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
102
+ #poetry.lock
103
+
104
+ # pdm
105
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
106
+ #pdm.lock
107
+ # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
108
+ # in version control.
109
+ # https://pdm.fming.dev/latest/usage/project/#working-with-version-control
110
+ .pdm.toml
111
+ .pdm-python
112
+ .pdm-build/
113
+
114
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
115
+ __pypackages__/
116
+
117
+ # Celery stuff
118
+ celerybeat-schedule
119
+ celerybeat.pid
120
+
121
+ # SageMath parsed files
122
+ *.sage.py
123
+
124
+ # Environments
125
+ .env
126
+ .venv
127
+ env/
128
+ venv/
129
+ ENV/
130
+ env.bak/
131
+ venv.bak/
132
+
133
+ # Spyder project settings
134
+ .spyderproject
135
+ .spyproject
136
+
137
+ # Rope project settings
138
+ .ropeproject
139
+
140
+ # mkdocs documentation
141
+ /site
142
+
143
+ # mypy
144
+ .mypy_cache/
145
+ .dmypy.json
146
+ dmypy.json
147
+
148
+ # Pyre type checker
149
+ .pyre/
150
+
151
+ # pytype static type analyzer
152
+ .pytype/
153
+
154
+ # Cython debug symbols
155
+ cython_debug/
156
+
157
+ # IDE
158
+ .idea
159
+ .vscode/!launch.json
160
+ .vscode/!tasks.json
161
+
162
+ # OS
163
+ .DS_Store
164
+
165
+ *_old/
File without changes
@@ -0,0 +1,32 @@
1
+ Metadata-Version: 2.5
2
+ Name: yaddd-pydantic
3
+ Version: 0.1.0
4
+ Summary: Pydantic v2 integration for yaddd: PydanticVO value objects as first-class model fields
5
+ Project-URL: Homepage, https://github.com/netstuff/yaddd
6
+ Author-email: Yuri Andreev <andreev.jr@gmail.com>
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Programming Language :: Python
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Programming Language :: Python :: 3.14
16
+ Classifier: Topic :: Internet
17
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
18
+ Requires-Python: >=3.12
19
+ Requires-Dist: pydantic>=2.10
20
+ Requires-Dist: yaddd>=0.1
21
+ Description-Content-Type: text/markdown
22
+
23
+ # yaddd-pydantic
24
+ Pydantic v2 integration for [yaddd](https://github.com/pyDDD/yaddd).
25
+
26
+ `PydanticVO[V]` makes yaddd value objects first-class field types in pydantic
27
+ models: pydantic constraints double as VO validation, models accept both raw
28
+ values and VO instances, dumps produce primitives, and JSON Schema shows the
29
+ primitive shape. `SensitiveValueObject` masking is preserved.
30
+
31
+ Wiring is explicit: subclass `PydanticVO` in your domain, no auto-discovery.
32
+ Conformance is proven by `VoSerializationContract` from `yaddd.testing`.
@@ -0,0 +1,10 @@
1
+ # yaddd-pydantic
2
+ Pydantic v2 integration for [yaddd](https://github.com/pyDDD/yaddd).
3
+
4
+ `PydanticVO[V]` makes yaddd value objects first-class field types in pydantic
5
+ models: pydantic constraints double as VO validation, models accept both raw
6
+ values and VO instances, dumps produce primitives, and JSON Schema shows the
7
+ primitive shape. `SensitiveValueObject` masking is preserved.
8
+
9
+ Wiring is explicit: subclass `PydanticVO` in your domain, no auto-discovery.
10
+ Conformance is proven by `VoSerializationContract` from `yaddd.testing`.
@@ -0,0 +1,39 @@
1
+ [project]
2
+ name = "yaddd-pydantic"
3
+ description = "Pydantic v2 integration for yaddd: PydanticVO value objects as first-class model fields"
4
+ authors = [
5
+ {name = "Yuri Andreev", email = "andreev.jr@gmail.com"},
6
+ ]
7
+ license = 'MIT'
8
+ license-files = ['LICENSE']
9
+ classifiers = [
10
+ 'Development Status :: 4 - Beta',
11
+ 'Programming Language :: Python',
12
+ 'Programming Language :: Python :: 3.12',
13
+ 'Programming Language :: Python :: 3.13',
14
+ 'Programming Language :: Python :: 3.14',
15
+ 'Intended Audience :: Developers',
16
+ 'Operating System :: OS Independent',
17
+ 'Topic :: Software Development :: Libraries :: Python Modules',
18
+ 'Topic :: Internet',
19
+ ]
20
+ requires-python = '>=3.12'
21
+ dependencies = [
22
+ "yaddd>=0.1",
23
+ "pydantic>=2.10",
24
+ ]
25
+ version = "0.1.0"
26
+ readme = 'README.md'
27
+
28
+ [project.urls]
29
+ Homepage = 'https://github.com/netstuff/yaddd'
30
+
31
+ [tool.uv.sources]
32
+ yaddd = { workspace = true }
33
+
34
+ [build-system]
35
+ requires = ["hatchling"]
36
+ build-backend = "hatchling.build"
37
+
38
+ [tool.hatch.build.targets.wheel]
39
+ packages = ["src/yaddd_pydantic"]
@@ -0,0 +1,6 @@
1
+ """Pydantic v2 integration for yaddd."""
2
+
3
+ from yaddd_pydantic.value_objects import PydanticVO
4
+
5
+
6
+ __all__ = ["PydanticVO"]
File without changes
@@ -0,0 +1,90 @@
1
+ """Pydantic v2 value objects: VOs as first-class field types in pydantic models."""
2
+
3
+ from typing import Any, ClassVar, Self, cast
4
+
5
+ from pydantic import GetCoreSchemaHandler, GetJsonSchemaHandler, TypeAdapter
6
+ from pydantic.json_schema import JsonSchemaValue
7
+ from pydantic_core import CoreSchema, core_schema
8
+ from yaddd.domain.value_object import ValueObject
9
+
10
+
11
+ __all__ = ["PydanticVO"]
12
+
13
+
14
+ class PydanticVO[V](ValueObject[V]):
15
+ """``ValueObject`` validated and serialized by pydantic v2.
16
+
17
+ Subclass and declare ``pydantic_type`` — the type pydantic uses for
18
+ validation and JSON Schema::
19
+
20
+ class PositiveTotal(PydanticVO[int]):
21
+ pydantic_type = Annotated[int, Field(gt=0)]
22
+
23
+ Behavior:
24
+
25
+ - ``validate`` runs the ``TypeAdapter(pydantic_type)`` chain, so pydantic
26
+ constraints double as VO validation — even outside any model;
27
+ - a VO field in a ``BaseModel`` accepts both a raw value (validated into
28
+ a VO instance) and an existing VO instance (kept as is);
29
+ - serialization produces the primitive: ``model_dump()`` and
30
+ ``model_dump_json()`` contain ``vo.value``, never a VO repr;
31
+ - JSON Schema (OpenAPI) shows the ``pydantic_type`` shape.
32
+
33
+ Combine with ``SensitiveValueObject`` to keep masking::
34
+
35
+ class Password(SensitiveValueObject[str], PydanticVO[str]):
36
+ pydantic_type = Annotated[str, Field(min_length=8)]
37
+
38
+ Masking is preserved: the model's ``repr`` uses the VO's masked
39
+ ``__repr__`` and ``str`` still raises ``SensitiveValueAccessError``.
40
+ A pydantic dump is an explicit serialization act, so ``model_dump()`` /
41
+ ``model_dump_json()`` contain the raw value — treat dumped output as
42
+ sensitive and keep it out of logs.
43
+ """
44
+
45
+ pydantic_type: ClassVar[Any]
46
+
47
+ _adapter_instance: ClassVar[TypeAdapter[Any] | None] = None
48
+
49
+ @classmethod
50
+ def validate(cls, value: V) -> V:
51
+ """Validate the raw value through the ``pydantic_type`` chain."""
52
+ return cast("V", cls._type_adapter().validate_python(value))
53
+
54
+ @classmethod
55
+ def __get_pydantic_core_schema__(cls, source_type: Any, handler: GetCoreSchemaHandler) -> CoreSchema:
56
+ """Accept raw values and VO instances; serialize to the primitive."""
57
+ return core_schema.no_info_plain_validator_function(
58
+ cls._accept,
59
+ serialization=core_schema.plain_serializer_function_ser_schema(
60
+ _serialize,
61
+ return_schema=cls._type_adapter().core_schema,
62
+ ),
63
+ )
64
+
65
+ @classmethod
66
+ def __get_pydantic_json_schema__(cls, schema: CoreSchema, handler: GetJsonSchemaHandler) -> JsonSchemaValue:
67
+ """JSON Schema comes from ``pydantic_type``: the primitive shape."""
68
+ json_schema = cls._type_adapter().json_schema()
69
+ json_schema["title"] = cls.__name__
70
+ return json_schema
71
+
72
+ @classmethod
73
+ def _type_adapter(cls) -> TypeAdapter[Any]:
74
+ """Lazily built and cached per concrete VO class."""
75
+ adapter = cls.__dict__.get("_adapter_instance")
76
+ if adapter is None:
77
+ adapter = TypeAdapter(cls.pydantic_type)
78
+ cls._adapter_instance = adapter
79
+ return cast("TypeAdapter[Any]", adapter)
80
+
81
+ @classmethod
82
+ def _accept(cls, value: Any) -> Self:
83
+ """Pass VO instances through; validate raw values into VOs."""
84
+ if isinstance(value, cls):
85
+ return value
86
+ return cls(value)
87
+
88
+
89
+ def _serialize(vo: ValueObject[Any]) -> Any:
90
+ return vo.value
@@ -0,0 +1,116 @@
1
+ import json
2
+ from collections.abc import Callable
3
+ from typing import Annotated, Any
4
+
5
+ import pytest
6
+ from pydantic import BaseModel, Field, TypeAdapter, ValidationError
7
+
8
+ from yaddd.domain import SensitiveValueObject, ValueObject
9
+ from yaddd.testing import VoSerializationContract
10
+ from yaddd_pydantic import PydanticVO
11
+
12
+
13
+ class PositiveTotal(PydanticVO[int]):
14
+ pydantic_type = Annotated[int, Field(gt=0)]
15
+
16
+
17
+ class Secret(SensitiveValueObject[str], PydanticVO[str]):
18
+ pydantic_type = Annotated[str, Field(min_length=8)]
19
+
20
+
21
+ class OrderModel(BaseModel):
22
+ total: PositiveTotal
23
+
24
+
25
+ class Credentials(BaseModel):
26
+ password: Secret
27
+
28
+
29
+ def test_raw_input_becomes_vo_instance():
30
+ model = OrderModel(total=5)
31
+
32
+ assert isinstance(model.total, PositiveTotal)
33
+ assert model.total.value == 5
34
+
35
+
36
+ def test_vo_instance_input_is_preserved():
37
+ total = PositiveTotal(10)
38
+
39
+ assert OrderModel(total=total).total is total
40
+
41
+
42
+ def test_pydantic_constraint_violation_raises():
43
+ with pytest.raises(ValidationError):
44
+ OrderModel(total=-1)
45
+
46
+
47
+ def test_vo_construction_outside_model_uses_pydantic_validation():
48
+ with pytest.raises(ValidationError):
49
+ PositiveTotal(0)
50
+
51
+
52
+ def test_model_dump_contains_primitive():
53
+ dumped = OrderModel(total=5).model_dump()
54
+
55
+ assert dumped["total"] == 5
56
+ assert not isinstance(dumped["total"], ValueObject)
57
+
58
+
59
+ def test_model_dump_json_contains_primitive():
60
+ assert json.loads(OrderModel(total=5).model_dump_json())["total"] == 5
61
+
62
+
63
+ def test_json_schema_shows_primitive_shape():
64
+ schema = OrderModel.model_json_schema()
65
+
66
+ assert schema["properties"]["total"]["type"] == "integer"
67
+ assert schema["properties"]["total"]["exclusiveMinimum"] == 0
68
+
69
+
70
+ def test_sensitive_vo_repr_is_masked():
71
+ model = Credentials(password="super-secret-1")
72
+
73
+ assert "super-secret-1" not in repr(model)
74
+ assert "MASKED" in repr(model)
75
+
76
+
77
+ def test_sensitive_vo_explicit_dump_contains_raw_value():
78
+ assert Credentials(password="super-secret-1").model_dump()["password"] == "super-secret-1"
79
+
80
+
81
+ def test_sensitive_vo_constraint_violation_raises():
82
+ with pytest.raises(ValidationError):
83
+ Credentials(password="short")
84
+
85
+
86
+ class TestPydanticVoSerialization(VoSerializationContract):
87
+ """The contract suite proves serialization conformance via pydantic hooks."""
88
+
89
+ @pytest.fixture
90
+ def vo(self) -> ValueObject[Any]:
91
+ return PositiveTotal(5)
92
+
93
+ @pytest.fixture
94
+ def invalid_raw(self) -> Any:
95
+ return -1
96
+
97
+ @pytest.fixture
98
+ def dump(self) -> Callable[[ValueObject[Any]], Any]:
99
+ adapter: TypeAdapter[PositiveTotal] = TypeAdapter(PositiveTotal)
100
+ return adapter.dump_python
101
+
102
+ @pytest.fixture
103
+ def load(self) -> Callable[[Any], ValueObject[Any]]:
104
+ adapter: TypeAdapter[PositiveTotal] = TypeAdapter(PositiveTotal)
105
+ return adapter.validate_python
106
+
107
+ @pytest.fixture
108
+ def sensitive_vo(self) -> ValueObject[Any]:
109
+ return Secret("super-secret-1")
110
+
111
+ @pytest.fixture
112
+ def container_repr(self) -> Callable[[ValueObject[Any]], str]:
113
+ def make_repr(vo: ValueObject[Any]) -> str:
114
+ return repr(Credentials(password=vo))
115
+
116
+ return make_repr