pypersistable 1.0.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,19 @@
1
+ name: release
2
+
3
+ on:
4
+ push:
5
+ tags: ["v*"]
6
+
7
+ jobs:
8
+ publish:
9
+ runs-on: ubuntu-latest
10
+ environment: pypi
11
+ permissions:
12
+ id-token: write
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+ with:
16
+ fetch-depth: 0
17
+ - uses: astral-sh/setup-uv@v6
18
+ - run: uv build
19
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Nick Young
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,214 @@
1
+ Metadata-Version: 2.4
2
+ Name: pypersistable
3
+ Version: 1.0.0
4
+ Summary: Core Object Model
5
+ Author-email: Nick Young <nick.young.github@gmail.com>
6
+ License: MIT License
7
+
8
+ Copyright (c) 2026 Nick Young
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Project-URL: homepage, https://github.com/nickyoung-github/persistable
29
+ Classifier: License :: OSI Approved :: MIT License
30
+ Classifier: Programming Language :: Python :: 3
31
+ Classifier: Programming Language :: Python :: 3 :: Only
32
+ Classifier: Programming Language :: Python :: 3.13
33
+ Classifier: Programming Language :: Python :: 3.14
34
+ Classifier: Operating System :: OS Independent
35
+ Requires-Python: >=3.13
36
+ Description-Content-Type: text/markdown
37
+ License-File: LICENSE
38
+ Requires-Dist: anyschema
39
+ Requires-Dist: duckdb
40
+ Requires-Dist: fsspec
41
+ Requires-Dist: narwhals
42
+ Requires-Dist: orjson
43
+ Requires-Dist: pyarrow
44
+ Requires-Dist: pydantic>2.0
45
+ Provides-Extra: test
46
+ Requires-Dist: pytest; extra == "test"
47
+ Requires-Dist: pytest-cov; extra == "test"
48
+ Dynamic: license-file
49
+
50
+ # pypersistable: Define and persist python objects with minimal boilerplate
51
+
52
+ Any object that can be handled by `pydantic.TypeAdaptor` will work (pydantic models, dataclasses etc.)
53
+ Objects can be persisted as JSON or parquet (with some caveats).
54
+
55
+
56
+ ## Defining persistable objects
57
+
58
+ ### `TypeRegistry`
59
+ Persistable types must be registered with `TypeRegistry`. This can be done explicitly or using entry points.
60
+ While we could have avoided a registry, the alternative is persisting implementation-specific info,
61
+ such as the module and class names, making code refactoring very difficult.
62
+ You can optionally supply a `root` parameter to group types, e.g., you might specify a root of `Animals`
63
+ for classes `Dog`, `Cat`, `Bunyip` etc. See [DiscriminatedUnion](#discriminatedunion) for another use of root.
64
+
65
+ ### `Persistable`
66
+ `Persistable` is the abstract base class for persistable objects. All it does it provide an `object_id`
67
+ for serialisation and ways to form that id for retrieval. There are two species of `Persistable`:
68
+
69
+ - `Unique` - As the name suggests, this is the base for types where all fields form the id. E.g.,
70
+ ```python
71
+ class Address(Unique):
72
+ street_number: str # Could be 221B, for example
73
+ street_name: str
74
+ town: str
75
+ postcode: str
76
+ country: Country
77
+ ```
78
+ - `Identifiable` - A base class for objects where a subset of fields form their id. Such fields
79
+ are declared using `IdField`. E.g.,
80
+ ```python
81
+ class Customer(Identifiable):
82
+ name: IdField[str]
83
+ address: Address
84
+ ```
85
+
86
+ ### `Reference`
87
+ In classes which have user-defined types as attributes, do we want those nested objects to be persisted
88
+ by value in the parent object's payload or saved separately, with the parent persisting a reference?
89
+ The answer is very much, "it depends". If you definitely want the nested payload to be embedded, that is
90
+ easy: simply declare your type like `Customer` above. However, you could instead re-write it as:
91
+
92
+ ```python
93
+ class Customer(Identifiable):
94
+ name: IdField[str]
95
+ address: Reference[Address]
96
+ ```
97
+
98
+ This means that if we had multiple customers (e.g. family members) living at the same address,
99
+ when persisting those customers, the `Address` would be persisted once and shared, with a reference being
100
+ saved in the `Customer` payload.
101
+
102
+ You can force references to be serialised by value via the `by_reference` paramter on `ObjectStore.write`.
103
+ `ObjectStore.read` will always check for persisted references.
104
+
105
+ ### `DiscriminatedUnion`
106
+ Polymorphism in persisted classes tends to work quite badly and should be avoided. Persistable objects
107
+ often also get used in contexts where we need to know a schema, such as web services.
108
+ `fastapi` based services, using pydantic models is a good example.
109
+
110
+ A better approach is to declare the valid types using a python `Union`. pydantic can try and infer the
111
+ actual type on deserialisation but it can be preferable to indicate that explicitly.
112
+ `DiscriminatedUnion` does this for you by:
113
+ - Including a type specifier in the JSON schema
114
+ - Adding that type specifier (which uses `TypeRegistry.registered_name`) to the serialisation payload
115
+ - Extracting and removing the type specifier from the deserialisation payload and selecting the appropriate type
116
+
117
+ `DiscriminatedUnion` can be combined with `__subclasses__` with one major caveat:
118
+ you need to ensure that all subclasses of your type have been imported or you will get inconsistent results.
119
+ `TypeRegistry.types_for_root` can help you here, e.g.,
120
+
121
+ ```python
122
+ Animals = DiscriminatedUnion[TypeRegistry.types_for_root("Animals")]
123
+ ```
124
+
125
+ Stack Overflow and its ilk are littered with mediocre implementations of this pattern, so if you want
126
+ to use `DiscriminatedUnion` standalone, you can supply your own `Discriminator` and use it without
127
+ `TypeRegistry`:
128
+
129
+ ```python
130
+ Animals = DiscriminatedUnion[
131
+ Animal.__subclasses__(),
132
+ Discriminator(
133
+ field="_type",
134
+ discriminator=lambda t: t.__qualname__
135
+ )
136
+ ]
137
+ ```
138
+
139
+ ## Storing persistable objects
140
+
141
+ `ObjectStore` is an abstract base class for implementing an object store, such as a database.
142
+ A file system-based store `FSStore` is provided. Objects are written into directories, formed
143
+ from the store's root, the `TypeRegistry` root and registered name.
144
+ Each write creates a new file, whose name is the timestamp of the write, which corresponds to
145
+ `MetaData.entry_time` in the persisted record.
146
+
147
+ Reading uses `duckdb` to read the stored parquet/json files. Currently only querying by an object's id
148
+ is supported, but this approach also allows for querying by attributes of the object in future.
149
+
150
+ A nightly job to compact the timestamped files into a single file will improve read performance.
151
+
152
+ `FSStore` uses [fsspec](https://pypi.org/project/fsspec/), which supports a wide range of file systems.
153
+ For parquet schema generation it uses [anyschema](https://pypi.org/project/anyschema/), extended with
154
+ some handling for dataclasses and references.
155
+
156
+ > [Note]
157
+ > Parquet cannot handle union types natively. Currently they are only support via a `Reference`, e.g.:
158
+
159
+ ```python
160
+ class Foo:
161
+ animal: Reference[DiscriminatedUnion[Dog, Cat, Wildebeest]]
162
+ ```
163
+
164
+ ### Writing
165
+
166
+ ```python
167
+ store = FSStore(r"file:///users/hades/ObjectStore", format=FSStoreFormat.Parquet)
168
+ best_boy = Dog(name="Cerberus")
169
+ store.write([best_boy])
170
+ ```
171
+
172
+ Will result in:
173
+
174
+ ```
175
+ /users/hades/ObjectStore # FSStore root
176
+ /Animals # TypeRegistry root
177
+ /Dog # TypeRegistry registered_name
178
+ /2026-11-08T13:01:55.249821+00:00.parquet # File name
179
+ ```
180
+
181
+ If you save an object which contains references, multiple objects may be created:
182
+
183
+ ```python
184
+ blofeld = Customer(
185
+ name="Ernst Stavro Blofeld",
186
+ address=Address(
187
+ street_numer=1,
188
+ street_name="The Hollowed Out Volcano",
189
+ town="",
190
+ postcode="HV1 1ZZ",
191
+ country=Country.Krakatoa
192
+ )
193
+ )
194
+ store.write([blofeld])
195
+ ```
196
+
197
+ This would create:
198
+
199
+ ```
200
+ /users/hades/ObjectStore # FSStore root
201
+ /Customer # TypeRegistry registered_name
202
+ /2026-11-08T13:01:55.249821+00:00.parquet # File name
203
+ /Address # TypeRegistry registered_name
204
+ /2026-11-08T13:01:55.249821+00:00.parquet # File name
205
+ ```
206
+
207
+ #### Reading
208
+ Objects are read by creating an id with `make_object_id`:
209
+
210
+ ```python
211
+ best_boy_id = Dog.make_object_id(name="Cerberus")
212
+ loaded = store.read([best_boy_id])
213
+ best_boy = loaded[best_boy_id]
214
+ ```
@@ -0,0 +1,165 @@
1
+ # pypersistable: Define and persist python objects with minimal boilerplate
2
+
3
+ Any object that can be handled by `pydantic.TypeAdaptor` will work (pydantic models, dataclasses etc.)
4
+ Objects can be persisted as JSON or parquet (with some caveats).
5
+
6
+
7
+ ## Defining persistable objects
8
+
9
+ ### `TypeRegistry`
10
+ Persistable types must be registered with `TypeRegistry`. This can be done explicitly or using entry points.
11
+ While we could have avoided a registry, the alternative is persisting implementation-specific info,
12
+ such as the module and class names, making code refactoring very difficult.
13
+ You can optionally supply a `root` parameter to group types, e.g., you might specify a root of `Animals`
14
+ for classes `Dog`, `Cat`, `Bunyip` etc. See [DiscriminatedUnion](#discriminatedunion) for another use of root.
15
+
16
+ ### `Persistable`
17
+ `Persistable` is the abstract base class for persistable objects. All it does it provide an `object_id`
18
+ for serialisation and ways to form that id for retrieval. There are two species of `Persistable`:
19
+
20
+ - `Unique` - As the name suggests, this is the base for types where all fields form the id. E.g.,
21
+ ```python
22
+ class Address(Unique):
23
+ street_number: str # Could be 221B, for example
24
+ street_name: str
25
+ town: str
26
+ postcode: str
27
+ country: Country
28
+ ```
29
+ - `Identifiable` - A base class for objects where a subset of fields form their id. Such fields
30
+ are declared using `IdField`. E.g.,
31
+ ```python
32
+ class Customer(Identifiable):
33
+ name: IdField[str]
34
+ address: Address
35
+ ```
36
+
37
+ ### `Reference`
38
+ In classes which have user-defined types as attributes, do we want those nested objects to be persisted
39
+ by value in the parent object's payload or saved separately, with the parent persisting a reference?
40
+ The answer is very much, "it depends". If you definitely want the nested payload to be embedded, that is
41
+ easy: simply declare your type like `Customer` above. However, you could instead re-write it as:
42
+
43
+ ```python
44
+ class Customer(Identifiable):
45
+ name: IdField[str]
46
+ address: Reference[Address]
47
+ ```
48
+
49
+ This means that if we had multiple customers (e.g. family members) living at the same address,
50
+ when persisting those customers, the `Address` would be persisted once and shared, with a reference being
51
+ saved in the `Customer` payload.
52
+
53
+ You can force references to be serialised by value via the `by_reference` paramter on `ObjectStore.write`.
54
+ `ObjectStore.read` will always check for persisted references.
55
+
56
+ ### `DiscriminatedUnion`
57
+ Polymorphism in persisted classes tends to work quite badly and should be avoided. Persistable objects
58
+ often also get used in contexts where we need to know a schema, such as web services.
59
+ `fastapi` based services, using pydantic models is a good example.
60
+
61
+ A better approach is to declare the valid types using a python `Union`. pydantic can try and infer the
62
+ actual type on deserialisation but it can be preferable to indicate that explicitly.
63
+ `DiscriminatedUnion` does this for you by:
64
+ - Including a type specifier in the JSON schema
65
+ - Adding that type specifier (which uses `TypeRegistry.registered_name`) to the serialisation payload
66
+ - Extracting and removing the type specifier from the deserialisation payload and selecting the appropriate type
67
+
68
+ `DiscriminatedUnion` can be combined with `__subclasses__` with one major caveat:
69
+ you need to ensure that all subclasses of your type have been imported or you will get inconsistent results.
70
+ `TypeRegistry.types_for_root` can help you here, e.g.,
71
+
72
+ ```python
73
+ Animals = DiscriminatedUnion[TypeRegistry.types_for_root("Animals")]
74
+ ```
75
+
76
+ Stack Overflow and its ilk are littered with mediocre implementations of this pattern, so if you want
77
+ to use `DiscriminatedUnion` standalone, you can supply your own `Discriminator` and use it without
78
+ `TypeRegistry`:
79
+
80
+ ```python
81
+ Animals = DiscriminatedUnion[
82
+ Animal.__subclasses__(),
83
+ Discriminator(
84
+ field="_type",
85
+ discriminator=lambda t: t.__qualname__
86
+ )
87
+ ]
88
+ ```
89
+
90
+ ## Storing persistable objects
91
+
92
+ `ObjectStore` is an abstract base class for implementing an object store, such as a database.
93
+ A file system-based store `FSStore` is provided. Objects are written into directories, formed
94
+ from the store's root, the `TypeRegistry` root and registered name.
95
+ Each write creates a new file, whose name is the timestamp of the write, which corresponds to
96
+ `MetaData.entry_time` in the persisted record.
97
+
98
+ Reading uses `duckdb` to read the stored parquet/json files. Currently only querying by an object's id
99
+ is supported, but this approach also allows for querying by attributes of the object in future.
100
+
101
+ A nightly job to compact the timestamped files into a single file will improve read performance.
102
+
103
+ `FSStore` uses [fsspec](https://pypi.org/project/fsspec/), which supports a wide range of file systems.
104
+ For parquet schema generation it uses [anyschema](https://pypi.org/project/anyschema/), extended with
105
+ some handling for dataclasses and references.
106
+
107
+ > [Note]
108
+ > Parquet cannot handle union types natively. Currently they are only support via a `Reference`, e.g.:
109
+
110
+ ```python
111
+ class Foo:
112
+ animal: Reference[DiscriminatedUnion[Dog, Cat, Wildebeest]]
113
+ ```
114
+
115
+ ### Writing
116
+
117
+ ```python
118
+ store = FSStore(r"file:///users/hades/ObjectStore", format=FSStoreFormat.Parquet)
119
+ best_boy = Dog(name="Cerberus")
120
+ store.write([best_boy])
121
+ ```
122
+
123
+ Will result in:
124
+
125
+ ```
126
+ /users/hades/ObjectStore # FSStore root
127
+ /Animals # TypeRegistry root
128
+ /Dog # TypeRegistry registered_name
129
+ /2026-11-08T13:01:55.249821+00:00.parquet # File name
130
+ ```
131
+
132
+ If you save an object which contains references, multiple objects may be created:
133
+
134
+ ```python
135
+ blofeld = Customer(
136
+ name="Ernst Stavro Blofeld",
137
+ address=Address(
138
+ street_numer=1,
139
+ street_name="The Hollowed Out Volcano",
140
+ town="",
141
+ postcode="HV1 1ZZ",
142
+ country=Country.Krakatoa
143
+ )
144
+ )
145
+ store.write([blofeld])
146
+ ```
147
+
148
+ This would create:
149
+
150
+ ```
151
+ /users/hades/ObjectStore # FSStore root
152
+ /Customer # TypeRegistry registered_name
153
+ /2026-11-08T13:01:55.249821+00:00.parquet # File name
154
+ /Address # TypeRegistry registered_name
155
+ /2026-11-08T13:01:55.249821+00:00.parquet # File name
156
+ ```
157
+
158
+ #### Reading
159
+ Objects are read by creating an id with `make_object_id`:
160
+
161
+ ```python
162
+ best_boy_id = Dog.make_object_id(name="Cerberus")
163
+ loaded = store.read([best_boy_id])
164
+ best_boy = loaded[best_boy_id]
165
+ ```
File without changes
@@ -0,0 +1,78 @@
1
+ import datetime as dt
2
+ from dataclasses import dataclass
3
+ from enum import StrEnum, auto
4
+ from pathlib import Path
5
+
6
+ from pypersistable import (
7
+ Discriminated,
8
+ Identifiable,
9
+ IdField,
10
+ Reference,
11
+ TypeRegistry,
12
+ Unique,
13
+ )
14
+
15
+ __registry = TypeRegistry()
16
+
17
+
18
+ @dataclass(kw_only=True)
19
+ class Counterparty(Identifiable):
20
+ name: IdField[str]
21
+ location: str
22
+ contact_name: str
23
+
24
+ __registry.register("Counterparty", Counterparty)
25
+
26
+
27
+ @dataclass(kw_only=True)
28
+ class CustomerBook(Identifiable):
29
+ counterparty: IdField[Counterparty]
30
+ sales_coverage: tuple[str, ...]
31
+
32
+ __registry.register("CustomerBook", CustomerBook)
33
+
34
+
35
+
36
+ @dataclass(kw_only=True)
37
+ class TradingBook(Identifiable):
38
+ name: IdField[str]
39
+ owner: str
40
+
41
+ __registry.register("TradingBook", TradingBook)
42
+
43
+
44
+ @dataclass(kw_only=True)
45
+ class Option(Unique):
46
+ ticker: str
47
+ strike: float
48
+ maturity: dt.datetime
49
+
50
+ __registry.register("Option", Option, Path("Instrument"))
51
+
52
+ Instrument = Discriminated[Path("Instrument")]
53
+
54
+
55
+ class TradeAction(StrEnum):
56
+ Buy = auto()
57
+ Sell = auto()
58
+ Pay = auto()
59
+ Receive = auto()
60
+
61
+
62
+ @dataclass(kw_only=True)
63
+ class Position(Unique):
64
+ quantity: float
65
+ action: TradeAction
66
+ instrument: Reference[Instrument]
67
+
68
+ __registry.register("Position", Position)
69
+
70
+
71
+ @dataclass(kw_only=True)
72
+ class Deal(Identifiable):
73
+ deal_id: IdField[str]
74
+ book_from: Reference[TradingBook]
75
+ book_to: Reference[TradingBook | CustomerBook]
76
+ position: list[Position]
77
+
78
+ __registry.register("Deal", Deal)
@@ -0,0 +1,48 @@
1
+
2
+
3
+ [project]
4
+ name = "pypersistable"
5
+ description = "Core Object Model"
6
+ authors = [
7
+ {name = "Nick Young", email = "nick.young.github@gmail.com"}
8
+ ]
9
+ readme = "README.md"
10
+ license = {file = "LICENSE"}
11
+ requires-python = ">=3.13"
12
+
13
+ classifiers = [
14
+ "License :: OSI Approved :: MIT License",
15
+ "Programming Language :: Python :: 3",
16
+ 'Programming Language :: Python :: 3 :: Only',
17
+ 'Programming Language :: Python :: 3.13',
18
+ 'Programming Language :: Python :: 3.14',
19
+ "Operating System :: OS Independent"
20
+ ]
21
+
22
+ dynamic = ["version"]
23
+
24
+ dependencies = [
25
+ "anyschema",
26
+ "duckdb",
27
+ "fsspec",
28
+ "narwhals",
29
+ "orjson",
30
+ "pyarrow",
31
+ "pydantic>2.0",
32
+ ]
33
+
34
+ [project.optional-dependencies]
35
+ test = [
36
+ "pytest",
37
+ "pytest-cov"
38
+ ]
39
+
40
+ [project.urls]
41
+ homepage = "https://github.com/nickyoung-github/persistable"
42
+
43
+ [build-system]
44
+ requires = ["setuptools>=64", "setuptools-scm>=8"]
45
+ build-backend = "setuptools.build_meta"
46
+
47
+ [tool.setuptools_scm]
48
+ version_file = "persistable/_version.py"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,20 @@
1
+ from .discriminated_union import Discriminated, DiscriminatedUnion, Discriminator
2
+ from .fs_store import FSStore, FSStoreFormat
3
+ from .object_store import ObjectStore
4
+ from .persistable import Identifiable, IdField, Unique
5
+ from .reference import Reference
6
+ from .type_registry import TypeRegistry
7
+
8
+ __all__ = (
9
+ "Discriminated",
10
+ "DiscriminatedUnion",
11
+ "Discriminator",
12
+ "FSStore",
13
+ "FSStoreFormat",
14
+ "IdField",
15
+ "Identifiable",
16
+ "ObjectStore",
17
+ "Reference",
18
+ "TypeRegistry",
19
+ "Unique",
20
+ )
@@ -0,0 +1,75 @@
1
+ from pathlib import Path
2
+ from types import NoneType
3
+
4
+ import narwhals as nw
5
+ from anyschema._dependencies import is_dataclass
6
+ from anyschema.adapters import dataclass_adapter
7
+ from anyschema.exceptions import UnsupportedDTypeError
8
+ from anyschema.parsers import (
9
+ AnnotatedStep,
10
+ ForwardRefStep,
11
+ PyTypeStep,
12
+ UnionTypeStep,
13
+ make_pipeline,
14
+ )
15
+ from anyschema.parsers._base import ParserStep
16
+ from anyschema.parsers.annotated_types import AnnotatedTypesStep
17
+ from anyschema.parsers.pydantic import PydanticTypeStep
18
+ from anyschema.typing import FieldConstraints, FieldMetadata, FieldType
19
+ from narwhals.dtypes import DType
20
+
21
+ from .persistable import Persistable
22
+
23
+
24
+ class DataclassTypeStep(ParserStep):
25
+ def parse(
26
+ self,
27
+ input_type: FieldType,
28
+ constraints: FieldConstraints,
29
+ metadata: FieldMetadata,
30
+ ) -> DType | None:
31
+ if is_dataclass(input_type):
32
+ return nw.Struct(
33
+ [
34
+ nw.Field(
35
+ name=field_name,
36
+ dtype=self.pipeline.parse(field_info, field_constraints, field_metadata, strict=True),
37
+ )
38
+ for field_name, field_info, field_constraints, field_metadata in dataclass_adapter(input_type)
39
+ ]
40
+ )
41
+
42
+
43
+ class _UnionTypeStep(UnionTypeStep):
44
+ # Replaces the anyschema one, so we can handle references
45
+
46
+ def _parse_union(self, union: tuple[FieldType, ...]) -> FieldType:
47
+ if len(union) != 2:
48
+ msg = "Union with more than two types is not supported."
49
+ raise UnsupportedDTypeError(msg)
50
+
51
+ field0, field1 = union
52
+
53
+ if field0 is not NoneType and field1 is not NoneType:
54
+ msg = "Union with mixed types is not supported."
55
+
56
+ for f in (field0, field1):
57
+ if f is not Path and isinstance(f, type) and not issubclass(f, Persistable):
58
+ raise UnsupportedDTypeError(msg)
59
+
60
+ return str
61
+
62
+ return field1 if field0 is NoneType else field0
63
+
64
+
65
+ PARSER_STEPS = (
66
+ ForwardRefStep(),
67
+ _UnionTypeStep(),
68
+ AnnotatedStep(),
69
+ AnnotatedTypesStep(),
70
+ DataclassTypeStep(),
71
+ PydanticTypeStep(),
72
+ PyTypeStep()
73
+ )
74
+
75
+ PIPELINE = make_pipeline(PARSER_STEPS)