pydantic-marshmallow 1.0.0__py3-none-any.whl → 1.0.1__py3-none-any.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.
- pydantic_marshmallow/__init__.py +11 -5
- pydantic_marshmallow/bridge.py +131 -31
- pydantic_marshmallow/type_mapping.py +31 -3
- {pydantic_marshmallow-1.0.0.dist-info → pydantic_marshmallow-1.0.1.dist-info}/METADATA +135 -4
- pydantic_marshmallow-1.0.1.dist-info/RECORD +12 -0
- pydantic_marshmallow-1.0.0.dist-info/RECORD +0 -12
- {pydantic_marshmallow-1.0.0.dist-info → pydantic_marshmallow-1.0.1.dist-info}/WHEEL +0 -0
- {pydantic_marshmallow-1.0.0.dist-info → pydantic_marshmallow-1.0.1.dist-info}/licenses/LICENSE +0 -0
- {pydantic_marshmallow-1.0.0.dist-info → pydantic_marshmallow-1.0.1.dist-info}/top_level.txt +0 -0
pydantic_marshmallow/__init__.py
CHANGED
|
@@ -51,16 +51,22 @@ Features:
|
|
|
51
51
|
|
|
52
52
|
# Re-export Marshmallow's validators - these work with PydanticSchema
|
|
53
53
|
# Also export hooks for convenience
|
|
54
|
+
# Note: We re-export Marshmallow's @validates and @validates_schema above.
|
|
55
|
+
# Our bridge's _do_load calls Marshmallow's native validator system,
|
|
56
|
+
# so `from marshmallow import validates` works correctly with PydanticSchema.
|
|
57
|
+
# Version is managed by setuptools-scm from git tags
|
|
58
|
+
from importlib.metadata import version as _version
|
|
59
|
+
|
|
54
60
|
from marshmallow import EXCLUDE, INCLUDE, RAISE, post_dump, post_load, pre_dump, pre_load, validates, validates_schema
|
|
55
61
|
|
|
56
62
|
from .bridge import HybridModel, PydanticSchema, pydantic_schema, schema_for
|
|
57
63
|
from .errors import BridgeValidationError
|
|
58
64
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
__version__ = "0.
|
|
65
|
+
try:
|
|
66
|
+
__version__ = _version("pydantic-marshmallow")
|
|
67
|
+
except Exception:
|
|
68
|
+
# Fallback for development or environments without installed package metadata
|
|
69
|
+
__version__ = "0.0.0.dev0"
|
|
64
70
|
__all__ = [
|
|
65
71
|
"EXCLUDE",
|
|
66
72
|
"INCLUDE",
|
pydantic_marshmallow/bridge.py
CHANGED
|
@@ -7,7 +7,9 @@ Flow: Input → Marshmallow pre_load → PYDANTIC VALIDATES → Marshmallow post
|
|
|
7
7
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
|
+
import threading
|
|
10
11
|
from collections.abc import Callable, Sequence, Set as AbstractSet
|
|
12
|
+
from functools import lru_cache
|
|
11
13
|
from typing import Any, ClassVar, Generic, TypeVar, cast, get_args, get_origin
|
|
12
14
|
|
|
13
15
|
from marshmallow import EXCLUDE, INCLUDE, RAISE, Schema, fields as ma_fields
|
|
@@ -15,6 +17,7 @@ from marshmallow.decorators import VALIDATES, VALIDATES_SCHEMA
|
|
|
15
17
|
from marshmallow.error_store import ErrorStore
|
|
16
18
|
from marshmallow.exceptions import ValidationError as MarshmallowValidationError
|
|
17
19
|
from marshmallow.schema import SchemaMeta
|
|
20
|
+
from marshmallow.utils import missing as ma_missing
|
|
18
21
|
from pydantic import BaseModel, ConfigDict, ValidationError as PydanticValidationError
|
|
19
22
|
|
|
20
23
|
from .errors import BridgeValidationError, convert_pydantic_errors, format_pydantic_error
|
|
@@ -23,14 +26,51 @@ from .validators import cache_validators
|
|
|
23
26
|
|
|
24
27
|
M = TypeVar("M", bound=BaseModel)
|
|
25
28
|
|
|
29
|
+
# =============================================================================
|
|
30
|
+
# Module-level Caches
|
|
31
|
+
# =============================================================================
|
|
32
|
+
# Thread Safety: All caches use thread-safe implementations to support both
|
|
33
|
+
# GIL-enabled Python and free-threaded Python 3.14+ (PEP 703).
|
|
34
|
+
# - @lru_cache: Inherently thread-safe (uses internal lock)
|
|
35
|
+
# - Dict caches: Protected by threading.Lock for write operations
|
|
36
|
+
#
|
|
37
|
+
# Cache Invalidation: These caches assume Pydantic models are immutable after
|
|
38
|
+
# definition. Dynamic model modification at runtime (monkey-patching, adding
|
|
39
|
+
# fields) will result in stale cached data. This is an accepted trade-off for
|
|
40
|
+
# performance gains.
|
|
41
|
+
# =============================================================================
|
|
42
|
+
|
|
43
|
+
# Thread lock for cache writes (supports free-threaded Python 3.14+)
|
|
44
|
+
# Use RLock (reentrant) since from_model() may be called recursively
|
|
45
|
+
_cache_lock = threading.RLock()
|
|
46
|
+
|
|
26
47
|
# Module-level cache for HybridModel schemas
|
|
27
48
|
_hybrid_schema_cache: dict[type[Any], type[PydanticSchema[Any]]] = {}
|
|
28
49
|
|
|
50
|
+
# Cache for schema_for/from_model - key is (model, schema_name, frozen options)
|
|
51
|
+
# This avoids recreating schema classes for the same model+options
|
|
52
|
+
_schema_class_cache: dict[tuple[type[Any], str | None, tuple[tuple[str, Any], ...]], type[Any]] = {}
|
|
53
|
+
|
|
29
54
|
# Field validator registry: maps (schema_class, field_name) -> list of validator functions
|
|
30
55
|
_field_validators: dict[tuple[type[Any], str], list[Callable[..., Any]]] = {}
|
|
31
56
|
_schema_validators: dict[type[Any], list[Callable[..., Any]]] = {}
|
|
32
57
|
|
|
33
58
|
|
|
59
|
+
@lru_cache(maxsize=1024)
|
|
60
|
+
def _get_model_field_names_with_aliases(model_class: type[BaseModel]) -> frozenset[str]:
|
|
61
|
+
"""
|
|
62
|
+
Get cached frozenset of model field names including aliases.
|
|
63
|
+
|
|
64
|
+
Thread-safe via @lru_cache. Caches the result per model class to avoid
|
|
65
|
+
repeated computation in the load() hot path.
|
|
66
|
+
"""
|
|
67
|
+
field_names = set(model_class.model_fields.keys())
|
|
68
|
+
for field_info in model_class.model_fields.values():
|
|
69
|
+
if field_info.alias:
|
|
70
|
+
field_names.add(field_info.alias)
|
|
71
|
+
return frozenset(field_names)
|
|
72
|
+
|
|
73
|
+
|
|
34
74
|
class PydanticSchemaMeta(SchemaMeta):
|
|
35
75
|
"""
|
|
36
76
|
Custom metaclass that adds Pydantic model fields BEFORE Marshmallow processes them.
|
|
@@ -389,6 +429,7 @@ class PydanticSchema(Schema, Generic[M], metaclass=PydanticSchemaMeta):
|
|
|
389
429
|
data: dict[str, Any],
|
|
390
430
|
partial: bool | Sequence[str] | AbstractSet[str] | None = None,
|
|
391
431
|
original_data: Any | None = None,
|
|
432
|
+
skip_model_dump: bool = False,
|
|
392
433
|
) -> tuple[dict[str, Any], M | None]:
|
|
393
434
|
"""
|
|
394
435
|
Use Pydantic to validate and coerce the input data.
|
|
@@ -396,6 +437,13 @@ class PydanticSchema(Schema, Generic[M], metaclass=PydanticSchemaMeta):
|
|
|
396
437
|
Filters out marshmallow.missing sentinel values before Pydantic validation,
|
|
397
438
|
allowing Pydantic to use its own defaults for missing fields.
|
|
398
439
|
|
|
440
|
+
Args:
|
|
441
|
+
data: Input data to validate
|
|
442
|
+
partial: Partial validation mode
|
|
443
|
+
original_data: Original input data for error reporting
|
|
444
|
+
skip_model_dump: If True and not partial, skip model_dump() and return
|
|
445
|
+
empty dict. Use when validators don't need the dict.
|
|
446
|
+
|
|
399
447
|
Returns:
|
|
400
448
|
Tuple of (validated_data_dict, model_instance)
|
|
401
449
|
The instance is returned to avoid redundant validation later.
|
|
@@ -404,8 +452,6 @@ class PydanticSchema(Schema, Generic[M], metaclass=PydanticSchemaMeta):
|
|
|
404
452
|
return data, None
|
|
405
453
|
|
|
406
454
|
# Filter out marshmallow.missing values - Pydantic should use its defaults
|
|
407
|
-
from marshmallow.utils import missing as ma_missing
|
|
408
|
-
|
|
409
455
|
clean_data = {
|
|
410
456
|
k: v for k, v in data.items()
|
|
411
457
|
if v is not ma_missing
|
|
@@ -420,6 +466,9 @@ class PydanticSchema(Schema, Generic[M], metaclass=PydanticSchemaMeta):
|
|
|
420
466
|
else:
|
|
421
467
|
# Let Pydantic do all the validation - KEEP THE INSTANCE
|
|
422
468
|
instance = self._model_class.model_validate(clean_data)
|
|
469
|
+
# OPTIMIZATION: Skip model_dump if not needed for validators
|
|
470
|
+
if skip_model_dump:
|
|
471
|
+
return {}, cast(M, instance)
|
|
423
472
|
# Return both the dict (for validators) and instance (for result)
|
|
424
473
|
validated_data = instance.model_dump(by_alias=False)
|
|
425
474
|
# Cast to M since model_validate returns the correct model type
|
|
@@ -545,6 +594,11 @@ class PydanticSchema(Schema, Generic[M], metaclass=PydanticSchemaMeta):
|
|
|
545
594
|
|
|
546
595
|
This ensures 100% Marshmallow hook compatibility.
|
|
547
596
|
"""
|
|
597
|
+
# PERFORMANCE: Hoist frequently accessed attributes to local variables
|
|
598
|
+
# This avoids repeated self.__dict__ lookups in the hot path
|
|
599
|
+
model_class = self._model_class
|
|
600
|
+
hooks = self._hooks
|
|
601
|
+
|
|
548
602
|
# Resolve settings
|
|
549
603
|
if many is None:
|
|
550
604
|
many = self.many
|
|
@@ -576,8 +630,8 @@ class PydanticSchema(Schema, Generic[M], metaclass=PydanticSchemaMeta):
|
|
|
576
630
|
|
|
577
631
|
# Step 1: Run pre_load hooks ONLY if they exist (PERFORMANCE OPTIMIZATION)
|
|
578
632
|
# Skipping _invoke_load_processors when empty saves ~5ms per 10k loads
|
|
579
|
-
if
|
|
580
|
-
|
|
633
|
+
if hooks.get("pre_load"):
|
|
634
|
+
processed_data_raw = self._invoke_load_processors(
|
|
581
635
|
"pre_load",
|
|
582
636
|
data,
|
|
583
637
|
many=False,
|
|
@@ -585,17 +639,17 @@ class PydanticSchema(Schema, Generic[M], metaclass=PydanticSchemaMeta):
|
|
|
585
639
|
partial=partial,
|
|
586
640
|
)
|
|
587
641
|
else:
|
|
588
|
-
|
|
642
|
+
processed_data_raw = data
|
|
589
643
|
|
|
590
|
-
#
|
|
591
|
-
|
|
592
|
-
model_fields = set(self._model_class.model_fields.keys())
|
|
593
|
-
# Also include aliases in known fields
|
|
594
|
-
for _field_name, field_info in self._model_class.model_fields.items():
|
|
595
|
-
if field_info.alias:
|
|
596
|
-
model_fields.add(field_info.alias)
|
|
644
|
+
# Type narrowing: at this point (many=False path), data is always a dict
|
|
645
|
+
processed_data: dict[str, Any] = cast(dict[str, Any], processed_data_raw)
|
|
597
646
|
|
|
598
|
-
|
|
647
|
+
# Step 2: Handle unknown fields based on setting
|
|
648
|
+
# PERFORMANCE: Use cached field names instead of computing every time
|
|
649
|
+
model_field_names: frozenset[str] | None = None
|
|
650
|
+
if model_class:
|
|
651
|
+
model_field_names = _get_model_field_names_with_aliases(model_class)
|
|
652
|
+
unkn_fields = set(processed_data.keys()) - model_field_names
|
|
599
653
|
|
|
600
654
|
if unkn_fields:
|
|
601
655
|
if unknown_setting == RAISE:
|
|
@@ -604,17 +658,36 @@ class PydanticSchema(Schema, Generic[M], metaclass=PydanticSchemaMeta):
|
|
|
604
658
|
if unknown_setting == EXCLUDE:
|
|
605
659
|
# Remove unknown fields
|
|
606
660
|
processed_data = {
|
|
607
|
-
k: v for k, v in processed_data.items() if k in
|
|
661
|
+
k: v for k, v in processed_data.items() if k in model_field_names
|
|
608
662
|
}
|
|
609
663
|
# INCLUDE: keep unknown fields in the result (handled below)
|
|
610
664
|
|
|
611
665
|
# Step 3: Pydantic validates the transformed data
|
|
612
666
|
# Returns (validated_dict, instance) - instance reused to avoid double validation
|
|
613
|
-
pydantic_instance = None
|
|
614
|
-
|
|
667
|
+
pydantic_instance: M | None = None
|
|
668
|
+
validated_data: dict[str, Any]
|
|
669
|
+
|
|
670
|
+
# OPTIMIZATION: Determine if we need model_dump() for validators/result
|
|
671
|
+
# Skip expensive model_dump() when not needed
|
|
672
|
+
has_validators = bool(
|
|
673
|
+
hooks[VALIDATES]
|
|
674
|
+
or hooks[VALIDATES_SCHEMA]
|
|
675
|
+
or self._field_validators_cache
|
|
676
|
+
or self._schema_validators_cache
|
|
677
|
+
)
|
|
678
|
+
needs_dict = (
|
|
679
|
+
not return_instance # Need dict for result
|
|
680
|
+
or unknown_setting == INCLUDE # Need dict to merge unknown fields
|
|
681
|
+
or has_validators # Need dict for validators
|
|
682
|
+
)
|
|
683
|
+
|
|
684
|
+
if model_class:
|
|
615
685
|
try:
|
|
616
686
|
validated_data, pydantic_instance = self._validate_with_pydantic(
|
|
617
|
-
processed_data,
|
|
687
|
+
processed_data,
|
|
688
|
+
partial=partial,
|
|
689
|
+
original_data=data,
|
|
690
|
+
skip_model_dump=not needs_dict,
|
|
618
691
|
)
|
|
619
692
|
except MarshmallowValidationError as pydantic_error:
|
|
620
693
|
# Call handle_error for Pydantic validation errors
|
|
@@ -623,8 +696,8 @@ class PydanticSchema(Schema, Generic[M], metaclass=PydanticSchemaMeta):
|
|
|
623
696
|
raise
|
|
624
697
|
|
|
625
698
|
# If INCLUDE, add unknown fields back to validated data
|
|
626
|
-
if unknown_setting == INCLUDE and
|
|
627
|
-
for field in (set(processed_data.keys()) -
|
|
699
|
+
if unknown_setting == INCLUDE and model_field_names is not None:
|
|
700
|
+
for field in (set(processed_data.keys()) - model_field_names):
|
|
628
701
|
validated_data[field] = processed_data[field]
|
|
629
702
|
else:
|
|
630
703
|
validated_data = processed_data
|
|
@@ -635,8 +708,8 @@ class PydanticSchema(Schema, Generic[M], metaclass=PydanticSchemaMeta):
|
|
|
635
708
|
error_store_cls: Callable[[], Any] = cast(Callable[[], Any], ErrorStore)
|
|
636
709
|
error_store: Any = error_store_cls()
|
|
637
710
|
|
|
638
|
-
# 4a: Run Marshmallow's native @validates decorators (from
|
|
639
|
-
if
|
|
711
|
+
# 4a: Run Marshmallow's native @validates decorators (from hooks)
|
|
712
|
+
if hooks[VALIDATES]:
|
|
640
713
|
self._invoke_field_validators(
|
|
641
714
|
error_store=error_store,
|
|
642
715
|
data=validated_data,
|
|
@@ -656,7 +729,7 @@ class PydanticSchema(Schema, Generic[M], metaclass=PydanticSchemaMeta):
|
|
|
656
729
|
|
|
657
730
|
# Step 5: Run schema validators (BOTH Marshmallow native AND our custom)
|
|
658
731
|
# 5a: Run Marshmallow's native @validates_schema decorators
|
|
659
|
-
if
|
|
732
|
+
if hooks[VALIDATES_SCHEMA]:
|
|
660
733
|
self._invoke_schema_validators(
|
|
661
734
|
error_store=error_store,
|
|
662
735
|
pass_many=True,
|
|
@@ -690,7 +763,7 @@ class PydanticSchema(Schema, Generic[M], metaclass=PydanticSchemaMeta):
|
|
|
690
763
|
self.handle_error(error, data, many=False)
|
|
691
764
|
|
|
692
765
|
# Step 6: Prepare result based on return_instance flag
|
|
693
|
-
if
|
|
766
|
+
if model_class and return_instance:
|
|
694
767
|
if not partial:
|
|
695
768
|
# OPTIMIZATION: Reuse the instance from _validate_with_pydantic
|
|
696
769
|
result = pydantic_instance if pydantic_instance is not None else validated_data
|
|
@@ -701,7 +774,7 @@ class PydanticSchema(Schema, Generic[M], metaclass=PydanticSchemaMeta):
|
|
|
701
774
|
construct_data = {}
|
|
702
775
|
fields_set = set()
|
|
703
776
|
|
|
704
|
-
for field_name, field_info in
|
|
777
|
+
for field_name, field_info in model_class.model_fields.items():
|
|
705
778
|
if field_name in validated_data:
|
|
706
779
|
construct_data[field_name] = validated_data[field_name]
|
|
707
780
|
fields_set.add(field_name)
|
|
@@ -718,14 +791,14 @@ class PydanticSchema(Schema, Generic[M], metaclass=PydanticSchemaMeta):
|
|
|
718
791
|
construct_data[field_name] = None
|
|
719
792
|
|
|
720
793
|
result = cast(
|
|
721
|
-
M,
|
|
794
|
+
M, model_class.model_construct(_fields_set=fields_set, **construct_data)
|
|
722
795
|
)
|
|
723
796
|
else:
|
|
724
797
|
# Return dict instead of instance
|
|
725
798
|
result = validated_data
|
|
726
799
|
|
|
727
800
|
# Step 7: Run post_load hooks ONLY if they exist (PERFORMANCE OPTIMIZATION)
|
|
728
|
-
if postprocess and
|
|
801
|
+
if postprocess and hooks.get("post_load"):
|
|
729
802
|
result = self._invoke_load_processors(
|
|
730
803
|
"post_load",
|
|
731
804
|
result,
|
|
@@ -989,7 +1062,7 @@ class PydanticSchema(Schema, Generic[M], metaclass=PydanticSchemaMeta):
|
|
|
989
1062
|
obj = obj.model_dump(by_alias=False)
|
|
990
1063
|
|
|
991
1064
|
# Let Marshmallow handle the standard dump
|
|
992
|
-
result: dict[str, Any] = super().dump(obj, many=False)
|
|
1065
|
+
result: dict[str, Any] = cast(dict[str, Any], super().dump(obj, many=False))
|
|
993
1066
|
|
|
994
1067
|
# Apply field exclusions
|
|
995
1068
|
if fields_to_exclude:
|
|
@@ -1032,6 +1105,20 @@ class PydanticSchema(Schema, Generic[M], metaclass=PydanticSchemaMeta):
|
|
|
1032
1105
|
Returns:
|
|
1033
1106
|
A PydanticSchema subclass
|
|
1034
1107
|
"""
|
|
1108
|
+
# Build cache key from model, schema_name, and options
|
|
1109
|
+
# Sort options to ensure consistent keys
|
|
1110
|
+
# Note: schema_name must be included since it affects the generated class name
|
|
1111
|
+
cache_key: tuple[type[Any], str | None, tuple[tuple[str, Any], ...]] | None = None
|
|
1112
|
+
try:
|
|
1113
|
+
cache_key = (model, schema_name, tuple(sorted(meta_options.items())))
|
|
1114
|
+
# Thread-safe read (atomic dict lookup)
|
|
1115
|
+
cached = _schema_class_cache.get(cache_key)
|
|
1116
|
+
if cached is not None:
|
|
1117
|
+
return cached
|
|
1118
|
+
except TypeError:
|
|
1119
|
+
# Unhashable options (e.g., list values) - skip cache
|
|
1120
|
+
cache_key = None
|
|
1121
|
+
|
|
1035
1122
|
name = schema_name or f"{model.__name__}Schema"
|
|
1036
1123
|
|
|
1037
1124
|
# Extract field filtering options
|
|
@@ -1063,6 +1150,12 @@ class PydanticSchema(Schema, Generic[M], metaclass=PydanticSchemaMeta):
|
|
|
1063
1150
|
class_dict: dict[str, Any] = {"Meta": Meta, **fields}
|
|
1064
1151
|
|
|
1065
1152
|
schema_cls = type(name, (cls,), class_dict)
|
|
1153
|
+
|
|
1154
|
+
# Thread-safe cache write (supports free-threaded Python 3.14+)
|
|
1155
|
+
if cache_key is not None:
|
|
1156
|
+
with _cache_lock:
|
|
1157
|
+
_schema_class_cache[cache_key] = schema_cls
|
|
1158
|
+
|
|
1066
1159
|
return schema_cls
|
|
1067
1160
|
|
|
1068
1161
|
|
|
@@ -1155,10 +1248,17 @@ class HybridModel(BaseModel):
|
|
|
1155
1248
|
|
|
1156
1249
|
@classmethod
|
|
1157
1250
|
def marshmallow_schema(cls) -> type[PydanticSchema[Any]]:
|
|
1158
|
-
"""Get or create the Marshmallow schema for this model."""
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1251
|
+
"""Get or create the Marshmallow schema for this model (thread-safe)."""
|
|
1252
|
+
# Thread-safe read
|
|
1253
|
+
cached = _hybrid_schema_cache.get(cls)
|
|
1254
|
+
if cached is not None:
|
|
1255
|
+
return cached
|
|
1256
|
+
# Thread-safe write
|
|
1257
|
+
with _cache_lock:
|
|
1258
|
+
# Double-check after acquiring lock
|
|
1259
|
+
if cls not in _hybrid_schema_cache:
|
|
1260
|
+
_hybrid_schema_cache[cls] = PydanticSchema.from_model(cls)
|
|
1261
|
+
return _hybrid_schema_cache[cls]
|
|
1162
1262
|
|
|
1163
1263
|
@classmethod
|
|
1164
1264
|
def ma_load(cls, data: dict[str, Any], **kwargs: Any) -> HybridModel:
|
|
@@ -5,8 +5,9 @@ This module provides type-to-field conversions, leveraging Marshmallow's native
|
|
|
5
5
|
TYPE_MAPPING for basic types and adding support for generic collections.
|
|
6
6
|
"""
|
|
7
7
|
from enum import Enum as PyEnum
|
|
8
|
+
from functools import lru_cache
|
|
8
9
|
from types import UnionType
|
|
9
|
-
from typing import Any, Literal, Union, get_args, get_origin
|
|
10
|
+
from typing import Any, Literal, Union, cast, get_args, get_origin
|
|
10
11
|
|
|
11
12
|
from marshmallow import Schema, fields as ma_fields
|
|
12
13
|
from pydantic import BaseModel
|
|
@@ -15,6 +16,20 @@ from pydantic import BaseModel
|
|
|
15
16
|
_processing_models: set[type[Any]] = set()
|
|
16
17
|
|
|
17
18
|
|
|
19
|
+
# Cache for simple type -> field class lookups (str, int, datetime, etc.)
|
|
20
|
+
# These are the most common types and benefit most from caching.
|
|
21
|
+
# maxsize=512 handles large codebases with ~80KB memory overhead.
|
|
22
|
+
@lru_cache(maxsize=512)
|
|
23
|
+
def _get_simple_field_class(type_hint: type) -> type[ma_fields.Field]:
|
|
24
|
+
"""
|
|
25
|
+
Cached lookup for simple, hashable types in Marshmallow's TYPE_MAPPING.
|
|
26
|
+
|
|
27
|
+
This avoids repeated dict lookups and isinstance checks for common types
|
|
28
|
+
like str, int, float, bool, datetime, etc.
|
|
29
|
+
"""
|
|
30
|
+
return Schema.TYPE_MAPPING.get(type_hint, ma_fields.Raw)
|
|
31
|
+
|
|
32
|
+
|
|
18
33
|
def type_to_marshmallow_field(type_hint: Any) -> ma_fields.Field:
|
|
19
34
|
"""
|
|
20
35
|
Map a Python type to a Marshmallow field instance.
|
|
@@ -36,6 +51,18 @@ def type_to_marshmallow_field(type_hint: Any) -> ma_fields.Field:
|
|
|
36
51
|
Returns:
|
|
37
52
|
An appropriate Marshmallow field instance
|
|
38
53
|
"""
|
|
54
|
+
# FAST PATH: Simple types (str, int, float, bool, datetime, etc.)
|
|
55
|
+
# Check TYPE_MAPPING first to skip all the isinstance/origin checks.
|
|
56
|
+
# This handles ~60-80% of fields in typical models, but we must not
|
|
57
|
+
# short-circuit for Enums or Pydantic models, which have specialized handling.
|
|
58
|
+
if (
|
|
59
|
+
isinstance(type_hint, type)
|
|
60
|
+
and type_hint in Schema.TYPE_MAPPING
|
|
61
|
+
and not issubclass(type_hint, (PyEnum, BaseModel))
|
|
62
|
+
):
|
|
63
|
+
# Cast needed for mypy: type_hint is confirmed to be a type at this point
|
|
64
|
+
return _get_simple_field_class(cast(type, type_hint))()
|
|
65
|
+
|
|
39
66
|
origin = get_origin(type_hint)
|
|
40
67
|
args = get_args(type_hint)
|
|
41
68
|
|
|
@@ -134,5 +161,6 @@ def type_to_marshmallow_field(type_hint: Any) -> ma_fields.Field:
|
|
|
134
161
|
# Use Marshmallow's native TYPE_MAPPING for basic types
|
|
135
162
|
# This ensures we stay in sync with Marshmallow's type handling
|
|
136
163
|
resolved = origin if origin else type_hint
|
|
137
|
-
|
|
138
|
-
|
|
164
|
+
if isinstance(resolved, type):
|
|
165
|
+
return _get_simple_field_class(resolved)()
|
|
166
|
+
return ma_fields.Raw()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pydantic-marshmallow
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.1
|
|
4
4
|
Summary: Bring Pydantic's power to Marshmallow: type annotations, validators, and automatic schema generation
|
|
5
5
|
Author-email: Your Name <your.email@example.com>
|
|
6
6
|
License: MIT
|
|
@@ -24,8 +24,8 @@ Classifier: Typing :: Typed
|
|
|
24
24
|
Requires-Python: >=3.10
|
|
25
25
|
Description-Content-Type: text/markdown
|
|
26
26
|
License-File: LICENSE
|
|
27
|
-
Requires-Dist: marshmallow
|
|
28
|
-
Requires-Dist: pydantic
|
|
27
|
+
Requires-Dist: marshmallow<5.0.0,>=3.18.0
|
|
28
|
+
Requires-Dist: pydantic<3.0.0,>=2.0.0
|
|
29
29
|
Requires-Dist: typing-extensions>=4.0.0
|
|
30
30
|
Requires-Dist: orjson>=3.9.0
|
|
31
31
|
Provides-Extra: dev
|
|
@@ -67,6 +67,29 @@ Bridge Pydantic's power with Marshmallow's ecosystem. Use Pydantic models for va
|
|
|
67
67
|
|
|
68
68
|
📖 **[Documentation](https://mockodin.github.io/pydantic-marshmallow)** | 🐙 **[GitHub](https://github.com/mockodin/pydantic-marshmallow)**
|
|
69
69
|
|
|
70
|
+
## Why pydantic-marshmallow?
|
|
71
|
+
|
|
72
|
+
Get the best of both worlds: **Pydantic's speed** with **Marshmallow's ecosystem**.
|
|
73
|
+
|
|
74
|
+
### Performance
|
|
75
|
+
|
|
76
|
+
pydantic-marshmallow uses Pydantic's Rust-powered validation engine under the hood, delivering significant performance improvements over native Marshmallow—especially for nested data structures:
|
|
77
|
+
|
|
78
|
+
| Operation | pydantic-marshmallow | Marshmallow | Speedup |
|
|
79
|
+
|-----------|---------------------|-------------|---------|
|
|
80
|
+
| Simple load | 3.0 µs | 5.3 µs | **1.8x faster** |
|
|
81
|
+
| Nested model | 3.5 µs | 11.6 µs | **3.3x faster** |
|
|
82
|
+
| Deep nested (4 levels) | 5.6 µs | 32.3 µs | **5.8x faster** |
|
|
83
|
+
| Batch (100 items) | 255 µs | 474 µs | **1.9x faster** |
|
|
84
|
+
|
|
85
|
+
*Benchmarks run on Python 3.11. Run `python -m benchmarks.run_benchmarks` to reproduce.*
|
|
86
|
+
|
|
87
|
+
### Why it matters
|
|
88
|
+
|
|
89
|
+
- **Existing Marshmallow projects**: Incrementally adopt Pydantic validation without rewriting your API layer
|
|
90
|
+
- **Flask/webargs/apispec users**: Keep your integrations, get faster validation
|
|
91
|
+
- **Performance-sensitive APIs**: Nested model validation is 3-6x faster than native Marshmallow
|
|
92
|
+
|
|
70
93
|
## Features
|
|
71
94
|
|
|
72
95
|
- **Pydantic Validation**: Leverage Pydantic's Rust-powered validation engine
|
|
@@ -213,7 +236,7 @@ class User(BaseModel):
|
|
|
213
236
|
first: str
|
|
214
237
|
last: str
|
|
215
238
|
|
|
216
|
-
@computed_field
|
|
239
|
+
@computed_field # type: ignore[misc]
|
|
217
240
|
@property
|
|
218
241
|
def full_name(self) -> str:
|
|
219
242
|
return f"{self.first} {self.last}"
|
|
@@ -237,6 +260,21 @@ schema.dump(user, exclude_unset=True)
|
|
|
237
260
|
schema.dump(user, exclude_defaults=True)
|
|
238
261
|
```
|
|
239
262
|
|
|
263
|
+
## JSON Serialization
|
|
264
|
+
|
|
265
|
+
Direct JSON string support:
|
|
266
|
+
|
|
267
|
+
```python
|
|
268
|
+
# Deserialize from JSON string
|
|
269
|
+
user = schema.loads('{"name": "Alice", "email": "alice@example.com", "age": 30}')
|
|
270
|
+
|
|
271
|
+
# Serialize to JSON string
|
|
272
|
+
json_str = schema.dumps(user)
|
|
273
|
+
|
|
274
|
+
# Batch operations
|
|
275
|
+
users = schema.loads('[{"name": "Alice", ...}, {"name": "Bob", ...}]', many=True)
|
|
276
|
+
```
|
|
277
|
+
|
|
240
278
|
## Flask-Marshmallow Integration
|
|
241
279
|
|
|
242
280
|
```python
|
|
@@ -288,6 +326,99 @@ spec = APISpec(
|
|
|
288
326
|
spec.components.schema("User", schema=UserSchema)
|
|
289
327
|
```
|
|
290
328
|
|
|
329
|
+
## flask-smorest Integration
|
|
330
|
+
|
|
331
|
+
Build REST APIs with automatic OpenAPI documentation:
|
|
332
|
+
|
|
333
|
+
```python
|
|
334
|
+
from flask import Flask
|
|
335
|
+
from flask_smorest import Api, Blueprint
|
|
336
|
+
from pydantic import BaseModel, Field
|
|
337
|
+
from pydantic_marshmallow import schema_for
|
|
338
|
+
|
|
339
|
+
app = Flask(__name__)
|
|
340
|
+
app.config["API_TITLE"] = "My API"
|
|
341
|
+
app.config["API_VERSION"] = "v1"
|
|
342
|
+
app.config["OPENAPI_VERSION"] = "3.0.2"
|
|
343
|
+
|
|
344
|
+
api = Api(app)
|
|
345
|
+
blp = Blueprint("users", __name__, url_prefix="/users")
|
|
346
|
+
|
|
347
|
+
class UserCreate(BaseModel):
|
|
348
|
+
name: str = Field(min_length=1)
|
|
349
|
+
email: str
|
|
350
|
+
|
|
351
|
+
UserCreateSchema = schema_for(UserCreate)
|
|
352
|
+
UserSchema = schema_for(User)
|
|
353
|
+
|
|
354
|
+
@blp.post("/")
|
|
355
|
+
@blp.arguments(UserCreateSchema)
|
|
356
|
+
@blp.response(201, UserSchema)
|
|
357
|
+
def create_user(data):
|
|
358
|
+
# data is a Pydantic UserCreate instance
|
|
359
|
+
user = User(id=1, name=data.name, email=data.email)
|
|
360
|
+
return UserSchema().dump(user)
|
|
361
|
+
|
|
362
|
+
api.register_blueprint(blp)
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
## flask-rebar Integration
|
|
366
|
+
|
|
367
|
+
Build REST APIs with automatic Swagger documentation:
|
|
368
|
+
|
|
369
|
+
```python
|
|
370
|
+
from flask import Flask
|
|
371
|
+
from flask_rebar import Rebar, get_validated_body
|
|
372
|
+
from pydantic_marshmallow import schema_for
|
|
373
|
+
|
|
374
|
+
app = Flask(__name__)
|
|
375
|
+
rebar = Rebar()
|
|
376
|
+
registry = rebar.create_handler_registry()
|
|
377
|
+
|
|
378
|
+
UserCreateSchema = schema_for(UserCreate)
|
|
379
|
+
UserSchema = schema_for(User)
|
|
380
|
+
|
|
381
|
+
@registry.handles(
|
|
382
|
+
rule="/users",
|
|
383
|
+
method="POST",
|
|
384
|
+
request_body_schema=UserCreateSchema(),
|
|
385
|
+
response_body_schema=UserSchema(),
|
|
386
|
+
)
|
|
387
|
+
def create_user():
|
|
388
|
+
data = get_validated_body() # Pydantic UserCreate instance
|
|
389
|
+
user = User(id=1, name=data.name, email=data.email)
|
|
390
|
+
return UserSchema().dump(user)
|
|
391
|
+
|
|
392
|
+
rebar.init_app(app)
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
## SQLAlchemy Pattern
|
|
396
|
+
|
|
397
|
+
Use Pydantic for API validation alongside SQLAlchemy ORM:
|
|
398
|
+
|
|
399
|
+
```python
|
|
400
|
+
from sqlalchemy.orm import Session
|
|
401
|
+
from pydantic_marshmallow import schema_for
|
|
402
|
+
|
|
403
|
+
# Pydantic model for API validation
|
|
404
|
+
class UserCreate(BaseModel):
|
|
405
|
+
name: str = Field(min_length=1)
|
|
406
|
+
email: str
|
|
407
|
+
|
|
408
|
+
UserCreateSchema = schema_for(UserCreate)
|
|
409
|
+
|
|
410
|
+
def create_user(session: Session, data: dict):
|
|
411
|
+
# Validate API input with Pydantic
|
|
412
|
+
schema = UserCreateSchema()
|
|
413
|
+
validated = schema.load(data) # Returns Pydantic model
|
|
414
|
+
|
|
415
|
+
# Create ORM object from validated data
|
|
416
|
+
orm_user = UserModel(name=validated.name, email=validated.email)
|
|
417
|
+
session.add(orm_user)
|
|
418
|
+
session.commit()
|
|
419
|
+
return orm_user
|
|
420
|
+
```
|
|
421
|
+
|
|
291
422
|
## HybridModel
|
|
292
423
|
|
|
293
424
|
For models that need both Pydantic and Marshmallow APIs:
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
pydantic_marshmallow/__init__.py,sha256=EQON4HpiOFDtC33MrrE6WFW68GBtDlx1IqbDWxbqghU,3100
|
|
2
|
+
pydantic_marshmallow/bridge.py,sha256=MOBwr0casMgaPjP5GAcYjnG8bGgNpmpEa-zagjkClEY,52147
|
|
3
|
+
pydantic_marshmallow/errors.py,sha256=slWs9m6Iid32ZUg8uxzNbFoZWFqh7ZuiG5qPlcOaBuc,5304
|
|
4
|
+
pydantic_marshmallow/field_conversion.py,sha256=xFQtR63DwTDq-hGA_8Xe8sO6lFW6BCZNmnk5oxz5B6w,4262
|
|
5
|
+
pydantic_marshmallow/py.typed,sha256=GehYepOFQ5fvmwg3UbxoFj6w91p2kWD3KUBXL_rJRUI,86
|
|
6
|
+
pydantic_marshmallow/type_mapping.py,sha256=NYTJdmTVL-EjNW93ysb2KN9ASAj0mQu8rQpa_vOgqbU,6529
|
|
7
|
+
pydantic_marshmallow/validators.py,sha256=hg7ZTrjGHcyzfOtGy6T1LLGx4puRD3VSCLf0KN0PE8g,6686
|
|
8
|
+
pydantic_marshmallow-1.0.1.dist-info/licenses/LICENSE,sha256=y1B14nDO7pR7JdBNaWTLDt9MPBVRVuKZTUiRkw_eVgg,1071
|
|
9
|
+
pydantic_marshmallow-1.0.1.dist-info/METADATA,sha256=yNbwhXPJSg0WZAc8tfFo4U6fy57gX0eVNRf4JPLHO4g,14302
|
|
10
|
+
pydantic_marshmallow-1.0.1.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
11
|
+
pydantic_marshmallow-1.0.1.dist-info/top_level.txt,sha256=sQCtMe_dpBvCrrBM6YIz1UQmGSf-XjdoBUeA2uyuoh4,21
|
|
12
|
+
pydantic_marshmallow-1.0.1.dist-info/RECORD,,
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
pydantic_marshmallow/__init__.py,sha256=IeZH-ZcLxyy_OMfIn-IxbPRxskOnPxY94E7FLJEEBcA,2831
|
|
2
|
-
pydantic_marshmallow/bridge.py,sha256=JkDF-BozWZtzTl31RMqRHPEV_eKbeybnsuEPNdrU--Q,47688
|
|
3
|
-
pydantic_marshmallow/errors.py,sha256=slWs9m6Iid32ZUg8uxzNbFoZWFqh7ZuiG5qPlcOaBuc,5304
|
|
4
|
-
pydantic_marshmallow/field_conversion.py,sha256=xFQtR63DwTDq-hGA_8Xe8sO6lFW6BCZNmnk5oxz5B6w,4262
|
|
5
|
-
pydantic_marshmallow/py.typed,sha256=GehYepOFQ5fvmwg3UbxoFj6w91p2kWD3KUBXL_rJRUI,86
|
|
6
|
-
pydantic_marshmallow/type_mapping.py,sha256=4gFVZV7WVywyugTyiE-y7saWaHmcaPeCs0vQjBQBQ8I,5285
|
|
7
|
-
pydantic_marshmallow/validators.py,sha256=hg7ZTrjGHcyzfOtGy6T1LLGx4puRD3VSCLf0KN0PE8g,6686
|
|
8
|
-
pydantic_marshmallow-1.0.0.dist-info/licenses/LICENSE,sha256=y1B14nDO7pR7JdBNaWTLDt9MPBVRVuKZTUiRkw_eVgg,1071
|
|
9
|
-
pydantic_marshmallow-1.0.0.dist-info/METADATA,sha256=gVyLe10AtxyW4hJ9NTe43gIyPPBwigeu_BLWyopYfdg,10556
|
|
10
|
-
pydantic_marshmallow-1.0.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
11
|
-
pydantic_marshmallow-1.0.0.dist-info/top_level.txt,sha256=sQCtMe_dpBvCrrBM6YIz1UQmGSf-XjdoBUeA2uyuoh4,21
|
|
12
|
-
pydantic_marshmallow-1.0.0.dist-info/RECORD,,
|
|
File without changes
|
{pydantic_marshmallow-1.0.0.dist-info → pydantic_marshmallow-1.0.1.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|
|
File without changes
|