async-orm-kit 1.0.0__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 (92) hide show
  1. async_orm_kit/__init__.py +151 -0
  2. async_orm_kit/_logging.py +58 -0
  3. async_orm_kit/_version.py +1 -0
  4. async_orm_kit/config.py +31 -0
  5. async_orm_kit/exceptions.py +353 -0
  6. async_orm_kit/integrations/__init__.py +15 -0
  7. async_orm_kit/integrations/alembic.py +56 -0
  8. async_orm_kit/integrations/fastapi.py +134 -0
  9. async_orm_kit/integrations/storage.py +213 -0
  10. async_orm_kit/manager/__init__.py +3 -0
  11. async_orm_kit/manager/manager.py +568 -0
  12. async_orm_kit/models/__init__.py +3 -0
  13. async_orm_kit/models/base.py +568 -0
  14. async_orm_kit/models/fields/__init__.py +23 -0
  15. async_orm_kit/models/fields/exceptions.py +10 -0
  16. async_orm_kit/models/fields/typed/__init__.py +11 -0
  17. async_orm_kit/models/fields/typed/base.py +155 -0
  18. async_orm_kit/models/fields/typed/file.py +697 -0
  19. async_orm_kit/models/fields/typed/string.py +38 -0
  20. async_orm_kit/models/fields/validators.py +110 -0
  21. async_orm_kit/models/meta.py +181 -0
  22. async_orm_kit/models/mixins.py +19 -0
  23. async_orm_kit/observers/__init__.py +4 -0
  24. async_orm_kit/observers/mixin.py +26 -0
  25. async_orm_kit/observers/model_observer.py +20 -0
  26. async_orm_kit/observers/queryset_observer.py +38 -0
  27. async_orm_kit/py.typed +0 -0
  28. async_orm_kit/query/__init__.py +0 -0
  29. async_orm_kit/query/aggregations/__init__.py +8 -0
  30. async_orm_kit/query/aggregations/avg.py +71 -0
  31. async_orm_kit/query/aggregations/base.py +317 -0
  32. async_orm_kit/query/aggregations/count.py +115 -0
  33. async_orm_kit/query/aggregations/max.py +76 -0
  34. async_orm_kit/query/aggregations/min.py +76 -0
  35. async_orm_kit/query/aggregations/sum.py +73 -0
  36. async_orm_kit/query/compiler.py +574 -0
  37. async_orm_kit/query/debugger.py +269 -0
  38. async_orm_kit/query/expressions/__init__.py +62 -0
  39. async_orm_kit/query/expressions/analysis.py +56 -0
  40. async_orm_kit/query/expressions/case.py +321 -0
  41. async_orm_kit/query/expressions/collections.py +251 -0
  42. async_orm_kit/query/expressions/f.py +362 -0
  43. async_orm_kit/query/expressions/functions.py +202 -0
  44. async_orm_kit/query/expressions/subquery.py +206 -0
  45. async_orm_kit/query/expressions/value.py +101 -0
  46. async_orm_kit/query/expressions/window.py +95 -0
  47. async_orm_kit/query/lookups/__init__.py +121 -0
  48. async_orm_kit/query/lookups/conditions.py +315 -0
  49. async_orm_kit/query/lookups/correlated.py +86 -0
  50. async_orm_kit/query/lookups/json_paths.py +167 -0
  51. async_orm_kit/query/lookups/parser.py +276 -0
  52. async_orm_kit/query/lookups/resolver/__init__.py +5 -0
  53. async_orm_kit/query/lookups/resolver/context.py +150 -0
  54. async_orm_kit/query/lookups/resolver/resolver.py +572 -0
  55. async_orm_kit/query/lookups/resolver/types.py +150 -0
  56. async_orm_kit/query/mixins/__init__.py +0 -0
  57. async_orm_kit/query/mixins/combinable.py +376 -0
  58. async_orm_kit/query/mixins/orderable.py +208 -0
  59. async_orm_kit/query/observability.py +346 -0
  60. async_orm_kit/query/planning/__init__.py +0 -0
  61. async_orm_kit/query/planning/directives.py +117 -0
  62. async_orm_kit/query/planning/factory.py +40 -0
  63. async_orm_kit/query/planning/passes/__init__.py +15 -0
  64. async_orm_kit/query/planning/passes/base.py +47 -0
  65. async_orm_kit/query/planning/passes/cartesian_detection.py +74 -0
  66. async_orm_kit/query/planning/passes/depth_splitting.py +78 -0
  67. async_orm_kit/query/planning/passes/join_promotion.py +81 -0
  68. async_orm_kit/query/planning/passes/path_conflicts.py +103 -0
  69. async_orm_kit/query/planning/passes/redundant_path.py +67 -0
  70. async_orm_kit/query/planning/passes/strategy_selection.py +49 -0
  71. async_orm_kit/query/planning/planner.py +160 -0
  72. async_orm_kit/query/planning/protocols.py +78 -0
  73. async_orm_kit/query/postgres/__init__.py +0 -0
  74. async_orm_kit/query/postgres/optimizer.py +189 -0
  75. async_orm_kit/query/q.py +596 -0
  76. async_orm_kit/query/relations/__init__.py +0 -0
  77. async_orm_kit/query/relations/enums.py +16 -0
  78. async_orm_kit/query/relations/graph.py +908 -0
  79. async_orm_kit/query/relations/node.py +421 -0
  80. async_orm_kit/queryset/__init__.py +4 -0
  81. async_orm_kit/queryset/builder.py +1259 -0
  82. async_orm_kit/queryset/compound.py +379 -0
  83. async_orm_kit/queryset/executor.py +718 -0
  84. async_orm_kit/queryset/queryset.py +1726 -0
  85. async_orm_kit/queryset/state.py +164 -0
  86. async_orm_kit/queryset/validators.py +361 -0
  87. async_orm_kit/queryset/writer.py +755 -0
  88. async_orm_kit/types.py +23 -0
  89. async_orm_kit-1.0.0.dist-info/METADATA +318 -0
  90. async_orm_kit-1.0.0.dist-info/RECORD +92 -0
  91. async_orm_kit-1.0.0.dist-info/WHEEL +4 -0
  92. async_orm_kit-1.0.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,151 @@
1
+ """Async-first, Django-inspired query layer built on SQLAlchemy."""
2
+
3
+ from async_orm_kit._version import __version__
4
+ from async_orm_kit.config import DEFAULT_CONFIG, ORMConfig
5
+ from async_orm_kit.exceptions import (
6
+ DatabaseError,
7
+ ExpressionTypeError,
8
+ FilterError,
9
+ InvalidLookupError,
10
+ InvalidRelationshipValueError,
11
+ ModelAttributeError,
12
+ MultipleObjectsReturned,
13
+ ObjectDoesNotExist,
14
+ QuerySetError,
15
+ QueryStateError,
16
+ RelationshipError,
17
+ )
18
+ from async_orm_kit.integrations.storage import AsyncStorageKitAdapter
19
+ from async_orm_kit.manager import Manager
20
+ from async_orm_kit.models import Model
21
+ from async_orm_kit.models.fields import (
22
+ FieldFile,
23
+ FieldValidationError,
24
+ FileField,
25
+ StringField,
26
+ )
27
+ from async_orm_kit.models.meta import Base, ModelMeta
28
+ from async_orm_kit.query.aggregations import (
29
+ Aggregate,
30
+ AggregateFunction,
31
+ Avg,
32
+ Count,
33
+ Max,
34
+ Min,
35
+ Sum,
36
+ )
37
+ from async_orm_kit.query.compiler import QueryCompiler
38
+ from async_orm_kit.query.debugger import NPlusOneDetector, QueryDebugger, query_debugger
39
+ from async_orm_kit.query.expressions import Case, F, FExpression, Value, When
40
+ from async_orm_kit.query.expressions.collections import (
41
+ ArrayContains,
42
+ ArrayIndex,
43
+ ArrayLength,
44
+ ArrayOverlap,
45
+ JSONContains,
46
+ JSONHasKey,
47
+ JSONPath,
48
+ )
49
+ from async_orm_kit.query.expressions.functions import (
50
+ Abs,
51
+ Cast,
52
+ Coalesce,
53
+ Concat,
54
+ Extract,
55
+ Function,
56
+ Length,
57
+ Lower,
58
+ NullIf,
59
+ Round,
60
+ Trim,
61
+ Upper,
62
+ )
63
+ from async_orm_kit.query.expressions.subquery import Exists, OuterRef, Subquery
64
+ from async_orm_kit.query.expressions.window import (
65
+ DenseRank,
66
+ Lag,
67
+ Lead,
68
+ Rank,
69
+ RowNumber,
70
+ Window,
71
+ )
72
+ from async_orm_kit.query.observability import QueryProfile, QueryProfiler, QueryRecord
73
+ from async_orm_kit.query.q import Q
74
+ from async_orm_kit.queryset import QuerySet
75
+ from async_orm_kit.queryset.compound import CompoundQuerySet
76
+
77
+ __all__ = [
78
+ "ArrayContains",
79
+ "ArrayIndex",
80
+ "ArrayLength",
81
+ "ArrayOverlap",
82
+ "JSONContains",
83
+ "JSONHasKey",
84
+ "JSONPath",
85
+ "CompoundQuerySet",
86
+ "Abs",
87
+ "Cast",
88
+ "Coalesce",
89
+ "Concat",
90
+ "Extract",
91
+ "Length",
92
+ "Lower",
93
+ "NullIf",
94
+ "Round",
95
+ "Trim",
96
+ "Upper",
97
+ "Window",
98
+ "RowNumber",
99
+ "Rank",
100
+ "DenseRank",
101
+ "Lag",
102
+ "Lead",
103
+ "Exists",
104
+ "OuterRef",
105
+ "Subquery",
106
+ "Aggregate",
107
+ "AggregateFunction",
108
+ "AsyncStorageKitAdapter",
109
+ "Avg",
110
+ "Base",
111
+ "Case",
112
+ "Count",
113
+ "DEFAULT_CONFIG",
114
+ "DatabaseError",
115
+ "ExpressionTypeError",
116
+ "F",
117
+ "Function",
118
+ "FExpression",
119
+ "FieldFile",
120
+ "FieldValidationError",
121
+ "FileField",
122
+ "FilterError",
123
+ "InvalidLookupError",
124
+ "InvalidRelationshipValueError",
125
+ "Manager",
126
+ "Max",
127
+ "Min",
128
+ "Model",
129
+ "ModelAttributeError",
130
+ "ModelMeta",
131
+ "MultipleObjectsReturned",
132
+ "ORMConfig",
133
+ "ObjectDoesNotExist",
134
+ "Q",
135
+ "QueryProfiler",
136
+ "QueryProfile",
137
+ "QueryRecord",
138
+ "QueryDebugger",
139
+ "NPlusOneDetector",
140
+ "query_debugger",
141
+ "QueryCompiler",
142
+ "QuerySet",
143
+ "QuerySetError",
144
+ "QueryStateError",
145
+ "RelationshipError",
146
+ "StringField",
147
+ "Sum",
148
+ "Value",
149
+ "When",
150
+ "__version__",
151
+ ]
@@ -0,0 +1,58 @@
1
+ """Small structured-context adapter over Python's standard logging package.
2
+
3
+ The ORM does not require a particular logging framework. Keyword context is
4
+ attached to each LogRecord as ``orm_context`` so applications can route it
5
+ through their own formatters or structured logging handlers.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import logging
11
+ from typing import Any
12
+
13
+
14
+ class ContextLogger:
15
+ """Logger that accepts structlog-style keyword context without a dependency."""
16
+
17
+ def __init__(self, name: str) -> None:
18
+ self._logger = logging.getLogger(name)
19
+
20
+ def _log(self, level: int, event: object, *args: Any, **kwargs: Any) -> None:
21
+ exc_info = kwargs.pop("exc_info", None)
22
+ stack_info = kwargs.pop("stack_info", False)
23
+ stacklevel = kwargs.pop("stacklevel", 1)
24
+ extra = kwargs.pop("extra", None)
25
+ record_extra = dict(extra or {})
26
+ if kwargs:
27
+ record_extra["orm_context"] = kwargs
28
+ self._logger.log(
29
+ level,
30
+ event,
31
+ *args,
32
+ exc_info=exc_info,
33
+ stack_info=stack_info,
34
+ stacklevel=stacklevel + 1,
35
+ extra=record_extra or None,
36
+ )
37
+
38
+ def debug(self, event: object, *args: Any, **kwargs: Any) -> None:
39
+ self._log(logging.DEBUG, event, *args, **kwargs)
40
+
41
+ def info(self, event: object, *args: Any, **kwargs: Any) -> None:
42
+ self._log(logging.INFO, event, *args, **kwargs)
43
+
44
+ def warning(self, event: object, *args: Any, **kwargs: Any) -> None:
45
+ self._log(logging.WARNING, event, *args, **kwargs)
46
+
47
+ def error(self, event: object, *args: Any, **kwargs: Any) -> None:
48
+ self._log(logging.ERROR, event, *args, **kwargs)
49
+
50
+ def exception(self, event: object, *args: Any, **kwargs: Any) -> None:
51
+ kwargs.setdefault("exc_info", True)
52
+ self._log(logging.ERROR, event, *args, **kwargs)
53
+
54
+
55
+ def get_logger(name: str) -> ContextLogger:
56
+ """Return a context-aware logger backed by stdlib logging."""
57
+
58
+ return ContextLogger(name)
@@ -0,0 +1 @@
1
+ __version__ = "1.0.0"
@@ -0,0 +1,31 @@
1
+ """Configuration primitives for async-orm-kit.
2
+
3
+ Configuration is explicit and immutable. Applications can create separate
4
+ ORMConfig instances for independent query policies without relying on global
5
+ project settings.
6
+ """
7
+
8
+ from dataclasses import dataclass
9
+
10
+
11
+ @dataclass(frozen=True, slots=True)
12
+ class ORMConfig:
13
+ """Runtime configuration for ORM query compilation.
14
+
15
+ Args:
16
+ max_joinedload_depth: Maximum relationship depth kept in a joined eager
17
+ loading chain before the planner prefers select-in loading.
18
+ """
19
+
20
+ max_joinedload_depth: int = 2
21
+
22
+ def __post_init__(self) -> None:
23
+ if isinstance(self.max_joinedload_depth, bool) or not isinstance(
24
+ self.max_joinedload_depth, int
25
+ ):
26
+ raise TypeError("max_joinedload_depth must be an integer")
27
+ if self.max_joinedload_depth < 1:
28
+ raise ValueError("max_joinedload_depth must be at least 1")
29
+
30
+
31
+ DEFAULT_CONFIG = ORMConfig()
@@ -0,0 +1,353 @@
1
+ """QuerySet exception hierarchy for ORM operations.
2
+
3
+ This module defines a comprehensive exception hierarchy for handling errors
4
+ during QuerySet construction, compilation, and execution. All exceptions inherit
5
+ from QuerySetError to allow catching all ORM-related errors.
6
+
7
+ The exception hierarchy is designed to provide specific error types for different
8
+ failure scenarios, enabling precise error handling and debugging.
9
+ """
10
+
11
+
12
+ class QuerySetError(Exception):
13
+ """Base exception for all QuerySet-related errors.
14
+
15
+ This is the root exception class for the custom ORM. All QuerySet operations
16
+ that fail will raise this exception or one of its subclasses. Use this for
17
+ broad exception handling when you want to catch any ORM error.
18
+
19
+ Example:
20
+ >>> try:
21
+ ... qs = User.objects.filter(invalid_field="value")
22
+ ... except QuerySetError as e:
23
+ ... logger.error(f"QuerySet operation failed: {e}")
24
+
25
+ Note:
26
+ Catch specific subclasses when you need granular error handling.
27
+ Use this base class only for generic error handling or logging.
28
+ """
29
+
30
+ pass
31
+
32
+
33
+ class ModelAttributeError(QuerySetError):
34
+ """Raised when accessing or filtering by non-existent model attributes.
35
+
36
+ This exception occurs when you reference a field or attribute that doesn't
37
+ exist on the model class. It helps catch typos and prevents silent failures
38
+ when building queries with invalid field names.
39
+
40
+ Example:
41
+ >>> # Model has 'username' but you typed 'user_name'
42
+ >>> User.objects.filter(user_name="john")
43
+ ModelAttributeError: Model 'User' has no attribute 'user_name'
44
+
45
+ >>> # Correct usage
46
+ >>> User.objects.filter(username="john")
47
+
48
+ """
49
+
50
+ pass
51
+
52
+
53
+ class RelationshipError(QuerySetError):
54
+ """Raised when relationship navigation or joining fails.
55
+
56
+ This exception occurs when attempting to traverse relationships that don't
57
+ exist, are misconfigured, or when the relationship path is invalid. Common
58
+ causes include missing foreign keys, incorrect relationship names, or
59
+ attempting to join through non-relationship fields.
60
+
61
+ Raises:
62
+ RelationshipError: When select_related() or prefetch_related() references
63
+ invalid relationships, or when filter() uses invalid relationship paths.
64
+
65
+ Example:
66
+ >>> # Wrong: 'author' relationship doesn't exist on Post
67
+ >>> Post.objects.select_related("author")
68
+ RelationshipError: Model 'Post' has no relationship 'author'
69
+
70
+ >>> # Correct: use the actual relationship name
71
+ >>> Post.objects.select_related("user")
72
+
73
+ >>> # Wrong: trying to join through a non-relationship field
74
+ >>> Post.objects.filter(created_at__user__name="john")
75
+ RelationshipError: Field 'created_at' is not a relationship
76
+
77
+ Note:
78
+ Performance: Raised at query construction time (O(1)).
79
+ Validate relationship paths early to avoid runtime failures.
80
+ """
81
+
82
+ pass
83
+
84
+
85
+ class FilterError(QuerySetError):
86
+ """Raised when filter operations fail due to invalid syntax or values.
87
+
88
+ This is a general exception for filter-related errors that don't fit into
89
+ more specific categories. It covers issues like invalid filter syntax,
90
+ incompatible value types, or malformed filter expressions.
91
+
92
+ Example:
93
+ >>> # Wrong: invalid filter syntax
94
+ >>> User.objects.filter(age__between=25) # missing second value
95
+ FilterError: 'between' lookup requires two values
96
+
97
+ >>> # Correct
98
+ >>> User.objects.filter(age__between=(25, 35))
99
+
100
+ Note:
101
+ This is a base class for more specific filter errors like
102
+ InvalidLookupError and InvalidRelationshipValueError.
103
+ """
104
+
105
+ pass
106
+
107
+
108
+ class DatabaseError(QuerySetError):
109
+ """Raised when database operations fail during query execution.
110
+
111
+ This exception wraps database-level errors that occur when executing queries,
112
+ such as connection failures, constraint violations, or SQL syntax errors.
113
+ It provides a consistent interface for handling database failures regardless
114
+ of the underlying database driver.
115
+
116
+ Raises:
117
+ DatabaseError: When query execution fails due to database issues.
118
+
119
+ Example:
120
+ >>> try:
121
+ ... user = User.objects.create(email="duplicate@example.com")
122
+ ... except DatabaseError as e:
123
+ ... if "unique constraint" in str(e).lower():
124
+ ... logger.warning("Duplicate email attempted")
125
+ ... else:
126
+ ... raise
127
+
128
+ Note:
129
+ Performance: Only raised at query execution time, not construction.
130
+ This indicates a runtime issue, not a code error.
131
+ Consider implementing retry logic for transient database failures.
132
+ """
133
+
134
+ def __init__(
135
+ self, message: str = "Database error", cause: Exception | None = None, *args
136
+ ):
137
+ self.message = message
138
+ self.cause = cause
139
+ super().__init__(message, *args)
140
+
141
+
142
+ class InvalidLookupError(FilterError):
143
+ """Raised when an unsupported or forbidden lookup operator is used.
144
+
145
+ This exception occurs when you use a lookup that either doesn't exist or
146
+ isn't supported for the field type you're filtering on. It helps prevent
147
+ queries that would fail at the database level or produce unexpected results.
148
+
149
+ Args:
150
+ message: The error message.
151
+ lookup: The invalid lookup name that was attempted.
152
+ field_name: The field on which the lookup was attempted.
153
+
154
+ Raises:
155
+ InvalidLookupError: When using non-existent lookups or incompatible
156
+ lookup-field combinations.
157
+
158
+ Example:
159
+ >>> # Wrong: 'contains' doesn't work on integer fields
160
+ >>> User.objects.filter(age__contains=5)
161
+ InvalidLookupError: Lookup 'contains' not supported for IntegerField
162
+
163
+ >>> # Wrong: typo in lookup name
164
+ >>> User.objects.filter(name__icontain="john")
165
+ InvalidLookupError: Unknown lookup 'icontain'. Did you mean 'icontains'?
166
+
167
+ >>> # Correct
168
+ >>> User.objects.filter(name__icontains="john")
169
+ >>> User.objects.filter(age__gte=18)
170
+
171
+ Note:
172
+ Performance: Raised at query construction time (O(1)).
173
+ Valid lookups depend on field type: strings support 'contains',
174
+ 'icontains', 'startswith'; numbers support 'gt', 'gte', 'lt', 'lte';
175
+ dates support 'year', 'month', 'day', etc.
176
+ """
177
+
178
+ def __init__(self, message: str, lookup: str, field_name: str, *args):
179
+ self.lookup = lookup
180
+ self.field_name = field_name
181
+ super().__init__(message, *args)
182
+
183
+
184
+ class InvalidRelationshipValueError(FilterError):
185
+ """Raised when an invalid value is passed to a relationship comparison.
186
+
187
+ This exception occurs when filtering by relationships with incorrect value
188
+ types. The most common mistake is passing a model class instead of a model
189
+ instance, or passing a non-model object where a relationship is expected.
190
+
191
+ Args:
192
+ message: The error message.
193
+ value: The invalid value that was passed.
194
+ field_name: The relationship field being filtered.
195
+
196
+ Raises:
197
+ InvalidRelationshipValueError: When filtering relationships with
198
+ model classes, primitives, or other invalid types.
199
+
200
+ Example:
201
+ >>> # Wrong: passing a class instead of an instance
202
+ >>> Floor.objects.filter(building=DBBuilding)
203
+ InvalidRelationshipValueError: Expected DBBuilding instance, got type
204
+ >>> # Wrong: passing a primitive value
205
+ >>> Floor.objects.filter(building=123)
206
+ InvalidRelationshipValueError: Expected DBBuilding instance, got int
207
+
208
+ >>> # Correct: pass a mapped instance
209
+ >>> building = await DBBuilding.objects.get(id=1)
210
+ >>> floors = await Floor.objects.filter(building=building)
211
+
212
+ >>> # Also correct: filter by relationship's primary key
213
+ >>> floors = await Floor.objects.filter(building_id=1)
214
+
215
+ Note:
216
+ Performance: Raised at query construction time (O(1)).
217
+ To filter by ID without fetching the instance, use the foreign key
218
+ field directly (e.g., 'building_id' instead of 'building').
219
+ """
220
+
221
+ def __init__(self, message: str, value: str, field_name: str, *args):
222
+ self.value = value
223
+ self.field_name = field_name
224
+ super().__init__(message, *args)
225
+
226
+
227
+ class QueryStateError(QuerySetError):
228
+ """Raised when queryset methods are called in an invalid state or order.
229
+
230
+ This exception enforces the correct sequencing of QuerySet operations.
231
+ Certain methods are incompatible or must be called in a specific order.
232
+ This prevents building queries that would produce unexpected results or
233
+ fail at execution time.
234
+
235
+ Raises:
236
+ QueryStateError: When methods are called in an incompatible order or
237
+ when the QuerySet is in a state that doesn't support the operation.
238
+
239
+ Example:
240
+ >>> # Wrong: select_related() after values() - can't join after projection
241
+ >>> User.objects.values("name").select_related("profile")
242
+ QueryStateError: Cannot call select_related() after values()
243
+
244
+ >>> # Wrong: values() after only() - conflicting field selections
245
+ >>> User.objects.only("name").values("email")
246
+ QueryStateError: Cannot call values() after only()
247
+
248
+ >>> # Wrong: order_by() with field not in values() selection
249
+ >>> User.objects.values("name").order_by("email")
250
+ QueryStateError: Cannot order by 'email' - not in values() selection
251
+
252
+ >>> # Wrong: having() without aggregates
253
+ >>> User.objects.having(age__gt=18)
254
+ QueryStateError: Cannot use having() without aggregate functions
255
+
256
+ >>> # Correct: proper method ordering
257
+ >>> User.objects.select_related("profile").values("name", "profile__bio")
258
+ >>> User.objects.values("name").order_by("name")
259
+ >>> User.objects.annotate(post_count=Count("posts")).having(post_count__gt=5)
260
+
261
+ Note:
262
+ Performance: Raised at query construction time (O(1)).
263
+ Method order matters: select_related/prefetch_related → filter/exclude →
264
+ annotate → values/only → order_by → having → limit/offset.
265
+ Not all combinations are invalid, but some fundamentally conflict.
266
+ """
267
+
268
+ pass
269
+
270
+
271
+ class ExpressionTypeError(QuerySetError):
272
+ """Raised when an invalid type is passed where a string or expression is expected.
273
+
274
+ This exception occurs when you pass an expression object (like F() or Q())
275
+ in a context that requires additional metadata, such as ordering direction.
276
+ It helps catch cases where expressions need to be wrapped with .asc() or
277
+ .desc() for proper SQL generation.
278
+
279
+ Raises:
280
+ ExpressionTypeError: When passing bare expressions without required
281
+ metadata, or when using incompatible types in expression contexts.
282
+
283
+ Example:
284
+ >>> # Wrong: bare F expression in order_by() - missing direction
285
+ >>> Product.objects.order_by(F("price") * F("quantity"))
286
+ ExpressionTypeError: Expression in order_by() must specify .asc() or .desc()
287
+
288
+ >>> # Wrong: passing a non-string, non-expression type
289
+ >>> User.objects.order_by(123)
290
+ ExpressionTypeError: order_by() expects str or Expression, got int
291
+
292
+ >>> # Correct: wrap expressions with ordering direction
293
+ >>> Product.objects.order_by((F("price") * F("quantity")).desc())
294
+
295
+ >>> # Correct: use string references for simple cases
296
+ >>> User.objects.order_by("created_at", "-updated_at")
297
+
298
+ >>> # Correct: expressions in annotate() don't need direction
299
+ >>> Product.objects.annotate(total=F("price") * F("quantity"))
300
+
301
+ Note:
302
+ Performance: Raised at query construction time (O(1)).
303
+ String field references are simpler and preferred for basic ordering.
304
+ Use F() expressions when you need computed values or complex operations.
305
+ Always call .asc() or .desc() on expressions used in order_by().
306
+ """
307
+
308
+ pass
309
+
310
+
311
+ class ObjectDoesNotExist(QuerySetError):
312
+ """Base exception for all DoesNotExist exceptions.
313
+
314
+ This is the parent class for all model-specific DoesNotExist exceptions.
315
+ Use this to catch DoesNotExist from any model.
316
+
317
+ Example:
318
+ >>> try:
319
+ ... obj = await SomeModel.objects.get(id=1)
320
+ ... except SomeModel.DoesNotExist:
321
+ ... # Catches DoesNotExist from any model
322
+ ... pass
323
+ """
324
+
325
+ pass
326
+
327
+
328
+ class MultipleObjectsReturned(QuerySetError):
329
+ """Raised when .get() returns multiple objects instead of one.
330
+
331
+ This exception occurs when using .get() to retrieve a single object but
332
+ multiple rows match the filter criteria. It indicates the query needs
333
+ more specific filters to uniquely identify one object.
334
+
335
+ Raises:
336
+ MultipleObjectsReturned: When .get() returns more than one result.
337
+
338
+ Example:
339
+ >>> # Wrong: multiple users with this name exist
340
+ >>> user = await User.objects.get(name="John")
341
+ MultipleObjectsReturned: get() returned multiple results
342
+
343
+ >>> # Correct: use more specific filters
344
+ >>> user = await User.objects.get(email="john@example.com")>>> # Or use filter() if you expect multiple results
345
+ >>> users = await User.objects.filter(name="John")
346
+
347
+ Note:
348
+ Performance: Only raised at query execution time.
349
+ Use .filter() when you expect multiple results.
350
+ Use .get() only when filters guarantee uniqueness (e.g., by primary key).
351
+ """
352
+
353
+ pass
@@ -0,0 +1,15 @@
1
+ """Optional integrations for third-party async libraries.
2
+
3
+ Importing this package does not import the optional Alembic or FastAPI packages.
4
+ """
5
+
6
+ from .alembic import get_target_metadata
7
+ from .fastapi import create_session_dependency, register_exception_handlers
8
+ from .storage import AsyncStorageKitAdapter
9
+
10
+ __all__ = [
11
+ "AsyncStorageKitAdapter",
12
+ "create_session_dependency",
13
+ "get_target_metadata",
14
+ "register_exception_handlers",
15
+ ]
@@ -0,0 +1,56 @@
1
+ """Alembic helpers for Async ORM Kit model metadata.
2
+
3
+ Alembic remains an optional development/migration dependency. This module only
4
+ uses SQLAlchemy's public MetaData API and does not import Alembic itself.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from importlib import import_module
10
+ from types import ModuleType
11
+
12
+ from sqlalchemy import MetaData
13
+
14
+ from async_orm_kit.models.meta import Base
15
+
16
+
17
+ def get_target_metadata(*model_modules: str | ModuleType) -> MetaData:
18
+ """Import application model modules and return Async ORM Kit metadata.
19
+
20
+ Alembic autogenerate can only compare tables that have been registered with
21
+ SQLAlchemy's declarative metadata. Pass the application modules that define
22
+ your models before returning ``Base.metadata``::
23
+
24
+ target_metadata = get_target_metadata("myapp.models")
25
+
26
+ Modules are imported in the order supplied. Import errors intentionally
27
+ propagate so a broken model import cannot silently produce an incomplete
28
+ migration. Calling this function without arguments returns the metadata for
29
+ models that have already been imported.
30
+
31
+ Args:
32
+ *model_modules: Trusted importable module names or already-imported
33
+ module objects containing application model declarations.
34
+
35
+ Returns:
36
+ The shared SQLAlchemy MetaData instance used by ``async_orm_kit.Model``.
37
+
38
+ Raises:
39
+ TypeError: If a supplied value is neither a module name nor a module.
40
+ ValueError: If a supplied module name is empty or whitespace.
41
+ ImportError: If a named application module cannot be imported.
42
+ """
43
+ for module in model_modules:
44
+ if isinstance(module, str):
45
+ if not module.strip():
46
+ raise ValueError("model module names must not be empty")
47
+ import_module(module)
48
+ elif not isinstance(module, ModuleType):
49
+ raise TypeError(
50
+ "model_modules values must be module names or module objects"
51
+ )
52
+
53
+ return Base.metadata
54
+
55
+
56
+ __all__ = ["get_target_metadata"]