serialite 0.3.8__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.
- serialite-0.3.8/LICENSE +19 -0
- serialite-0.3.8/PKG-INFO +69 -0
- serialite-0.3.8/README.md +40 -0
- serialite-0.3.8/pyproject.toml +114 -0
- serialite-0.3.8/src/serialite/__init__.py +35 -0
- serialite-0.3.8/src/serialite/_base.py +169 -0
- serialite-0.3.8/src/serialite/_dataclass.py +31 -0
- serialite-0.3.8/src/serialite/_decorators.py +246 -0
- serialite-0.3.8/src/serialite/_descriptors.py +14 -0
- serialite-0.3.8/src/serialite/_dispatcher.py +293 -0
- serialite-0.3.8/src/serialite/_fields_serializer.py +409 -0
- serialite-0.3.8/src/serialite/_implementations/__init__.py +30 -0
- serialite-0.3.8/src/serialite/_implementations/_array.py +52 -0
- serialite-0.3.8/src/serialite/_implementations/_boolean.py +20 -0
- serialite-0.3.8/src/serialite/_implementations/_date_time.py +33 -0
- serialite-0.3.8/src/serialite/_implementations/_dictionary.py +143 -0
- serialite-0.3.8/src/serialite/_implementations/_float.py +48 -0
- serialite-0.3.8/src/serialite/_implementations/_integer.py +53 -0
- serialite-0.3.8/src/serialite/_implementations/_json.py +20 -0
- serialite-0.3.8/src/serialite/_implementations/_list.py +56 -0
- serialite-0.3.8/src/serialite/_implementations/_literal.py +32 -0
- serialite-0.3.8/src/serialite/_implementations/_none.py +20 -0
- serialite-0.3.8/src/serialite/_implementations/_ordered_set.py +56 -0
- serialite-0.3.8/src/serialite/_implementations/_path.py +22 -0
- serialite-0.3.8/src/serialite/_implementations/_reserved.py +40 -0
- serialite-0.3.8/src/serialite/_implementations/_set.py +54 -0
- serialite-0.3.8/src/serialite/_implementations/_string.py +31 -0
- serialite-0.3.8/src/serialite/_implementations/_tuple.py +81 -0
- serialite-0.3.8/src/serialite/_implementations/_union.py +78 -0
- serialite-0.3.8/src/serialite/_implementations/_uuid.py +27 -0
- serialite-0.3.8/src/serialite/_mixins.py +137 -0
- serialite-0.3.8/src/serialite/_monkey_patches.py +196 -0
- serialite-0.3.8/src/serialite/_numeric_check.py +21 -0
- serialite-0.3.8/src/serialite/_result.py +77 -0
- serialite-0.3.8/src/serialite/_stable_set.py +52 -0
serialite-0.3.8/LICENSE
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Copyright (c) 2022-2025 David R Hagen
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
5
|
+
in the Software without restriction, including without limitation the rights
|
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
8
|
+
furnished to do so, subject to the following conditions:
|
|
9
|
+
|
|
10
|
+
The above copyright notice and this permission notice shall be included in all
|
|
11
|
+
copies or substantial portions of the Software.
|
|
12
|
+
|
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
19
|
+
SOFTWARE.
|
serialite-0.3.8/PKG-INFO
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: serialite
|
|
3
|
+
Version: 0.3.8
|
|
4
|
+
Summary: A serialization library for Python
|
|
5
|
+
Keywords: serialization,deserialization,serde,pydantic,fastapi
|
|
6
|
+
Author: David Hagen
|
|
7
|
+
Author-email: David Hagen <david@drhagen.com>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
18
|
+
Requires-Dist: typing-extensions~=4.3
|
|
19
|
+
Requires-Dist: fastapi>=0.100,<0.111 ; extra == 'fastapi'
|
|
20
|
+
Requires-Dist: pydantic~=1.10 ; extra == 'fastapi'
|
|
21
|
+
Requires-Dist: numpy>=1.25,<2 ; extra == 'numpy'
|
|
22
|
+
Requires-Dist: ordered-set~=4.1 ; extra == 'ordered-set'
|
|
23
|
+
Requires-Python: >=3.12
|
|
24
|
+
Project-URL: Repository, https://github.com/drhagen/serialite
|
|
25
|
+
Provides-Extra: fastapi
|
|
26
|
+
Provides-Extra: numpy
|
|
27
|
+
Provides-Extra: ordered-set
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
# Serialite
|
|
31
|
+
|
|
32
|
+
Serialite is a library serializing and deserializing arbitrarily complex objects in Python. You
|
|
33
|
+
apply the `@serializable` decorator to a dataclass to automatically create `to_data` and `from_data`
|
|
34
|
+
methods using the type annotations. Or for more control, inherit from the `SerializableMixin` and
|
|
35
|
+
implement the class attribute `__fields_serializer__`. For even more control, inherit from the
|
|
36
|
+
abstract base class `Serializer` and implement the `to_data` and `from_data` methods directly.
|
|
37
|
+
|
|
38
|
+
## Basics
|
|
39
|
+
|
|
40
|
+
The abstract base class is `Serializer`:
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
class Serializer(Generic[Output]):
|
|
44
|
+
def from_data(self, data: Json) -> DeserializationSuccess[Output]: ...
|
|
45
|
+
def to_data(self, value: Output) -> Json: ...
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
The class is generic in the type of the object that it serializes. The two abstract methods
|
|
49
|
+
`from_data` and `to_data` are the key to the whole design, which revolves around getting objects to
|
|
50
|
+
and from JSON-serializable data, which are objects constructed entirely of `bool`s, `int`s,
|
|
51
|
+
`float`s, `list`s, and `dict`s. Such structures can be consumed by `json.dumps` to produce a string
|
|
52
|
+
and produced by `json.loads` after consuming a string. By basing the serialization around JSON
|
|
53
|
+
serializable data, complex structures can be built up or torn down piece by piece while
|
|
54
|
+
alternatively building up complex error messages during deserialization which pinpoint the location
|
|
55
|
+
in the structure where the bad data exist.
|
|
56
|
+
|
|
57
|
+
For new classes, it is recommended that the `Serializer` be implemented on the class itself. There is
|
|
58
|
+
an abstract base class `Serializable` that classes can inherit from to indicate this. There is a mixin
|
|
59
|
+
`SerializableMixin` that provides an implementation of `from_data` and `to_data` for any class that
|
|
60
|
+
implements the `__fields_serializer` class attribute.
|
|
61
|
+
|
|
62
|
+
For `dataclass`es, it is even easier. There is a decorator `serializable` that inserts
|
|
63
|
+
`SerializableMixin` into the list of base classes after the `dataclass` decorator has run and also
|
|
64
|
+
generates `__fields_serializer__` from the data class attributes.
|
|
65
|
+
|
|
66
|
+
Finding the correct serializer for each type can be a pain, so
|
|
67
|
+
`serializer(cls: type) -> Serializer` is provided as a convenience function. This is a single
|
|
68
|
+
dispatch function, which looks up the serializer registered for a particular type. For example,
|
|
69
|
+
`serializer(list[float])` will return `ListSerializer(FloatSerializer)`.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Serialite
|
|
2
|
+
|
|
3
|
+
Serialite is a library serializing and deserializing arbitrarily complex objects in Python. You
|
|
4
|
+
apply the `@serializable` decorator to a dataclass to automatically create `to_data` and `from_data`
|
|
5
|
+
methods using the type annotations. Or for more control, inherit from the `SerializableMixin` and
|
|
6
|
+
implement the class attribute `__fields_serializer__`. For even more control, inherit from the
|
|
7
|
+
abstract base class `Serializer` and implement the `to_data` and `from_data` methods directly.
|
|
8
|
+
|
|
9
|
+
## Basics
|
|
10
|
+
|
|
11
|
+
The abstract base class is `Serializer`:
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
class Serializer(Generic[Output]):
|
|
15
|
+
def from_data(self, data: Json) -> DeserializationSuccess[Output]: ...
|
|
16
|
+
def to_data(self, value: Output) -> Json: ...
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
The class is generic in the type of the object that it serializes. The two abstract methods
|
|
20
|
+
`from_data` and `to_data` are the key to the whole design, which revolves around getting objects to
|
|
21
|
+
and from JSON-serializable data, which are objects constructed entirely of `bool`s, `int`s,
|
|
22
|
+
`float`s, `list`s, and `dict`s. Such structures can be consumed by `json.dumps` to produce a string
|
|
23
|
+
and produced by `json.loads` after consuming a string. By basing the serialization around JSON
|
|
24
|
+
serializable data, complex structures can be built up or torn down piece by piece while
|
|
25
|
+
alternatively building up complex error messages during deserialization which pinpoint the location
|
|
26
|
+
in the structure where the bad data exist.
|
|
27
|
+
|
|
28
|
+
For new classes, it is recommended that the `Serializer` be implemented on the class itself. There is
|
|
29
|
+
an abstract base class `Serializable` that classes can inherit from to indicate this. There is a mixin
|
|
30
|
+
`SerializableMixin` that provides an implementation of `from_data` and `to_data` for any class that
|
|
31
|
+
implements the `__fields_serializer` class attribute.
|
|
32
|
+
|
|
33
|
+
For `dataclass`es, it is even easier. There is a decorator `serializable` that inserts
|
|
34
|
+
`SerializableMixin` into the list of base classes after the `dataclass` decorator has run and also
|
|
35
|
+
generates `__fields_serializer__` from the data class attributes.
|
|
36
|
+
|
|
37
|
+
Finding the correct serializer for each type can be a pain, so
|
|
38
|
+
`serializer(cls: type) -> Serializer` is provided as a convenience function. This is a single
|
|
39
|
+
dispatch function, which looks up the serializer registered for a particular type. For example,
|
|
40
|
+
`serializer(list[float])` will return `ListSerializer(FloatSerializer)`.
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["uv_build == 0.8.*"]
|
|
3
|
+
build-backend = "uv_build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "serialite"
|
|
7
|
+
version = "0.3.8"
|
|
8
|
+
description = "A serialization library for Python"
|
|
9
|
+
authors = [ { name = "David Hagen", email = "david@drhagen.com" } ]
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
readme = "README.md"
|
|
13
|
+
keywords = ["serialization", "deserialization", "serde", "pydantic", "fastapi"]
|
|
14
|
+
requires-python = ">=3.12"
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 4 - Beta",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Operating System :: OS Independent",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Programming Language :: Python :: 3.13",
|
|
23
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
dependencies = [
|
|
27
|
+
"typing_extensions ~= 4.3",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[project.optional-dependencies]
|
|
31
|
+
fastapi = [
|
|
32
|
+
"fastapi >=0.100,<0.111",
|
|
33
|
+
"pydantic ~= 1.10",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
numpy = [
|
|
37
|
+
"numpy >=1.25,<2",
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
ordered-set = [
|
|
41
|
+
"ordered-set ~= 4.1",
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
[project.urls]
|
|
45
|
+
Repository = "https://github.com/drhagen/serialite"
|
|
46
|
+
|
|
47
|
+
[dependency-groups]
|
|
48
|
+
nox = [
|
|
49
|
+
"nox-uv == 0.6.*",
|
|
50
|
+
]
|
|
51
|
+
test = [
|
|
52
|
+
"pytest == 8.*",
|
|
53
|
+
"coverage == 7.*",
|
|
54
|
+
"httpx", # Needed by starlette testclient
|
|
55
|
+
]
|
|
56
|
+
lint = [
|
|
57
|
+
"ruff == 0.14.*"
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
[tool.uv]
|
|
61
|
+
default-groups = "all"
|
|
62
|
+
python-preference = "only-managed"
|
|
63
|
+
|
|
64
|
+
[tool.coverage.run]
|
|
65
|
+
branch = true
|
|
66
|
+
source_pkgs = ["serialite"]
|
|
67
|
+
relative_files = true
|
|
68
|
+
|
|
69
|
+
[tool.coverage.report]
|
|
70
|
+
exclude_lines = [
|
|
71
|
+
"pragma: no cover",
|
|
72
|
+
"raise NotImplementedError",
|
|
73
|
+
"if TYPE_CHECKING:",
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
[tool.coverage.paths]
|
|
77
|
+
source = [
|
|
78
|
+
"src/",
|
|
79
|
+
"**/site-packages/",
|
|
80
|
+
]
|
|
81
|
+
|
|
82
|
+
[tool.ruff]
|
|
83
|
+
src = ["src"]
|
|
84
|
+
line-length = 99
|
|
85
|
+
|
|
86
|
+
[tool.ruff.lint]
|
|
87
|
+
extend-select = [
|
|
88
|
+
"I", # isort
|
|
89
|
+
"N", # pep8-naming
|
|
90
|
+
"RUF", # ruff
|
|
91
|
+
"B", # flake8-bugbear
|
|
92
|
+
"N", # flake8-broken-line
|
|
93
|
+
"C4", # flake8-comprehensions
|
|
94
|
+
"PIE", # flake8-pie
|
|
95
|
+
"PT", # flake8-pytest-style
|
|
96
|
+
"PTH", # flake8-use-pathlib
|
|
97
|
+
"ERA", # flake8-eradicate
|
|
98
|
+
]
|
|
99
|
+
# F402: Variable shadowing is perfectly fine
|
|
100
|
+
# PT011: testing for broad exceptions is fine
|
|
101
|
+
extend-ignore = ["F402", "PT011"]
|
|
102
|
+
|
|
103
|
+
[tool.ruff.lint.per-file-ignores]
|
|
104
|
+
# F401: unused-import; Allow unused imports in __init__.py files
|
|
105
|
+
"__init__.py" = ["F401"]
|
|
106
|
+
|
|
107
|
+
[tool.ruff.lint.isort]
|
|
108
|
+
extra-standard-library = ["typing_extensions"]
|
|
109
|
+
|
|
110
|
+
[tool.ruff.lint.pep8-naming]
|
|
111
|
+
classmethod-decorators = ["serialite._descriptors.classproperty"]
|
|
112
|
+
|
|
113
|
+
[tool.ruff.lint.flake8-bugbear]
|
|
114
|
+
extend-immutable-calls = ["serialite.field", "fastapi.Body"]
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from ._base import Serializable, Serializer
|
|
2
|
+
from ._dataclass import field
|
|
3
|
+
from ._decorators import abstract_serializable, serializable
|
|
4
|
+
from ._dispatcher import serializer
|
|
5
|
+
from ._fields_serializer import (
|
|
6
|
+
AccessPermissions,
|
|
7
|
+
FieldsSerializer,
|
|
8
|
+
FieldsSerializerField,
|
|
9
|
+
MultiField,
|
|
10
|
+
SingleField,
|
|
11
|
+
empty_default,
|
|
12
|
+
no_default,
|
|
13
|
+
)
|
|
14
|
+
from ._implementations import * # noqa: F403
|
|
15
|
+
from ._mixins import AbstractSerializableMixin, SerializableMixin
|
|
16
|
+
from ._monkey_patches import (
|
|
17
|
+
monkey_patch_fastapi_create_cloned_field,
|
|
18
|
+
monkey_patch_pydantic_get_flat_models_from_model,
|
|
19
|
+
monkey_patch_pydantic_instancecheck,
|
|
20
|
+
monkey_patch_pydantic_model_type_schema,
|
|
21
|
+
monkey_patch_pydantic_subclasscheck,
|
|
22
|
+
)
|
|
23
|
+
from ._result import (
|
|
24
|
+
DeserializationError,
|
|
25
|
+
DeserializationFailure,
|
|
26
|
+
DeserializationResult,
|
|
27
|
+
DeserializationSuccess,
|
|
28
|
+
)
|
|
29
|
+
from ._stable_set import StableSet
|
|
30
|
+
|
|
31
|
+
monkey_patch_pydantic_subclasscheck()
|
|
32
|
+
monkey_patch_pydantic_instancecheck()
|
|
33
|
+
monkey_patch_fastapi_create_cloned_field()
|
|
34
|
+
monkey_patch_pydantic_get_flat_models_from_model()
|
|
35
|
+
monkey_patch_pydantic_model_type_schema()
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
__all__ = ["Serializable", "Serializer"]
|
|
4
|
+
|
|
5
|
+
from abc import abstractmethod
|
|
6
|
+
from typing import TYPE_CHECKING, Any, Generic, TypeVar
|
|
7
|
+
|
|
8
|
+
from ._descriptors import classproperty
|
|
9
|
+
from ._result import DeserializationFailure, DeserializationResult
|
|
10
|
+
from ._stable_set import StableSet
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from pydantic.typing import AbstractSetIntStr, MappingIntStrAny
|
|
14
|
+
|
|
15
|
+
Output = TypeVar("Output")
|
|
16
|
+
SerializableOutput = TypeVar("SerializableOutput", bound="Serializable")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Serializer(Generic[Output]):
|
|
20
|
+
"""Serialize and deserialize a particular object."""
|
|
21
|
+
|
|
22
|
+
@abstractmethod
|
|
23
|
+
def from_data(self, data: Any) -> DeserializationResult[Output]:
|
|
24
|
+
"""Deserialize an object from data."""
|
|
25
|
+
raise NotImplementedError()
|
|
26
|
+
|
|
27
|
+
@abstractmethod
|
|
28
|
+
def to_data(self, value: Output) -> Any:
|
|
29
|
+
"""Serialize an object to data."""
|
|
30
|
+
raise NotImplementedError()
|
|
31
|
+
|
|
32
|
+
def collect_openapi_models(
|
|
33
|
+
self, parent_models: StableSet[Serializer]
|
|
34
|
+
) -> StableSet[Serializer]:
|
|
35
|
+
"""Collect the set of OpenAPI models required for this Serializer.
|
|
36
|
+
|
|
37
|
+
`Serializer`s that represent models should return a set containing
|
|
38
|
+
themselves.
|
|
39
|
+
|
|
40
|
+
`Serializer`s with child `Serializer`s that may be models should add
|
|
41
|
+
themselves to `parent_models` and pass that to all children's
|
|
42
|
+
`collect_openapi_models` method. The parent `Serializer` and all models
|
|
43
|
+
received from children should be combined and returned.
|
|
44
|
+
|
|
45
|
+
The default is to return no models.
|
|
46
|
+
"""
|
|
47
|
+
return StableSet()
|
|
48
|
+
|
|
49
|
+
def to_openapi_schema(self, refs: dict[Serializer, str], force: bool = False) -> Any:
|
|
50
|
+
"""Generate the OpenAPI schema representation for this class.
|
|
51
|
+
|
|
52
|
+
Each serializer should check if its fully qualified name exists in
|
|
53
|
+
`refs` and return a '$ref', unless `force` is true, in which case it
|
|
54
|
+
should return its full schema, but not pass `force` to its child
|
|
55
|
+
serializers.
|
|
56
|
+
|
|
57
|
+
The default is no schema.
|
|
58
|
+
"""
|
|
59
|
+
return {}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class Serializable(Serializer[SerializableOutput]):
|
|
63
|
+
"""Classes that serialize instances of themselves."""
|
|
64
|
+
|
|
65
|
+
# There is no way to indicate in Python's type system that
|
|
66
|
+
# type[Serializable] is an instance of Serializer. So these signatures
|
|
67
|
+
# appear inconsistent with the base class.
|
|
68
|
+
@classmethod
|
|
69
|
+
@abstractmethod
|
|
70
|
+
def from_data(cls, data: Any) -> DeserializationResult[Output]:
|
|
71
|
+
pass
|
|
72
|
+
|
|
73
|
+
@abstractmethod
|
|
74
|
+
def to_data(self: SerializableOutput) -> Any:
|
|
75
|
+
pass
|
|
76
|
+
|
|
77
|
+
@classmethod
|
|
78
|
+
def collect_openapi_models(cls, parent_models: StableSet[Serializer]) -> StableSet[Serializer]:
|
|
79
|
+
return StableSet()
|
|
80
|
+
|
|
81
|
+
@classmethod
|
|
82
|
+
def to_openapi_schema(cls, refs: dict[Serializer, str], force: bool = False) -> Any:
|
|
83
|
+
return {}
|
|
84
|
+
|
|
85
|
+
# All attributes and methods below this point are for Pydantic
|
|
86
|
+
# compatibility. These methods along with the appropriate monkey patching
|
|
87
|
+
# allow all Serialite Serializables to be used as Pydantic BaseModels.
|
|
88
|
+
|
|
89
|
+
# This flag is used by the issubclass(_, BaseModel) monkey patch to identify
|
|
90
|
+
# classes that claim to be subclasses of BaseModel.
|
|
91
|
+
_is_pydantic_base_model = True
|
|
92
|
+
|
|
93
|
+
# This flag protects dataclasses from conversion
|
|
94
|
+
__processed__ = True
|
|
95
|
+
|
|
96
|
+
@classmethod
|
|
97
|
+
def validate(cls, value: Any, values, field, config):
|
|
98
|
+
# This is the canonical name for the main Pydantic validator. It does
|
|
99
|
+
# not have to have this name as long as it is returned by
|
|
100
|
+
# __get_validators__.
|
|
101
|
+
|
|
102
|
+
# FastAPI passes in both data to be parsed and instances of the object
|
|
103
|
+
# itself for some reason. Politely return the object if we get an
|
|
104
|
+
# instance of this class.
|
|
105
|
+
if isinstance(value, cls):
|
|
106
|
+
return value
|
|
107
|
+
|
|
108
|
+
result = cls.from_data(value)
|
|
109
|
+
|
|
110
|
+
if isinstance(result, DeserializationFailure):
|
|
111
|
+
# Must raise ValueError, TypeError, or AssertionError for Pydantic
|
|
112
|
+
# to catch it. Or we could construct a Pydantic ValidationError.
|
|
113
|
+
# https://pydantic-docs.helpmanual.io/usage/validators/
|
|
114
|
+
raise ValueError(result.error)
|
|
115
|
+
else:
|
|
116
|
+
return result.or_die()
|
|
117
|
+
|
|
118
|
+
@classmethod
|
|
119
|
+
def __get_validators__(cls):
|
|
120
|
+
# FastAPI uses pydantic.fields.ModelField to convert a type hint into a
|
|
121
|
+
# Pydantic parser. ModelField relies on this field to get the parser.
|
|
122
|
+
# The call chain is here: APIRoute > get_dependant > get_param_field >
|
|
123
|
+
# create_response_field > ModelField > prepare > populate_validators >
|
|
124
|
+
# __get_validators__
|
|
125
|
+
|
|
126
|
+
yield cls.validate
|
|
127
|
+
|
|
128
|
+
@classproperty
|
|
129
|
+
def __config__(cls):
|
|
130
|
+
# FastAPI accesses this attribute in a few places. It does not matter.
|
|
131
|
+
try:
|
|
132
|
+
from pydantic import BaseConfig
|
|
133
|
+
|
|
134
|
+
return BaseConfig
|
|
135
|
+
except ImportError:
|
|
136
|
+
# Protect against auto-documentation programs trying to get all
|
|
137
|
+
# properties.
|
|
138
|
+
return None
|
|
139
|
+
|
|
140
|
+
def dict(
|
|
141
|
+
self,
|
|
142
|
+
include: AbstractSetIntStr | MappingIntStrAny = None,
|
|
143
|
+
exclude: AbstractSetIntStr | MappingIntStrAny = None,
|
|
144
|
+
by_alias: bool = False,
|
|
145
|
+
skip_defaults: bool = None, # noqa: RUF013
|
|
146
|
+
exclude_unset: bool = False,
|
|
147
|
+
exclude_defaults: bool = False,
|
|
148
|
+
exclude_none: bool = False,
|
|
149
|
+
):
|
|
150
|
+
# FastAPI and Pydantic can only serialize instances of the validator,
|
|
151
|
+
# which is why dict is implemented on Serializable instead of plain Serializer.
|
|
152
|
+
# Serialite Serializers simply cannot be passed directly to FastAPI.
|
|
153
|
+
|
|
154
|
+
data = self.to_data()
|
|
155
|
+
if exclude is not None:
|
|
156
|
+
data = {key: value for key, value in data.items() if key not in exclude}
|
|
157
|
+
|
|
158
|
+
if hasattr(self, "__subclass_serializers__"):
|
|
159
|
+
# Pydantic has no concept of algebraic data types. It does not use
|
|
160
|
+
# the field to determine how to serialize an object. It purely asks
|
|
161
|
+
# the object itself how it should be serialized via the dict
|
|
162
|
+
# method. This implementation of dict assumes that anytime a
|
|
163
|
+
# subclass of an abstract serializable is serialized, it was
|
|
164
|
+
# supposed to be serialized with the abstract serializer, not the
|
|
165
|
+
# concrete class. This is not a good assumption, but Pydantic is not
|
|
166
|
+
# expressive enough to have two serializers for the same object.
|
|
167
|
+
return {"_type": self.__class__.__name__} | data
|
|
168
|
+
else:
|
|
169
|
+
return data
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
__all__ = ["field"]
|
|
2
|
+
|
|
3
|
+
import dataclasses
|
|
4
|
+
from dataclasses import MISSING, Field
|
|
5
|
+
|
|
6
|
+
from ._base import Serializer
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def field(
|
|
10
|
+
*,
|
|
11
|
+
default=MISSING,
|
|
12
|
+
default_factory=MISSING,
|
|
13
|
+
serializer: Serializer | type = MISSING,
|
|
14
|
+
init: bool = True,
|
|
15
|
+
repr: bool = True,
|
|
16
|
+
hash: bool | None = None,
|
|
17
|
+
compare: bool = True,
|
|
18
|
+
metadata: dict | None = None,
|
|
19
|
+
kw_only: bool = MISSING,
|
|
20
|
+
) -> Field:
|
|
21
|
+
serializer_metadata = (metadata if metadata is not None else {}) | {"serializer": serializer}
|
|
22
|
+
return dataclasses.field(
|
|
23
|
+
default=default,
|
|
24
|
+
default_factory=default_factory,
|
|
25
|
+
init=init,
|
|
26
|
+
repr=repr,
|
|
27
|
+
hash=hash,
|
|
28
|
+
compare=compare,
|
|
29
|
+
metadata=serializer_metadata,
|
|
30
|
+
kw_only=kw_only,
|
|
31
|
+
)
|