fastapi-restly 3.0.0rc1__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.
- fastapi_restly/__init__.py +131 -0
- fastapi_restly/_exceptions.py +194 -0
- fastapi_restly/db/__init__.py +48 -0
- fastapi_restly/db/_globals.py +92 -0
- fastapi_restly/db/_proxy.py +37 -0
- fastapi_restly/db/_session.py +232 -0
- fastapi_restly/models/__init__.py +30 -0
- fastapi_restly/models/_base.py +90 -0
- fastapi_restly/models/_helpers.py +40 -0
- fastapi_restly/py.typed +0 -0
- fastapi_restly/pytest_fixtures.py +27 -0
- fastapi_restly/query/__init__.py +13 -0
- fastapi_restly/query/_impl.py +567 -0
- fastapi_restly/query/_shared.py +22 -0
- fastapi_restly/schemas/__init__.py +35 -0
- fastapi_restly/schemas/_base.py +496 -0
- fastapi_restly/schemas/_generator.py +379 -0
- fastapi_restly/testing/__init__.py +29 -0
- fastapi_restly/testing/_client.py +98 -0
- fastapi_restly/testing/_fixtures.py +212 -0
- fastapi_restly/views/__init__.py +47 -0
- fastapi_restly/views/_async.py +260 -0
- fastapi_restly/views/_base.py +1262 -0
- fastapi_restly/views/_openapi.py +200 -0
- fastapi_restly/views/_react_admin.py +363 -0
- fastapi_restly/views/_sync.py +251 -0
- fastapi_restly-3.0.0rc1.dist-info/METADATA +371 -0
- fastapi_restly-3.0.0rc1.dist-info/RECORD +31 -0
- fastapi_restly-3.0.0rc1.dist-info/WHEEL +5 -0
- fastapi_restly-3.0.0rc1.dist-info/licenses/LICENSE +21 -0
- fastapi_restly-3.0.0rc1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# Database layer
|
|
2
|
+
from .db import (
|
|
3
|
+
AsyncSessionDep,
|
|
4
|
+
FRGlobals,
|
|
5
|
+
RestlyContext,
|
|
6
|
+
SessionDep,
|
|
7
|
+
activate_savepoint_only_mode,
|
|
8
|
+
async_open_session,
|
|
9
|
+
configure,
|
|
10
|
+
deactivate_savepoint_only_mode,
|
|
11
|
+
get_async_engine,
|
|
12
|
+
get_engine,
|
|
13
|
+
get_fr_globals,
|
|
14
|
+
open_session,
|
|
15
|
+
use_fr_globals,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
# Model base classes
|
|
19
|
+
from .models import (
|
|
20
|
+
DataclassBase,
|
|
21
|
+
IDBase,
|
|
22
|
+
IDMixin,
|
|
23
|
+
IDStampsBase,
|
|
24
|
+
TimestampsMixin,
|
|
25
|
+
async_get_one_or_create,
|
|
26
|
+
get_one_or_create,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
# List endpoint query parameters
|
|
30
|
+
from .query import apply_list_params, create_list_params_schema
|
|
31
|
+
|
|
32
|
+
# Schema utilities
|
|
33
|
+
from .schemas import (
|
|
34
|
+
BaseSchema,
|
|
35
|
+
IDRef,
|
|
36
|
+
IDSchema,
|
|
37
|
+
IDStampsSchema,
|
|
38
|
+
ReadOnly,
|
|
39
|
+
TimestampsSchemaMixin,
|
|
40
|
+
WriteOnly,
|
|
41
|
+
create_schema_from_model,
|
|
42
|
+
resolve_ids_to_sqlalchemy_objects,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
# Views
|
|
46
|
+
from .views import (
|
|
47
|
+
AsyncReactAdminView,
|
|
48
|
+
AsyncRestView,
|
|
49
|
+
ReactAdminView,
|
|
50
|
+
RestView,
|
|
51
|
+
View,
|
|
52
|
+
async_make_new_object,
|
|
53
|
+
async_save_object,
|
|
54
|
+
async_update_object,
|
|
55
|
+
delete,
|
|
56
|
+
get,
|
|
57
|
+
include_view,
|
|
58
|
+
make_new_object,
|
|
59
|
+
patch,
|
|
60
|
+
post,
|
|
61
|
+
put,
|
|
62
|
+
route,
|
|
63
|
+
save_object,
|
|
64
|
+
update_object,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# Public API surface for fastapi-restly.
|
|
68
|
+
#
|
|
69
|
+
# Anything not listed here is considered internal and may change without
|
|
70
|
+
# warning. Submodule ``__init__.py`` files have their own (narrower)
|
|
71
|
+
# ``__all__`` lists for submodule-level imports such as
|
|
72
|
+
# ``from fastapi_restly.schemas import ReadOnly``.
|
|
73
|
+
__all__ = [
|
|
74
|
+
# Database — session context managers
|
|
75
|
+
"async_open_session",
|
|
76
|
+
"open_session",
|
|
77
|
+
# Database — FastAPI dependencies
|
|
78
|
+
"AsyncSessionDep",
|
|
79
|
+
"SessionDep",
|
|
80
|
+
# Database — engine access
|
|
81
|
+
"get_async_engine",
|
|
82
|
+
"get_engine",
|
|
83
|
+
# Database — setup & utilities
|
|
84
|
+
"configure",
|
|
85
|
+
"activate_savepoint_only_mode",
|
|
86
|
+
"deactivate_savepoint_only_mode",
|
|
87
|
+
"RestlyContext",
|
|
88
|
+
"FRGlobals",
|
|
89
|
+
"get_fr_globals",
|
|
90
|
+
"use_fr_globals",
|
|
91
|
+
# Models
|
|
92
|
+
"DataclassBase",
|
|
93
|
+
"IDBase",
|
|
94
|
+
"IDMixin",
|
|
95
|
+
"IDStampsBase",
|
|
96
|
+
"TimestampsMixin",
|
|
97
|
+
"async_get_one_or_create",
|
|
98
|
+
"get_one_or_create",
|
|
99
|
+
# List endpoint query parameters
|
|
100
|
+
"apply_list_params",
|
|
101
|
+
"create_list_params_schema",
|
|
102
|
+
# Schemas
|
|
103
|
+
"BaseSchema",
|
|
104
|
+
"IDRef",
|
|
105
|
+
"IDSchema",
|
|
106
|
+
"IDStampsSchema",
|
|
107
|
+
"ReadOnly",
|
|
108
|
+
"WriteOnly",
|
|
109
|
+
"TimestampsSchemaMixin",
|
|
110
|
+
"create_schema_from_model",
|
|
111
|
+
"resolve_ids_to_sqlalchemy_objects",
|
|
112
|
+
# Views
|
|
113
|
+
"RestView",
|
|
114
|
+
"AsyncRestView",
|
|
115
|
+
"AsyncReactAdminView",
|
|
116
|
+
"ReactAdminView",
|
|
117
|
+
"View",
|
|
118
|
+
"async_make_new_object",
|
|
119
|
+
"async_save_object",
|
|
120
|
+
"async_update_object",
|
|
121
|
+
"delete",
|
|
122
|
+
"get",
|
|
123
|
+
"include_view",
|
|
124
|
+
"make_new_object",
|
|
125
|
+
"patch",
|
|
126
|
+
"post",
|
|
127
|
+
"put",
|
|
128
|
+
"route",
|
|
129
|
+
"save_object",
|
|
130
|
+
"update_object",
|
|
131
|
+
]
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"""Default FastAPI exception handlers installed by fastapi-restly.
|
|
2
|
+
|
|
3
|
+
Currently this module provides a translation layer from SQLAlchemy
|
|
4
|
+
:class:`~sqlalchemy.exc.IntegrityError` (unique-constraint, foreign-key,
|
|
5
|
+
not-null, and check-constraint violations) into a clean HTTP 409 Conflict
|
|
6
|
+
response. Without this handler, an ``IntegrityError`` bubbles up to FastAPI
|
|
7
|
+
and turns into a 500 Internal Server Error, which is misleading for clients
|
|
8
|
+
(the server is fine; the request conflicts with the current state of the
|
|
9
|
+
resource).
|
|
10
|
+
|
|
11
|
+
The handler is installed automatically by :func:`fastapi_restly.configure`
|
|
12
|
+
and as a fallback by :func:`fastapi_restly.include_view`. Users can opt out
|
|
13
|
+
by calling ``fr.configure(install_default_exception_handlers=False)`` or by
|
|
14
|
+
registering their own handler for ``IntegrityError`` *before* the framework
|
|
15
|
+
gets a chance to install one.
|
|
16
|
+
|
|
17
|
+
The detail-message extraction is best-effort: it understands the most common
|
|
18
|
+
PostgreSQL SQLSTATE codes (via psycopg's ``orig.pgcode``) and the SQLite
|
|
19
|
+
error-message conventions. For unrecognised dialects/messages we fall back
|
|
20
|
+
to a generic conflict message that includes a truncated version of the
|
|
21
|
+
underlying error text.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
from typing import Any
|
|
27
|
+
|
|
28
|
+
from fastapi import FastAPI, Request
|
|
29
|
+
from fastapi.responses import JSONResponse
|
|
30
|
+
from sqlalchemy.exc import IntegrityError
|
|
31
|
+
|
|
32
|
+
# Maximum length of original-error text we are willing to echo back. Keeps
|
|
33
|
+
# response bodies sane and avoids accidentally leaking long SQL strings.
|
|
34
|
+
_MAX_ORIG_TEXT_LENGTH = 500
|
|
35
|
+
|
|
36
|
+
# Marker stored on ``app.state`` so we know we've already installed our
|
|
37
|
+
# handlers on this FastAPI instance. Public so it is easy to inspect from
|
|
38
|
+
# tests or user code.
|
|
39
|
+
_HANDLERS_INSTALLED_FLAG = "_fr_default_exception_handlers_installed"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# ---------------------------------------------------------------------------
|
|
43
|
+
# Detail extraction
|
|
44
|
+
# ---------------------------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# PostgreSQL SQLSTATE codes — see
|
|
48
|
+
# https://www.postgresql.org/docs/current/errcodes-appendix.html (class 23
|
|
49
|
+
# "Integrity Constraint Violation").
|
|
50
|
+
_PG_SQLSTATE_DETAILS: dict[str, str] = {
|
|
51
|
+
"23505": "Unique constraint violated",
|
|
52
|
+
"23503": "Foreign key constraint violated",
|
|
53
|
+
"23502": "Not-null constraint violated",
|
|
54
|
+
"23514": "Check constraint violated",
|
|
55
|
+
"23000": "Integrity constraint violated",
|
|
56
|
+
"23001": "Restrict violation",
|
|
57
|
+
"23P01": "Exclusion constraint violated",
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _extract_postgres_detail(orig: Any) -> str | None:
|
|
62
|
+
"""Return a user-facing detail message for a Postgres-driver error.
|
|
63
|
+
|
|
64
|
+
Looks at ``orig.pgcode`` (set by psycopg / psycopg2 / asyncpg-via-psycopg)
|
|
65
|
+
and, when available, ``orig.diag.constraint_name`` /
|
|
66
|
+
``orig.diag.column_name`` to enrich the message.
|
|
67
|
+
"""
|
|
68
|
+
pgcode = getattr(orig, "pgcode", None)
|
|
69
|
+
if not pgcode:
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
base = _PG_SQLSTATE_DETAILS.get(pgcode)
|
|
73
|
+
if base is None:
|
|
74
|
+
return None
|
|
75
|
+
|
|
76
|
+
# ``diag`` is a psycopg-specific attribute holding fielded error info.
|
|
77
|
+
diag = getattr(orig, "diag", None)
|
|
78
|
+
constraint_name = getattr(diag, "constraint_name", None) if diag else None
|
|
79
|
+
column_name = getattr(diag, "column_name", None) if diag else None
|
|
80
|
+
|
|
81
|
+
if pgcode == "23505" and constraint_name:
|
|
82
|
+
return f"{base}: {constraint_name}"
|
|
83
|
+
if pgcode == "23503" and constraint_name:
|
|
84
|
+
return f"{base}: {constraint_name}"
|
|
85
|
+
if pgcode == "23502" and column_name:
|
|
86
|
+
return f"{base} on column {column_name!r}"
|
|
87
|
+
if pgcode == "23514" and constraint_name:
|
|
88
|
+
return f"{base}: {constraint_name}"
|
|
89
|
+
return base
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
# Mapping from SQLite error-message prefixes to a clean detail message.
|
|
93
|
+
# SQLite's IntegrityError.args[0] (and ``str(orig)``) follow predictable
|
|
94
|
+
# patterns, e.g. ``"UNIQUE constraint failed: user.username"``.
|
|
95
|
+
_SQLITE_PREFIX_DETAILS: tuple[tuple[str, str], ...] = (
|
|
96
|
+
("UNIQUE constraint failed:", "Unique constraint violated"),
|
|
97
|
+
("FOREIGN KEY constraint failed", "Foreign key constraint violated"),
|
|
98
|
+
("NOT NULL constraint failed:", "Not-null constraint violated"),
|
|
99
|
+
("CHECK constraint failed:", "Check constraint violated"),
|
|
100
|
+
("PRIMARY KEY must be unique", "Unique constraint violated (primary key)"),
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _extract_sqlite_detail(orig: Any) -> str | None:
|
|
105
|
+
"""Return a user-facing detail message for a SQLite-driver error."""
|
|
106
|
+
text = str(orig).strip()
|
|
107
|
+
if not text:
|
|
108
|
+
return None
|
|
109
|
+
|
|
110
|
+
for prefix, base in _SQLITE_PREFIX_DETAILS:
|
|
111
|
+
if not text.startswith(prefix):
|
|
112
|
+
continue
|
|
113
|
+
# Try to surface the column / constraint info that SQLite tacks on
|
|
114
|
+
# after the colon. ``UNIQUE constraint failed: user.username`` →
|
|
115
|
+
# ``"Unique constraint violated on user.username"``.
|
|
116
|
+
remainder = text[len(prefix) :].strip().lstrip(":").strip()
|
|
117
|
+
if remainder:
|
|
118
|
+
return f"{base} on {remainder}"
|
|
119
|
+
return base
|
|
120
|
+
return None
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _build_integrity_detail(exc: IntegrityError) -> str:
|
|
124
|
+
"""Build a clean HTTP 409 detail message from a SQLAlchemy IntegrityError.
|
|
125
|
+
|
|
126
|
+
Best-effort across dialects:
|
|
127
|
+
|
|
128
|
+
* PostgreSQL — switches on ``exc.orig.pgcode`` (SQLSTATE class 23).
|
|
129
|
+
* SQLite — pattern-matches ``str(exc.orig)`` against known prefixes.
|
|
130
|
+
* Anything else — returns a generic fallback that includes a truncated
|
|
131
|
+
copy of the original error text so the body is still useful for
|
|
132
|
+
debugging without being huge.
|
|
133
|
+
"""
|
|
134
|
+
orig = getattr(exc, "orig", None)
|
|
135
|
+
if orig is not None:
|
|
136
|
+
pg_detail = _extract_postgres_detail(orig)
|
|
137
|
+
if pg_detail is not None:
|
|
138
|
+
return pg_detail
|
|
139
|
+
|
|
140
|
+
sqlite_detail = _extract_sqlite_detail(orig)
|
|
141
|
+
if sqlite_detail is not None:
|
|
142
|
+
return sqlite_detail
|
|
143
|
+
|
|
144
|
+
# Generic fallback. Prefer the original driver error text (it's usually
|
|
145
|
+
# the most informative); truncate so we don't dump a giant SQL statement.
|
|
146
|
+
raw = str(orig) if orig is not None else str(exc)
|
|
147
|
+
raw = raw.strip()
|
|
148
|
+
if len(raw) > _MAX_ORIG_TEXT_LENGTH:
|
|
149
|
+
raw = raw[:_MAX_ORIG_TEXT_LENGTH] + "...(truncated)"
|
|
150
|
+
|
|
151
|
+
base = "Conflict with current state of the resource"
|
|
152
|
+
if raw:
|
|
153
|
+
return f"{base}: {raw}"
|
|
154
|
+
return base
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
# ---------------------------------------------------------------------------
|
|
158
|
+
# The handler & registration helper
|
|
159
|
+
# ---------------------------------------------------------------------------
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def integrity_error_handler(request: Request, exc: Exception) -> JSONResponse:
|
|
163
|
+
"""Translate a SQLAlchemy IntegrityError into HTTP 409 Conflict.
|
|
164
|
+
|
|
165
|
+
Signature uses ``Exception`` rather than ``IntegrityError`` to satisfy
|
|
166
|
+
Starlette's exception-handler typing; we narrow at runtime.
|
|
167
|
+
"""
|
|
168
|
+
assert isinstance(exc, IntegrityError) # noqa: S101 - registered for IntegrityError only
|
|
169
|
+
detail = _build_integrity_detail(exc)
|
|
170
|
+
return JSONResponse(status_code=409, content={"detail": detail})
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def register_default_exception_handlers(app: FastAPI) -> None:
|
|
174
|
+
"""Idempotently install fastapi-restly default exception handlers on ``app``.
|
|
175
|
+
|
|
176
|
+
* Skips if a handler for :class:`IntegrityError` is already registered on
|
|
177
|
+
``app`` — we always defer to the user.
|
|
178
|
+
* Skips if we have already installed handlers on this ``app`` instance
|
|
179
|
+
(so calling from both :func:`fastapi_restly.configure` and
|
|
180
|
+
:func:`fastapi_restly.include_view` is safe).
|
|
181
|
+
"""
|
|
182
|
+
if getattr(app.state, _HANDLERS_INSTALLED_FLAG, False):
|
|
183
|
+
return
|
|
184
|
+
|
|
185
|
+
# Respect a user-registered handler if one is already in place.
|
|
186
|
+
if IntegrityError in app.exception_handlers:
|
|
187
|
+
setattr(app.state, _HANDLERS_INSTALLED_FLAG, True)
|
|
188
|
+
return
|
|
189
|
+
|
|
190
|
+
app.add_exception_handler(IntegrityError, integrity_error_handler)
|
|
191
|
+
setattr(app.state, _HANDLERS_INSTALLED_FLAG, True)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
__all__ = ["integrity_error_handler", "register_default_exception_handlers"]
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from ._globals import (
|
|
2
|
+
FRGlobals,
|
|
3
|
+
RestlyContext,
|
|
4
|
+
fr_globals,
|
|
5
|
+
get_fr_globals,
|
|
6
|
+
use_fr_globals,
|
|
7
|
+
)
|
|
8
|
+
from ._proxy import async_open_session, open_session
|
|
9
|
+
from ._session import (
|
|
10
|
+
AsyncSessionDep,
|
|
11
|
+
SessionDep,
|
|
12
|
+
activate_savepoint_only_mode,
|
|
13
|
+
async_generate_session,
|
|
14
|
+
configure,
|
|
15
|
+
deactivate_savepoint_only_mode,
|
|
16
|
+
generate_session,
|
|
17
|
+
get_async_engine,
|
|
18
|
+
get_engine,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
# Public API for ``fastapi_restly.db``.
|
|
22
|
+
#
|
|
23
|
+
# ``async_generate_session`` and ``generate_session`` remain importable for
|
|
24
|
+
# advanced users (and existing tests) who plug a custom session generator
|
|
25
|
+
# into ``fr_globals``, but they are not part of the supported public API
|
|
26
|
+
# and may move into a private module in a future release.
|
|
27
|
+
__all__ = [
|
|
28
|
+
# Session context managers
|
|
29
|
+
"async_open_session",
|
|
30
|
+
"open_session",
|
|
31
|
+
# FastAPI dependencies
|
|
32
|
+
"AsyncSessionDep",
|
|
33
|
+
"SessionDep",
|
|
34
|
+
# Engine access
|
|
35
|
+
"get_async_engine",
|
|
36
|
+
"get_engine",
|
|
37
|
+
# Setup
|
|
38
|
+
"configure",
|
|
39
|
+
# Savepoint mode
|
|
40
|
+
"activate_savepoint_only_mode",
|
|
41
|
+
"deactivate_savepoint_only_mode",
|
|
42
|
+
# Globals
|
|
43
|
+
"RestlyContext",
|
|
44
|
+
"FRGlobals",
|
|
45
|
+
"fr_globals",
|
|
46
|
+
"get_fr_globals",
|
|
47
|
+
"use_fr_globals",
|
|
48
|
+
]
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
from collections.abc import AsyncIterator, Callable, Iterator
|
|
2
|
+
from contextlib import contextmanager
|
|
3
|
+
from contextvars import ContextVar, Token
|
|
4
|
+
|
|
5
|
+
from sqlalchemy.ext.asyncio import AsyncSession as SA_AsyncSession
|
|
6
|
+
from sqlalchemy.ext.asyncio import async_sessionmaker
|
|
7
|
+
from sqlalchemy.orm import Session as SA_Session
|
|
8
|
+
from sqlalchemy.orm import sessionmaker
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class RestlyContext:
|
|
12
|
+
"""Container for Restly runtime state.
|
|
13
|
+
|
|
14
|
+
Most applications use the default process-wide context by calling
|
|
15
|
+
``fr.configure(...)`` directly. Create a ``RestlyContext`` when you need
|
|
16
|
+
isolated Restly state in the same process, for example for tests or
|
|
17
|
+
multiple FastAPI apps.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
__slots__ = (
|
|
21
|
+
"async_database_url",
|
|
22
|
+
"async_make_session",
|
|
23
|
+
"database_url",
|
|
24
|
+
"make_session",
|
|
25
|
+
"session_generator",
|
|
26
|
+
"sync_session_generator",
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
async_database_url: str | None
|
|
30
|
+
async_make_session: async_sessionmaker[SA_AsyncSession] | None
|
|
31
|
+
database_url: str | None
|
|
32
|
+
make_session: sessionmaker[SA_Session] | None
|
|
33
|
+
session_generator: Callable[[], AsyncIterator[SA_AsyncSession]] | None
|
|
34
|
+
sync_session_generator: Callable[[], Iterator[SA_Session]] | None
|
|
35
|
+
|
|
36
|
+
def __init__(self) -> None:
|
|
37
|
+
self.async_database_url = None
|
|
38
|
+
self.async_make_session = None
|
|
39
|
+
self.database_url = None
|
|
40
|
+
self.make_session = None
|
|
41
|
+
self.session_generator = None
|
|
42
|
+
self.sync_session_generator = None
|
|
43
|
+
|
|
44
|
+
def __enter__(self) -> "RestlyContext":
|
|
45
|
+
token = _restly_context_ctx.set(self)
|
|
46
|
+
_restly_context_token_stack.set(_restly_context_token_stack.get() + (token,))
|
|
47
|
+
return self
|
|
48
|
+
|
|
49
|
+
def __exit__(self, *exc_info: object) -> None:
|
|
50
|
+
token_stack = _restly_context_token_stack.get()
|
|
51
|
+
if not token_stack:
|
|
52
|
+
raise RuntimeError("RestlyContext was exited without being entered.")
|
|
53
|
+
token = token_stack[-1]
|
|
54
|
+
_restly_context_token_stack.set(token_stack[:-1])
|
|
55
|
+
_restly_context_ctx.reset(token)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
FRGlobals = RestlyContext
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
_default_context = RestlyContext()
|
|
62
|
+
_restly_context_ctx: ContextVar[RestlyContext | None] = ContextVar(
|
|
63
|
+
"fastapi_restly_context", default=None
|
|
64
|
+
)
|
|
65
|
+
_restly_context_token_stack: ContextVar[tuple[Token[RestlyContext | None], ...]] = (
|
|
66
|
+
ContextVar("fastapi_restly_context_token_stack", default=())
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _get_restly_context() -> RestlyContext:
|
|
71
|
+
return _restly_context_ctx.get() or _default_context
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def get_fr_globals() -> FRGlobals:
|
|
75
|
+
return _get_restly_context()
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@contextmanager
|
|
79
|
+
def use_fr_globals(globals_obj: FRGlobals) -> Iterator[None]:
|
|
80
|
+
with globals_obj:
|
|
81
|
+
yield
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class _FRGlobalsProxy:
|
|
85
|
+
def __getattr__(self, name: str):
|
|
86
|
+
return getattr(_get_restly_context(), name)
|
|
87
|
+
|
|
88
|
+
def __setattr__(self, name: str, value):
|
|
89
|
+
setattr(_get_restly_context(), name, value)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
fr_globals = _FRGlobalsProxy()
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from contextlib import asynccontextmanager, contextmanager
|
|
2
|
+
from typing import AsyncIterator, Iterator
|
|
3
|
+
|
|
4
|
+
from sqlalchemy.ext.asyncio import AsyncSession as SA_AsyncSession
|
|
5
|
+
from sqlalchemy.orm import Session as SA_Session
|
|
6
|
+
|
|
7
|
+
from ._globals import fr_globals
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@asynccontextmanager
|
|
11
|
+
async def async_open_session() -> AsyncIterator[SA_AsyncSession]:
|
|
12
|
+
"""Open an async database session for use outside of request context.
|
|
13
|
+
|
|
14
|
+
Example::
|
|
15
|
+
|
|
16
|
+
async with fr.async_open_session() as session:
|
|
17
|
+
result = await session.execute(select(User))
|
|
18
|
+
"""
|
|
19
|
+
if fr_globals.async_make_session is None:
|
|
20
|
+
raise RuntimeError("Call fr.configure() before using async_open_session().")
|
|
21
|
+
async with fr_globals.async_make_session() as sess:
|
|
22
|
+
yield sess
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@contextmanager
|
|
26
|
+
def open_session() -> Iterator[SA_Session]:
|
|
27
|
+
"""Open a sync database session for use outside of request context.
|
|
28
|
+
|
|
29
|
+
Example::
|
|
30
|
+
|
|
31
|
+
with fr.open_session() as session:
|
|
32
|
+
result = session.execute(select(User))
|
|
33
|
+
"""
|
|
34
|
+
if fr_globals.make_session is None:
|
|
35
|
+
raise RuntimeError("Call fr.configure() before using open_session().")
|
|
36
|
+
with fr_globals.make_session() as sess:
|
|
37
|
+
yield sess
|