fastapi-toolsets 4.1.0__tar.gz → 4.1.1__tar.gz
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.
- {fastapi_toolsets-4.1.0 → fastapi_toolsets-4.1.1}/PKG-INFO +1 -1
- {fastapi_toolsets-4.1.0 → fastapi_toolsets-4.1.1}/pyproject.toml +1 -1
- {fastapi_toolsets-4.1.0 → fastapi_toolsets-4.1.1}/src/fastapi_toolsets/__init__.py +1 -1
- {fastapi_toolsets-4.1.0 → fastapi_toolsets-4.1.1}/src/fastapi_toolsets/db.py +35 -5
- {fastapi_toolsets-4.1.0 → fastapi_toolsets-4.1.1}/src/fastapi_toolsets/exceptions/__init__.py +4 -0
- {fastapi_toolsets-4.1.0 → fastapi_toolsets-4.1.1}/src/fastapi_toolsets/exceptions/exceptions.py +29 -0
- {fastapi_toolsets-4.1.0 → fastapi_toolsets-4.1.1}/LICENSE +0 -0
- {fastapi_toolsets-4.1.0 → fastapi_toolsets-4.1.1}/README.md +0 -0
- {fastapi_toolsets-4.1.0 → fastapi_toolsets-4.1.1}/src/fastapi_toolsets/_imports.py +0 -0
- {fastapi_toolsets-4.1.0 → fastapi_toolsets-4.1.1}/src/fastapi_toolsets/cli/__init__.py +0 -0
- {fastapi_toolsets-4.1.0 → fastapi_toolsets-4.1.1}/src/fastapi_toolsets/cli/app.py +0 -0
- {fastapi_toolsets-4.1.0 → fastapi_toolsets-4.1.1}/src/fastapi_toolsets/cli/commands/__init__.py +0 -0
- {fastapi_toolsets-4.1.0 → fastapi_toolsets-4.1.1}/src/fastapi_toolsets/cli/commands/fixtures.py +0 -0
- {fastapi_toolsets-4.1.0 → fastapi_toolsets-4.1.1}/src/fastapi_toolsets/cli/config.py +0 -0
- {fastapi_toolsets-4.1.0 → fastapi_toolsets-4.1.1}/src/fastapi_toolsets/cli/pyproject.py +0 -0
- {fastapi_toolsets-4.1.0 → fastapi_toolsets-4.1.1}/src/fastapi_toolsets/cli/utils.py +0 -0
- {fastapi_toolsets-4.1.0 → fastapi_toolsets-4.1.1}/src/fastapi_toolsets/crud/__init__.py +0 -0
- {fastapi_toolsets-4.1.0 → fastapi_toolsets-4.1.1}/src/fastapi_toolsets/crud/factory.py +0 -0
- {fastapi_toolsets-4.1.0 → fastapi_toolsets-4.1.1}/src/fastapi_toolsets/crud/search.py +0 -0
- {fastapi_toolsets-4.1.0 → fastapi_toolsets-4.1.1}/src/fastapi_toolsets/dependencies.py +0 -0
- {fastapi_toolsets-4.1.0 → fastapi_toolsets-4.1.1}/src/fastapi_toolsets/exceptions/handler.py +0 -0
- {fastapi_toolsets-4.1.0 → fastapi_toolsets-4.1.1}/src/fastapi_toolsets/fixtures/__init__.py +0 -0
- {fastapi_toolsets-4.1.0 → fastapi_toolsets-4.1.1}/src/fastapi_toolsets/fixtures/enum.py +0 -0
- {fastapi_toolsets-4.1.0 → fastapi_toolsets-4.1.1}/src/fastapi_toolsets/fixtures/registry.py +0 -0
- {fastapi_toolsets-4.1.0 → fastapi_toolsets-4.1.1}/src/fastapi_toolsets/fixtures/utils.py +0 -0
- {fastapi_toolsets-4.1.0 → fastapi_toolsets-4.1.1}/src/fastapi_toolsets/logger.py +0 -0
- {fastapi_toolsets-4.1.0 → fastapi_toolsets-4.1.1}/src/fastapi_toolsets/metrics/__init__.py +0 -0
- {fastapi_toolsets-4.1.0 → fastapi_toolsets-4.1.1}/src/fastapi_toolsets/metrics/handler.py +0 -0
- {fastapi_toolsets-4.1.0 → fastapi_toolsets-4.1.1}/src/fastapi_toolsets/metrics/registry.py +0 -0
- {fastapi_toolsets-4.1.0 → fastapi_toolsets-4.1.1}/src/fastapi_toolsets/models/__init__.py +0 -0
- {fastapi_toolsets-4.1.0 → fastapi_toolsets-4.1.1}/src/fastapi_toolsets/models/columns.py +0 -0
- {fastapi_toolsets-4.1.0 → fastapi_toolsets-4.1.1}/src/fastapi_toolsets/models/watched.py +0 -0
- {fastapi_toolsets-4.1.0 → fastapi_toolsets-4.1.1}/src/fastapi_toolsets/py.typed +0 -0
- {fastapi_toolsets-4.1.0 → fastapi_toolsets-4.1.1}/src/fastapi_toolsets/pytest/__init__.py +0 -0
- {fastapi_toolsets-4.1.0 → fastapi_toolsets-4.1.1}/src/fastapi_toolsets/pytest/plugin.py +0 -0
- {fastapi_toolsets-4.1.0 → fastapi_toolsets-4.1.1}/src/fastapi_toolsets/pytest/utils.py +0 -0
- {fastapi_toolsets-4.1.0 → fastapi_toolsets-4.1.1}/src/fastapi_toolsets/schemas.py +0 -0
- {fastapi_toolsets-4.1.0 → fastapi_toolsets-4.1.1}/src/fastapi_toolsets/security/__init__.py +0 -0
- {fastapi_toolsets-4.1.0 → fastapi_toolsets-4.1.1}/src/fastapi_toolsets/security/abc.py +0 -0
- {fastapi_toolsets-4.1.0 → fastapi_toolsets-4.1.1}/src/fastapi_toolsets/security/oauth.py +0 -0
- {fastapi_toolsets-4.1.0 → fastapi_toolsets-4.1.1}/src/fastapi_toolsets/security/sources/__init__.py +0 -0
- {fastapi_toolsets-4.1.0 → fastapi_toolsets-4.1.1}/src/fastapi_toolsets/security/sources/bearer.py +0 -0
- {fastapi_toolsets-4.1.0 → fastapi_toolsets-4.1.1}/src/fastapi_toolsets/security/sources/cookie.py +0 -0
- {fastapi_toolsets-4.1.0 → fastapi_toolsets-4.1.1}/src/fastapi_toolsets/security/sources/header.py +0 -0
- {fastapi_toolsets-4.1.0 → fastapi_toolsets-4.1.1}/src/fastapi_toolsets/security/sources/multi.py +0 -0
- {fastapi_toolsets-4.1.0 → fastapi_toolsets-4.1.1}/src/fastapi_toolsets/types.py +0 -0
|
@@ -6,13 +6,22 @@ from contextlib import AbstractAsyncContextManager, asynccontextmanager
|
|
|
6
6
|
from enum import Enum
|
|
7
7
|
from typing import Any, TypeVar, cast
|
|
8
8
|
|
|
9
|
+
import asyncpg
|
|
9
10
|
from sqlalchemy import Table, delete, text, tuple_
|
|
11
|
+
from sqlalchemy import exc as sa_exc
|
|
10
12
|
from sqlalchemy.dialects.postgresql import insert as pg_insert
|
|
11
13
|
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
|
|
12
14
|
from sqlalchemy.orm import DeclarativeBase, QueryableAttribute
|
|
13
15
|
from sqlalchemy.orm.relationships import RelationshipProperty
|
|
14
16
|
|
|
15
|
-
from .exceptions import NotFoundError
|
|
17
|
+
from .exceptions import LockTimeoutError, NotFoundError, PoolExhaustedError
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _is_lock_not_available(e: sa_exc.DBAPIError) -> bool:
|
|
21
|
+
return e.orig is not None and isinstance(
|
|
22
|
+
e.orig.__cause__, asyncpg.exceptions.LockNotAvailableError
|
|
23
|
+
)
|
|
24
|
+
|
|
16
25
|
|
|
17
26
|
__all__ = [
|
|
18
27
|
"LockMode",
|
|
@@ -65,7 +74,10 @@ def create_db_dependency(
|
|
|
65
74
|
|
|
66
75
|
async def get_db() -> AsyncGenerator[_SessionT, None]:
|
|
67
76
|
async with session_maker() as session:
|
|
68
|
-
|
|
77
|
+
try:
|
|
78
|
+
await session.connection()
|
|
79
|
+
except sa_exc.TimeoutError as e:
|
|
80
|
+
raise PoolExhaustedError() from e
|
|
69
81
|
yield session
|
|
70
82
|
if session.in_transaction():
|
|
71
83
|
await session.commit()
|
|
@@ -198,6 +210,18 @@ def lock_tables(
|
|
|
198
210
|
await session.execute(text(f"LOCK {table_names} IN {mode.value} MODE"))
|
|
199
211
|
yield session
|
|
200
212
|
await session.commit()
|
|
213
|
+
except sa_exc.TimeoutError as e:
|
|
214
|
+
await session.rollback()
|
|
215
|
+
raise PoolExhaustedError(
|
|
216
|
+
f"Connection pool exhausted while locking '{table_names}'. "
|
|
217
|
+
) from e
|
|
218
|
+
except sa_exc.DBAPIError as e:
|
|
219
|
+
await session.rollback()
|
|
220
|
+
if _is_lock_not_available(e):
|
|
221
|
+
raise LockTimeoutError(
|
|
222
|
+
f"Lock on '{table_names}' could not be acquired within {timeout}."
|
|
223
|
+
) from e
|
|
224
|
+
raise # pragma: no cover
|
|
201
225
|
except BaseException:
|
|
202
226
|
await session.rollback()
|
|
203
227
|
raise
|
|
@@ -229,8 +253,7 @@ async def advisory_lock(
|
|
|
229
253
|
is already held.
|
|
230
254
|
|
|
231
255
|
Raises:
|
|
232
|
-
|
|
233
|
-
in time.
|
|
256
|
+
LockTimeoutError: If *timeout* is set and the lock cannot be acquired in time.
|
|
234
257
|
|
|
235
258
|
Example:
|
|
236
259
|
```python
|
|
@@ -268,7 +291,14 @@ async def advisory_lock(
|
|
|
268
291
|
if timeout is not None and not nowait:
|
|
269
292
|
await session.execute(text(f"SET LOCAL lock_timeout='{timeout}'"))
|
|
270
293
|
|
|
271
|
-
|
|
294
|
+
try:
|
|
295
|
+
result = await session.execute(acquire_sql, params)
|
|
296
|
+
except sa_exc.DBAPIError as e:
|
|
297
|
+
if _is_lock_not_available(e):
|
|
298
|
+
raise LockTimeoutError(
|
|
299
|
+
f"Advisory lock {key!r} could not be acquired within {timeout}."
|
|
300
|
+
) from e
|
|
301
|
+
raise # pragma: no cover
|
|
272
302
|
acquired = result.scalar() if nowait else True
|
|
273
303
|
try:
|
|
274
304
|
yield acquired
|
{fastapi_toolsets-4.1.0 → fastapi_toolsets-4.1.1}/src/fastapi_toolsets/exceptions/__init__.py
RENAMED
|
@@ -8,8 +8,10 @@ from .exceptions import (
|
|
|
8
8
|
InvalidFacetFilterError,
|
|
9
9
|
InvalidOrderFieldError,
|
|
10
10
|
InvalidSearchColumnError,
|
|
11
|
+
LockTimeoutError,
|
|
11
12
|
NoSearchableFieldsError,
|
|
12
13
|
NotFoundError,
|
|
14
|
+
PoolExhaustedError,
|
|
13
15
|
UnauthorizedError,
|
|
14
16
|
UnsupportedFacetTypeError,
|
|
15
17
|
generate_error_responses,
|
|
@@ -26,8 +28,10 @@ __all__ = [
|
|
|
26
28
|
"InvalidFacetFilterError",
|
|
27
29
|
"InvalidOrderFieldError",
|
|
28
30
|
"InvalidSearchColumnError",
|
|
31
|
+
"LockTimeoutError",
|
|
29
32
|
"NoSearchableFieldsError",
|
|
30
33
|
"NotFoundError",
|
|
34
|
+
"PoolExhaustedError",
|
|
31
35
|
"UnauthorizedError",
|
|
32
36
|
"UnsupportedFacetTypeError",
|
|
33
37
|
]
|
{fastapi_toolsets-4.1.0 → fastapi_toolsets-4.1.1}/src/fastapi_toolsets/exceptions/exceptions.py
RENAMED
|
@@ -223,6 +223,35 @@ class InvalidOrderFieldError(ApiException):
|
|
|
223
223
|
)
|
|
224
224
|
|
|
225
225
|
|
|
226
|
+
class PoolExhaustedError(ApiException):
|
|
227
|
+
"""HTTP 503 - Database connection pool is exhausted."""
|
|
228
|
+
|
|
229
|
+
api_error = ApiError(
|
|
230
|
+
code=503,
|
|
231
|
+
msg="Service Unavailable",
|
|
232
|
+
desc=(
|
|
233
|
+
"The database connection pool is exhausted. "
|
|
234
|
+
"Too many concurrent requests are holding connections. "
|
|
235
|
+
"Retry shortly or contact support if the issue persists."
|
|
236
|
+
),
|
|
237
|
+
err_code="DB-503-POOL",
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
class LockTimeoutError(ApiException):
|
|
242
|
+
"""HTTP 503 - A database lock could not be acquired within the timeout."""
|
|
243
|
+
|
|
244
|
+
api_error = ApiError(
|
|
245
|
+
code=503,
|
|
246
|
+
msg="Service Unavailable",
|
|
247
|
+
desc=(
|
|
248
|
+
"A database lock could not be acquired within the allowed timeout. "
|
|
249
|
+
"The resource is under heavy contention. Retry shortly."
|
|
250
|
+
),
|
|
251
|
+
err_code="DB-503-LOCK",
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
|
|
226
255
|
def generate_error_responses(
|
|
227
256
|
*errors: type[ApiException],
|
|
228
257
|
) -> dict[int | str, dict[str, Any]]:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fastapi_toolsets-4.1.0 → fastapi_toolsets-4.1.1}/src/fastapi_toolsets/cli/commands/__init__.py
RENAMED
|
File without changes
|
{fastapi_toolsets-4.1.0 → fastapi_toolsets-4.1.1}/src/fastapi_toolsets/cli/commands/fixtures.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fastapi_toolsets-4.1.0 → fastapi_toolsets-4.1.1}/src/fastapi_toolsets/exceptions/handler.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fastapi_toolsets-4.1.0 → fastapi_toolsets-4.1.1}/src/fastapi_toolsets/security/sources/__init__.py
RENAMED
|
File without changes
|
{fastapi_toolsets-4.1.0 → fastapi_toolsets-4.1.1}/src/fastapi_toolsets/security/sources/bearer.py
RENAMED
|
File without changes
|
{fastapi_toolsets-4.1.0 → fastapi_toolsets-4.1.1}/src/fastapi_toolsets/security/sources/cookie.py
RENAMED
|
File without changes
|
{fastapi_toolsets-4.1.0 → fastapi_toolsets-4.1.1}/src/fastapi_toolsets/security/sources/header.py
RENAMED
|
File without changes
|
{fastapi_toolsets-4.1.0 → fastapi_toolsets-4.1.1}/src/fastapi_toolsets/security/sources/multi.py
RENAMED
|
File without changes
|
|
File without changes
|