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.
Files changed (46) hide show
  1. {fastapi_toolsets-4.1.0 → fastapi_toolsets-4.1.1}/PKG-INFO +1 -1
  2. {fastapi_toolsets-4.1.0 → fastapi_toolsets-4.1.1}/pyproject.toml +1 -1
  3. {fastapi_toolsets-4.1.0 → fastapi_toolsets-4.1.1}/src/fastapi_toolsets/__init__.py +1 -1
  4. {fastapi_toolsets-4.1.0 → fastapi_toolsets-4.1.1}/src/fastapi_toolsets/db.py +35 -5
  5. {fastapi_toolsets-4.1.0 → fastapi_toolsets-4.1.1}/src/fastapi_toolsets/exceptions/__init__.py +4 -0
  6. {fastapi_toolsets-4.1.0 → fastapi_toolsets-4.1.1}/src/fastapi_toolsets/exceptions/exceptions.py +29 -0
  7. {fastapi_toolsets-4.1.0 → fastapi_toolsets-4.1.1}/LICENSE +0 -0
  8. {fastapi_toolsets-4.1.0 → fastapi_toolsets-4.1.1}/README.md +0 -0
  9. {fastapi_toolsets-4.1.0 → fastapi_toolsets-4.1.1}/src/fastapi_toolsets/_imports.py +0 -0
  10. {fastapi_toolsets-4.1.0 → fastapi_toolsets-4.1.1}/src/fastapi_toolsets/cli/__init__.py +0 -0
  11. {fastapi_toolsets-4.1.0 → fastapi_toolsets-4.1.1}/src/fastapi_toolsets/cli/app.py +0 -0
  12. {fastapi_toolsets-4.1.0 → fastapi_toolsets-4.1.1}/src/fastapi_toolsets/cli/commands/__init__.py +0 -0
  13. {fastapi_toolsets-4.1.0 → fastapi_toolsets-4.1.1}/src/fastapi_toolsets/cli/commands/fixtures.py +0 -0
  14. {fastapi_toolsets-4.1.0 → fastapi_toolsets-4.1.1}/src/fastapi_toolsets/cli/config.py +0 -0
  15. {fastapi_toolsets-4.1.0 → fastapi_toolsets-4.1.1}/src/fastapi_toolsets/cli/pyproject.py +0 -0
  16. {fastapi_toolsets-4.1.0 → fastapi_toolsets-4.1.1}/src/fastapi_toolsets/cli/utils.py +0 -0
  17. {fastapi_toolsets-4.1.0 → fastapi_toolsets-4.1.1}/src/fastapi_toolsets/crud/__init__.py +0 -0
  18. {fastapi_toolsets-4.1.0 → fastapi_toolsets-4.1.1}/src/fastapi_toolsets/crud/factory.py +0 -0
  19. {fastapi_toolsets-4.1.0 → fastapi_toolsets-4.1.1}/src/fastapi_toolsets/crud/search.py +0 -0
  20. {fastapi_toolsets-4.1.0 → fastapi_toolsets-4.1.1}/src/fastapi_toolsets/dependencies.py +0 -0
  21. {fastapi_toolsets-4.1.0 → fastapi_toolsets-4.1.1}/src/fastapi_toolsets/exceptions/handler.py +0 -0
  22. {fastapi_toolsets-4.1.0 → fastapi_toolsets-4.1.1}/src/fastapi_toolsets/fixtures/__init__.py +0 -0
  23. {fastapi_toolsets-4.1.0 → fastapi_toolsets-4.1.1}/src/fastapi_toolsets/fixtures/enum.py +0 -0
  24. {fastapi_toolsets-4.1.0 → fastapi_toolsets-4.1.1}/src/fastapi_toolsets/fixtures/registry.py +0 -0
  25. {fastapi_toolsets-4.1.0 → fastapi_toolsets-4.1.1}/src/fastapi_toolsets/fixtures/utils.py +0 -0
  26. {fastapi_toolsets-4.1.0 → fastapi_toolsets-4.1.1}/src/fastapi_toolsets/logger.py +0 -0
  27. {fastapi_toolsets-4.1.0 → fastapi_toolsets-4.1.1}/src/fastapi_toolsets/metrics/__init__.py +0 -0
  28. {fastapi_toolsets-4.1.0 → fastapi_toolsets-4.1.1}/src/fastapi_toolsets/metrics/handler.py +0 -0
  29. {fastapi_toolsets-4.1.0 → fastapi_toolsets-4.1.1}/src/fastapi_toolsets/metrics/registry.py +0 -0
  30. {fastapi_toolsets-4.1.0 → fastapi_toolsets-4.1.1}/src/fastapi_toolsets/models/__init__.py +0 -0
  31. {fastapi_toolsets-4.1.0 → fastapi_toolsets-4.1.1}/src/fastapi_toolsets/models/columns.py +0 -0
  32. {fastapi_toolsets-4.1.0 → fastapi_toolsets-4.1.1}/src/fastapi_toolsets/models/watched.py +0 -0
  33. {fastapi_toolsets-4.1.0 → fastapi_toolsets-4.1.1}/src/fastapi_toolsets/py.typed +0 -0
  34. {fastapi_toolsets-4.1.0 → fastapi_toolsets-4.1.1}/src/fastapi_toolsets/pytest/__init__.py +0 -0
  35. {fastapi_toolsets-4.1.0 → fastapi_toolsets-4.1.1}/src/fastapi_toolsets/pytest/plugin.py +0 -0
  36. {fastapi_toolsets-4.1.0 → fastapi_toolsets-4.1.1}/src/fastapi_toolsets/pytest/utils.py +0 -0
  37. {fastapi_toolsets-4.1.0 → fastapi_toolsets-4.1.1}/src/fastapi_toolsets/schemas.py +0 -0
  38. {fastapi_toolsets-4.1.0 → fastapi_toolsets-4.1.1}/src/fastapi_toolsets/security/__init__.py +0 -0
  39. {fastapi_toolsets-4.1.0 → fastapi_toolsets-4.1.1}/src/fastapi_toolsets/security/abc.py +0 -0
  40. {fastapi_toolsets-4.1.0 → fastapi_toolsets-4.1.1}/src/fastapi_toolsets/security/oauth.py +0 -0
  41. {fastapi_toolsets-4.1.0 → fastapi_toolsets-4.1.1}/src/fastapi_toolsets/security/sources/__init__.py +0 -0
  42. {fastapi_toolsets-4.1.0 → fastapi_toolsets-4.1.1}/src/fastapi_toolsets/security/sources/bearer.py +0 -0
  43. {fastapi_toolsets-4.1.0 → fastapi_toolsets-4.1.1}/src/fastapi_toolsets/security/sources/cookie.py +0 -0
  44. {fastapi_toolsets-4.1.0 → fastapi_toolsets-4.1.1}/src/fastapi_toolsets/security/sources/header.py +0 -0
  45. {fastapi_toolsets-4.1.0 → fastapi_toolsets-4.1.1}/src/fastapi_toolsets/security/sources/multi.py +0 -0
  46. {fastapi_toolsets-4.1.0 → fastapi_toolsets-4.1.1}/src/fastapi_toolsets/types.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastapi-toolsets
3
- Version: 4.1.0
3
+ Version: 4.1.1
4
4
  Summary: Production-ready utilities for FastAPI applications
5
5
  Keywords: fastapi,sqlalchemy,postgresql
6
6
  Author: d3vyce
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "fastapi-toolsets"
3
- version = "4.1.0"
3
+ version = "4.1.1"
4
4
  description = "Production-ready utilities for FastAPI applications"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -21,4 +21,4 @@ Example usage:
21
21
  return Response(data={"user": user.username}, message="Success")
22
22
  """
23
23
 
24
- __version__ = "4.1.0"
24
+ __version__ = "4.1.1"
@@ -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
- await session.connection()
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
- sqlalchemy.exc.DBAPIError: If *timeout* is set and the lock cannot be acquired
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
- result = await session.execute(acquire_sql, params)
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
@@ -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
  ]
@@ -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]]: