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,134 @@
1
+ """Optional FastAPI integration for request-scoped SQLAlchemy sessions.
2
+
3
+ FastAPI is imported lazily by the exception-handler registration function.
4
+ Importing this module does not require the optional FastAPI package.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from collections.abc import AsyncIterator, Callable
10
+ from typing import Any
11
+
12
+ from sqlalchemy.ext.asyncio import AsyncSession
13
+
14
+ from async_orm_kit._logging import get_logger
15
+ from async_orm_kit.exceptions import (
16
+ DatabaseError,
17
+ MultipleObjectsReturned,
18
+ ObjectDoesNotExist,
19
+ )
20
+
21
+ logger = get_logger(__name__)
22
+
23
+ SessionFactory = Callable[[], AsyncSession]
24
+
25
+
26
+ def create_session_dependency(
27
+ session_factory: SessionFactory,
28
+ ) -> Callable[[], AsyncIterator[AsyncSession]]:
29
+ """Create a FastAPI yield dependency for one request-scoped AsyncSession.
30
+
31
+ The dependency closes the session after the request and rolls back an
32
+ active transaction when request execution raises or is cancelled. It does
33
+ not commit successful requests: transaction ownership remains explicit and
34
+ belongs to the application/service layer.
35
+
36
+ A SQLAlchemy AsyncSession is not safe for concurrent use by multiple tasks.
37
+ The dependency creates a fresh session for every request; do not pass the
38
+ yielded session into concurrently running tasks.
39
+
40
+ Args:
41
+ session_factory: A callable such as SQLAlchemy's async_sessionmaker.
42
+
43
+ Returns:
44
+ An async-generator dependency suitable for ``Depends(...)``.
45
+
46
+ Raises:
47
+ TypeError: If ``session_factory`` is not callable.
48
+ """
49
+ if not callable(session_factory):
50
+ raise TypeError("session_factory must be callable")
51
+
52
+ async def dependency() -> AsyncIterator[AsyncSession]:
53
+ async with session_factory() as session:
54
+ try:
55
+ yield session
56
+ except BaseException:
57
+ # Roll back only the transaction owned by this session. Preserve
58
+ # the original exception/cancellation if rollback itself fails.
59
+ if session.in_transaction():
60
+ try:
61
+ await session.rollback()
62
+ except Exception:
63
+ # Session context exit still closes the connection. The
64
+ # original request failure is more useful to propagate.
65
+ logger.exception("Request session rollback failed")
66
+ raise
67
+
68
+ dependency.__name__ = "get_async_orm_session"
69
+ dependency.__qualname__ = "get_async_orm_session"
70
+ return dependency
71
+
72
+
73
+ def register_exception_handlers(app: Any) -> Any:
74
+ """Register safe HTTP mappings for common Async ORM Kit exceptions.
75
+
76
+ Defaults:
77
+ * ``ObjectDoesNotExist`` -> HTTP 404.
78
+ * ``MultipleObjectsReturned`` -> HTTP 409.
79
+ * ``DatabaseError`` -> HTTP 500 with no database details disclosed.
80
+
81
+ Query construction errors are deliberately not translated globally because
82
+ they may indicate programming errors rather than invalid client input.
83
+ Applications can register more specific handlers for their own API contract.
84
+
85
+ Args:
86
+ app: A FastAPI application instance.
87
+
88
+ Returns:
89
+ The same application instance, allowing setup chaining.
90
+
91
+ Raises:
92
+ ImportError: If the optional FastAPI dependency is not installed.
93
+ TypeError: If ``app`` does not expose ``add_exception_handler``.
94
+ """
95
+ try:
96
+ from fastapi.responses import JSONResponse
97
+ except ImportError as exc:
98
+ raise ImportError(
99
+ "FastAPI integration requires the optional dependency. "
100
+ "Install async-orm-kit[fastapi]."
101
+ ) from exc
102
+
103
+ add_handler = getattr(app, "add_exception_handler", None)
104
+ if not callable(add_handler):
105
+ raise TypeError("app must be a FastAPI-compatible application")
106
+
107
+ async def object_not_found(request: Any, exc: ObjectDoesNotExist) -> JSONResponse:
108
+ return JSONResponse(
109
+ status_code=404,
110
+ content={"detail": "Resource not found"},
111
+ )
112
+
113
+ async def multiple_objects(
114
+ request: Any, exc: MultipleObjectsReturned
115
+ ) -> JSONResponse:
116
+ return JSONResponse(
117
+ status_code=409,
118
+ content={"detail": "Multiple resources matched the request"},
119
+ )
120
+
121
+ async def database_failure(request: Any, exc: DatabaseError) -> JSONResponse:
122
+ # Do not return exc.message, exc.cause, SQL, parameters, or credentials.
123
+ return JSONResponse(
124
+ status_code=500,
125
+ content={"detail": "A database operation failed"},
126
+ )
127
+
128
+ add_handler(ObjectDoesNotExist, object_not_found)
129
+ add_handler(MultipleObjectsReturned, multiple_objects)
130
+ add_handler(DatabaseError, database_failure)
131
+ return app
132
+
133
+
134
+ __all__ = ["create_session_dependency", "register_exception_handlers"]
@@ -0,0 +1,213 @@
1
+ """Adapter for the separately distributed ``async-storage-kit`` package.
2
+
3
+ The adapter intentionally uses structural typing and does not import
4
+ ``async_storage_kit`` at runtime. Applications install the optional ``storage``
5
+ extra and pass a configured backend (or registry proxy) to the adapter.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import io
11
+ from collections.abc import AsyncIterator
12
+ from typing import Any, Literal, Protocol, runtime_checkable
13
+
14
+ ConflictPolicy = Literal["rename", "overwrite", "error"]
15
+
16
+
17
+ @runtime_checkable
18
+ class AsyncStorage(Protocol):
19
+ """The stable common subset of async-storage-kit's public contract."""
20
+
21
+ async def save(
22
+ self,
23
+ name: str,
24
+ content: Any,
25
+ max_length: int | None = None,
26
+ *,
27
+ on_conflict: ConflictPolicy = "rename",
28
+ ) -> str: ...
29
+
30
+ def iter_chunks(
31
+ self, name: str, chunk_size: int = 256 * 1024
32
+ ) -> AsyncIterator[bytes]: ...
33
+
34
+ async def read(self, name: str) -> bytes: ...
35
+
36
+ async def delete(self, name: str, raise_on_missing: bool = False) -> bool: ...
37
+
38
+ async def exists(self, name: str) -> bool: ...
39
+
40
+ async def size(self, name: str) -> int: ...
41
+
42
+ async def get_modified_time(self, name: str) -> Any: ...
43
+
44
+ async def metadata(self, name: str) -> Any: ...
45
+
46
+
47
+ class LazyStorageProxyLike(Protocol):
48
+ """Structural type for async-storage-kit's lazily resolving proxy."""
49
+
50
+ @property
51
+ def backend(self) -> AsyncStorage: ...
52
+
53
+
54
+ class AsyncStorageKitAdapter:
55
+ """Adapt an ``async-storage-kit`` backend to ORM ``FileField``.
56
+
57
+ ``async-storage-kit`` exposes byte reads and async chunk iteration instead
58
+ of an ``open()`` method. The legacy ``FieldFile.open()`` API is therefore
59
+ implemented with an in-memory ``BytesIO``; use ``FieldFile.iter_chunks()``
60
+ for bounded-memory downloads.
61
+
62
+ Args:
63
+ storage: Configured ``async-storage-kit`` backend or registry proxy.
64
+ on_conflict: Default save collision policy (``rename``, ``overwrite``
65
+ or ``error``). The storage backend performs collision handling.
66
+
67
+ The adapter does not own or close the backend. Configure backend lifetime
68
+ at application scope and inject this adapter into ``FileField(storage=...)``.
69
+ """
70
+
71
+ _CONFLICT_POLICIES = frozenset({"rename", "overwrite", "error"})
72
+
73
+ def __init__(
74
+ self,
75
+ storage: AsyncStorage | LazyStorageProxyLike,
76
+ *,
77
+ on_conflict: ConflictPolicy = "rename",
78
+ ) -> None:
79
+ if on_conflict not in self._CONFLICT_POLICIES:
80
+ raise ValueError("on_conflict must be 'rename', 'overwrite', or 'error'.")
81
+
82
+ if storage is None:
83
+ raise TypeError("storage must be a configured async storage backend")
84
+
85
+ # Do not inspect methods here: async-storage-kit's LazyStorageProxy
86
+ # resolves its backend from __getattr__, so eager validation would
87
+ # defeat lazy construction. Required methods are checked when invoked.
88
+ self._storage = storage
89
+ self._on_conflict = on_conflict
90
+
91
+ @property
92
+ def on_conflict(self) -> ConflictPolicy:
93
+ return self._on_conflict
94
+
95
+ def _required_method(self, name: str) -> Any:
96
+ method = getattr(self._storage, name, None)
97
+ if not callable(method):
98
+ raise TypeError(
99
+ f"Storage backend does not implement required method {name}()"
100
+ )
101
+ return method
102
+
103
+ @property
104
+ def backend(self) -> AsyncStorage | LazyStorageProxyLike:
105
+ """Return the injected backend or proxy without resolving or owning it."""
106
+ return self._storage
107
+
108
+ async def save(
109
+ self,
110
+ name: str,
111
+ content: Any,
112
+ *,
113
+ max_length: int | None = None,
114
+ ) -> str:
115
+ saved_name = await self._required_method("save")(
116
+ name,
117
+ content,
118
+ max_length=max_length,
119
+ on_conflict=self.on_conflict,
120
+ )
121
+ if not isinstance(saved_name, str) or not saved_name:
122
+ raise TypeError("Storage backend save() must return a non-empty name.")
123
+ return saved_name
124
+
125
+ async def open(self, name: str, mode: str = "rb") -> io.BytesIO:
126
+ """Return a buffered binary file handle for the legacy FileField API.
127
+
128
+ This operation materializes the entire object in memory. Prefer
129
+ ``iter_chunks()`` for large objects.
130
+ """
131
+ if mode != "rb":
132
+ raise ValueError(
133
+ "AsyncStorageKitAdapter.open() supports binary read mode 'rb' only."
134
+ )
135
+ return io.BytesIO(await self._required_method("read")(name))
136
+
137
+ async def read(self, name: str) -> bytes:
138
+ return await self._required_method("read")(name)
139
+
140
+ def iter_chunks(
141
+ self, name: str, chunk_size: int = 256 * 1024
142
+ ) -> AsyncIterator[bytes]:
143
+ if isinstance(chunk_size, bool) or not isinstance(chunk_size, int):
144
+ raise TypeError("chunk_size must be an integer")
145
+ if chunk_size < 1:
146
+ raise ValueError("chunk_size must be positive")
147
+ return self._required_method("iter_chunks")(name, chunk_size=chunk_size)
148
+
149
+ async def delete(self, name: str) -> None:
150
+ await self._required_method("delete")(name)
151
+
152
+ async def exists(self, name: str) -> bool:
153
+ return await self._required_method("exists")(name)
154
+
155
+ async def size(self, name: str) -> int:
156
+ return await self._required_method("size")(name)
157
+
158
+ def generate_filename(self, name: str) -> str:
159
+ """Use the backend's public filename helper when it provides one.
160
+
161
+ ``generate_filename`` is a convenience on async-storage-kit's concrete
162
+ ``StorageBackend`` but is intentionally not required by its structural
163
+ ``AsyncStorage`` protocol. Custom protocol implementations may validate
164
+ names in ``save()`` and therefore need no extra helper.
165
+ """
166
+ generator = getattr(self._storage, "generate_filename", None)
167
+ return generator(name) if callable(generator) else name
168
+
169
+ def path(self, name: str) -> str:
170
+ method = getattr(self._storage, "path", None)
171
+ if not callable(method):
172
+ raise NotImplementedError(
173
+ "The configured storage has no local path capability"
174
+ )
175
+ return method(name)
176
+
177
+ def url(self, name: str) -> str:
178
+ method = getattr(self._storage, "url", None)
179
+ if not callable(method):
180
+ raise NotImplementedError(
181
+ "The configured storage has no public URL capability"
182
+ )
183
+ return method(name)
184
+
185
+ async def get_presigned_url(self, name: str, expire: int = 3600) -> str:
186
+ method = getattr(self._storage, "get_presigned_url", None)
187
+ if not callable(method):
188
+ raise NotImplementedError(
189
+ "The configured storage has no signed URL capability"
190
+ )
191
+ return await method(name, expire=expire)
192
+
193
+ async def get_modified_time(self, name: str) -> Any:
194
+ return await self._required_method("get_modified_time")(name)
195
+
196
+ async def get_accessed_time(self, name: str) -> Any:
197
+ method = getattr(self._storage, "get_accessed_time", None)
198
+ if not callable(method):
199
+ raise NotImplementedError(
200
+ "The configured storage does not track access time"
201
+ )
202
+ return await method(name)
203
+
204
+ async def get_created_time(self, name: str) -> Any:
205
+ method = getattr(self._storage, "get_created_time", None)
206
+ if not callable(method):
207
+ raise NotImplementedError(
208
+ "The configured storage does not track creation time"
209
+ )
210
+ return await method(name)
211
+
212
+ async def metadata(self, name: str) -> Any:
213
+ return await self._required_method("metadata")(name)
@@ -0,0 +1,3 @@
1
+ from .manager import Manager
2
+
3
+ __all__ = ["Manager"]