strawchemy 0.2.8__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.
Files changed (63) hide show
  1. strawchemy/__init__.py +13 -0
  2. strawchemy/config.py +29 -0
  3. strawchemy/dto/__init__.py +19 -0
  4. strawchemy/dto/backend/__init__.py +0 -0
  5. strawchemy/dto/backend/dataclass.py +164 -0
  6. strawchemy/dto/backend/pydantic.py +147 -0
  7. strawchemy/dto/base.py +563 -0
  8. strawchemy/dto/constants.py +5 -0
  9. strawchemy/dto/exceptions.py +12 -0
  10. strawchemy/dto/factories/__init__.py +0 -0
  11. strawchemy/dto/factories/pydantic_sqlalchemy.py +15 -0
  12. strawchemy/dto/inspectors/__init__.py +0 -0
  13. strawchemy/dto/inspectors/sqlalchemy.py +274 -0
  14. strawchemy/dto/types.py +125 -0
  15. strawchemy/dto/utils.py +71 -0
  16. strawchemy/graph.py +438 -0
  17. strawchemy/graphql/__init__.py +16 -0
  18. strawchemy/graphql/constants.py +24 -0
  19. strawchemy/graphql/dto.py +596 -0
  20. strawchemy/graphql/factory.py +1281 -0
  21. strawchemy/graphql/filters.py +327 -0
  22. strawchemy/graphql/inspector.py +21 -0
  23. strawchemy/graphql/typing.py +40 -0
  24. strawchemy/mapper.py +187 -0
  25. strawchemy/pydantic.py +19 -0
  26. strawchemy/sqlalchemy/__init__.py +13 -0
  27. strawchemy/sqlalchemy/_executor.py +233 -0
  28. strawchemy/sqlalchemy/_query.py +343 -0
  29. strawchemy/sqlalchemy/_scope.py +478 -0
  30. strawchemy/sqlalchemy/_transpiler.py +744 -0
  31. strawchemy/sqlalchemy/exceptions.py +12 -0
  32. strawchemy/sqlalchemy/filters/__init__.py +29 -0
  33. strawchemy/sqlalchemy/filters/base.py +380 -0
  34. strawchemy/sqlalchemy/filters/geo.py +53 -0
  35. strawchemy/sqlalchemy/hook.py +62 -0
  36. strawchemy/sqlalchemy/inspector.py +86 -0
  37. strawchemy/sqlalchemy/repository/__init__.py +7 -0
  38. strawchemy/sqlalchemy/repository/_async.py +102 -0
  39. strawchemy/sqlalchemy/repository/_base.py +68 -0
  40. strawchemy/sqlalchemy/repository/_sync.py +104 -0
  41. strawchemy/sqlalchemy/typing.py +68 -0
  42. strawchemy/strawberry/__init__.py +7 -0
  43. strawchemy/strawberry/_field.py +369 -0
  44. strawchemy/strawberry/_instance.py +27 -0
  45. strawchemy/strawberry/_registry.py +221 -0
  46. strawchemy/strawberry/_utils.py +75 -0
  47. strawchemy/strawberry/exceptions.py +6 -0
  48. strawchemy/strawberry/factory.py +882 -0
  49. strawchemy/strawberry/inspector.py +73 -0
  50. strawchemy/strawberry/pydantic.py +396 -0
  51. strawchemy/strawberry/repository/__init__.py +6 -0
  52. strawchemy/strawberry/repository/_async.py +264 -0
  53. strawchemy/strawberry/repository/_node.py +114 -0
  54. strawchemy/strawberry/repository/_sync.py +273 -0
  55. strawchemy/strawberry/typing.py +28 -0
  56. strawchemy/testing/__init__.py +0 -0
  57. strawchemy/testing/pytest.py +87 -0
  58. strawchemy/typing.py +7 -0
  59. strawchemy/utils.py +49 -0
  60. strawchemy-0.2.8.dist-info/METADATA +19 -0
  61. strawchemy-0.2.8.dist-info/RECORD +63 -0
  62. strawchemy-0.2.8.dist-info/WHEEL +4 -0
  63. strawchemy-0.2.8.dist-info/licenses/LICENCE +21 -0
strawchemy/__init__.py ADDED
@@ -0,0 +1,13 @@
1
+ """Custom DTO implementation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .mapper import Strawchemy
6
+ from .sqlalchemy.hook import QueryHook
7
+ from .strawberry import ModelInstance
8
+ from .strawberry.repository import StrawchemyAsyncRepository
9
+
10
+ __all__ = ("ModelInstance", "QueryHook", "StrawchemyAsyncRepository", "strawchemy")
11
+
12
+
13
+ strawchemy = Strawchemy()
strawchemy/config.py ADDED
@@ -0,0 +1,29 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import TYPE_CHECKING
5
+
6
+ from .sqlalchemy import SQLAlchemyGraphQLInspector
7
+ from .strawberry import default_session_getter
8
+ from .strawberry.repository import StrawchemyAsyncRepository, StrawchemySyncRepository
9
+
10
+ if TYPE_CHECKING:
11
+ from typing import Any
12
+
13
+ from .graphql.inspector import GraphQLInspectorProtocol
14
+ from .sqlalchemy.typing import FilterMap
15
+ from .strawberry.typing import AsyncSessionGetter
16
+
17
+
18
+ @dataclass
19
+ class StrawchemyConfig:
20
+ session_getter: AsyncSessionGetter = default_session_getter
21
+ auto_snake_case: bool = True
22
+ repository_type: type[StrawchemyAsyncRepository[Any] | StrawchemySyncRepository[Any]] = StrawchemyAsyncRepository
23
+ filter_overrides: FilterMap | None = None
24
+ execution_options: dict[str, Any] | None = None
25
+
26
+ inspector: GraphQLInspectorProtocol[Any, Any] = field(init=False)
27
+
28
+ def __post_init__(self) -> None:
29
+ self.inspector = SQLAlchemyGraphQLInspector(filter_overrides=self.filter_overrides)
@@ -0,0 +1,19 @@
1
+ """Custom DTO implementation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .base import DTOFieldDefinition, ModelFieldT, ModelInspector, ModelT
6
+ from .types import DTOConfig, Purpose, PurposeConfig
7
+ from .utils import config, field
8
+
9
+ __all__ = (
10
+ "DTOConfig",
11
+ "DTOFieldDefinition",
12
+ "ModelFieldT",
13
+ "ModelInspector",
14
+ "ModelT",
15
+ "Purpose",
16
+ "PurposeConfig",
17
+ "config",
18
+ "field",
19
+ )
File without changes
@@ -0,0 +1,164 @@
1
+ """Data Transfer Object (DTO) classes for dataclasses.
2
+
3
+ This module defines base classes and utilities for working with DTOs that
4
+ are based on Python dataclasses. It provides a way to map data between
5
+ dataclasses and other data models, such as SQLAlchemy models.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import dataclasses
11
+ from dataclasses import dataclass
12
+ from inspect import getmodule
13
+ from typing import TYPE_CHECKING, Any, NamedTuple, TypeAlias, TypeVar, cast, override
14
+
15
+ from strawchemy.dto.base import DTOBackend, DTOBase, MappedDTO, ModelFieldT, ModelT
16
+ from strawchemy.dto.types import DTO_MISSING, DTOMissingType
17
+
18
+ if TYPE_CHECKING:
19
+ from collections.abc import Callable, Iterable
20
+
21
+ from strawchemy.dto.base import DTOFieldDefinition
22
+
23
+ __all__ = ("DataclassDTO", "DataclassDTOBackend", "DataclassDTOT", "MappedDataclassDTO")
24
+
25
+ DataclassDTOT = TypeVar("DataclassDTOT", bound="DataclassDTO[Any] | MappedDataclassDTO[Any]")
26
+
27
+
28
+ class DataclassDTO(DTOBase[ModelT]): ...
29
+
30
+
31
+ class BasicFieldInfo(NamedTuple):
32
+ name: str
33
+ type: Any
34
+
35
+
36
+ class FullFieldInfo(NamedTuple):
37
+ name: str
38
+ type: Any
39
+ field: dataclasses.Field[Any]
40
+
41
+
42
+ DataclassFieldInfo: TypeAlias = BasicFieldInfo | FullFieldInfo
43
+
44
+
45
+ @dataclass
46
+ class MappedDataclassDTO(MappedDTO[ModelT]):
47
+ @override
48
+ def to_mapped(self, **override: Any) -> ModelT:
49
+ """Create an instance of `self.__sqla_model__`.
50
+
51
+ Fill the bound SQLAlchemy model recursively with values from this dataclass.
52
+ """
53
+ as_model = {}
54
+ dc_fields: dict[str, dataclasses.Field[Any]] = {}
55
+ if dataclasses.is_dataclass(self.__dto_model__):
56
+ dc_fields = {f.name: f for f in dataclasses.fields(self.__dto_model__)}
57
+
58
+ for dc_field in dataclasses.fields(self):
59
+ if (value := override.get(dc_field.name, DTO_MISSING)) and value is not DTO_MISSING:
60
+ as_model[dc_field.name] = value
61
+ continue
62
+ if (field := dc_fields.get(dc_field.name, None)) and not field.init:
63
+ continue
64
+
65
+ value: ModelT | MappedDataclassDTO[ModelT] | list[ModelT] | list[MappedDataclassDTO[ModelT]]
66
+ value = getattr(self, dc_field.name)
67
+
68
+ if isinstance(value, list | tuple):
69
+ value = [el.to_mapped() if isinstance(el, MappedDataclassDTO) else cast(ModelT, el) for el in value]
70
+ if isinstance(value, MappedDataclassDTO):
71
+ value = value.to_mapped()
72
+ as_model[dc_field.name] = value
73
+ try:
74
+ return self.__dto_model__(**(as_model | override))
75
+ except TypeError as error:
76
+ original_message = error.args[0] if isinstance(error.args[0], str) else repr(error)
77
+ msg = f"{original_message} (model: {self.__dto_model__.__name__})"
78
+ raise TypeError(msg) from error
79
+
80
+
81
+ class DataclassDTOBackend(DTOBackend[DataclassDTOT]):
82
+ def __init__(self, dto_base: type[DataclassDTOT]) -> None:
83
+ self.dto_base = dto_base
84
+
85
+ def _construct_field_info(self, field_def: DTOFieldDefinition[ModelT, ModelFieldT]) -> DataclassFieldInfo:
86
+ if not isinstance(field_def.default_factory, DTOMissingType):
87
+ return FullFieldInfo(
88
+ field_def.name, field_def.type_, dataclasses.field(default_factory=field_def.default_factory)
89
+ )
90
+ if not isinstance(field_def.default, DTOMissingType):
91
+ return FullFieldInfo(field_def.name, field_def.type_, dataclasses.field(default=field_def.default))
92
+
93
+ return BasicFieldInfo(field_def.name, field_def.type_)
94
+
95
+ @override
96
+ def build(
97
+ self,
98
+ name: str,
99
+ model: type[Any],
100
+ field_definitions: Iterable[DTOFieldDefinition[Any, ModelFieldT]],
101
+ base: type[Any] | None = None,
102
+ repr: bool = True,
103
+ eq: bool = True,
104
+ order: bool = False,
105
+ unsafe_hash: bool = False,
106
+ frozen: bool = False,
107
+ match_args: bool = True,
108
+ kw_only: bool = True,
109
+ slots: bool = False,
110
+ **kwargs: Any,
111
+ ) -> type[DataclassDTOT]:
112
+ namespace: dict[str, Any] = {}
113
+ fields: list[DataclassFieldInfo] = []
114
+ post_init_validator: list[Callable[[DataclassDTOT], None]] = []
115
+
116
+ for field in field_definitions:
117
+ field_info = self._construct_field_info(field)
118
+ fields.append(field_info)
119
+
120
+ if field.purpose_config.validator is not None:
121
+
122
+ def _validator(
123
+ self: DataclassDTOT,
124
+ _name: str = field.name,
125
+ _validator_function: Callable[[Any], Any] = field.purpose_config.validator,
126
+ ) -> None:
127
+ value = getattr(self, _name)
128
+ if value is not DTO_MISSING:
129
+ return setattr(self, _name, _validator_function(value))
130
+ return setattr(self, _name, _validator_function(value))
131
+
132
+ post_init_validator.append(_validator)
133
+
134
+ if post_init_validator:
135
+
136
+ def post_init(
137
+ self: DataclassDTOT, _validators: list[Callable[[DataclassDTOT], None]] = post_init_validator
138
+ ) -> None:
139
+ for validator in _validators:
140
+ validator(self)
141
+
142
+ namespace["__post_init__"] = post_init
143
+
144
+ module = __name__
145
+ if model_module := getmodule(model):
146
+ module = model_module.__name__
147
+
148
+ bases = (self.dto_base, base) if base else (self.dto_base,)
149
+
150
+ return dataclasses.make_dataclass(
151
+ name,
152
+ fields=fields,
153
+ bases=bases,
154
+ module=module,
155
+ repr=repr,
156
+ eq=eq,
157
+ order=order,
158
+ unsafe_hash=unsafe_hash,
159
+ frozen=frozen,
160
+ match_args=match_args,
161
+ kw_only=kw_only,
162
+ slots=slots,
163
+ namespace=namespace,
164
+ )
@@ -0,0 +1,147 @@
1
+ """Pydantic DTO implementation.
2
+
3
+ This module provides classes and utilities for creating Data Transfer Objects (DTOs)
4
+ using Pydantic. It includes base classes for Pydantic DTOs, a backend class
5
+ for generating DTOs from models, and support for mapping DTOs to SQLAlchemy models.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import dataclasses
11
+ from inspect import getmodule
12
+ from typing import TYPE_CHECKING, Annotated, Any, TypeVar, cast, override
13
+
14
+ from pydantic import BaseModel, BeforeValidator, ConfigDict, create_model
15
+ from pydantic.fields import Field, FieldInfo
16
+ from strawchemy.dto.base import DTOBackend, DTOBase, DTOFieldDefinition, MappedDTO, ModelFieldT, ModelT
17
+ from strawchemy.dto.types import DTO_MISSING, DTOMissingType
18
+
19
+ if TYPE_CHECKING:
20
+ from collections.abc import Iterable
21
+
22
+ from pydantic.functional_validators import _V2Validator
23
+
24
+
25
+ __all__ = ("MappedPydanticDTO", "PydanticDTO", "PydanticDTOBackend")
26
+
27
+
28
+ PydanticDTOT = TypeVar("PydanticDTOT", bound="PydanticDTO[Any] | MappedPydanticDTO[Any]")
29
+
30
+
31
+ class _PydanticDTOBase(BaseModel):
32
+ model_config = ConfigDict(from_attributes=True, arbitrary_types_allowed=True, defer_build=True)
33
+
34
+
35
+ class PydanticDTO(_PydanticDTOBase, DTOBase[ModelT]): ...
36
+
37
+
38
+ class MappedPydanticDTO(_PydanticDTOBase, MappedDTO[ModelT]):
39
+ @override
40
+ def to_mapped(self, **override: Any) -> ModelT:
41
+ """Create an instance of `self.__sqla_model__`.
42
+
43
+ Fill the bound SQLAlchemy model recursively with values from this dataclass.
44
+ """
45
+ as_model = {}
46
+ dc_fields: dict[str, dataclasses.Field[Any]] = {}
47
+ if dataclasses.is_dataclass(self.__dto_model__):
48
+ dc_fields = {f.name: f for f in dataclasses.fields(self.__dto_model__)}
49
+
50
+ for name, pydantic_field in self.model_fields.items():
51
+ if (value := override.get(name, DTO_MISSING)) and value is not DTO_MISSING:
52
+ as_model[name] = value
53
+ continue
54
+ if (field := dc_fields.get(name, None)) and not field.init:
55
+ continue
56
+
57
+ value: ModelT | MappedPydanticDTO[ModelT] | list[ModelT] | list[MappedPydanticDTO[ModelT]]
58
+ value = getattr(self, name)
59
+
60
+ if isinstance(value, list | tuple):
61
+ value = [el.to_mapped() if isinstance(el, MappedPydanticDTO) else cast(ModelT, el) for el in value]
62
+ if isinstance(value, MappedPydanticDTO):
63
+ value = value.to_mapped()
64
+ as_model[pydantic_field.alias or name] = value
65
+ try:
66
+ return cast("ModelT", self.__dto_model__(**(as_model | override)))
67
+ except TypeError as error:
68
+ original_message = error.args[0] if isinstance(error.args[0], str) else repr(error)
69
+ msg = f"{original_message} (model: {self.__dto_model__.__name__})"
70
+ raise TypeError(msg) from error
71
+
72
+
73
+ class PydanticDTOBackend(DTOBackend[PydanticDTOT]):
74
+ """Implements DTO factory using pydantic."""
75
+
76
+ def __init__(self, dto_base: type[PydanticDTOT]) -> None:
77
+ self.dto_base = dto_base
78
+
79
+ def _construct_field_info(self, field_def: DTOFieldDefinition[ModelT, ModelFieldT]) -> FieldInfo:
80
+ """Build a `FieldInfo instance reflecting the given field_def."""
81
+ kwargs: dict[str, Any] = {}
82
+ if field_def.required:
83
+ kwargs["default"] = ...
84
+ elif not isinstance(field_def.default_factory, DTOMissingType):
85
+ kwargs["default_factory"] = field_def.default_factory
86
+ elif not isinstance(field_def.default, DTOMissingType):
87
+ kwargs["default"] = field_def.default
88
+ if field_def.purpose_config.alias:
89
+ kwargs["alias"] = field_def.model_field_name
90
+ return Field(**kwargs)
91
+
92
+ @override
93
+ def update_forward_refs(self, dto: type[PydanticDTOT], namespace: dict[str, type[PydanticDTOT]]) -> None:
94
+ dto.model_rebuild(_types_namespace=namespace, raise_errors=False)
95
+
96
+ @override
97
+ def build(
98
+ self,
99
+ name: str,
100
+ model: type[ModelT],
101
+ field_definitions: Iterable[DTOFieldDefinition[ModelT, ModelFieldT]],
102
+ base: type[Any] | None = None,
103
+ config_dict: ConfigDict | None = None,
104
+ docstring: bool = True,
105
+ **kwargs: Any,
106
+ ) -> type[PydanticDTOT]:
107
+ fields: dict[str, tuple[Any, FieldInfo]] = {}
108
+ validators: dict[str, _V2Validator] = {}
109
+ base_annotations = base.__annotations__ if base else {}
110
+
111
+ for field_def in field_definitions:
112
+ field_type = field_def.type_
113
+ validator: BeforeValidator | None = None
114
+ if field_def.purpose_config.validator:
115
+ validator = BeforeValidator(field_def.purpose_config.validator)
116
+ if validator:
117
+ field_type = Annotated[field_type, validator]
118
+ fields[field_def.name] = (field_type, self._construct_field_info(field_def))
119
+
120
+ # Copy fields from base to avoid Pydantic warning about shadowing fields
121
+ for f_name in base_annotations:
122
+ field_info: FieldInfo = Field()
123
+ attribute = getattr(base, f_name, DTO_MISSING)
124
+ if not isinstance(attribute, DTOMissingType):
125
+ field_info = attribute if isinstance(attribute, FieldInfo) else Field(default=attribute)
126
+ field_type = fields[f_name][0] if f_name in fields else base_annotations[f_name]
127
+ fields[f_name] = (field_type, field_info)
128
+
129
+ module = __name__
130
+ if model_module := getmodule(model):
131
+ module = model_module.__name__
132
+
133
+ dto = create_model(
134
+ name,
135
+ __base__=(self.dto_base,),
136
+ __config__=None,
137
+ __module__=module,
138
+ __validators__=validators,
139
+ __doc__=f"Pydantic generated DTO for {model.__name__} model" if docstring else None,
140
+ __cls_kwargs__=None,
141
+ **fields,
142
+ )
143
+
144
+ if config_dict:
145
+ cls_body = {"model_config": config_dict} if config_dict else {}
146
+ return type(dto.__name__, (dto,), cls_body)
147
+ return dto