structtype 0.3.0__cp313-cp313-win_amd64.whl
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.
- structtype/__init__.py +17 -0
- structtype/__init__.pyi +350 -0
- structtype/_adapter.py +128 -0
- structtype/_core.cp313-win_amd64.pyd +0 -0
- structtype/_inspect.py +1183 -0
- structtype/_json_schema.py +532 -0
- structtype/_utils.py +410 -0
- structtype/_version.py +24 -0
- structtype/py.typed +0 -0
- structtype-0.3.0.dist-info/METADATA +80 -0
- structtype-0.3.0.dist-info/RECORD +14 -0
- structtype-0.3.0.dist-info/WHEEL +5 -0
- structtype-0.3.0.dist-info/licenses/LICENSE +27 -0
- structtype-0.3.0.dist-info/top_level.txt +1 -0
structtype/_utils.py
ADDED
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
# type: ignore
|
|
2
|
+
import collections
|
|
3
|
+
import sys
|
|
4
|
+
import types
|
|
5
|
+
import typing
|
|
6
|
+
from typing import Any, _AnnotatedAlias, _GenericAlias # noqa: F401
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
from typing_extensions import get_type_hints as _get_type_hints
|
|
10
|
+
except Exception:
|
|
11
|
+
from typing import get_type_hints as _get_type_hints
|
|
12
|
+
|
|
13
|
+
try:
|
|
14
|
+
from typing_extensions import NotRequired, Required
|
|
15
|
+
except Exception:
|
|
16
|
+
try:
|
|
17
|
+
from typing import NotRequired, Required
|
|
18
|
+
except Exception:
|
|
19
|
+
Required = NotRequired = None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def get_type_hints(obj):
|
|
23
|
+
return _get_type_hints(obj, include_extras=True)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
PY_312PLUS = sys.version_info >= (3, 12)
|
|
27
|
+
|
|
28
|
+
# The `is_class` argument was new in 3.11, but was backported to 3.9 and 3.10.
|
|
29
|
+
# It's _likely_ to be available for 3.9/3.10, but may not be. Easiest way to
|
|
30
|
+
# check is to try it and see. This check can be removed when we drop support
|
|
31
|
+
# for Python 3.10.
|
|
32
|
+
try:
|
|
33
|
+
typing.ForwardRef("Foo", is_class=True)
|
|
34
|
+
except TypeError:
|
|
35
|
+
|
|
36
|
+
def _forward_ref(value):
|
|
37
|
+
return typing.ForwardRef(value, is_argument=False)
|
|
38
|
+
|
|
39
|
+
else:
|
|
40
|
+
|
|
41
|
+
def _forward_ref(value):
|
|
42
|
+
return typing.ForwardRef(value, is_argument=False, is_class=True)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# Python 3.13 adds a new mandatory type_params kwarg to _eval_type
|
|
46
|
+
if sys.version_info >= (3, 13):
|
|
47
|
+
|
|
48
|
+
def _eval_type(t, globalns, localns):
|
|
49
|
+
return typing._eval_type(t, globalns, localns, ())
|
|
50
|
+
|
|
51
|
+
else:
|
|
52
|
+
_eval_type = typing._eval_type
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
if sys.version_info >= (3, 14):
|
|
56
|
+
from annotationlib import get_annotations as _get_class_annotations
|
|
57
|
+
else:
|
|
58
|
+
|
|
59
|
+
def _get_class_annotations(cls):
|
|
60
|
+
return cls.__dict__.get("__annotations__", {})
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _apply_params(obj, mapping):
|
|
64
|
+
if isinstance(obj, typing.TypeVar):
|
|
65
|
+
return mapping.get(obj, obj)
|
|
66
|
+
|
|
67
|
+
try:
|
|
68
|
+
parameters = tuple(obj.__parameters__)
|
|
69
|
+
except Exception:
|
|
70
|
+
# Not parameterized or __parameters__ is invalid, ignore
|
|
71
|
+
return obj
|
|
72
|
+
|
|
73
|
+
if not parameters:
|
|
74
|
+
# Not parametrized
|
|
75
|
+
return obj
|
|
76
|
+
|
|
77
|
+
# Parametrized
|
|
78
|
+
args = tuple(mapping.get(p, p) for p in parameters)
|
|
79
|
+
return obj[args]
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _get_class_mro_and_typevar_mappings(obj):
|
|
83
|
+
mapping = {}
|
|
84
|
+
|
|
85
|
+
# in Python 3.10 a natively produced 'types.GenericAlias' (e.g. 'list[int]', or the
|
|
86
|
+
# 'Base[int]' produced when a 'Generic' subclass inherits a builtin's
|
|
87
|
+
# '__class_getitem__') satisfies 'isinstance(_, type)', unlike on 3.11+. we still
|
|
88
|
+
# want to treat those as parametrised aliases, not bare classes
|
|
89
|
+
if isinstance(obj, type) and not isinstance(obj, types.GenericAlias):
|
|
90
|
+
cls = obj
|
|
91
|
+
else:
|
|
92
|
+
cls = obj.__origin__
|
|
93
|
+
|
|
94
|
+
def inner(c, scope):
|
|
95
|
+
if isinstance(c, type) and not isinstance(c, types.GenericAlias):
|
|
96
|
+
cls = c
|
|
97
|
+
new_scope = {}
|
|
98
|
+
else:
|
|
99
|
+
cls = typing.get_origin(c)
|
|
100
|
+
if cls in (None, object, typing.Generic) or cls in mapping:
|
|
101
|
+
return
|
|
102
|
+
|
|
103
|
+
if hasattr(cls, "__parameters__"):
|
|
104
|
+
# 'cls' carries its own type vars. This covers both ordinary
|
|
105
|
+
# 'typing._GenericAlias' bases and the 'types.GenericAlias' that
|
|
106
|
+
# get produced when a user-defined 'Generic' subclass inherits a
|
|
107
|
+
# builtin's '__class_getitem__' (e.g. 'class Base(Mapping[str, T])',
|
|
108
|
+
# whose 'Base[...]' is a 'types.GenericAlias'). Map cls's own
|
|
109
|
+
# type vars onto the resolved args, applying '_apply_params' so any
|
|
110
|
+
# outer bindings in 'scope' (e.g. 'U -> int') are propagated.
|
|
111
|
+
params = cls.__parameters__
|
|
112
|
+
args = tuple(_apply_params(a, scope) for a in typing.get_args(c))
|
|
113
|
+
assert len(params) == len(args)
|
|
114
|
+
new_scope = dict(zip(params, args))
|
|
115
|
+
else:
|
|
116
|
+
# a true built-in generic (e.g. 'collections.abc.Mapping[str, T]')
|
|
117
|
+
# whose '__origin__' has no '__parameters__'; the unresolved type
|
|
118
|
+
# vars and args live on the alias itself, not the origin.
|
|
119
|
+
new_scope = dict(zip(c.__parameters__, typing.get_args(c)))
|
|
120
|
+
mapping[cls] = new_scope
|
|
121
|
+
|
|
122
|
+
if issubclass(cls, typing.Generic):
|
|
123
|
+
bases = getattr(cls, "__orig_bases__", cls.__bases__)
|
|
124
|
+
for b in bases:
|
|
125
|
+
inner(b, new_scope)
|
|
126
|
+
|
|
127
|
+
inner(obj, {})
|
|
128
|
+
return cls.__mro__, mapping
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def get_class_annotations(obj):
|
|
132
|
+
"""Get the annotations for a class.
|
|
133
|
+
|
|
134
|
+
This is similar to ``typing.get_type_hints``, except:
|
|
135
|
+
|
|
136
|
+
- We maintain it
|
|
137
|
+
- It leaves extras like ``Annotated``/``ClassVar`` alone
|
|
138
|
+
- It resolves any parametrized generics in the class mro. The returned
|
|
139
|
+
mapping may still include ``TypeVar`` values, but those should be treated
|
|
140
|
+
as their unparametrized variants (i.e. equal to ``Any`` for the common case).
|
|
141
|
+
|
|
142
|
+
Note that this function doesn't check that Generic types are being used
|
|
143
|
+
properly - invalid uses of `Generic` may slip through without complaint.
|
|
144
|
+
|
|
145
|
+
The assumption here is that the user is making use of a static analysis
|
|
146
|
+
tool like ``mypy``/``pyright`` already, which would catch misuse of these
|
|
147
|
+
APIs.
|
|
148
|
+
"""
|
|
149
|
+
hints = {}
|
|
150
|
+
mro, typevar_mappings = _get_class_mro_and_typevar_mappings(obj)
|
|
151
|
+
|
|
152
|
+
for cls in mro:
|
|
153
|
+
if cls in (typing.Generic, object):
|
|
154
|
+
continue
|
|
155
|
+
|
|
156
|
+
mapping = typevar_mappings.get(cls)
|
|
157
|
+
cls_locals = dict(vars(cls))
|
|
158
|
+
|
|
159
|
+
if PY_312PLUS:
|
|
160
|
+
# resolve type parameters (e.g. class Foo[T]: pass)
|
|
161
|
+
cls_locals.update({p.__name__: p for p in cls.__type_params__})
|
|
162
|
+
|
|
163
|
+
try:
|
|
164
|
+
cls_module = cls.__module__
|
|
165
|
+
except AttributeError:
|
|
166
|
+
cls_globals = {}
|
|
167
|
+
else:
|
|
168
|
+
cls_globals = getattr(sys.modules.get(cls_module, None), "__dict__", {})
|
|
169
|
+
|
|
170
|
+
ann = _get_class_annotations(cls)
|
|
171
|
+
for name, value in ann.items():
|
|
172
|
+
if name in hints:
|
|
173
|
+
continue
|
|
174
|
+
if isinstance(value, str):
|
|
175
|
+
value = _forward_ref(value)
|
|
176
|
+
value = _eval_type(value, cls_locals, cls_globals)
|
|
177
|
+
if mapping is not None:
|
|
178
|
+
value = _apply_params(value, mapping)
|
|
179
|
+
if value is None:
|
|
180
|
+
value = type(None)
|
|
181
|
+
hints[name] = value
|
|
182
|
+
return hints
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
# A mapping from a type annotation (or annotation __origin__) to the concrete
|
|
186
|
+
# python type that structtype will use when decoding. THIS IS PRIVATE FOR A
|
|
187
|
+
# REASON. DON'T MUCK WITH THIS.
|
|
188
|
+
_CONCRETE_TYPES = {
|
|
189
|
+
list: list,
|
|
190
|
+
tuple: tuple,
|
|
191
|
+
set: set,
|
|
192
|
+
frozenset: frozenset,
|
|
193
|
+
dict: dict,
|
|
194
|
+
typing.List: list,
|
|
195
|
+
typing.Tuple: tuple,
|
|
196
|
+
typing.Set: set,
|
|
197
|
+
typing.FrozenSet: frozenset,
|
|
198
|
+
typing.Dict: dict,
|
|
199
|
+
typing.Collection: list,
|
|
200
|
+
typing.MutableSequence: list,
|
|
201
|
+
typing.Sequence: list,
|
|
202
|
+
typing.MutableMapping: dict,
|
|
203
|
+
typing.Mapping: dict,
|
|
204
|
+
typing.MutableSet: set,
|
|
205
|
+
typing.AbstractSet: set,
|
|
206
|
+
collections.abc.Collection: list,
|
|
207
|
+
collections.abc.MutableSequence: list,
|
|
208
|
+
collections.abc.Sequence: list,
|
|
209
|
+
collections.abc.MutableSet: set,
|
|
210
|
+
collections.abc.Set: set,
|
|
211
|
+
collections.abc.MutableMapping: dict,
|
|
212
|
+
collections.abc.Mapping: dict,
|
|
213
|
+
}
|
|
214
|
+
if sys.version_info >= (3, 15):
|
|
215
|
+
_CONCRETE_TYPES.update({frozendict: frozendict}) # noqa: F821
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def get_typeddict_info(obj):
|
|
219
|
+
if isinstance(obj, type):
|
|
220
|
+
cls = obj
|
|
221
|
+
else:
|
|
222
|
+
cls = obj.__origin__
|
|
223
|
+
|
|
224
|
+
raw_hints = get_class_annotations(obj)
|
|
225
|
+
|
|
226
|
+
if hasattr(cls, "__required_keys__"):
|
|
227
|
+
required = set(cls.__required_keys__)
|
|
228
|
+
elif cls.__total__:
|
|
229
|
+
required = set(raw_hints)
|
|
230
|
+
else:
|
|
231
|
+
required = set()
|
|
232
|
+
|
|
233
|
+
# Both `typing.TypedDict` and `typing_extensions.TypedDict` have a bug
|
|
234
|
+
# where `Required`/`NotRequired` aren't properly detected at runtime when
|
|
235
|
+
# `__future__.annotations` is enabled, meaning the `__required_keys__`
|
|
236
|
+
# isn't correct. This code block works around this issue by amending the
|
|
237
|
+
# set of required keys as needed, while also stripping off any
|
|
238
|
+
# `Required`/`NotRequired` wrappers.
|
|
239
|
+
hints = {}
|
|
240
|
+
for k, v in raw_hints.items():
|
|
241
|
+
origin = getattr(v, "__origin__", False)
|
|
242
|
+
if origin is Required:
|
|
243
|
+
required.add(k)
|
|
244
|
+
hints[k] = v.__args__[0]
|
|
245
|
+
elif origin is NotRequired:
|
|
246
|
+
required.discard(k)
|
|
247
|
+
hints[k] = v.__args__[0]
|
|
248
|
+
else:
|
|
249
|
+
hints[k] = v
|
|
250
|
+
|
|
251
|
+
# This can happen if there is a bug in the TypedDict implementation;
|
|
252
|
+
# such a bug was present in Python 3.14.
|
|
253
|
+
if not all(k in hints for k in required):
|
|
254
|
+
raise RuntimeError(
|
|
255
|
+
f"Required set {required} contains keys that are no in hints: {hints.keys()}"
|
|
256
|
+
)
|
|
257
|
+
return hints, required
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def get_dataclass_info(obj):
|
|
261
|
+
if isinstance(obj, type):
|
|
262
|
+
cls = obj
|
|
263
|
+
else:
|
|
264
|
+
cls = obj.__origin__
|
|
265
|
+
hints = get_class_annotations(obj)
|
|
266
|
+
required = []
|
|
267
|
+
optional = []
|
|
268
|
+
defaults = []
|
|
269
|
+
|
|
270
|
+
if hasattr(cls, "__dataclass_fields__"):
|
|
271
|
+
from dataclasses import _FIELD, _FIELD_INITVAR, MISSING
|
|
272
|
+
|
|
273
|
+
for field in cls.__dataclass_fields__.values():
|
|
274
|
+
if field._field_type is not _FIELD:
|
|
275
|
+
if field._field_type is _FIELD_INITVAR:
|
|
276
|
+
raise TypeError(
|
|
277
|
+
"dataclasses with `InitVar` fields are not supported"
|
|
278
|
+
)
|
|
279
|
+
continue
|
|
280
|
+
name = field.name
|
|
281
|
+
typ = hints[name]
|
|
282
|
+
if field.default is not MISSING:
|
|
283
|
+
defaults.append(field.default)
|
|
284
|
+
optional.append((name, typ, False))
|
|
285
|
+
elif field.default_factory is not MISSING:
|
|
286
|
+
defaults.append(field.default_factory)
|
|
287
|
+
optional.append((name, typ, True))
|
|
288
|
+
else:
|
|
289
|
+
required.append((name, typ, False))
|
|
290
|
+
|
|
291
|
+
required.extend(optional)
|
|
292
|
+
|
|
293
|
+
pre_init = None
|
|
294
|
+
post_init = getattr(cls, "__post_init__", None)
|
|
295
|
+
else:
|
|
296
|
+
from attrs import NOTHING, Factory
|
|
297
|
+
|
|
298
|
+
fields_with_validators = []
|
|
299
|
+
|
|
300
|
+
for field in cls.__attrs_attrs__:
|
|
301
|
+
name = field.name
|
|
302
|
+
typ = hints[name]
|
|
303
|
+
default = field.default
|
|
304
|
+
if default is not NOTHING:
|
|
305
|
+
if isinstance(default, Factory):
|
|
306
|
+
if default.takes_self:
|
|
307
|
+
raise NotImplementedError(
|
|
308
|
+
"Support for default factories with `takes_self=True` "
|
|
309
|
+
"is not implemented. File a GitHub issue if you need "
|
|
310
|
+
"this feature!"
|
|
311
|
+
)
|
|
312
|
+
defaults.append(default.factory)
|
|
313
|
+
optional.append((name, typ, True))
|
|
314
|
+
else:
|
|
315
|
+
defaults.append(default)
|
|
316
|
+
optional.append((name, typ, False))
|
|
317
|
+
else:
|
|
318
|
+
required.append((name, typ, False))
|
|
319
|
+
|
|
320
|
+
if field.validator is not None:
|
|
321
|
+
fields_with_validators.append(field)
|
|
322
|
+
|
|
323
|
+
required.extend(optional)
|
|
324
|
+
|
|
325
|
+
pre_init = getattr(cls, "__attrs_pre_init__", None)
|
|
326
|
+
post_init = getattr(cls, "__attrs_post_init__", None)
|
|
327
|
+
|
|
328
|
+
if fields_with_validators:
|
|
329
|
+
post_init = _wrap_attrs_validators(fields_with_validators, post_init)
|
|
330
|
+
|
|
331
|
+
return cls, tuple(required), tuple(defaults), pre_init, post_init
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def get_pydantic_info(obj):
|
|
335
|
+
"""Extract field info from a Pydantic v2 BaseModel."""
|
|
336
|
+
if isinstance(obj, type):
|
|
337
|
+
cls = obj
|
|
338
|
+
else:
|
|
339
|
+
cls = obj.__origin__
|
|
340
|
+
hints = get_class_annotations(obj)
|
|
341
|
+
required = []
|
|
342
|
+
optional = []
|
|
343
|
+
defaults = []
|
|
344
|
+
|
|
345
|
+
for name, field_info in cls.model_fields.items():
|
|
346
|
+
typ = hints.get(name, Any)
|
|
347
|
+
if field_info.is_required():
|
|
348
|
+
required.append((name, typ, False))
|
|
349
|
+
else:
|
|
350
|
+
df = field_info.default_factory
|
|
351
|
+
if df is not None:
|
|
352
|
+
defaults.append(df)
|
|
353
|
+
optional.append((name, typ, True))
|
|
354
|
+
else:
|
|
355
|
+
defaults.append(field_info.default)
|
|
356
|
+
optional.append((name, typ, False))
|
|
357
|
+
|
|
358
|
+
required.extend(optional)
|
|
359
|
+
return cls, tuple(required), tuple(defaults)
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def _wrap_attrs_validators(fields, post_init):
|
|
363
|
+
def inner(obj):
|
|
364
|
+
for field in fields:
|
|
365
|
+
field.validator(obj, field, getattr(obj, field.name))
|
|
366
|
+
if post_init is not None:
|
|
367
|
+
post_init(obj)
|
|
368
|
+
|
|
369
|
+
return inner
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def rebuild(cls, kwargs):
|
|
373
|
+
"""Used to unpickle Structs with keyword-only fields"""
|
|
374
|
+
return cls(**kwargs)
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def convert_generic_alias(origin, args):
|
|
378
|
+
# subscribed typing._GenericAlias instances are cached within the typing module
|
|
379
|
+
# we make use of this fact, by storing a __structtype_cache__ attribute on the
|
|
380
|
+
# subscribed instance. only subscribed types are cached, so
|
|
381
|
+
# 'typing._GenericAlias(list, int) is typing._GenericAlias(list, int)' would be
|
|
382
|
+
# false.
|
|
383
|
+
# to achieve the same behaviour when re-creating a typing._GenericAlias from a
|
|
384
|
+
# types.GenericAlias, we first construct a temporary *unbound*
|
|
385
|
+
# typing._GenericAlias, on which we then call __getattr__. effectively doing
|
|
386
|
+
# typing._GenericAlias(list, T)[int], for which
|
|
387
|
+
# 'typing._GenericAlias(list, T)[int] is typing._GenericAlias(list, T)[int]'
|
|
388
|
+
# holds true
|
|
389
|
+
try:
|
|
390
|
+
params = origin.__parameters__
|
|
391
|
+
except AttributeError:
|
|
392
|
+
if not isinstance(origin, type):
|
|
393
|
+
# a special form such as 'typing.Literal', whose args are values rather than
|
|
394
|
+
# type parameters. Ordinary subscription yields the canonical, interned
|
|
395
|
+
# alias of the right subclass (e.g. 'typing.Literal[...]' -> '_LiteralGenericAlias').
|
|
396
|
+
return origin[args]
|
|
397
|
+
|
|
398
|
+
# a non-generic class with type arguments. only reachable for e.g.
|
|
399
|
+
# manually-built 'types.GenericAlias' instances and is probably nonsense or at
|
|
400
|
+
# least not somthing we can meaningfully represent, so complain about it here,
|
|
401
|
+
# rather than silently dropping the arguments.
|
|
402
|
+
raise TypeError(f"{origin.__name__!r} is not a generic type")
|
|
403
|
+
|
|
404
|
+
# a regularly-parametrised generic. Create a new typing._GenericAlias with the
|
|
405
|
+
# origin's unbound type params (e.g. for a 'Mapping[str, int]' this is a
|
|
406
|
+
# '_GenericAlias(Mapping, (~K, ~V))'), then bind it to the concrete args by
|
|
407
|
+
# subscripting with the args *tuple* (i.e. 'alias[(int, str)]', not
|
|
408
|
+
# 'alias[int, str]'), so generics with more than one type var work
|
|
409
|
+
alias = _GenericAlias(origin, params)
|
|
410
|
+
return alias[args]
|
structtype/_version.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# file generated by vcs-versioning
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"__version__",
|
|
7
|
+
"__version_tuple__",
|
|
8
|
+
"version",
|
|
9
|
+
"version_tuple",
|
|
10
|
+
"__commit_id__",
|
|
11
|
+
"commit_id",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
version: str
|
|
15
|
+
__version__: str
|
|
16
|
+
__version_tuple__: tuple[int | str, ...]
|
|
17
|
+
version_tuple: tuple[int | str, ...]
|
|
18
|
+
commit_id: str | None
|
|
19
|
+
__commit_id__: str | None
|
|
20
|
+
|
|
21
|
+
__version__ = version = '0.3.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 3, 0)
|
|
23
|
+
|
|
24
|
+
__commit_id__ = commit_id = None
|
structtype/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: structtype
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Fast Struct type with validation and JSON serialization for Python.
|
|
5
|
+
Maintainer-email: Wolfgang Langner <tds333@mailbox.org>
|
|
6
|
+
License-Expression: BSD-3-Clause
|
|
7
|
+
Project-URL: Homepage, https://github.com/tds333/structtype
|
|
8
|
+
Project-URL: Issue Tracker, https://github.com/tds333/structtype/issues
|
|
9
|
+
Project-URL: Source, https://github.com/tds333/structtype
|
|
10
|
+
Keywords: JSON,schema,serialization,struct,validation
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Environment :: WebAssembly :: Emscripten
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Dynamic: license-file
|
|
23
|
+
|
|
24
|
+
# structtype
|
|
25
|
+
|
|
26
|
+
Fast Struct type with validation + JSON serialization for Python.
|
|
27
|
+
|
|
28
|
+
- **High performance** — 5-60x faster than dataclasses, attrs, or pydantic for common operations
|
|
29
|
+
- **Schema validation** — familiar Python type annotations, enforced at decode time
|
|
30
|
+
- **No runtime dependencies** — built on a monolithic C extension based on msgspec
|
|
31
|
+
- **Rich type support** — nested Structs, dataclasses, TypedDicts, enums, UUID, Decimal, datetime, and more
|
|
32
|
+
- **JSON Schema generation** — auto-generate JSON Schema 2020-12 / OpenAPI 3.1 specs
|
|
33
|
+
|
|
34
|
+
## Install
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install structtype
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
uv add structtype
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Requires Python ≥ 3.10.
|
|
45
|
+
|
|
46
|
+
## Quick Example
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
from structtype import Struct
|
|
50
|
+
|
|
51
|
+
class User(Struct):
|
|
52
|
+
name: str
|
|
53
|
+
groups: set[str] = set()
|
|
54
|
+
email: str | None = None
|
|
55
|
+
|
|
56
|
+
alice = User("alice", groups={"admin", "engineering"})
|
|
57
|
+
|
|
58
|
+
# Serialize to JSON
|
|
59
|
+
alice.struct_dump_json()
|
|
60
|
+
# b'{"name":"alice","groups":["admin","engineering"],"email":null}'
|
|
61
|
+
|
|
62
|
+
# Deserialize and validate
|
|
63
|
+
User.struct_validate_json(
|
|
64
|
+
b'{"name":"alice","groups":["admin","engineering"],"email":null}'
|
|
65
|
+
)
|
|
66
|
+
# User(name='alice', groups={"admin", "engineering"}, email=None)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Documentation
|
|
70
|
+
|
|
71
|
+
Full documentation is available at **https://tds333.github.io/structtype/**.
|
|
72
|
+
|
|
73
|
+
## Benchmarks
|
|
74
|
+
|
|
75
|
+
structtype is as fast as msgspec and about 3-5x faster than pydantic.
|
|
76
|
+
|
|
77
|
+
## License
|
|
78
|
+
|
|
79
|
+
New BSD. See the [License File](LICENSE).
|
|
80
|
+
The core is based on the work of Jim Crist-Harif from msgspec.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
structtype/__init__.py,sha256=QcV_qQ6ybqunYrm9DTCpJjUAN3LYXD2v0RAjLd19Ops,387
|
|
2
|
+
structtype/__init__.pyi,sha256=P52MMOcSyIcqq0vpRUwJ-A7Xs3_l3qNbkTsALkFzJYI,11101
|
|
3
|
+
structtype/_adapter.py,sha256=ybqo8Bnk5qZaLeT4AjIyvGCm8gwIEaSUskaPY4c8JYY,4106
|
|
4
|
+
structtype/_core.cp313-win_amd64.pyd,sha256=bp_YTrcoZ1GT22znbxp7iGY8EBWexQKj_nU-PNwKrPY,283648
|
|
5
|
+
structtype/_inspect.py,sha256=ItEA54f13J3wOB4hdMVqFzyqLf4YL5Ozv7MyS6hWJFU,34523
|
|
6
|
+
structtype/_json_schema.py,sha256=-KuNxyyc8YVI2o-ZH75VRbjAyc3EuZoBd_Q_TWP7ol0,19315
|
|
7
|
+
structtype/_utils.py,sha256=fdXcdqu257GEHuhoB3WIIzt3yv4T7xr0lwDShMwLtaQ,14335
|
|
8
|
+
structtype/_version.py,sha256=mOtRuCjwCaETSVM2o_uwyhF-FRPR1OeF-XL-EF6phZM,544
|
|
9
|
+
structtype/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
|
+
structtype-0.3.0.dist-info/licenses/LICENSE,sha256=leWp9SAS7Rk9nG38qtpl86hT6BZYsog7p3xfMkTWSBg,1499
|
|
11
|
+
structtype-0.3.0.dist-info/METADATA,sha256=HAzB5GRmPdZx0F2M5X7vA5oEbBLpA0AyX5p9NOOfi5Q,2528
|
|
12
|
+
structtype-0.3.0.dist-info/WHEEL,sha256=AP_wkikJcUkDPUGvTNydv7g0R7BhW1u_fVHNKW7TJyI,101
|
|
13
|
+
structtype-0.3.0.dist-info/top_level.txt,sha256=hQPg6sdg7t2YGfKJRsRLTGlfSNSvjW9FwXH-yQxfnyQ,11
|
|
14
|
+
structtype-0.3.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
Copyright (c) 2026, Wolfgang Langner
|
|
2
|
+
All rights reserved.
|
|
3
|
+
|
|
4
|
+
Redistribution and use in source and binary forms, with or without
|
|
5
|
+
modification, are permitted provided that the following conditions are met:
|
|
6
|
+
|
|
7
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
8
|
+
list of conditions and the following disclaimer.
|
|
9
|
+
|
|
10
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
11
|
+
this list of conditions and the following disclaimer in the documentation
|
|
12
|
+
and/or other materials provided with the distribution.
|
|
13
|
+
|
|
14
|
+
3. Neither the name of the copyright holder nor the names of its contributors
|
|
15
|
+
may be used to endorse or promote products derived from this software
|
|
16
|
+
without specific prior written permission.
|
|
17
|
+
|
|
18
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
19
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
20
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
21
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
|
22
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
23
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
24
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
25
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
26
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
27
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
structtype
|