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,38 @@
|
|
|
1
|
+
from sqlalchemy import String
|
|
2
|
+
|
|
3
|
+
from async_orm_kit.models.fields.typed.base import BaseValidatedType, ValidatorType
|
|
4
|
+
from async_orm_kit.models.fields.validators import (
|
|
5
|
+
MaxLengthValidator,
|
|
6
|
+
MinLengthValidator,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class StringField(BaseValidatedType):
|
|
11
|
+
cache_ok = True
|
|
12
|
+
impl = String
|
|
13
|
+
|
|
14
|
+
def __init__(
|
|
15
|
+
self,
|
|
16
|
+
max_length: int | None = None,
|
|
17
|
+
min_length: int | None = None,
|
|
18
|
+
validators: list[ValidatorType] | None = None,
|
|
19
|
+
**kwargs,
|
|
20
|
+
):
|
|
21
|
+
self.min_length = min_length
|
|
22
|
+
self.max_length = max_length
|
|
23
|
+
|
|
24
|
+
v: list[ValidatorType] = list(validators or [])
|
|
25
|
+
|
|
26
|
+
if max_length is not None:
|
|
27
|
+
v.append(MaxLengthValidator(max_length))
|
|
28
|
+
|
|
29
|
+
if min_length is not None:
|
|
30
|
+
v.append(MinLengthValidator(min_length))
|
|
31
|
+
|
|
32
|
+
super().__init__(validators=v, **kwargs)
|
|
33
|
+
|
|
34
|
+
def load_dialect_impl(self, dialect):
|
|
35
|
+
"""Build the dialect string type with the configured maximum length."""
|
|
36
|
+
if self.max_length:
|
|
37
|
+
return dialect.type_descriptor(String(self.max_length))
|
|
38
|
+
return dialect.type_descriptor(String())
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from async_orm_kit.models.fields.exceptions import FieldValidationError
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class FieldValidator:
|
|
8
|
+
def __call__(self, value: Any) -> None:
|
|
9
|
+
"""
|
|
10
|
+
Base validator interface.
|
|
11
|
+
|
|
12
|
+
Contract:
|
|
13
|
+
- Do nothing if `value` is valid.
|
|
14
|
+
- Raise FieldValidationError (or subclass) if invalid.
|
|
15
|
+
- Does NOT return or transform the value.
|
|
16
|
+
"""
|
|
17
|
+
return None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class MaxLengthValidator(FieldValidator):
|
|
21
|
+
def __init__(self, limit: int):
|
|
22
|
+
self.limit = limit
|
|
23
|
+
|
|
24
|
+
def __call__(self, value) -> None:
|
|
25
|
+
if value is None:
|
|
26
|
+
return
|
|
27
|
+
try:
|
|
28
|
+
length = len(value)
|
|
29
|
+
except TypeError:
|
|
30
|
+
raise FieldValidationError(
|
|
31
|
+
f"Ensure this value has at most {self.limit} characters "
|
|
32
|
+
f"(it has a non-measurable length).",
|
|
33
|
+
code="max_length",
|
|
34
|
+
) from None
|
|
35
|
+
if length > self.limit:
|
|
36
|
+
raise FieldValidationError(
|
|
37
|
+
f"Ensure this value has at most {self.limit} characters "
|
|
38
|
+
f"(it has {length}).",
|
|
39
|
+
code="max_length",
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class MinLengthValidator(FieldValidator):
|
|
44
|
+
def __init__(self, limit: int):
|
|
45
|
+
self.limit = limit
|
|
46
|
+
|
|
47
|
+
def __call__(self, value) -> None:
|
|
48
|
+
if value is None:
|
|
49
|
+
return
|
|
50
|
+
try:
|
|
51
|
+
length = len(value)
|
|
52
|
+
except TypeError:
|
|
53
|
+
raise FieldValidationError(
|
|
54
|
+
f"Ensure this value has at least {self.limit} characters "
|
|
55
|
+
f"(it has a non-measurable length).",
|
|
56
|
+
code="min_length",
|
|
57
|
+
) from None
|
|
58
|
+
if length < self.limit:
|
|
59
|
+
raise FieldValidationError(
|
|
60
|
+
f"Ensure this value has at least {self.limit} characters "
|
|
61
|
+
f"(it has {length}).",
|
|
62
|
+
code="min_length",
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class RangeValidator(FieldValidator):
|
|
67
|
+
def __init__(self, *, min: int | None = None, max: int | None = None):
|
|
68
|
+
self.min = min
|
|
69
|
+
self.max = max
|
|
70
|
+
|
|
71
|
+
def __call__(self, value) -> None:
|
|
72
|
+
if value is None:
|
|
73
|
+
return
|
|
74
|
+
|
|
75
|
+
# Non-numeric or non-orderable values
|
|
76
|
+
try:
|
|
77
|
+
_ = value < value # just to trigger TypeError for non-comparables
|
|
78
|
+
except TypeError:
|
|
79
|
+
raise FieldValidationError(
|
|
80
|
+
"Ensure this value is a comparable numeric-type.",
|
|
81
|
+
code="invalid_type",
|
|
82
|
+
) from None
|
|
83
|
+
|
|
84
|
+
if self.min is not None and value < self.min:
|
|
85
|
+
raise FieldValidationError(
|
|
86
|
+
f"Ensure this value is greater than or equal to {self.min} "
|
|
87
|
+
f"(it is {value}).",
|
|
88
|
+
code="min_value",
|
|
89
|
+
)
|
|
90
|
+
if self.max is not None and value > self.max:
|
|
91
|
+
raise FieldValidationError(
|
|
92
|
+
f"Ensure this value is less than or equal to {self.max} "
|
|
93
|
+
f"(it is {value}).",
|
|
94
|
+
code="max_value",
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class Regex(FieldValidator):
|
|
99
|
+
def __init__(self, pattern: str | re.Pattern, message: str = "Invalid format"):
|
|
100
|
+
self.pattern = re.compile(pattern) if isinstance(pattern, str) else pattern
|
|
101
|
+
self.message = message
|
|
102
|
+
|
|
103
|
+
def __call__(self, value) -> None:
|
|
104
|
+
if value is None:
|
|
105
|
+
return
|
|
106
|
+
if not self.pattern.match(value):
|
|
107
|
+
raise FieldValidationError(
|
|
108
|
+
self.message,
|
|
109
|
+
code="regex",
|
|
110
|
+
)
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from sqlalchemy.orm import DeclarativeBase
|
|
6
|
+
|
|
7
|
+
from async_orm_kit._logging import get_logger
|
|
8
|
+
from async_orm_kit.exceptions import MultipleObjectsReturned, ObjectDoesNotExist
|
|
9
|
+
from async_orm_kit.manager import Manager
|
|
10
|
+
from async_orm_kit.models.fields.typed.base import BaseValidatedType
|
|
11
|
+
|
|
12
|
+
logger = get_logger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Base(DeclarativeBase):
|
|
16
|
+
"""SQLAlchemy declarative base for all ORM models.
|
|
17
|
+
Provides the foundation for all model classes in the framework.
|
|
18
|
+
Models should inherit from ``Model`` rather than using this directly.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ModelMeta(type(Base)): # type: ignore[misc] # SQLAlchemy supplies the declarative metaclass dynamically.
|
|
25
|
+
"""Metaclass for automatic manager registration and model initialization.
|
|
26
|
+
|
|
27
|
+
Handles the setup of managers with observers and model-specific exceptions
|
|
28
|
+
during class creation. Automatically discovers managers from the class
|
|
29
|
+
hierarchy, creates model-specific exception classes, and registers observers.
|
|
30
|
+
|
|
31
|
+
Responsibilities:
|
|
32
|
+
- Create model-specific ``DoesNotExist`` and ``MultipleObjectsReturned``
|
|
33
|
+
exception classes for each model
|
|
34
|
+
- Discover and register Manager instances with their observers from class and parent classes
|
|
35
|
+
- Add default ``objects`` manager if no managers are defined
|
|
36
|
+
- Collect and register model and queryset observers from class hierarchy
|
|
37
|
+
- Call ``contribute_to_class()`` on each manager to bind it to the model
|
|
38
|
+
|
|
39
|
+
Class Attributes Created:
|
|
40
|
+
DoesNotExist: Model-specific exception for missing objects.
|
|
41
|
+
MultipleObjectsReturned: Model-specific exception for ambiguous queries.
|
|
42
|
+
_managers: List of manager attribute names on this model.
|
|
43
|
+
_default_manager: The default manager instance (typically ``objects``).
|
|
44
|
+
_model_observers: List of registered model lifecycle observers.
|
|
45
|
+
|
|
46
|
+
Example:
|
|
47
|
+
>>> class User(Model): # Uses ModelMeta automatically
|
|
48
|
+
... id: Mapped[int] = mapped_column(primary_key=True)
|
|
49
|
+
... username: Mapped[str]
|
|
50
|
+
...
|
|
51
|
+
... # Custom manager
|
|
52
|
+
... active = ActiveUserManager()
|
|
53
|
+
>>>
|
|
54
|
+
>>> # Model-specific exceptions created automatically
|
|
55
|
+
>>> try:
|
|
56
|
+
... await User.objects.get(id=999)
|
|
57
|
+
... except User.DoesNotExist:
|
|
58
|
+
... print("User not found")
|
|
59
|
+
>>>
|
|
60
|
+
>>> # Managers registered and accessible
|
|
61
|
+
>>> User._managers
|
|
62
|
+
['objects', 'active']
|
|
63
|
+
>>> User._default_manager
|
|
64
|
+
<Manager for User>
|
|
65
|
+
|
|
66
|
+
Note:
|
|
67
|
+
This metaclass is applied automatically when inheriting from ``Model``.
|
|
68
|
+
Abstract models (``__abstract__ = True``) skip manager registration.
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
def __new__(
|
|
72
|
+
mcs,
|
|
73
|
+
name: str,
|
|
74
|
+
bases: tuple[type, ...],
|
|
75
|
+
namespace: dict[str, Any],
|
|
76
|
+
**kwargs: Any,
|
|
77
|
+
) -> type:
|
|
78
|
+
"""Create and initialize a new model class.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
name: Name of the class being created.
|
|
82
|
+
bases: Tuple of base classes.
|
|
83
|
+
namespace: Class namespace dictionary containing attributes and methods.
|
|
84
|
+
**kwargs: Additional keyword arguments passed to parent metaclass.
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
Newly created and initialized model class with managers and
|
|
88
|
+
exceptions registered.
|
|
89
|
+
"""
|
|
90
|
+
# Create the class
|
|
91
|
+
cls = super().__new__(mcs, name, bases, namespace, **kwargs)
|
|
92
|
+
|
|
93
|
+
# Skip for abstract base classes
|
|
94
|
+
if namespace.get("__abstract__", False):
|
|
95
|
+
return cls
|
|
96
|
+
|
|
97
|
+
# Create model-specific exceptions
|
|
98
|
+
cls.DoesNotExist = type(
|
|
99
|
+
f"{name}.DoesNotExist",
|
|
100
|
+
(ObjectDoesNotExist,),
|
|
101
|
+
{"__module__": cls.__module__},
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
cls.MultipleObjectsReturned = type(
|
|
105
|
+
f"{name}.MultipleObjectsReturned",
|
|
106
|
+
(MultipleObjectsReturned,),
|
|
107
|
+
{"__module__": cls.__module__},
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
cls._model_field_types = {} # {field_name: field_instance}
|
|
111
|
+
cls._file_fields = {}
|
|
112
|
+
if hasattr(cls, "__table__"):
|
|
113
|
+
for column in cls.__table__.columns:
|
|
114
|
+
if isinstance(column.type, BaseValidatedType):
|
|
115
|
+
attr_name = column.key
|
|
116
|
+
attr = getattr(cls, attr_name)
|
|
117
|
+
column.type.contribute_to_class(cls, attr_name, attr)
|
|
118
|
+
|
|
119
|
+
# Collect inherited managers and observers in MRO order. Managers must
|
|
120
|
+
# be cloned for each concrete model because they hold a model binding and
|
|
121
|
+
# mutable hooks/observer registries.
|
|
122
|
+
inherited_managers: dict[str, Manager] = {}
|
|
123
|
+
observers: list[Any] = []
|
|
124
|
+
queryset_observers: list[Any] = []
|
|
125
|
+
|
|
126
|
+
def append_unique(target: list[Any], values: Any) -> None:
|
|
127
|
+
for value in values or ():
|
|
128
|
+
if all(existing is not value for existing in target):
|
|
129
|
+
target.append(value)
|
|
130
|
+
|
|
131
|
+
# Resolve manager attributes using normal Python MRO precedence. A
|
|
132
|
+
# nearer non-manager attribute intentionally shadows a base manager.
|
|
133
|
+
seen_attributes: set[str] = set()
|
|
134
|
+
for base in cls.__mro__[1:]:
|
|
135
|
+
for attr_name, attr_value in base.__dict__.items():
|
|
136
|
+
if attr_name in seen_attributes:
|
|
137
|
+
continue
|
|
138
|
+
seen_attributes.add(attr_name)
|
|
139
|
+
if isinstance(attr_value, Manager):
|
|
140
|
+
inherited_managers[attr_name] = attr_value
|
|
141
|
+
|
|
142
|
+
# Observer registration order is base-to-derived and stable. Identity
|
|
143
|
+
# de-duplication avoids requiring observer instances to be hashable.
|
|
144
|
+
for base in reversed(cls.__mro__[1:]):
|
|
145
|
+
append_unique(observers, base.__dict__.get("model_observers", ()))
|
|
146
|
+
append_unique(
|
|
147
|
+
queryset_observers,
|
|
148
|
+
base.__dict__.get("queryset_observers", ()),
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
managers: dict[str, Manager] = {
|
|
152
|
+
name: manager.clone_for_model()
|
|
153
|
+
for name, manager in inherited_managers.items()
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
# Explicit managers on this class override inherited managers.
|
|
157
|
+
for attr_name, attr_value in namespace.items():
|
|
158
|
+
if isinstance(attr_value, Manager):
|
|
159
|
+
managers[attr_name] = attr_value
|
|
160
|
+
|
|
161
|
+
append_unique(observers, namespace.get("model_observers", ()))
|
|
162
|
+
append_unique(queryset_observers, namespace.get("queryset_observers", ()))
|
|
163
|
+
|
|
164
|
+
# Add a fresh default manager when no manager is inherited or declared.
|
|
165
|
+
if not managers:
|
|
166
|
+
managers["objects"] = Manager()
|
|
167
|
+
|
|
168
|
+
# Bind every manager to this model and give it an independent observer list.
|
|
169
|
+
for manager_name, manager in managers.items():
|
|
170
|
+
manager._observers = list(manager._observers)
|
|
171
|
+
append_unique(manager._observers, queryset_observers)
|
|
172
|
+
manager.contribute_to_class(cls, manager_name)
|
|
173
|
+
|
|
174
|
+
cls._managers = list(managers)
|
|
175
|
+
cls._default_manager = managers.get("objects") or next(
|
|
176
|
+
iter(managers.values()), None
|
|
177
|
+
)
|
|
178
|
+
cls._model_observers = observers
|
|
179
|
+
cls._queryset_observers = queryset_observers
|
|
180
|
+
|
|
181
|
+
return cls
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from typing import ClassVar
|
|
2
|
+
|
|
3
|
+
from async_orm_kit.observers import BaseModelObserver, BaseQuerySetObserver
|
|
4
|
+
from async_orm_kit.observers.mixin import ObserverMixin
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ModelObserverMixin(ObserverMixin):
|
|
8
|
+
"""Mixin to add observer support to Model"""
|
|
9
|
+
|
|
10
|
+
model_observers: ClassVar[list[BaseModelObserver]] = []
|
|
11
|
+
_model_observers: ClassVar[list[BaseModelObserver]] = []
|
|
12
|
+
queryset_observers: ClassVar[list[BaseQuerySetObserver]] = []
|
|
13
|
+
_queryset_observers: ClassVar[list[BaseQuerySetObserver]] = []
|
|
14
|
+
|
|
15
|
+
async def _trigger_model_observers(self, event: str, **kwargs) -> None:
|
|
16
|
+
"""Trigger model-level observers"""
|
|
17
|
+
await self._trigger_observers(
|
|
18
|
+
self._model_observers, event, instance=self, **kwargs
|
|
19
|
+
)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import inspect
|
|
4
|
+
from collections.abc import Iterable
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ObserverMixin:
|
|
9
|
+
"""Shared, deterministic dispatcher for model and QuerySet observers."""
|
|
10
|
+
|
|
11
|
+
async def _trigger_observers(
|
|
12
|
+
self, observers: Iterable[Any], event: str, **kwargs: Any
|
|
13
|
+
) -> None:
|
|
14
|
+
"""Invoke observers in registration order.
|
|
15
|
+
|
|
16
|
+
Both synchronous and asynchronous handlers are supported. Exceptions are
|
|
17
|
+
intentionally propagated to the caller; observer failures must never be
|
|
18
|
+
hidden in untracked background tasks.
|
|
19
|
+
"""
|
|
20
|
+
for observer in observers:
|
|
21
|
+
handler = getattr(observer, event, None)
|
|
22
|
+
if handler is None:
|
|
23
|
+
continue
|
|
24
|
+
result = handler(**kwargs)
|
|
25
|
+
if inspect.isawaitable(result):
|
|
26
|
+
await result
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from typing import TYPE_CHECKING
|
|
2
|
+
|
|
3
|
+
if TYPE_CHECKING:
|
|
4
|
+
from async_orm_kit.models import Model
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class BaseModelObserver:
|
|
8
|
+
"""Base observer for single model instance lifecycle"""
|
|
9
|
+
|
|
10
|
+
async def pre_save(self, instance: "Model", **kwargs) -> None:
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
async def post_save(self, instance: "Model", created: bool, **kwargs) -> None:
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
async def pre_delete(self, instance: "Model", **kwargs) -> None:
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
async def post_delete(self, instance: "Model", **kwargs) -> None:
|
|
20
|
+
pass
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from typing import TYPE_CHECKING
|
|
2
|
+
|
|
3
|
+
if TYPE_CHECKING:
|
|
4
|
+
from async_orm_kit.queryset.queryset import QuerySet
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class BaseQuerySetObserver:
|
|
8
|
+
"""Base observer for queryset operations"""
|
|
9
|
+
|
|
10
|
+
async def pre_stream(self, queryset: "QuerySet", **kwargs) -> None:
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
async def post_stream(self, queryset: "QuerySet", **kwargs) -> None:
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
async def pre_execute(self, queryset: "QuerySet", **kwargs) -> None:
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
async def post_execute(self, queryset: "QuerySet", **kwargs) -> None:
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
async def pre_bulk_create(self, **kwargs) -> None:
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
async def post_bulk_create(self, **kwargs) -> None:
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
async def pre_bulk_update(self, queryset: "QuerySet", **kwargs) -> None:
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
async def post_bulk_update(self, queryset: "QuerySet", **kwargs) -> None:
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
async def pre_bulk_delete(self, queryset: "QuerySet", **kwargs) -> None:
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
async def post_bulk_delete(self, queryset: "QuerySet", **kwargs) -> None:
|
|
38
|
+
pass
|
async_orm_kit/py.typed
ADDED
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from sqlalchemy import ColumnElement, func
|
|
6
|
+
|
|
7
|
+
from async_orm_kit.query.aggregations.base import Aggregate
|
|
8
|
+
from async_orm_kit.query.lookups.resolver import ResolverContext
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from async_orm_kit.query.lookups.resolver import FieldResolver
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Avg(Aggregate):
|
|
15
|
+
"""Calculate average (mean) of numeric field values across grouped rows.
|
|
16
|
+
|
|
17
|
+
Generates SQL AVG() aggregate function for computing arithmetic mean of field
|
|
18
|
+
values. Supports DISTINCT modifier to average only unique values. Commonly used
|
|
19
|
+
with QuerySet.annotate() for per-group averages or QuerySet.aggregate() for
|
|
20
|
+
overall dataset average.
|
|
21
|
+
|
|
22
|
+
Handles NULL values per SQL standard: NULLs are excluded from calculation, and
|
|
23
|
+
AVG of all-NULL column returns NULL. Works with numeric types (integers, decimals,
|
|
24
|
+
floats) and may return decimal results even for integer inputs depending on
|
|
25
|
+
database dialect.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
field: Field to average. Accepts str column name, F() expression for computed
|
|
29
|
+
averages, Value() for literals, or Case() for conditional averaging.
|
|
30
|
+
distinct: Whether to average only distinct values. Useful for deduplicating
|
|
31
|
+
before averaging. Defaults to False.
|
|
32
|
+
**extra: Additional arguments like output_field for type coercion.
|
|
33
|
+
|
|
34
|
+
Returns:Decimal or float representing average value, or None if no non-NULL values.
|
|
35
|
+
|
|
36
|
+
Note:
|
|
37
|
+
Performance: AVG requires scanning all rows in group. On large datasets,
|
|
38
|
+
ensure grouped columns are indexed. DISTINCT adds deduplication overhead.
|
|
39
|
+
|
|
40
|
+
Database differences: PostgreSQL returns numeric type, MySQL returns double.
|
|
41
|
+
Use output_field parameter for consistent type handling across databases.
|
|
42
|
+
|
|
43
|
+
NULL handling: AVG(NULL, NULL, NULL) returns NULL, not 0. Use COALESCE or
|
|
44
|
+
Case() with default=Value(0) if you need zero for empty groups.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def resolve(
|
|
48
|
+
self,
|
|
49
|
+
field_resolver: FieldResolver,
|
|
50
|
+
context: ResolverContext = ResolverContext(),
|
|
51
|
+
) -> ColumnElement:
|
|
52
|
+
"""Resolve to SQLAlchemy AVG() function expression.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
field_resolver: FieldResolver for column resolution and join tracking.
|
|
56
|
+
context: ResolverContext for OR context and filter marking propagation.
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
SQLAlchemy func.avg() expression with optional DISTINCT modifier.
|
|
60
|
+
|
|
61
|
+
Example:
|
|
62
|
+
>>> # Generates: AVG(orders.amount)
|
|
63
|
+
>>> Avg("amount").resolve(field_resolver, context)
|
|
64
|
+
>>>
|
|
65
|
+
>>> # Generates: AVG(DISTINCT orders.amount)
|
|
66
|
+
>>> Avg("amount", distinct=True).resolve(field_resolver, context)
|
|
67
|
+
"""
|
|
68
|
+
col = self._resolve_column(field_resolver=field_resolver, context=context)
|
|
69
|
+
return self._finish(
|
|
70
|
+
func.avg(self._prepare_column(col, field_resolver, context))
|
|
71
|
+
)
|