typecraft 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,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Matt
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,34 @@
1
+ Metadata-Version: 2.3
2
+ Name: typecraft
3
+ Version: 0.1.0
4
+ Summary: Annotation-native toolkit for data validation, transformation, and type inspection
5
+ Author: mm21
6
+ Author-email: mm21.dev@gmail.com
7
+ Requires-Python: >=3.12,<4.0
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Natural Language :: English
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Programming Language :: Python :: Implementation :: CPython
16
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
17
+ Classifier: Typing :: Typed
18
+ Provides-Extra: toml
19
+ Requires-Dist: tomlkit (>=0.13.3,<0.14.0) ; extra == "toml"
20
+ Project-URL: Homepage, https://github.com/mm21/typecraft
21
+ Description-Content-Type: text/markdown
22
+
23
+ # TypeCraft
24
+
25
+ Annotation-native toolkit for data validation, transformation, and type inspection
26
+
27
+ Facilitates the following:
28
+
29
+ - **Validation and transformation**: Mechanism to validate and convert objects based on annotations, with user-defined source/destination types and conversion logic
30
+ - **Typing**: Utilities to extract metadata from `Annotated[]`, handle `Literal[]` and unions, and wrap type info in a user-friendly container
31
+ - **Data modeling**: Lightweight, pydantic-like modeling with validation
32
+ - Based on dataclasses, avoiding metaclass conflicts
33
+ - **TOML modeling**: Wrapper for `tomlkit` with user-defined model classes for documents, tables, and arrays
34
+
@@ -0,0 +1,11 @@
1
+ # TypeCraft
2
+
3
+ Annotation-native toolkit for data validation, transformation, and type inspection
4
+
5
+ Facilitates the following:
6
+
7
+ - **Validation and transformation**: Mechanism to validate and convert objects based on annotations, with user-defined source/destination types and conversion logic
8
+ - **Typing**: Utilities to extract metadata from `Annotated[]`, handle `Literal[]` and unions, and wrap type info in a user-friendly container
9
+ - **Data modeling**: Lightweight, pydantic-like modeling with validation
10
+ - Based on dataclasses, avoiding metaclass conflicts
11
+ - **TOML modeling**: Wrapper for `tomlkit` with user-defined model classes for documents, tables, and arrays
@@ -0,0 +1,70 @@
1
+ [project]
2
+ authors = [
3
+ {email = "mm21.dev@gmail.com", name = "mm21"}
4
+ ]
5
+ classifiers = [
6
+ "Development Status :: 3 - Alpha",
7
+ "License :: OSI Approved :: MIT License",
8
+ "Natural Language :: English",
9
+ "Operating System :: OS Independent",
10
+ "Programming Language :: Python :: 3",
11
+ "Programming Language :: Python :: 3.12",
12
+ "Programming Language :: Python :: 3.13",
13
+ "Programming Language :: Python :: Implementation :: CPython",
14
+ "Topic :: Software Development :: Libraries :: Python Modules",
15
+ "Typing :: Typed"
16
+ ]
17
+ description = "Annotation-native toolkit for data validation, transformation, and type inspection"
18
+ name = "typecraft"
19
+ readme = "README.md"
20
+ requires-python = ">=3.12,<4.0"
21
+ version = "0.1.0"
22
+
23
+ [project.urls]
24
+ homepage = "https://github.com/mm21/typecraft"
25
+
26
+ [project.scripts]
27
+
28
+ [build-system]
29
+ build-backend = "poetry.core.masonry.api"
30
+ requires = ["poetry-core>=1.0.0"]
31
+
32
+ [tool.poetry]
33
+ packages = [{include = "typecraft", from = "src"}]
34
+
35
+ [tool.poetry.dependencies]
36
+ tomlkit = {version = "^0.13.3", optional = true}
37
+
38
+ [tool.poetry.group.dev.dependencies]
39
+ autoflake = "^2.3.1"
40
+ black = "^25.1.0"
41
+ doit = "^0.36.0"
42
+ isort = "^6.0.1"
43
+ pytest = "^8.4.2"
44
+ toml-sort = "^0.24.2"
45
+
46
+ [tool.poetry.group.toml.dependencies]
47
+ tomlkit = "*"
48
+
49
+ [tool.poetry.extras]
50
+ toml = [
51
+ "tomlkit"
52
+ ]
53
+
54
+ [tool.autoflake]
55
+ remove_all_unused_imports = true
56
+ remove_unused_variables = true
57
+
58
+ [tool.black]
59
+ target-version = ["py312", "py313"]
60
+
61
+ [tool.isort]
62
+ profile = "black"
63
+
64
+ [tool.tomlsort]
65
+ no_sort_tables = true
66
+ sort_table_keys = true
67
+
68
+ [tool.pytest.ini_options]
69
+ addopts = "--import-mode=importlib -s -v -rA"
70
+ testpaths = "test"
@@ -0,0 +1,4 @@
1
+ """
2
+ TypeCraft: Annotation-native toolkit for data validation, transformation, and
3
+ type inspection
4
+ """
@@ -0,0 +1,308 @@
1
+ """
2
+ Low-level conversion capability, agnostic of validation vs serialization.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from abc import ABC, abstractmethod
8
+ from collections import defaultdict
9
+ from collections.abc import Callable, Sequence
10
+ from typing import Any, cast
11
+
12
+ from .inspecting.annotations import Annotation
13
+ from .inspecting.functions import ParameterInfo, SignatureInfo
14
+ from .typedefs import VarianceType
15
+
16
+
17
+ class ConverterFunction:
18
+ """
19
+ Encapsulates a validator or serializer function.
20
+ """
21
+
22
+ func: Callable[..., Any]
23
+ """
24
+ Converter function.
25
+ """
26
+
27
+ sig_info: SignatureInfo
28
+ """
29
+ Function signature.
30
+ """
31
+
32
+ obj_param: ParameterInfo
33
+ """
34
+ Parameter for object to be validated/serialized, must be positional.
35
+ """
36
+
37
+ annotation_param: ParameterInfo | None
38
+ """
39
+ Parameter for annotation, must be keyword.
40
+ """
41
+
42
+ context_param: ParameterInfo | None
43
+ """
44
+ Parameter for context, must be keyword.
45
+ """
46
+
47
+ def __init__(
48
+ self, func: Callable[..., Any], context_cls: type[BaseConversionContext]
49
+ ):
50
+ sig_info = SignatureInfo(func)
51
+
52
+ # get object parameter
53
+ obj_param = next(
54
+ (p for p in sig_info.get_params(positional=True)),
55
+ None,
56
+ )
57
+ assert (
58
+ obj_param
59
+ ), f"Function {func} does not take any positional params, must take obj as positional"
60
+
61
+ # get annotation parameter
62
+ annotation_param = next(
63
+ (p for p in sig_info.get_params(annotation=Annotation, keyword=True)),
64
+ None,
65
+ )
66
+
67
+ # get context parameter
68
+ context_param = next(
69
+ (p for p in sig_info.get_params(annotation=context_cls, keyword=True)),
70
+ None,
71
+ )
72
+
73
+ expected_param_count = sum(
74
+ int(p is not None) for p in (obj_param, annotation_param, context_param)
75
+ )
76
+ if expected_param_count != len(sig_info.params):
77
+ raise TypeError(
78
+ f"Unexpected param count: expected {expected_param_count}, got {len(sig_info.params)}"
79
+ )
80
+
81
+ self.func = func
82
+ self.sig_info = sig_info
83
+ self.obj_param = obj_param
84
+ self.annotation_param = annotation_param
85
+ self.context_param = context_param
86
+
87
+ def invoke(
88
+ self, obj: Any, annotation: Annotation, context: BaseConversionContext
89
+ ) -> Any:
90
+ kwargs: dict[str, Any] = {}
91
+
92
+ if param := self.annotation_param:
93
+ kwargs[param.parameter.name] = annotation
94
+ if param := self.context_param:
95
+ kwargs[param.parameter.name] = context
96
+
97
+ return self.func(obj, **kwargs)
98
+
99
+
100
+ class BaseTypedConverter[ConverterFuncT: Callable[..., Any]](ABC):
101
+ """
102
+ Base class for typed converters (validators and serializers).
103
+
104
+ Encapsulates common conversion parameters and logic for type-based
105
+ conversion between source and target annotations.
106
+ """
107
+
108
+ _source_annotation: Annotation
109
+ """
110
+ Annotation specifying type to convert from.
111
+ """
112
+
113
+ _target_annotation: Annotation
114
+ """
115
+ Annotation specifying type to convert to.
116
+ """
117
+
118
+ _func: ConverterFunction | None
119
+ """
120
+ Function taking source type and returning an instance of target type.
121
+ """
122
+
123
+ _variance: VarianceType
124
+ """
125
+ Variance with respect to a reference annotation, either source or target depending
126
+ on serialization vs validation.
127
+ """
128
+
129
+ def __init__(
130
+ self,
131
+ source_annotation: Any,
132
+ target_annotation: Any,
133
+ /,
134
+ *,
135
+ func: ConverterFuncT | None = None,
136
+ variance: VarianceType = "contravariant",
137
+ ):
138
+ self._source_annotation = Annotation._normalize(source_annotation)
139
+ self._target_annotation = Annotation._normalize(target_annotation)
140
+ self._func = ConverterFunction(func, self._get_context_cls()) if func else None
141
+ self._variance = variance
142
+
143
+ @property
144
+ def source_annotation(self) -> Annotation:
145
+ return self._source_annotation
146
+
147
+ @property
148
+ def target_annotation(self) -> Annotation:
149
+ return self._target_annotation
150
+
151
+ @property
152
+ def variance(self) -> VarianceType:
153
+ return self._variance
154
+
155
+ @abstractmethod
156
+ def can_convert(self, obj: Any, annotation: Annotation, /) -> bool:
157
+ """
158
+ Check if this converter can convert the given object with the given annotation.
159
+
160
+ The meaning of 'annotation' depends on the converter type:
161
+ - For validators: target annotation (converting TO)
162
+ - For serializers: source annotation (converting FROM)
163
+ """
164
+
165
+ @abstractmethod
166
+ def _get_context_cls(self) -> type[BaseConversionContext]:
167
+ """
168
+ Get the context class for this converter.
169
+ """
170
+
171
+ def _check_variance_match(
172
+ self,
173
+ annotation: Annotation,
174
+ reference_annotation: Annotation,
175
+ ) -> bool:
176
+ """Check if annotation matches reference based on variance."""
177
+ if self._variance == "invariant":
178
+ # exact match only
179
+ return annotation == reference_annotation
180
+ else:
181
+ # contravariant (default): annotation must be a subclass of reference
182
+ return annotation.is_subtype(reference_annotation)
183
+
184
+
185
+ class BaseConverterRegistry[ConverterT: BaseTypedConverter](ABC):
186
+ """
187
+ Base class for converter registries.
188
+
189
+ Provides efficient lookup of converters based on object type and annotation.
190
+ Converters are indexed by a key type for fast lookup, with fallback to
191
+ sequential search for contravariant matching.
192
+ """
193
+
194
+ _converter_map: dict[type, list[ConverterT]]
195
+ """
196
+ Converters grouped by key type for efficiency.
197
+ """
198
+
199
+ _converters: list[ConverterT]
200
+ """
201
+ List of all converters for fallback/contravariant matching.
202
+ """
203
+
204
+ def __init__(self, *converters: ConverterT):
205
+ self._converter_map = defaultdict(list)
206
+ self._converters = []
207
+ self.extend(converters)
208
+
209
+ def __len__(self) -> int:
210
+ return len(self._converters)
211
+
212
+ @property
213
+ def converters(self) -> list[ConverterT]:
214
+ """
215
+ Get converters currently registered.
216
+ """
217
+ return self._converters
218
+
219
+ def find(self, obj: Any, annotation: Annotation) -> ConverterT | None:
220
+ """
221
+ Find the first converter that can handle the conversion.
222
+
223
+ Searches in order:
224
+ 1. Exact key type matches
225
+ 2. All converters (for contravariant matching)
226
+ """
227
+ key_type = annotation.concrete_type
228
+
229
+ # first try converters registered for the exact key type
230
+ if key_type in self._converter_map:
231
+ for converter in self._converter_map[key_type]:
232
+ if converter.can_convert(obj, annotation):
233
+ return converter
234
+
235
+ # then try all converters (handles contravariant, generic cases)
236
+ for converter in self._converters:
237
+ if converter not in self._converter_map.get(key_type, []):
238
+ if converter.can_convert(obj, annotation):
239
+ return converter
240
+
241
+ return None
242
+
243
+ def extend(self, converters: Sequence[ConverterT]):
244
+ """
245
+ Register multiple converters.
246
+ """
247
+ for converter in converters:
248
+ self._register_converter(converter)
249
+
250
+ @abstractmethod
251
+ def _get_map_key_type(self, converter: ConverterT) -> type:
252
+ """
253
+ Get the type to use as key in the converter map for this converter.
254
+
255
+ - For validators: target type
256
+ - For serializers: source type
257
+ """
258
+
259
+ def _register_converter(self, converter: ConverterT):
260
+ """
261
+ Register a converter object.
262
+ """
263
+ map_key = self._get_map_key_type(converter)
264
+ self._converter_map[map_key].append(converter)
265
+ self._converters.append(converter)
266
+
267
+
268
+ class BaseConversionContext[RegistryT: BaseConverterRegistry](ABC):
269
+ """
270
+ Base class for conversion contexts.
271
+
272
+ Encapsulates conversion parameters and provides access to the converter
273
+ registry, propagated throughout the conversion process.
274
+ """
275
+
276
+ _registry: RegistryT
277
+
278
+ def __init__(self, *, registry: RegistryT | None = None):
279
+ self._registry = registry or self._create_default_registry()
280
+
281
+ @property
282
+ def registry(self) -> RegistryT:
283
+ return self._registry
284
+
285
+ @abstractmethod
286
+ def _create_default_registry(self) -> RegistryT:
287
+ """
288
+ Create a default registry.
289
+ """
290
+
291
+
292
+ def normalize_to_registry[ConverterT, RegistryT](
293
+ converter_cls: type[ConverterT],
294
+ registry_cls: type[RegistryT],
295
+ *converters_or_registry: Any,
296
+ ) -> RegistryT:
297
+ """
298
+ Take converters or registry and return a registry.
299
+ """
300
+ if len(converters_or_registry) == 1 and isinstance(
301
+ converters_or_registry[0], registry_cls
302
+ ):
303
+ registry = cast(RegistryT, converters_or_registry[0])
304
+ else:
305
+ assert all(isinstance(v, converter_cls) for v in converters_or_registry)
306
+ converters = cast(tuple[ConverterT, ...], converters_or_registry)
307
+ registry = registry_cls(*converters)
308
+ return registry
@@ -0,0 +1,4 @@
1
+ """
2
+ Specific models accommodating different formats. To use, install this package with the
3
+ respective extras group, e.g. `typecraft[toml]`.
4
+ """