tempest-fastapi-sdk 0.1.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.
- tempest_fastapi_sdk/__init__.py +110 -0
- tempest_fastapi_sdk/api/__init__.py +11 -0
- tempest_fastapi_sdk/api/handlers.py +59 -0
- tempest_fastapi_sdk/db/__init__.py +14 -0
- tempest_fastapi_sdk/db/_alembic_templates/__init__.py +1 -0
- tempest_fastapi_sdk/db/_alembic_templates/env.py.template +73 -0
- tempest_fastapi_sdk/db/connection.py +257 -0
- tempest_fastapi_sdk/db/migrations.py +353 -0
- tempest_fastapi_sdk/db/model.py +249 -0
- tempest_fastapi_sdk/db/repository.py +716 -0
- tempest_fastapi_sdk/exceptions/__init__.py +29 -0
- tempest_fastapi_sdk/exceptions/base.py +66 -0
- tempest_fastapi_sdk/exceptions/conflict.py +22 -0
- tempest_fastapi_sdk/exceptions/forbidden.py +18 -0
- tempest_fastapi_sdk/exceptions/jwt.py +25 -0
- tempest_fastapi_sdk/exceptions/not_found.py +22 -0
- tempest_fastapi_sdk/exceptions/unauthorized.py +23 -0
- tempest_fastapi_sdk/exceptions/upload.py +27 -0
- tempest_fastapi_sdk/exceptions/validation.py +22 -0
- tempest_fastapi_sdk/schemas/__init__.py +15 -0
- tempest_fastapi_sdk/schemas/base.py +64 -0
- tempest_fastapi_sdk/schemas/pagination.py +132 -0
- tempest_fastapi_sdk/schemas/response.py +69 -0
- tempest_fastapi_sdk/settings/__init__.py +7 -0
- tempest_fastapi_sdk/settings/base.py +43 -0
- tempest_fastapi_sdk/utils/__init__.py +61 -0
- tempest_fastapi_sdk/utils/datetime.py +35 -0
- tempest_fastapi_sdk/utils/dict.py +36 -0
- tempest_fastapi_sdk/utils/email.py +152 -0
- tempest_fastapi_sdk/utils/jwt.py +141 -0
- tempest_fastapi_sdk/utils/password.py +76 -0
- tempest_fastapi_sdk/utils/regex.py +275 -0
- tempest_fastapi_sdk/utils/upload.py +197 -0
- tempest_fastapi_sdk-0.1.0.dist-info/METADATA +1499 -0
- tempest_fastapi_sdk-0.1.0.dist-info/RECORD +36 -0
- tempest_fastapi_sdk-0.1.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""tempest-fastapi-sdk — shared FastAPI/SQLAlchemy/Pydantic primitives."""
|
|
2
|
+
|
|
3
|
+
from tempest_fastapi_sdk.api import (
|
|
4
|
+
app_exception_handler,
|
|
5
|
+
register_exception_handlers,
|
|
6
|
+
)
|
|
7
|
+
from tempest_fastapi_sdk.db import (
|
|
8
|
+
NAMING_CONVENTION,
|
|
9
|
+
AlembicHelper,
|
|
10
|
+
AsyncDatabaseManager,
|
|
11
|
+
BaseModel,
|
|
12
|
+
BaseRepository,
|
|
13
|
+
)
|
|
14
|
+
from tempest_fastapi_sdk.exceptions import (
|
|
15
|
+
AppException,
|
|
16
|
+
ConflictException,
|
|
17
|
+
ExpiredTokenException,
|
|
18
|
+
FileTooLargeException,
|
|
19
|
+
ForbiddenException,
|
|
20
|
+
InvalidFileTypeException,
|
|
21
|
+
InvalidTokenException,
|
|
22
|
+
NotFoundException,
|
|
23
|
+
UnauthorizedException,
|
|
24
|
+
ValidationException,
|
|
25
|
+
)
|
|
26
|
+
from tempest_fastapi_sdk.schemas import (
|
|
27
|
+
BasePaginationFilterSchema,
|
|
28
|
+
BasePaginationSchema,
|
|
29
|
+
BaseResponseSchema,
|
|
30
|
+
BaseSchema,
|
|
31
|
+
)
|
|
32
|
+
from tempest_fastapi_sdk.settings import BaseAppSettings
|
|
33
|
+
from tempest_fastapi_sdk.utils import (
|
|
34
|
+
CNPJ,
|
|
35
|
+
CNPJ_PATTERN,
|
|
36
|
+
CPF,
|
|
37
|
+
CPF_CNPJ_PATTERN,
|
|
38
|
+
CPF_PATTERN,
|
|
39
|
+
PHONE_BR_PATTERN,
|
|
40
|
+
CPFOrCNPJ,
|
|
41
|
+
EmailUtils,
|
|
42
|
+
JWTUtils,
|
|
43
|
+
PasswordUtils,
|
|
44
|
+
PhoneBR,
|
|
45
|
+
UploadUtils,
|
|
46
|
+
is_valid_cnpj,
|
|
47
|
+
is_valid_cpf,
|
|
48
|
+
is_valid_cpf_cnpj,
|
|
49
|
+
is_valid_phone_br,
|
|
50
|
+
modify_dict,
|
|
51
|
+
normalize_cnpj,
|
|
52
|
+
normalize_cpf,
|
|
53
|
+
normalize_cpf_cnpj,
|
|
54
|
+
normalize_phone_br,
|
|
55
|
+
only_digits,
|
|
56
|
+
to_utc,
|
|
57
|
+
utcnow,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
__version__: str = "0.1.0"
|
|
61
|
+
|
|
62
|
+
__all__: list[str] = [
|
|
63
|
+
"CNPJ",
|
|
64
|
+
"CNPJ_PATTERN",
|
|
65
|
+
"CPF",
|
|
66
|
+
"CPF_CNPJ_PATTERN",
|
|
67
|
+
"CPF_PATTERN",
|
|
68
|
+
"NAMING_CONVENTION",
|
|
69
|
+
"PHONE_BR_PATTERN",
|
|
70
|
+
"AlembicHelper",
|
|
71
|
+
"AppException",
|
|
72
|
+
"AsyncDatabaseManager",
|
|
73
|
+
"BaseAppSettings",
|
|
74
|
+
"BaseModel",
|
|
75
|
+
"BasePaginationFilterSchema",
|
|
76
|
+
"BasePaginationSchema",
|
|
77
|
+
"BaseRepository",
|
|
78
|
+
"BaseResponseSchema",
|
|
79
|
+
"BaseSchema",
|
|
80
|
+
"CPFOrCNPJ",
|
|
81
|
+
"ConflictException",
|
|
82
|
+
"EmailUtils",
|
|
83
|
+
"ExpiredTokenException",
|
|
84
|
+
"FileTooLargeException",
|
|
85
|
+
"ForbiddenException",
|
|
86
|
+
"InvalidFileTypeException",
|
|
87
|
+
"InvalidTokenException",
|
|
88
|
+
"JWTUtils",
|
|
89
|
+
"NotFoundException",
|
|
90
|
+
"PasswordUtils",
|
|
91
|
+
"PhoneBR",
|
|
92
|
+
"UnauthorizedException",
|
|
93
|
+
"UploadUtils",
|
|
94
|
+
"ValidationException",
|
|
95
|
+
"__version__",
|
|
96
|
+
"app_exception_handler",
|
|
97
|
+
"is_valid_cnpj",
|
|
98
|
+
"is_valid_cpf",
|
|
99
|
+
"is_valid_cpf_cnpj",
|
|
100
|
+
"is_valid_phone_br",
|
|
101
|
+
"modify_dict",
|
|
102
|
+
"normalize_cnpj",
|
|
103
|
+
"normalize_cpf",
|
|
104
|
+
"normalize_cpf_cnpj",
|
|
105
|
+
"normalize_phone_br",
|
|
106
|
+
"only_digits",
|
|
107
|
+
"register_exception_handlers",
|
|
108
|
+
"to_utc",
|
|
109
|
+
"utcnow",
|
|
110
|
+
]
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""FastAPI integration primitives exposed at module level."""
|
|
2
|
+
|
|
3
|
+
from tempest_fastapi_sdk.api.handlers import (
|
|
4
|
+
app_exception_handler,
|
|
5
|
+
register_exception_handlers,
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
__all__: list[str] = [
|
|
9
|
+
"app_exception_handler",
|
|
10
|
+
"register_exception_handlers",
|
|
11
|
+
]
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""FastAPI exception handlers for ``AppException`` and friends."""
|
|
2
|
+
|
|
3
|
+
from fastapi import FastAPI, Request
|
|
4
|
+
from fastapi.responses import JSONResponse
|
|
5
|
+
|
|
6
|
+
from tempest_fastapi_sdk.exceptions.base import AppException
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
async def app_exception_handler(
|
|
10
|
+
request: Request,
|
|
11
|
+
exc: AppException,
|
|
12
|
+
) -> JSONResponse:
|
|
13
|
+
"""Serialize an :class:`AppException` to the SDK JSON envelope.
|
|
14
|
+
|
|
15
|
+
Emits the shape::
|
|
16
|
+
|
|
17
|
+
{
|
|
18
|
+
"detail": "<message>",
|
|
19
|
+
"code": "<machine-readable code>",
|
|
20
|
+
"details": {...}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
request (Request): The incoming HTTP request. Unused — kept
|
|
25
|
+
for signature compatibility with FastAPI handlers.
|
|
26
|
+
exc (AppException): The exception raised.
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
JSONResponse: The serialized response.
|
|
30
|
+
"""
|
|
31
|
+
del request
|
|
32
|
+
return JSONResponse(
|
|
33
|
+
status_code=exc.status_code,
|
|
34
|
+
content={
|
|
35
|
+
"detail": exc.detail,
|
|
36
|
+
"code": exc.code,
|
|
37
|
+
"details": exc.details,
|
|
38
|
+
},
|
|
39
|
+
headers=exc.headers,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def register_exception_handlers(app: FastAPI) -> None:
|
|
44
|
+
"""Register the SDK's exception handlers on a FastAPI app.
|
|
45
|
+
|
|
46
|
+
Wires :class:`AppException` to :func:`app_exception_handler` so
|
|
47
|
+
every subclass returned by routers, services and repositories is
|
|
48
|
+
serialized consistently.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
app (FastAPI): The FastAPI application to wire.
|
|
52
|
+
"""
|
|
53
|
+
app.add_exception_handler(AppException, app_exception_handler) # type: ignore[arg-type]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
__all__: list[str] = [
|
|
57
|
+
"app_exception_handler",
|
|
58
|
+
"register_exception_handlers",
|
|
59
|
+
]
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Database primitives exposed at module level."""
|
|
2
|
+
|
|
3
|
+
from tempest_fastapi_sdk.db.connection import AsyncDatabaseManager
|
|
4
|
+
from tempest_fastapi_sdk.db.migrations import AlembicHelper
|
|
5
|
+
from tempest_fastapi_sdk.db.model import NAMING_CONVENTION, BaseModel
|
|
6
|
+
from tempest_fastapi_sdk.db.repository import BaseRepository
|
|
7
|
+
|
|
8
|
+
__all__: list[str] = [
|
|
9
|
+
"NAMING_CONVENTION",
|
|
10
|
+
"AlembicHelper",
|
|
11
|
+
"AsyncDatabaseManager",
|
|
12
|
+
"BaseModel",
|
|
13
|
+
"BaseRepository",
|
|
14
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Bundled Alembic templates loaded via importlib.resources."""
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Alembic environment script.
|
|
2
|
+
|
|
3
|
+
Generated by tempest-fastapi-sdk. Edit the metadata import below
|
|
4
|
+
when your project moves the BaseModel re-export.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import asyncio
|
|
8
|
+
from logging.config import fileConfig
|
|
9
|
+
|
|
10
|
+
from alembic import context
|
|
11
|
+
from sqlalchemy import pool
|
|
12
|
+
from sqlalchemy.engine import Connection
|
|
13
|
+
from sqlalchemy.ext.asyncio import async_engine_from_config
|
|
14
|
+
|
|
15
|
+
# >>> tempest-fastapi-sdk: metadata import (edit if needed) <<<
|
|
16
|
+
__METADATA_IMPORT__
|
|
17
|
+
# >>> end tempest-fastapi-sdk block <<<
|
|
18
|
+
|
|
19
|
+
config = context.config
|
|
20
|
+
if config.config_file_name is not None:
|
|
21
|
+
fileConfig(config.config_file_name)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def run_migrations_offline() -> None:
|
|
25
|
+
"""Run migrations in 'offline' mode, emitting SQL to stdout."""
|
|
26
|
+
url = config.get_main_option("sqlalchemy.url")
|
|
27
|
+
context.configure(
|
|
28
|
+
url=url,
|
|
29
|
+
target_metadata=target_metadata,
|
|
30
|
+
literal_binds=True,
|
|
31
|
+
dialect_opts={"paramstyle": "named"},
|
|
32
|
+
compare_type=True,
|
|
33
|
+
compare_server_default=True,
|
|
34
|
+
render_as_batch=True,
|
|
35
|
+
)
|
|
36
|
+
with context.begin_transaction():
|
|
37
|
+
context.run_migrations()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def do_run_migrations(connection: Connection) -> None:
|
|
41
|
+
"""Configure and run migrations against an open connection."""
|
|
42
|
+
context.configure(
|
|
43
|
+
connection=connection,
|
|
44
|
+
target_metadata=target_metadata,
|
|
45
|
+
compare_type=True,
|
|
46
|
+
compare_server_default=True,
|
|
47
|
+
render_as_batch=True,
|
|
48
|
+
)
|
|
49
|
+
with context.begin_transaction():
|
|
50
|
+
context.run_migrations()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
async def run_async_migrations() -> None:
|
|
54
|
+
"""Build the async engine and dispatch migrations."""
|
|
55
|
+
connectable = async_engine_from_config(
|
|
56
|
+
config.get_section(config.config_ini_section, {}),
|
|
57
|
+
prefix="sqlalchemy.",
|
|
58
|
+
poolclass=pool.NullPool,
|
|
59
|
+
)
|
|
60
|
+
async with connectable.connect() as connection:
|
|
61
|
+
await connection.run_sync(do_run_migrations)
|
|
62
|
+
await connectable.dispose()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def run_migrations_online() -> None:
|
|
66
|
+
"""Run migrations in 'online' mode against the configured database."""
|
|
67
|
+
asyncio.run(run_async_migrations())
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
if context.is_offline_mode():
|
|
71
|
+
run_migrations_offline()
|
|
72
|
+
else:
|
|
73
|
+
run_migrations_online()
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
"""Async database manager with engine/session lifecycle helpers."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import AsyncGenerator
|
|
4
|
+
from contextlib import asynccontextmanager
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from sqlalchemy import text
|
|
8
|
+
from sqlalchemy.engine import make_url
|
|
9
|
+
from sqlalchemy.ext.asyncio import (
|
|
10
|
+
AsyncEngine,
|
|
11
|
+
AsyncSession,
|
|
12
|
+
async_sessionmaker,
|
|
13
|
+
create_async_engine,
|
|
14
|
+
)
|
|
15
|
+
from sqlalchemy.pool import Pool
|
|
16
|
+
|
|
17
|
+
from tempest_fastapi_sdk.db.model import BaseModel
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class AsyncDatabaseManager:
|
|
21
|
+
"""Manage the async SQLAlchemy engine and session lifecycle.
|
|
22
|
+
|
|
23
|
+
Handles engine creation tailored to the database backend (SQLite
|
|
24
|
+
gets ``check_same_thread=False`` by default, everything else
|
|
25
|
+
gets a pooled config), session factory construction, and table
|
|
26
|
+
create/drop helpers. Designed to be instantiated once per
|
|
27
|
+
application and reused across requests.
|
|
28
|
+
|
|
29
|
+
Backend detection uses ``sqlalchemy.engine.make_url`` so URLs
|
|
30
|
+
like ``sqlite+aiosqlite://...`` are matched precisely without
|
|
31
|
+
relying on substring tricks.
|
|
32
|
+
|
|
33
|
+
Attributes:
|
|
34
|
+
db_url (str): The database connection URL.
|
|
35
|
+
is_sqlite (bool): Whether the URL targets a SQLite backend.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def __init__(
|
|
39
|
+
self,
|
|
40
|
+
db_url: str,
|
|
41
|
+
*,
|
|
42
|
+
echo: bool = False,
|
|
43
|
+
pool_size: int = 10,
|
|
44
|
+
max_overflow: int = 20,
|
|
45
|
+
pool_recycle: int = 3600,
|
|
46
|
+
connect_args: dict[str, Any] | None = None,
|
|
47
|
+
poolclass: type[Pool] | None = None,
|
|
48
|
+
**engine_kwargs: Any,
|
|
49
|
+
) -> None:
|
|
50
|
+
"""Initialize the manager (does not open connections yet).
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
db_url (str): The database connection URL.
|
|
54
|
+
echo (bool): Whether to emit SQL to stdout.
|
|
55
|
+
pool_size (int): Number of permanent connections in the
|
|
56
|
+
pool. Ignored for SQLite URLs.
|
|
57
|
+
max_overflow (int): Extra connections allowed above the
|
|
58
|
+
pool size. Ignored for SQLite URLs.
|
|
59
|
+
pool_recycle (int): Recycle connections older than this
|
|
60
|
+
many seconds. Ignored for SQLite URLs.
|
|
61
|
+
connect_args (dict[str, Any] | None): Driver-level
|
|
62
|
+
arguments forwarded to ``create_async_engine``
|
|
63
|
+
(e.g. ``{"ssl": "require"}`` for asyncpg). SQLite
|
|
64
|
+
always receives ``check_same_thread=False`` unless
|
|
65
|
+
explicitly overridden here.
|
|
66
|
+
poolclass (type[Pool] | None): Override SQLAlchemy's
|
|
67
|
+
default pool class. Useful for tests
|
|
68
|
+
(``poolclass=NullPool``) or specialized topologies.
|
|
69
|
+
**engine_kwargs: Any additional keyword arguments are
|
|
70
|
+
passed through to ``create_async_engine`` verbatim.
|
|
71
|
+
"""
|
|
72
|
+
self.db_url: str = db_url
|
|
73
|
+
self.is_sqlite: bool = make_url(db_url).get_backend_name() == "sqlite"
|
|
74
|
+
self._echo: bool = echo
|
|
75
|
+
self._pool_size: int = pool_size
|
|
76
|
+
self._max_overflow: int = max_overflow
|
|
77
|
+
self._pool_recycle: int = pool_recycle
|
|
78
|
+
self._connect_args: dict[str, Any] = dict(connect_args or {})
|
|
79
|
+
self._poolclass: type[Pool] | None = poolclass
|
|
80
|
+
self._engine_kwargs: dict[str, Any] = engine_kwargs
|
|
81
|
+
self._engine: AsyncEngine | None = None
|
|
82
|
+
self._session_maker: async_sessionmaker[AsyncSession] | None = None
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def is_connected(self) -> bool:
|
|
86
|
+
"""Whether the engine is currently initialized.
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
bool: ``True`` if :meth:`connect` has been called and
|
|
90
|
+
:meth:`disconnect` has not.
|
|
91
|
+
"""
|
|
92
|
+
return self._engine is not None
|
|
93
|
+
|
|
94
|
+
async def connect(self) -> None:
|
|
95
|
+
"""Create the engine and session factory if they don't exist.
|
|
96
|
+
|
|
97
|
+
Idempotent — calling twice is a no-op.
|
|
98
|
+
"""
|
|
99
|
+
if self._engine is not None:
|
|
100
|
+
return
|
|
101
|
+
|
|
102
|
+
kwargs: dict[str, Any] = {"echo": self._echo, **self._engine_kwargs}
|
|
103
|
+
connect_args = dict(self._connect_args)
|
|
104
|
+
|
|
105
|
+
if self.is_sqlite:
|
|
106
|
+
connect_args.setdefault("check_same_thread", False)
|
|
107
|
+
else:
|
|
108
|
+
kwargs.setdefault("pool_pre_ping", True)
|
|
109
|
+
kwargs.setdefault("pool_recycle", self._pool_recycle)
|
|
110
|
+
kwargs.setdefault("pool_size", self._pool_size)
|
|
111
|
+
kwargs.setdefault("max_overflow", self._max_overflow)
|
|
112
|
+
|
|
113
|
+
if connect_args:
|
|
114
|
+
kwargs["connect_args"] = connect_args
|
|
115
|
+
if self._poolclass is not None:
|
|
116
|
+
kwargs["poolclass"] = self._poolclass
|
|
117
|
+
|
|
118
|
+
self._engine = create_async_engine(self.db_url, **kwargs)
|
|
119
|
+
self._session_maker = async_sessionmaker(
|
|
120
|
+
self._engine,
|
|
121
|
+
expire_on_commit=False,
|
|
122
|
+
class_=AsyncSession,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
async def disconnect(self) -> None:
|
|
126
|
+
"""Dispose the engine and clear the session factory.
|
|
127
|
+
|
|
128
|
+
Safe to call multiple times.
|
|
129
|
+
"""
|
|
130
|
+
if self._engine is not None:
|
|
131
|
+
await self._engine.dispose()
|
|
132
|
+
self._engine = None
|
|
133
|
+
self._session_maker = None
|
|
134
|
+
|
|
135
|
+
def _require_session_maker(self) -> async_sessionmaker[AsyncSession]:
|
|
136
|
+
"""Return the session maker, raising if uninitialized.
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
async_sessionmaker[AsyncSession]: The configured factory.
|
|
140
|
+
|
|
141
|
+
Raises:
|
|
142
|
+
RuntimeError: If :meth:`connect` has not run yet.
|
|
143
|
+
"""
|
|
144
|
+
if self._session_maker is None:
|
|
145
|
+
raise RuntimeError(
|
|
146
|
+
"AsyncDatabaseManager is not connected. "
|
|
147
|
+
"Call await manager.connect() before using sessions."
|
|
148
|
+
)
|
|
149
|
+
return self._session_maker
|
|
150
|
+
|
|
151
|
+
async def get_session(self) -> AsyncSession:
|
|
152
|
+
"""Return a new ``AsyncSession`` bound to the engine.
|
|
153
|
+
|
|
154
|
+
Lazy-connects on first use. The caller is responsible for
|
|
155
|
+
closing the session (use :meth:`get_session_context` for
|
|
156
|
+
managed lifecycle).
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
AsyncSession: A new session.
|
|
160
|
+
"""
|
|
161
|
+
if self._engine is None:
|
|
162
|
+
await self.connect()
|
|
163
|
+
return self._require_session_maker()()
|
|
164
|
+
|
|
165
|
+
@asynccontextmanager
|
|
166
|
+
async def get_session_context(self) -> AsyncGenerator[AsyncSession]:
|
|
167
|
+
"""Yield a session that auto-commits on exit and rolls back on error.
|
|
168
|
+
|
|
169
|
+
Yields:
|
|
170
|
+
AsyncSession: A managed session.
|
|
171
|
+
|
|
172
|
+
Raises:
|
|
173
|
+
Exception: Re-raises whatever the caller raised inside
|
|
174
|
+
the ``async with`` block, after rolling back.
|
|
175
|
+
"""
|
|
176
|
+
if self._engine is None:
|
|
177
|
+
await self.connect()
|
|
178
|
+
session = self._require_session_maker()()
|
|
179
|
+
try:
|
|
180
|
+
yield session
|
|
181
|
+
await session.commit()
|
|
182
|
+
except Exception:
|
|
183
|
+
await session.rollback()
|
|
184
|
+
raise
|
|
185
|
+
finally:
|
|
186
|
+
await session.close()
|
|
187
|
+
|
|
188
|
+
async def session_dependency(self) -> AsyncGenerator[AsyncSession]:
|
|
189
|
+
"""FastAPI dependency yielding one session per request.
|
|
190
|
+
|
|
191
|
+
Use as ``Depends(db.session_dependency)``. Differs from
|
|
192
|
+
:meth:`get_session_context` in that it does **not** commit on
|
|
193
|
+
success — commits are the responsibility of the service /
|
|
194
|
+
repository layer. The session is closed when the request
|
|
195
|
+
scope ends; failures bubble up unchanged.
|
|
196
|
+
|
|
197
|
+
Yields:
|
|
198
|
+
AsyncSession: A request-scoped session.
|
|
199
|
+
"""
|
|
200
|
+
if self._engine is None:
|
|
201
|
+
await self.connect()
|
|
202
|
+
session = self._require_session_maker()()
|
|
203
|
+
try:
|
|
204
|
+
yield session
|
|
205
|
+
finally:
|
|
206
|
+
await session.close()
|
|
207
|
+
|
|
208
|
+
async def health_check(self) -> bool:
|
|
209
|
+
"""Return whether a trivial ``SELECT 1`` succeeds.
|
|
210
|
+
|
|
211
|
+
Suitable for ``/health`` endpoints. Swallows every exception
|
|
212
|
+
and returns ``False`` so callers can branch on the result
|
|
213
|
+
without dealing with driver-specific error types.
|
|
214
|
+
|
|
215
|
+
Returns:
|
|
216
|
+
bool: ``True`` when the database responded with ``1``,
|
|
217
|
+
``False`` on any failure.
|
|
218
|
+
"""
|
|
219
|
+
try:
|
|
220
|
+
if self._engine is None:
|
|
221
|
+
await self.connect()
|
|
222
|
+
async with self._require_session_maker()() as session:
|
|
223
|
+
result = await session.execute(text("SELECT 1"))
|
|
224
|
+
return result.scalar() == 1
|
|
225
|
+
except Exception:
|
|
226
|
+
return False
|
|
227
|
+
|
|
228
|
+
async def create_tables(self) -> None:
|
|
229
|
+
"""Issue ``CREATE TABLE`` for every model registered on ``BaseModel``.
|
|
230
|
+
|
|
231
|
+
Intended for tests and local development. Production schemas
|
|
232
|
+
should be managed by Alembic (see
|
|
233
|
+
:class:`tempest_fastapi_sdk.db.migrations.AlembicHelper`).
|
|
234
|
+
"""
|
|
235
|
+
if self._engine is None:
|
|
236
|
+
await self.connect()
|
|
237
|
+
if self._engine is None:
|
|
238
|
+
raise RuntimeError("Engine is not connected.")
|
|
239
|
+
async with self._engine.begin() as conn:
|
|
240
|
+
await conn.run_sync(BaseModel.metadata.create_all)
|
|
241
|
+
|
|
242
|
+
async def drop_tables(self) -> None:
|
|
243
|
+
"""Issue ``DROP TABLE`` for every model registered on ``BaseModel``.
|
|
244
|
+
|
|
245
|
+
Intended for tests and local development.
|
|
246
|
+
"""
|
|
247
|
+
if self._engine is None:
|
|
248
|
+
await self.connect()
|
|
249
|
+
if self._engine is None:
|
|
250
|
+
raise RuntimeError("Engine is not connected.")
|
|
251
|
+
async with self._engine.begin() as conn:
|
|
252
|
+
await conn.run_sync(BaseModel.metadata.drop_all)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
__all__: list[str] = [
|
|
256
|
+
"AsyncDatabaseManager",
|
|
257
|
+
]
|