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.
- async_orm_kit/__init__.py +151 -0
- async_orm_kit/_logging.py +58 -0
- async_orm_kit/_version.py +1 -0
- async_orm_kit/config.py +31 -0
- async_orm_kit/exceptions.py +353 -0
- async_orm_kit/integrations/__init__.py +15 -0
- async_orm_kit/integrations/alembic.py +56 -0
- async_orm_kit/integrations/fastapi.py +134 -0
- async_orm_kit/integrations/storage.py +213 -0
- async_orm_kit/manager/__init__.py +3 -0
- async_orm_kit/manager/manager.py +568 -0
- async_orm_kit/models/__init__.py +3 -0
- async_orm_kit/models/base.py +568 -0
- async_orm_kit/models/fields/__init__.py +23 -0
- async_orm_kit/models/fields/exceptions.py +10 -0
- async_orm_kit/models/fields/typed/__init__.py +11 -0
- async_orm_kit/models/fields/typed/base.py +155 -0
- async_orm_kit/models/fields/typed/file.py +697 -0
- async_orm_kit/models/fields/typed/string.py +38 -0
- async_orm_kit/models/fields/validators.py +110 -0
- async_orm_kit/models/meta.py +181 -0
- async_orm_kit/models/mixins.py +19 -0
- async_orm_kit/observers/__init__.py +4 -0
- async_orm_kit/observers/mixin.py +26 -0
- async_orm_kit/observers/model_observer.py +20 -0
- async_orm_kit/observers/queryset_observer.py +38 -0
- async_orm_kit/py.typed +0 -0
- async_orm_kit/query/__init__.py +0 -0
- async_orm_kit/query/aggregations/__init__.py +8 -0
- async_orm_kit/query/aggregations/avg.py +71 -0
- async_orm_kit/query/aggregations/base.py +317 -0
- async_orm_kit/query/aggregations/count.py +115 -0
- async_orm_kit/query/aggregations/max.py +76 -0
- async_orm_kit/query/aggregations/min.py +76 -0
- async_orm_kit/query/aggregations/sum.py +73 -0
- async_orm_kit/query/compiler.py +574 -0
- async_orm_kit/query/debugger.py +269 -0
- async_orm_kit/query/expressions/__init__.py +62 -0
- async_orm_kit/query/expressions/analysis.py +56 -0
- async_orm_kit/query/expressions/case.py +321 -0
- async_orm_kit/query/expressions/collections.py +251 -0
- async_orm_kit/query/expressions/f.py +362 -0
- async_orm_kit/query/expressions/functions.py +202 -0
- async_orm_kit/query/expressions/subquery.py +206 -0
- async_orm_kit/query/expressions/value.py +101 -0
- async_orm_kit/query/expressions/window.py +95 -0
- async_orm_kit/query/lookups/__init__.py +121 -0
- async_orm_kit/query/lookups/conditions.py +315 -0
- async_orm_kit/query/lookups/correlated.py +86 -0
- async_orm_kit/query/lookups/json_paths.py +167 -0
- async_orm_kit/query/lookups/parser.py +276 -0
- async_orm_kit/query/lookups/resolver/__init__.py +5 -0
- async_orm_kit/query/lookups/resolver/context.py +150 -0
- async_orm_kit/query/lookups/resolver/resolver.py +572 -0
- async_orm_kit/query/lookups/resolver/types.py +150 -0
- async_orm_kit/query/mixins/__init__.py +0 -0
- async_orm_kit/query/mixins/combinable.py +376 -0
- async_orm_kit/query/mixins/orderable.py +208 -0
- async_orm_kit/query/observability.py +346 -0
- async_orm_kit/query/planning/__init__.py +0 -0
- async_orm_kit/query/planning/directives.py +117 -0
- async_orm_kit/query/planning/factory.py +40 -0
- async_orm_kit/query/planning/passes/__init__.py +15 -0
- async_orm_kit/query/planning/passes/base.py +47 -0
- async_orm_kit/query/planning/passes/cartesian_detection.py +74 -0
- async_orm_kit/query/planning/passes/depth_splitting.py +78 -0
- async_orm_kit/query/planning/passes/join_promotion.py +81 -0
- async_orm_kit/query/planning/passes/path_conflicts.py +103 -0
- async_orm_kit/query/planning/passes/redundant_path.py +67 -0
- async_orm_kit/query/planning/passes/strategy_selection.py +49 -0
- async_orm_kit/query/planning/planner.py +160 -0
- async_orm_kit/query/planning/protocols.py +78 -0
- async_orm_kit/query/postgres/__init__.py +0 -0
- async_orm_kit/query/postgres/optimizer.py +189 -0
- async_orm_kit/query/q.py +596 -0
- async_orm_kit/query/relations/__init__.py +0 -0
- async_orm_kit/query/relations/enums.py +16 -0
- async_orm_kit/query/relations/graph.py +908 -0
- async_orm_kit/query/relations/node.py +421 -0
- async_orm_kit/queryset/__init__.py +4 -0
- async_orm_kit/queryset/builder.py +1259 -0
- async_orm_kit/queryset/compound.py +379 -0
- async_orm_kit/queryset/executor.py +718 -0
- async_orm_kit/queryset/queryset.py +1726 -0
- async_orm_kit/queryset/state.py +164 -0
- async_orm_kit/queryset/validators.py +361 -0
- async_orm_kit/queryset/writer.py +755 -0
- async_orm_kit/types.py +23 -0
- async_orm_kit-1.0.0.dist-info/METADATA +318 -0
- async_orm_kit-1.0.0.dist-info/RECORD +92 -0
- async_orm_kit-1.0.0.dist-info/WHEEL +4 -0
- 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"
|
async_orm_kit/config.py
ADDED
|
@@ -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"]
|