fixturify 0.1.9__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.
- fixturify/__init__.py +21 -0
- fixturify/_utils/__init__.py +7 -0
- fixturify/_utils/_constants.py +10 -0
- fixturify/_utils/_fixture_discovery.py +165 -0
- fixturify/_utils/_path_resolver.py +135 -0
- fixturify/http_d/__init__.py +80 -0
- fixturify/http_d/_config.py +214 -0
- fixturify/http_d/_decorator.py +267 -0
- fixturify/http_d/_exceptions.py +153 -0
- fixturify/http_d/_fixture_discovery.py +33 -0
- fixturify/http_d/_matcher.py +372 -0
- fixturify/http_d/_mock_context.py +154 -0
- fixturify/http_d/_models.py +205 -0
- fixturify/http_d/_patcher.py +524 -0
- fixturify/http_d/_player.py +222 -0
- fixturify/http_d/_recorder.py +1350 -0
- fixturify/http_d/_stubs/__init__.py +8 -0
- fixturify/http_d/_stubs/_aiohttp.py +220 -0
- fixturify/http_d/_stubs/_connection.py +478 -0
- fixturify/http_d/_stubs/_httpcore.py +269 -0
- fixturify/http_d/_stubs/_tornado.py +95 -0
- fixturify/http_d/_utils.py +194 -0
- fixturify/json_assert/__init__.py +13 -0
- fixturify/json_assert/_actual_saver.py +67 -0
- fixturify/json_assert/_assert.py +173 -0
- fixturify/json_assert/_comparator.py +183 -0
- fixturify/json_assert/_diff_formatter.py +265 -0
- fixturify/json_assert/_normalizer.py +83 -0
- fixturify/object_mapper/__init__.py +5 -0
- fixturify/object_mapper/_deserializers/__init__.py +19 -0
- fixturify/object_mapper/_deserializers/_base.py +186 -0
- fixturify/object_mapper/_deserializers/_dataclass.py +52 -0
- fixturify/object_mapper/_deserializers/_plain.py +55 -0
- fixturify/object_mapper/_deserializers/_pydantic_v1.py +38 -0
- fixturify/object_mapper/_deserializers/_pydantic_v2.py +41 -0
- fixturify/object_mapper/_deserializers/_sqlalchemy.py +72 -0
- fixturify/object_mapper/_deserializers/_sqlmodel.py +43 -0
- fixturify/object_mapper/_detectors/__init__.py +5 -0
- fixturify/object_mapper/_detectors/_type_detector.py +186 -0
- fixturify/object_mapper/_serializers/__init__.py +19 -0
- fixturify/object_mapper/_serializers/_base.py +260 -0
- fixturify/object_mapper/_serializers/_dataclass.py +55 -0
- fixturify/object_mapper/_serializers/_plain.py +49 -0
- fixturify/object_mapper/_serializers/_pydantic_v1.py +49 -0
- fixturify/object_mapper/_serializers/_pydantic_v2.py +49 -0
- fixturify/object_mapper/_serializers/_sqlalchemy.py +70 -0
- fixturify/object_mapper/_serializers/_sqlmodel.py +54 -0
- fixturify/object_mapper/mapper.py +256 -0
- fixturify/read_d/__init__.py +5 -0
- fixturify/read_d/_decorator.py +193 -0
- fixturify/read_d/_fixture_loader.py +88 -0
- fixturify/sql_d/__init__.py +7 -0
- fixturify/sql_d/_config.py +30 -0
- fixturify/sql_d/_decorator.py +373 -0
- fixturify/sql_d/_driver_registry.py +133 -0
- fixturify/sql_d/_executor.py +82 -0
- fixturify/sql_d/_fixture_discovery.py +55 -0
- fixturify/sql_d/_phase.py +10 -0
- fixturify/sql_d/_strategies/__init__.py +11 -0
- fixturify/sql_d/_strategies/_aiomysql.py +63 -0
- fixturify/sql_d/_strategies/_aiosqlite.py +29 -0
- fixturify/sql_d/_strategies/_asyncpg.py +34 -0
- fixturify/sql_d/_strategies/_base.py +118 -0
- fixturify/sql_d/_strategies/_mysql.py +70 -0
- fixturify/sql_d/_strategies/_psycopg.py +35 -0
- fixturify/sql_d/_strategies/_psycopg2.py +40 -0
- fixturify/sql_d/_strategies/_registry.py +109 -0
- fixturify/sql_d/_strategies/_sqlite.py +33 -0
- fixturify-0.1.9.dist-info/METADATA +122 -0
- fixturify-0.1.9.dist-info/RECORD +71 -0
- fixturify-0.1.9.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""Normalizer for converting various input types to comparable format."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import Any, Union
|
|
5
|
+
|
|
6
|
+
from fixturify.object_mapper import ObjectMapper
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class _Normalizer:
|
|
10
|
+
"""Converts various input types to dict/list for comparison."""
|
|
11
|
+
|
|
12
|
+
def normalize(self, data: Any) -> Union[dict, list]:
|
|
13
|
+
"""
|
|
14
|
+
Convert input data to dict or list for comparison.
|
|
15
|
+
|
|
16
|
+
- Objects → ObjectMapper → dict/list
|
|
17
|
+
- Dict → as-is
|
|
18
|
+
- JSON string → json.loads → dict/list
|
|
19
|
+
- List of objects → ObjectMapper → list
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
data: Input data to normalize
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
Normalized dict or list ready for comparison
|
|
26
|
+
|
|
27
|
+
Raises:
|
|
28
|
+
ValueError: If data cannot be normalized
|
|
29
|
+
"""
|
|
30
|
+
# Handle dict - return as-is
|
|
31
|
+
if isinstance(data, dict):
|
|
32
|
+
return data
|
|
33
|
+
|
|
34
|
+
# Handle list/tuple/set - may contain objects to serialize
|
|
35
|
+
if isinstance(data, (list, tuple, set)):
|
|
36
|
+
return self._normalize_collection(data)
|
|
37
|
+
|
|
38
|
+
# Handle JSON string
|
|
39
|
+
if isinstance(data, str):
|
|
40
|
+
try:
|
|
41
|
+
parsed = json.loads(data)
|
|
42
|
+
# Only accept JSON objects or arrays, not primitives
|
|
43
|
+
if not isinstance(parsed, (dict, list)):
|
|
44
|
+
raise ValueError(
|
|
45
|
+
f"JSON string must contain an object or array, got: {type(parsed).__name__}"
|
|
46
|
+
)
|
|
47
|
+
return parsed
|
|
48
|
+
except json.JSONDecodeError:
|
|
49
|
+
raise ValueError(
|
|
50
|
+
"String is not valid JSON. If comparing a string value, "
|
|
51
|
+
"wrap it in a dict or compare directly."
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
# Handle objects using ObjectMapper
|
|
55
|
+
try:
|
|
56
|
+
result = ObjectMapper(data).to_json()
|
|
57
|
+
# Ensure result is a dict or list (not a primitive)
|
|
58
|
+
if not isinstance(result, (dict, list)):
|
|
59
|
+
raise ValueError(
|
|
60
|
+
f"ObjectMapper returned a primitive ({type(result).__name__}). "
|
|
61
|
+
f"JsonAssert expects objects that serialize to dict or list."
|
|
62
|
+
)
|
|
63
|
+
return result
|
|
64
|
+
except Exception as e:
|
|
65
|
+
raise ValueError(f"Cannot normalize data to JSON: {e}") from e
|
|
66
|
+
|
|
67
|
+
def _normalize_collection(self, collection: Any) -> list:
|
|
68
|
+
"""Normalize a collection, serializing any objects within."""
|
|
69
|
+
result = []
|
|
70
|
+
for item in collection:
|
|
71
|
+
if isinstance(item, dict):
|
|
72
|
+
result.append(item)
|
|
73
|
+
elif isinstance(item, (list, tuple, set)):
|
|
74
|
+
result.append(self._normalize_collection(item))
|
|
75
|
+
elif isinstance(item, (str, int, float, bool, type(None))):
|
|
76
|
+
result.append(item)
|
|
77
|
+
else:
|
|
78
|
+
# Serialize object using ObjectMapper
|
|
79
|
+
try:
|
|
80
|
+
result.append(ObjectMapper(item).to_json())
|
|
81
|
+
except Exception as e:
|
|
82
|
+
raise ValueError(f"Cannot serialize collection item: {e}") from e
|
|
83
|
+
return result
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Deserializer implementations for different object types."""
|
|
2
|
+
|
|
3
|
+
from fixturify.object_mapper._deserializers._base import _BaseDeserializer
|
|
4
|
+
from fixturify.object_mapper._deserializers._dataclass import _DataclassDeserializer
|
|
5
|
+
from fixturify.object_mapper._deserializers._pydantic_v1 import _PydanticV1Deserializer
|
|
6
|
+
from fixturify.object_mapper._deserializers._pydantic_v2 import _PydanticV2Deserializer
|
|
7
|
+
from fixturify.object_mapper._deserializers._sqlalchemy import _SQLAlchemyDeserializer
|
|
8
|
+
from fixturify.object_mapper._deserializers._sqlmodel import _SQLModelDeserializer
|
|
9
|
+
from fixturify.object_mapper._deserializers._plain import _PlainObjectDeserializer
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"_BaseDeserializer",
|
|
13
|
+
"_DataclassDeserializer",
|
|
14
|
+
"_PydanticV1Deserializer",
|
|
15
|
+
"_PydanticV2Deserializer",
|
|
16
|
+
"_SQLAlchemyDeserializer",
|
|
17
|
+
"_SQLModelDeserializer",
|
|
18
|
+
"_PlainObjectDeserializer",
|
|
19
|
+
]
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"""Base deserializer class for all deserializers."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from abc import ABC, abstractmethod
|
|
5
|
+
from datetime import datetime, date, time
|
|
6
|
+
from enum import Enum
|
|
7
|
+
from typing import Any, Dict, Type, TypeVar, Union, get_type_hints
|
|
8
|
+
from uuid import UUID
|
|
9
|
+
|
|
10
|
+
T = TypeVar("T")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class _BaseDeserializer(ABC):
|
|
14
|
+
"""Abstract base class for all deserializers."""
|
|
15
|
+
|
|
16
|
+
@abstractmethod
|
|
17
|
+
def deserialize(self, data: dict, target_class: Type[T]) -> T:
|
|
18
|
+
"""
|
|
19
|
+
Convert a dictionary to an object of target_class.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
data: Dictionary data to deserialize
|
|
23
|
+
target_class: The class to create an instance of
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
Instance of target_class
|
|
27
|
+
"""
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
def _get_type_hints(self, cls: Type) -> Dict[str, Any]:
|
|
31
|
+
"""
|
|
32
|
+
Get type hints for a class, resolving forward references.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
cls: The class to get type hints for
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
Dictionary of field names to their types
|
|
39
|
+
"""
|
|
40
|
+
try:
|
|
41
|
+
# Try to get type hints with the class's module globals
|
|
42
|
+
module = sys.modules.get(cls.__module__, None)
|
|
43
|
+
globalns = getattr(module, "__dict__", {}) if module else {}
|
|
44
|
+
return get_type_hints(cls, globalns=globalns, localns=None)
|
|
45
|
+
except Exception:
|
|
46
|
+
# Fallback to __annotations__ if get_type_hints fails
|
|
47
|
+
return getattr(cls, "__annotations__", {})
|
|
48
|
+
|
|
49
|
+
def _convert_value(self, value: Any, target_type: Type) -> Any:
|
|
50
|
+
"""
|
|
51
|
+
Convert a value to the target type.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
value: The value to convert
|
|
55
|
+
target_type: The target type
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
Converted value
|
|
59
|
+
"""
|
|
60
|
+
# Handle None
|
|
61
|
+
if value is None:
|
|
62
|
+
return None
|
|
63
|
+
|
|
64
|
+
# Handle Any type
|
|
65
|
+
if target_type is Any:
|
|
66
|
+
return value
|
|
67
|
+
|
|
68
|
+
# Get the origin type for generic types
|
|
69
|
+
origin = getattr(target_type, "__origin__", None)
|
|
70
|
+
args = getattr(target_type, "__args__", ())
|
|
71
|
+
|
|
72
|
+
# Handle Optional (Union with None)
|
|
73
|
+
if origin is Union:
|
|
74
|
+
# Filter out NoneType from args
|
|
75
|
+
non_none_args = [arg for arg in args if arg is not type(None)]
|
|
76
|
+
if len(non_none_args) == 1:
|
|
77
|
+
# This is Optional[T]
|
|
78
|
+
return self._convert_value(value, non_none_args[0])
|
|
79
|
+
# For Union with multiple types, try each one
|
|
80
|
+
for arg in non_none_args:
|
|
81
|
+
try:
|
|
82
|
+
return self._convert_value(value, arg)
|
|
83
|
+
except (ValueError, TypeError):
|
|
84
|
+
continue
|
|
85
|
+
return value
|
|
86
|
+
|
|
87
|
+
# Handle List
|
|
88
|
+
if origin is list or target_type is list:
|
|
89
|
+
if not isinstance(value, list):
|
|
90
|
+
return value
|
|
91
|
+
item_type = args[0] if args else Any
|
|
92
|
+
return [self._convert_value(item, item_type) for item in value]
|
|
93
|
+
|
|
94
|
+
# Handle Set
|
|
95
|
+
if origin is set or target_type is set:
|
|
96
|
+
if not isinstance(value, (list, set)):
|
|
97
|
+
return value
|
|
98
|
+
item_type = args[0] if args else Any
|
|
99
|
+
return {self._convert_value(item, item_type) for item in value}
|
|
100
|
+
|
|
101
|
+
# Handle Tuple
|
|
102
|
+
if origin is tuple or target_type is tuple:
|
|
103
|
+
if not isinstance(value, (list, tuple)):
|
|
104
|
+
return value
|
|
105
|
+
if args:
|
|
106
|
+
# Handle Tuple[T, ...] (variable-length homogeneous tuple)
|
|
107
|
+
# In this case, args = (T, Ellipsis)
|
|
108
|
+
if len(args) >= 2 and args[-1] is ...:
|
|
109
|
+
item_type = args[0]
|
|
110
|
+
return tuple(self._convert_value(item, item_type) for item in value)
|
|
111
|
+
# Handle Tuple[T1, T2, ...] (fixed-length heterogeneous tuple)
|
|
112
|
+
return tuple(
|
|
113
|
+
self._convert_value(item, args[i] if i < len(args) else args[-1])
|
|
114
|
+
for i, item in enumerate(value)
|
|
115
|
+
)
|
|
116
|
+
return tuple(value)
|
|
117
|
+
|
|
118
|
+
# Handle Dict
|
|
119
|
+
if origin is dict or target_type is dict:
|
|
120
|
+
if not isinstance(value, dict):
|
|
121
|
+
return value
|
|
122
|
+
if len(args) >= 2:
|
|
123
|
+
key_type, val_type = args[0], args[1]
|
|
124
|
+
return {
|
|
125
|
+
self._convert_value(k, key_type): self._convert_value(v, val_type)
|
|
126
|
+
for k, v in value.items()
|
|
127
|
+
}
|
|
128
|
+
return value
|
|
129
|
+
|
|
130
|
+
# Handle datetime
|
|
131
|
+
if target_type is datetime:
|
|
132
|
+
if isinstance(value, str):
|
|
133
|
+
return datetime.fromisoformat(value)
|
|
134
|
+
return value
|
|
135
|
+
|
|
136
|
+
# Handle date
|
|
137
|
+
if target_type is date:
|
|
138
|
+
if isinstance(value, str):
|
|
139
|
+
return date.fromisoformat(value)
|
|
140
|
+
return value
|
|
141
|
+
|
|
142
|
+
# Handle time
|
|
143
|
+
if target_type is time:
|
|
144
|
+
if isinstance(value, str):
|
|
145
|
+
return time.fromisoformat(value)
|
|
146
|
+
return value
|
|
147
|
+
|
|
148
|
+
# Handle UUID
|
|
149
|
+
if target_type is UUID:
|
|
150
|
+
if isinstance(value, str):
|
|
151
|
+
return UUID(value)
|
|
152
|
+
return value
|
|
153
|
+
|
|
154
|
+
# Handle Enum
|
|
155
|
+
if isinstance(target_type, type) and issubclass(target_type, Enum):
|
|
156
|
+
return target_type(value)
|
|
157
|
+
|
|
158
|
+
# Handle primitives
|
|
159
|
+
if target_type in (str, int, float, bool):
|
|
160
|
+
if isinstance(value, target_type):
|
|
161
|
+
return value
|
|
162
|
+
try:
|
|
163
|
+
return target_type(value)
|
|
164
|
+
except (ValueError, TypeError):
|
|
165
|
+
return value
|
|
166
|
+
|
|
167
|
+
# Handle nested objects (dict to object)
|
|
168
|
+
if isinstance(value, dict) and isinstance(target_type, type):
|
|
169
|
+
return self._deserialize_nested(value, target_type)
|
|
170
|
+
|
|
171
|
+
return value
|
|
172
|
+
|
|
173
|
+
def _deserialize_nested(self, data: dict, target_class: Type[T]) -> T:
|
|
174
|
+
"""
|
|
175
|
+
Deserialize a nested object. Override in subclasses for specific handling.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
data: Dictionary data
|
|
179
|
+
target_class: The target class
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
Instance of target_class
|
|
183
|
+
"""
|
|
184
|
+
# This will be overridden by subclasses
|
|
185
|
+
# Default: try to instantiate with **data
|
|
186
|
+
return target_class(**data)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Dataclass deserializer."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import fields, is_dataclass
|
|
4
|
+
from typing import Any, Type, TypeVar
|
|
5
|
+
|
|
6
|
+
from fixturify.object_mapper._deserializers._base import _BaseDeserializer
|
|
7
|
+
|
|
8
|
+
T = TypeVar("T")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class _DataclassDeserializer(_BaseDeserializer):
|
|
12
|
+
"""Deserializer for dataclass objects."""
|
|
13
|
+
|
|
14
|
+
def deserialize(self, data: dict, target_class: Type[T]) -> T:
|
|
15
|
+
"""
|
|
16
|
+
Deserialize a dictionary to a dataclass.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
data: Dictionary data to deserialize
|
|
20
|
+
target_class: The dataclass to create an instance of
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
Instance of target_class
|
|
24
|
+
|
|
25
|
+
Raises:
|
|
26
|
+
TypeError: If target_class is not a dataclass
|
|
27
|
+
"""
|
|
28
|
+
if not is_dataclass(target_class):
|
|
29
|
+
raise TypeError(f"Expected a dataclass, got {target_class}")
|
|
30
|
+
|
|
31
|
+
# Get type hints for field type information
|
|
32
|
+
type_hints = self._get_type_hints(target_class)
|
|
33
|
+
|
|
34
|
+
# Build kwargs for dataclass constructor
|
|
35
|
+
kwargs = {}
|
|
36
|
+
dataclass_fields = {f.name for f in fields(target_class)}
|
|
37
|
+
|
|
38
|
+
for field_name in dataclass_fields:
|
|
39
|
+
if field_name in data:
|
|
40
|
+
value = data[field_name]
|
|
41
|
+
target_type = type_hints.get(field_name, Any)
|
|
42
|
+
kwargs[field_name] = self._convert_value(value, target_type)
|
|
43
|
+
|
|
44
|
+
return target_class(**kwargs)
|
|
45
|
+
|
|
46
|
+
def _deserialize_nested(self, data: dict, target_class: Type[T]) -> T:
|
|
47
|
+
"""Deserialize a nested object."""
|
|
48
|
+
if is_dataclass(target_class):
|
|
49
|
+
return self.deserialize(data, target_class)
|
|
50
|
+
|
|
51
|
+
# For non-dataclass nested objects, try basic instantiation
|
|
52
|
+
return target_class(**data)
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Plain Python object deserializer."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Type, TypeVar
|
|
4
|
+
|
|
5
|
+
from fixturify.object_mapper._deserializers._base import _BaseDeserializer
|
|
6
|
+
|
|
7
|
+
T = TypeVar("T")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class _PlainObjectDeserializer(_BaseDeserializer):
|
|
11
|
+
"""Deserializer for plain Python objects."""
|
|
12
|
+
|
|
13
|
+
def deserialize(self, data: dict, target_class: Type[T]) -> T:
|
|
14
|
+
"""
|
|
15
|
+
Deserialize a dictionary to a plain Python object.
|
|
16
|
+
|
|
17
|
+
Attempts to create an instance using:
|
|
18
|
+
1. __init__ with keyword arguments
|
|
19
|
+
2. __new__ followed by attribute assignment
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
data: Dictionary data to deserialize
|
|
23
|
+
target_class: The class to create an instance of
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
Instance of target_class
|
|
27
|
+
"""
|
|
28
|
+
# Get type hints for nested object conversion
|
|
29
|
+
type_hints = self._get_type_hints(target_class)
|
|
30
|
+
|
|
31
|
+
# Convert values to appropriate types
|
|
32
|
+
converted_data = {}
|
|
33
|
+
for key, value in data.items():
|
|
34
|
+
target_type = type_hints.get(key, Any)
|
|
35
|
+
converted_data[key] = self._convert_value(value, target_type)
|
|
36
|
+
|
|
37
|
+
# Try to create instance with __init__
|
|
38
|
+
try:
|
|
39
|
+
return target_class(**converted_data)
|
|
40
|
+
except TypeError:
|
|
41
|
+
# __init__ doesn't accept kwargs, try manual assignment
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
# Try creating with __new__ and manual assignment
|
|
45
|
+
try:
|
|
46
|
+
instance = object.__new__(target_class)
|
|
47
|
+
for key, value in converted_data.items():
|
|
48
|
+
setattr(instance, key, value)
|
|
49
|
+
return instance
|
|
50
|
+
except Exception as e:
|
|
51
|
+
raise ValueError(f"Cannot create instance of {target_class.__name__}: {e}")
|
|
52
|
+
|
|
53
|
+
def _deserialize_nested(self, data: dict, target_class: Type[T]) -> T:
|
|
54
|
+
"""Deserialize a nested plain object."""
|
|
55
|
+
return self.deserialize(data, target_class)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Pydantic v1 model deserializer."""
|
|
2
|
+
|
|
3
|
+
from typing import Type, TypeVar
|
|
4
|
+
|
|
5
|
+
from fixturify.object_mapper._deserializers._base import _BaseDeserializer
|
|
6
|
+
|
|
7
|
+
T = TypeVar("T")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class _PydanticV1Deserializer(_BaseDeserializer):
|
|
11
|
+
"""Deserializer for Pydantic v1 models."""
|
|
12
|
+
|
|
13
|
+
def deserialize(self, data: dict, target_class: Type[T]) -> T:
|
|
14
|
+
"""
|
|
15
|
+
Deserialize a dictionary to a Pydantic v1 model.
|
|
16
|
+
|
|
17
|
+
Pydantic v1 models accept dict data directly in their constructor
|
|
18
|
+
and handle validation/coercion internally.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
data: Dictionary data to deserialize
|
|
22
|
+
target_class: The Pydantic v1 model class
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
Instance of target_class
|
|
26
|
+
"""
|
|
27
|
+
# Pydantic v1 handles validation and type coercion
|
|
28
|
+
# Just pass the data directly
|
|
29
|
+
return target_class(**data)
|
|
30
|
+
|
|
31
|
+
def _deserialize_nested(self, data: dict, target_class: Type[T]) -> T:
|
|
32
|
+
"""Deserialize a nested Pydantic v1 model."""
|
|
33
|
+
# Check if it's a Pydantic v1 model
|
|
34
|
+
if hasattr(target_class, "__fields__") and hasattr(target_class, "schema"):
|
|
35
|
+
return self.deserialize(data, target_class)
|
|
36
|
+
|
|
37
|
+
# For non-Pydantic nested objects, try basic instantiation
|
|
38
|
+
return target_class(**data)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Pydantic v2 model deserializer."""
|
|
2
|
+
|
|
3
|
+
from typing import Type, TypeVar
|
|
4
|
+
|
|
5
|
+
from fixturify.object_mapper._deserializers._base import _BaseDeserializer
|
|
6
|
+
|
|
7
|
+
T = TypeVar("T")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class _PydanticV2Deserializer(_BaseDeserializer):
|
|
11
|
+
"""Deserializer for Pydantic v2 models."""
|
|
12
|
+
|
|
13
|
+
def deserialize(self, data: dict, target_class: Type[T]) -> T:
|
|
14
|
+
"""
|
|
15
|
+
Deserialize a dictionary to a Pydantic v2 model.
|
|
16
|
+
|
|
17
|
+
Pydantic v2 models accept dict data directly in their constructor
|
|
18
|
+
and handle validation/coercion internally. Can also use model_validate().
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
data: Dictionary data to deserialize
|
|
22
|
+
target_class: The Pydantic v2 model class
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
Instance of target_class
|
|
26
|
+
"""
|
|
27
|
+
# Pydantic v2 has model_validate for dict -> model conversion
|
|
28
|
+
if hasattr(target_class, "model_validate"):
|
|
29
|
+
return target_class.model_validate(data)
|
|
30
|
+
|
|
31
|
+
# Fallback to constructor
|
|
32
|
+
return target_class(**data)
|
|
33
|
+
|
|
34
|
+
def _deserialize_nested(self, data: dict, target_class: Type[T]) -> T:
|
|
35
|
+
"""Deserialize a nested Pydantic v2 model."""
|
|
36
|
+
# Check if it's a Pydantic v2 model
|
|
37
|
+
if hasattr(target_class, "model_fields"):
|
|
38
|
+
return self.deserialize(data, target_class)
|
|
39
|
+
|
|
40
|
+
# For non-Pydantic nested objects, try basic instantiation
|
|
41
|
+
return target_class(**data)
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""SQLAlchemy model deserializer."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Type, TypeVar
|
|
4
|
+
|
|
5
|
+
from fixturify.object_mapper._deserializers._base import _BaseDeserializer
|
|
6
|
+
|
|
7
|
+
T = TypeVar("T")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class _SQLAlchemyDeserializer(_BaseDeserializer):
|
|
11
|
+
"""Deserializer for SQLAlchemy models."""
|
|
12
|
+
|
|
13
|
+
def deserialize(self, data: dict, target_class: Type[T]) -> T:
|
|
14
|
+
"""
|
|
15
|
+
Deserialize a dictionary to a SQLAlchemy model.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
data: Dictionary data to deserialize
|
|
19
|
+
target_class: The SQLAlchemy model class
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
Instance of target_class
|
|
23
|
+
"""
|
|
24
|
+
# Get column information from mapper
|
|
25
|
+
mapper = getattr(target_class, "__mapper__", None)
|
|
26
|
+
if mapper is None:
|
|
27
|
+
# Fallback to simple instantiation
|
|
28
|
+
return target_class(**data)
|
|
29
|
+
|
|
30
|
+
# Get type hints for nested object conversion
|
|
31
|
+
type_hints = self._get_type_hints(target_class)
|
|
32
|
+
|
|
33
|
+
# Build kwargs, filtering to only column names
|
|
34
|
+
column_names = {col.key for col in mapper.columns}
|
|
35
|
+
kwargs = {}
|
|
36
|
+
|
|
37
|
+
for key, value in data.items():
|
|
38
|
+
if key in column_names:
|
|
39
|
+
target_type = type_hints.get(key, Any)
|
|
40
|
+
kwargs[key] = self._convert_value(value, target_type)
|
|
41
|
+
|
|
42
|
+
# Create instance
|
|
43
|
+
instance = target_class(**kwargs)
|
|
44
|
+
|
|
45
|
+
# Handle relationships if present in data
|
|
46
|
+
for relationship in mapper.relationships:
|
|
47
|
+
rel_name = relationship.key
|
|
48
|
+
if rel_name in data:
|
|
49
|
+
rel_data = data[rel_name]
|
|
50
|
+
rel_class = relationship.mapper.class_
|
|
51
|
+
|
|
52
|
+
if isinstance(rel_data, list):
|
|
53
|
+
rel_instances = [
|
|
54
|
+
self._deserialize_nested(item, rel_class) for item in rel_data
|
|
55
|
+
]
|
|
56
|
+
setattr(instance, rel_name, rel_instances)
|
|
57
|
+
elif isinstance(rel_data, dict):
|
|
58
|
+
rel_instance = self._deserialize_nested(rel_data, rel_class)
|
|
59
|
+
setattr(instance, rel_name, rel_instance)
|
|
60
|
+
|
|
61
|
+
return instance
|
|
62
|
+
|
|
63
|
+
def _deserialize_nested(self, data: dict, target_class: Type[T]) -> T:
|
|
64
|
+
"""Deserialize a nested SQLAlchemy model."""
|
|
65
|
+
# Check if it's a SQLAlchemy model
|
|
66
|
+
if hasattr(target_class, "__tablename__") and hasattr(
|
|
67
|
+
target_class, "__mapper__"
|
|
68
|
+
):
|
|
69
|
+
return self.deserialize(data, target_class)
|
|
70
|
+
|
|
71
|
+
# For non-SQLAlchemy nested objects, try basic instantiation
|
|
72
|
+
return target_class(**data)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""SQLModel model deserializer."""
|
|
2
|
+
|
|
3
|
+
from typing import Type, TypeVar
|
|
4
|
+
|
|
5
|
+
from fixturify.object_mapper._deserializers._base import _BaseDeserializer
|
|
6
|
+
|
|
7
|
+
T = TypeVar("T")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class _SQLModelDeserializer(_BaseDeserializer):
|
|
11
|
+
"""Deserializer for SQLModel models."""
|
|
12
|
+
|
|
13
|
+
def deserialize(self, data: dict, target_class: Type[T]) -> T:
|
|
14
|
+
"""
|
|
15
|
+
Deserialize a dictionary to a SQLModel model.
|
|
16
|
+
|
|
17
|
+
SQLModel combines Pydantic v2 and SQLAlchemy. We use Pydantic's
|
|
18
|
+
model_validate for deserialization as it handles validation.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
data: Dictionary data to deserialize
|
|
22
|
+
target_class: The SQLModel model class
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
Instance of target_class
|
|
26
|
+
"""
|
|
27
|
+
# SQLModel uses Pydantic v2's model_validate
|
|
28
|
+
if hasattr(target_class, "model_validate"):
|
|
29
|
+
return target_class.model_validate(data)
|
|
30
|
+
|
|
31
|
+
# Fallback to constructor
|
|
32
|
+
return target_class(**data)
|
|
33
|
+
|
|
34
|
+
def _deserialize_nested(self, data: dict, target_class: Type[T]) -> T:
|
|
35
|
+
"""Deserialize a nested SQLModel model."""
|
|
36
|
+
# Check if it's a SQLModel model
|
|
37
|
+
if hasattr(target_class, "__tablename__") and hasattr(
|
|
38
|
+
target_class, "model_fields"
|
|
39
|
+
):
|
|
40
|
+
return self.deserialize(data, target_class)
|
|
41
|
+
|
|
42
|
+
# For non-SQLModel nested objects, try basic instantiation
|
|
43
|
+
return target_class(**data)
|