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.
Files changed (35) hide show
  1. serialite-0.3.8/LICENSE +19 -0
  2. serialite-0.3.8/PKG-INFO +69 -0
  3. serialite-0.3.8/README.md +40 -0
  4. serialite-0.3.8/pyproject.toml +114 -0
  5. serialite-0.3.8/src/serialite/__init__.py +35 -0
  6. serialite-0.3.8/src/serialite/_base.py +169 -0
  7. serialite-0.3.8/src/serialite/_dataclass.py +31 -0
  8. serialite-0.3.8/src/serialite/_decorators.py +246 -0
  9. serialite-0.3.8/src/serialite/_descriptors.py +14 -0
  10. serialite-0.3.8/src/serialite/_dispatcher.py +293 -0
  11. serialite-0.3.8/src/serialite/_fields_serializer.py +409 -0
  12. serialite-0.3.8/src/serialite/_implementations/__init__.py +30 -0
  13. serialite-0.3.8/src/serialite/_implementations/_array.py +52 -0
  14. serialite-0.3.8/src/serialite/_implementations/_boolean.py +20 -0
  15. serialite-0.3.8/src/serialite/_implementations/_date_time.py +33 -0
  16. serialite-0.3.8/src/serialite/_implementations/_dictionary.py +143 -0
  17. serialite-0.3.8/src/serialite/_implementations/_float.py +48 -0
  18. serialite-0.3.8/src/serialite/_implementations/_integer.py +53 -0
  19. serialite-0.3.8/src/serialite/_implementations/_json.py +20 -0
  20. serialite-0.3.8/src/serialite/_implementations/_list.py +56 -0
  21. serialite-0.3.8/src/serialite/_implementations/_literal.py +32 -0
  22. serialite-0.3.8/src/serialite/_implementations/_none.py +20 -0
  23. serialite-0.3.8/src/serialite/_implementations/_ordered_set.py +56 -0
  24. serialite-0.3.8/src/serialite/_implementations/_path.py +22 -0
  25. serialite-0.3.8/src/serialite/_implementations/_reserved.py +40 -0
  26. serialite-0.3.8/src/serialite/_implementations/_set.py +54 -0
  27. serialite-0.3.8/src/serialite/_implementations/_string.py +31 -0
  28. serialite-0.3.8/src/serialite/_implementations/_tuple.py +81 -0
  29. serialite-0.3.8/src/serialite/_implementations/_union.py +78 -0
  30. serialite-0.3.8/src/serialite/_implementations/_uuid.py +27 -0
  31. serialite-0.3.8/src/serialite/_mixins.py +137 -0
  32. serialite-0.3.8/src/serialite/_monkey_patches.py +196 -0
  33. serialite-0.3.8/src/serialite/_numeric_check.py +21 -0
  34. serialite-0.3.8/src/serialite/_result.py +77 -0
  35. serialite-0.3.8/src/serialite/_stable_set.py +52 -0
@@ -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.
@@ -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
+ )