fastapi-sqla 3.1.2__py3-none-any.whl → 3.4.8__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.
Potentially problematic release.
This version of fastapi-sqla might be problematic. Click here for more details.
- fastapi_sqla/__init__.py +3 -1
- fastapi_sqla/_pytest_plugin.py +1 -6
- fastapi_sqla/async_pagination.py +2 -2
- fastapi_sqla/async_sqla.py +54 -33
- fastapi_sqla/aws_rds_iam_support.py +2 -1
- fastapi_sqla/base.py +26 -7
- fastapi_sqla/models.py +1 -1
- fastapi_sqla/pagination.py +3 -3
- fastapi_sqla/sqla.py +58 -38
- {fastapi_sqla-3.1.2.dist-info → fastapi_sqla-3.4.8.dist-info}/METADATA +46 -31
- fastapi_sqla-3.4.8.dist-info/RECORD +16 -0
- {fastapi_sqla-3.1.2.dist-info → fastapi_sqla-3.4.8.dist-info}/WHEEL +1 -1
- fastapi_sqla-3.1.2.dist-info/RECORD +0 -16
- {fastapi_sqla-3.1.2.dist-info → fastapi_sqla-3.4.8.dist-info}/LICENSE +0 -0
- {fastapi_sqla-3.1.2.dist-info → fastapi_sqla-3.4.8.dist-info}/entry_points.txt +0 -0
fastapi_sqla/__init__.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from fastapi_sqla.base import setup
|
|
1
|
+
from fastapi_sqla.base import setup, setup_middlewares, startup
|
|
2
2
|
from fastapi_sqla.models import Collection, Item, Page
|
|
3
3
|
from fastapi_sqla.pagination import Paginate, PaginateSignature, Pagination
|
|
4
4
|
from fastapi_sqla.sqla import (
|
|
@@ -22,6 +22,8 @@ __all__ = [
|
|
|
22
22
|
"SqlaSession",
|
|
23
23
|
"open_session",
|
|
24
24
|
"setup",
|
|
25
|
+
"setup_middlewares",
|
|
26
|
+
"startup",
|
|
25
27
|
]
|
|
26
28
|
|
|
27
29
|
|
fastapi_sqla/_pytest_plugin.py
CHANGED
|
@@ -28,7 +28,6 @@ def db_host():
|
|
|
28
28
|
When CI key is set in environment variables, it uses `postgres` as host name else,
|
|
29
29
|
host used is `localhost`
|
|
30
30
|
"""
|
|
31
|
-
|
|
32
31
|
return "postgres" if "CI" in os.environ else "localhost"
|
|
33
32
|
|
|
34
33
|
|
|
@@ -38,7 +37,6 @@ def db_user():
|
|
|
38
37
|
|
|
39
38
|
postgres
|
|
40
39
|
"""
|
|
41
|
-
|
|
42
40
|
return "postgres"
|
|
43
41
|
|
|
44
42
|
|
|
@@ -48,7 +46,6 @@ def db_url(db_host, db_user):
|
|
|
48
46
|
|
|
49
47
|
db url example postgresql://{db_user}@{db_host}/postgres
|
|
50
48
|
"""
|
|
51
|
-
|
|
52
49
|
return f"postgresql://{db_user}@{db_host}/postgres"
|
|
53
50
|
|
|
54
51
|
|
|
@@ -107,7 +104,6 @@ def sqla_reflection(sqla_modules, sqla_connection):
|
|
|
107
104
|
@fixture
|
|
108
105
|
def patch_engine_from_config(request, sqla_connection):
|
|
109
106
|
"""So that all DB operations are never written to db for real."""
|
|
110
|
-
|
|
111
107
|
if "dont_patch_engines" in request.keywords:
|
|
112
108
|
yield
|
|
113
109
|
else:
|
|
@@ -153,7 +149,7 @@ def async_sqlalchemy_url(db_url):
|
|
|
153
149
|
return format_async_async_sqlalchemy_url(db_url)
|
|
154
150
|
|
|
155
151
|
|
|
156
|
-
if asyncio_support:
|
|
152
|
+
if asyncio_support:
|
|
157
153
|
|
|
158
154
|
@fixture
|
|
159
155
|
def async_engine(async_sqlalchemy_url):
|
|
@@ -167,7 +163,6 @@ if asyncio_support: # noqa: C901
|
|
|
167
163
|
@fixture
|
|
168
164
|
async def patch_new_engine(request, async_sqla_connection):
|
|
169
165
|
"""So that all async DB operations are never written to db for real."""
|
|
170
|
-
|
|
171
166
|
if "dont_patch_engines" in request.keywords:
|
|
172
167
|
yield
|
|
173
168
|
else:
|
fastapi_sqla/async_pagination.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import math
|
|
2
|
-
from collections.abc import Awaitable, Callable
|
|
3
|
-
from typing import Annotated,
|
|
2
|
+
from collections.abc import Awaitable, Callable, Iterator
|
|
3
|
+
from typing import Annotated, Optional, Union, cast
|
|
4
4
|
|
|
5
5
|
from fastapi import Depends, Query
|
|
6
6
|
from sqlalchemy.sql import Select, func, select
|
fastapi_sqla/async_sqla.py
CHANGED
|
@@ -3,12 +3,13 @@ from contextlib import asynccontextmanager
|
|
|
3
3
|
from typing import Annotated
|
|
4
4
|
|
|
5
5
|
import structlog
|
|
6
|
-
from fastapi import Depends, Request
|
|
6
|
+
from fastapi import Depends, Request, Response
|
|
7
7
|
from fastapi.responses import PlainTextResponse
|
|
8
8
|
from sqlalchemy import text
|
|
9
9
|
from sqlalchemy.ext.asyncio import AsyncEngine
|
|
10
10
|
from sqlalchemy.ext.asyncio import AsyncSession as SqlaAsyncSession
|
|
11
11
|
from sqlalchemy.orm.session import sessionmaker
|
|
12
|
+
from starlette.types import ASGIApp, Message, Receive, Scope, Send
|
|
12
13
|
|
|
13
14
|
from fastapi_sqla import aws_aurora_support, aws_rds_iam_support
|
|
14
15
|
from fastapi_sqla.sqla import _DEFAULT_SESSION_KEY, Base, new_engine
|
|
@@ -88,9 +89,7 @@ async def open_session(
|
|
|
88
89
|
await session.close()
|
|
89
90
|
|
|
90
91
|
|
|
91
|
-
|
|
92
|
-
request: Request, call_next, key: str = _DEFAULT_SESSION_KEY
|
|
93
|
-
):
|
|
92
|
+
class AsyncSessionMiddleware:
|
|
94
93
|
"""Middleware which injects a new sqla async session into every request.
|
|
95
94
|
|
|
96
95
|
Handles creation of session, as well as commit, rollback, and closing of session.
|
|
@@ -108,36 +107,58 @@ async def add_session_to_request(
|
|
|
108
107
|
async def get_users(session: fastapi_sqla.AsyncSession):
|
|
109
108
|
return await session.execute(...) # use your session here
|
|
110
109
|
"""
|
|
111
|
-
async with open_session(key) as session:
|
|
112
|
-
setattr(request.state, f"{_ASYNC_REQUEST_SESSION_KEY}_{key}", session)
|
|
113
|
-
response = await call_next(request)
|
|
114
|
-
|
|
115
|
-
is_dirty = bool(session.dirty or session.deleted or session.new)
|
|
116
|
-
|
|
117
|
-
# try to commit after response, so that we can return a proper 500 response
|
|
118
|
-
# and not raise a true internal server error
|
|
119
|
-
if response.status_code < 400:
|
|
120
|
-
try:
|
|
121
|
-
await session.commit()
|
|
122
|
-
except Exception:
|
|
123
|
-
logger.exception("commit failed, returning http error")
|
|
124
|
-
response = PlainTextResponse(
|
|
125
|
-
content="Internal Server Error", status_code=500
|
|
126
|
-
)
|
|
127
|
-
|
|
128
|
-
if response.status_code >= 400:
|
|
129
|
-
# If ever a route handler returns an http exception, we do not want the
|
|
130
|
-
# session opened by current context manager to commit anything in db.
|
|
131
|
-
if is_dirty:
|
|
132
|
-
# optimistically only log if there were uncommitted changes
|
|
133
|
-
logger.warning(
|
|
134
|
-
"http error, rolling back possibly uncommitted changes",
|
|
135
|
-
status_code=response.status_code,
|
|
136
|
-
)
|
|
137
|
-
# since this is no-op if session is not dirty, we can always call it
|
|
138
|
-
await session.rollback()
|
|
139
110
|
|
|
140
|
-
|
|
111
|
+
def __init__(self, app: ASGIApp, key: str = _DEFAULT_SESSION_KEY) -> None:
|
|
112
|
+
self.app = app
|
|
113
|
+
self.key = key
|
|
114
|
+
|
|
115
|
+
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
|
|
116
|
+
if scope["type"] != "http":
|
|
117
|
+
return await self.app(scope, receive, send)
|
|
118
|
+
|
|
119
|
+
async with open_session(self.key) as session:
|
|
120
|
+
request = Request(scope=scope, receive=receive, send=send)
|
|
121
|
+
setattr(request.state, f"{_ASYNC_REQUEST_SESSION_KEY}_{self.key}", session)
|
|
122
|
+
|
|
123
|
+
async def send_wrapper(message: Message) -> None:
|
|
124
|
+
if message["type"] != "http.response.start":
|
|
125
|
+
return await send(message)
|
|
126
|
+
|
|
127
|
+
response: Response | None = None
|
|
128
|
+
status_code = message["status"]
|
|
129
|
+
is_dirty = bool(session.dirty or session.deleted or session.new)
|
|
130
|
+
|
|
131
|
+
# try to commit after response, so that we can return a proper 500
|
|
132
|
+
# and not raise a true internal server error
|
|
133
|
+
if status_code < 400:
|
|
134
|
+
try:
|
|
135
|
+
await session.commit()
|
|
136
|
+
except Exception:
|
|
137
|
+
logger.exception("commit failed, returning http error")
|
|
138
|
+
status_code = 500
|
|
139
|
+
response = PlainTextResponse(
|
|
140
|
+
content="Internal Server Error", status_code=500
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
if status_code >= 400:
|
|
144
|
+
# If ever a route handler returns an http exception,
|
|
145
|
+
# we do not want the current session to commit anything in db.
|
|
146
|
+
if is_dirty:
|
|
147
|
+
# optimistically only log if there were uncommitted changes
|
|
148
|
+
logger.warning(
|
|
149
|
+
"http error, rolling back possibly uncommitted changes",
|
|
150
|
+
status_code=status_code,
|
|
151
|
+
)
|
|
152
|
+
# since this is no-op if the session is not dirty,
|
|
153
|
+
# we can always call it
|
|
154
|
+
await session.rollback()
|
|
155
|
+
|
|
156
|
+
if response:
|
|
157
|
+
return await response(scope, receive, send)
|
|
158
|
+
|
|
159
|
+
return await send(message)
|
|
160
|
+
|
|
161
|
+
await self.app(scope, receive, send_wrapper)
|
|
141
162
|
|
|
142
163
|
|
|
143
164
|
class AsyncSessionDependency:
|
|
@@ -20,7 +20,8 @@ def setup(engine: Engine):
|
|
|
20
20
|
aws_rds_iam_enabled = lc_environ.get("fastapi_sqla_aws_rds_iam_enabled") == "true"
|
|
21
21
|
|
|
22
22
|
if aws_rds_iam_enabled:
|
|
23
|
-
|
|
23
|
+
if not boto3_installed:
|
|
24
|
+
raise ImportError(f"boto3 is required for RDS IAM : {boto3_installed_err}")
|
|
24
25
|
# Cache the client at startup
|
|
25
26
|
get_rds_client()
|
|
26
27
|
event.listen(engine, "do_connect", set_connection_token)
|
fastapi_sqla/base.py
CHANGED
|
@@ -2,6 +2,7 @@ import functools
|
|
|
2
2
|
import os
|
|
3
3
|
import re
|
|
4
4
|
|
|
5
|
+
from deprecated import deprecated
|
|
5
6
|
from fastapi import FastAPI
|
|
6
7
|
from sqlalchemy.engine import Engine
|
|
7
8
|
|
|
@@ -20,23 +21,41 @@ except ImportError as err: # pragma: no cover
|
|
|
20
21
|
_ENGINE_KEYS_REGEX = re.compile(r"fastapi_sqla__(?!_)(.+)(?<!_)__(?!_).+")
|
|
21
22
|
|
|
22
23
|
|
|
24
|
+
async def startup():
|
|
25
|
+
engine_keys = _get_engine_keys()
|
|
26
|
+
engines = {key: sqla.new_engine(key) for key in engine_keys}
|
|
27
|
+
for key, engine in engines.items():
|
|
28
|
+
if not _is_async_dialect(engine):
|
|
29
|
+
sqla.startup(key=key)
|
|
30
|
+
else:
|
|
31
|
+
await async_sqla.startup(key=key)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def setup_middlewares(app: FastAPI):
|
|
35
|
+
engine_keys = _get_engine_keys()
|
|
36
|
+
engines = {key: sqla.new_engine(key) for key in engine_keys}
|
|
37
|
+
for key, engine in engines.items():
|
|
38
|
+
if not _is_async_dialect(engine):
|
|
39
|
+
app.add_middleware(sqla.SessionMiddleware, key=key)
|
|
40
|
+
else:
|
|
41
|
+
app.add_middleware(async_sqla.AsyncSessionMiddleware, key=key)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@deprecated(
|
|
45
|
+
reason="FastAPI events are deprecated. This function will be remove in the upcoming major release." # noqa: E501
|
|
46
|
+
)
|
|
23
47
|
def setup(app: FastAPI):
|
|
24
48
|
engine_keys = _get_engine_keys()
|
|
25
49
|
engines = {key: sqla.new_engine(key) for key in engine_keys}
|
|
26
50
|
for key, engine in engines.items():
|
|
27
51
|
if not _is_async_dialect(engine):
|
|
28
52
|
app.add_event_handler("startup", functools.partial(sqla.startup, key=key))
|
|
29
|
-
app.
|
|
30
|
-
functools.partial(sqla.add_session_to_request, key=key)
|
|
31
|
-
)
|
|
53
|
+
app.add_middleware(sqla.SessionMiddleware, key=key)
|
|
32
54
|
else:
|
|
33
|
-
assert has_asyncio_support, asyncio_support_err
|
|
34
55
|
app.add_event_handler(
|
|
35
56
|
"startup", functools.partial(async_sqla.startup, key=key)
|
|
36
57
|
)
|
|
37
|
-
app.
|
|
38
|
-
functools.partial(async_sqla.add_session_to_request, key=key)
|
|
39
|
-
)
|
|
58
|
+
app.add_middleware(async_sqla.AsyncSessionMiddleware, key=key)
|
|
40
59
|
|
|
41
60
|
|
|
42
61
|
def _get_engine_keys() -> set[str]:
|
fastapi_sqla/models.py
CHANGED
|
@@ -3,7 +3,7 @@ from typing import Generic, TypeVar
|
|
|
3
3
|
from pydantic import BaseModel, Field
|
|
4
4
|
from pydantic import __version__ as pydantic_version
|
|
5
5
|
|
|
6
|
-
major, _, _ =
|
|
6
|
+
major, _, _ = (int(v) for v in pydantic_version.split("."))
|
|
7
7
|
is_pydantic2 = major == 2
|
|
8
8
|
if is_pydantic2:
|
|
9
9
|
GenericModel = BaseModel
|
fastapi_sqla/pagination.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import math
|
|
2
|
-
from collections.abc import Callable
|
|
2
|
+
from collections.abc import Callable, Iterator
|
|
3
3
|
from functools import singledispatch
|
|
4
|
-
from typing import Annotated,
|
|
4
|
+
from typing import Annotated, Optional, Union, cast
|
|
5
5
|
|
|
6
6
|
from fastapi import Depends, Query
|
|
7
7
|
from sqlalchemy.orm import Query as LegacyQuery
|
|
@@ -52,7 +52,7 @@ def paginate_query(
|
|
|
52
52
|
limit: int,
|
|
53
53
|
scalars: bool = True,
|
|
54
54
|
) -> Page: # pragma: no cover
|
|
55
|
-
"Dispatch on registered functions based on `query` type"
|
|
55
|
+
"""Dispatch on registered functions based on `query` type"""
|
|
56
56
|
raise NotImplementedError(f"no paginate_query registered for type {type(query)!r}")
|
|
57
57
|
|
|
58
58
|
|
fastapi_sqla/sqla.py
CHANGED
|
@@ -5,7 +5,7 @@ from contextlib import contextmanager
|
|
|
5
5
|
from typing import Annotated
|
|
6
6
|
|
|
7
7
|
import structlog
|
|
8
|
-
from fastapi import Depends, Request
|
|
8
|
+
from fastapi import Depends, Request, Response
|
|
9
9
|
from fastapi.concurrency import contextmanager_in_threadpool
|
|
10
10
|
from fastapi.responses import PlainTextResponse
|
|
11
11
|
from sqlalchemy import engine_from_config, text
|
|
@@ -13,6 +13,7 @@ from sqlalchemy.engine import Engine
|
|
|
13
13
|
from sqlalchemy.ext.declarative import DeferredReflection
|
|
14
14
|
from sqlalchemy.orm.session import Session as SqlaSession
|
|
15
15
|
from sqlalchemy.orm.session import sessionmaker
|
|
16
|
+
from starlette.types import ASGIApp, Message, Receive, Scope, Send
|
|
16
17
|
|
|
17
18
|
from fastapi_sqla import aws_aurora_support, aws_rds_iam_support
|
|
18
19
|
|
|
@@ -24,7 +25,7 @@ except ImportError:
|
|
|
24
25
|
DeclarativeBase = declarative_base() # type: ignore
|
|
25
26
|
|
|
26
27
|
try:
|
|
27
|
-
from sqlmodel import Session as SqlaSession # type: ignore
|
|
28
|
+
from sqlmodel import Session as SqlaSession # type: ignore
|
|
28
29
|
|
|
29
30
|
except ImportError:
|
|
30
31
|
pass
|
|
@@ -110,9 +111,7 @@ def open_session(key: str = _DEFAULT_SESSION_KEY) -> Generator[SqlaSession, None
|
|
|
110
111
|
session.close()
|
|
111
112
|
|
|
112
113
|
|
|
113
|
-
|
|
114
|
-
request: Request, call_next, key: str = _DEFAULT_SESSION_KEY
|
|
115
|
-
):
|
|
114
|
+
class SessionMiddleware:
|
|
116
115
|
"""Middleware which injects a new sqla session into every request.
|
|
117
116
|
|
|
118
117
|
Handles creation of session, as well as commit, rollback, and closing of session.
|
|
@@ -130,39 +129,60 @@ async def add_session_to_request(
|
|
|
130
129
|
def get_users(session: fastapi_sqla.Session):
|
|
131
130
|
return session.execute(...) # use your session here
|
|
132
131
|
"""
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
#
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
132
|
+
|
|
133
|
+
def __init__(self, app: ASGIApp, key: str = _DEFAULT_SESSION_KEY) -> None:
|
|
134
|
+
self.app = app
|
|
135
|
+
self.key = key
|
|
136
|
+
|
|
137
|
+
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
|
|
138
|
+
if scope["type"] != "http":
|
|
139
|
+
return await self.app(scope, receive, send)
|
|
140
|
+
|
|
141
|
+
async with contextmanager_in_threadpool(open_session(self.key)) as session:
|
|
142
|
+
request = Request(scope=scope, receive=receive, send=send)
|
|
143
|
+
setattr(request.state, f"{_REQUEST_SESSION_KEY}_{self.key}", session)
|
|
144
|
+
|
|
145
|
+
async def send_wrapper(message: Message) -> None:
|
|
146
|
+
if message["type"] != "http.response.start":
|
|
147
|
+
return await send(message)
|
|
148
|
+
|
|
149
|
+
response: Response | None = None
|
|
150
|
+
status_code = message["status"]
|
|
151
|
+
is_dirty = bool(session.dirty or session.deleted or session.new)
|
|
152
|
+
|
|
153
|
+
loop = asyncio.get_running_loop()
|
|
154
|
+
|
|
155
|
+
# try to commit after response, so that we can return a proper 500
|
|
156
|
+
# and not raise a true internal server error
|
|
157
|
+
if status_code < 400:
|
|
158
|
+
try:
|
|
159
|
+
await loop.run_in_executor(None, session.commit)
|
|
160
|
+
except Exception:
|
|
161
|
+
logger.exception("commit failed, returning http error")
|
|
162
|
+
status_code = 500
|
|
163
|
+
response = PlainTextResponse(
|
|
164
|
+
content="Internal Server Error", status_code=status_code
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
if status_code >= 400:
|
|
168
|
+
# If ever a route handler returns an http exception,
|
|
169
|
+
# we do not want the current session to commit anything in db.
|
|
170
|
+
if is_dirty:
|
|
171
|
+
# optimistically only log if there were uncommitted changes
|
|
172
|
+
logger.warning(
|
|
173
|
+
"http error, rolling back possibly uncommitted changes",
|
|
174
|
+
status_code=status_code,
|
|
175
|
+
)
|
|
176
|
+
# since this is no-op if the session is not dirty,
|
|
177
|
+
# we can always call it
|
|
178
|
+
await loop.run_in_executor(None, session.rollback)
|
|
179
|
+
|
|
180
|
+
if response:
|
|
181
|
+
return await response(scope, receive, send)
|
|
182
|
+
|
|
183
|
+
return await send(message)
|
|
184
|
+
|
|
185
|
+
await self.app(scope, receive, send_wrapper)
|
|
166
186
|
|
|
167
187
|
|
|
168
188
|
class SessionDependency:
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: fastapi-sqla
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.4.8
|
|
4
4
|
Summary: SQLAlchemy extension for FastAPI with support for pagination, asyncio, SQLModel, and pytest, ready for production.
|
|
5
5
|
Home-page: https://github.com/dialoguemd/fastapi-sqla
|
|
6
6
|
License: MIT
|
|
7
7
|
Keywords: FastAPI,SQLAlchemy,asyncio,pytest,alembic
|
|
8
8
|
Author: Hadrien David
|
|
9
9
|
Author-email: hadrien.david@dialogue.co
|
|
10
|
-
Requires-Python: >=3.9,<
|
|
10
|
+
Requires-Python: >=3.9,<3.14
|
|
11
11
|
Classifier: Development Status :: 5 - Production/Stable
|
|
12
12
|
Classifier: Environment :: Web Environment
|
|
13
13
|
Classifier: Framework :: AsyncIO
|
|
@@ -22,6 +22,7 @@ Classifier: Programming Language :: Python :: 3
|
|
|
22
22
|
Classifier: Programming Language :: Python :: 3.9
|
|
23
23
|
Classifier: Programming Language :: Python :: 3.10
|
|
24
24
|
Classifier: Programming Language :: Python :: 3.11
|
|
25
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
25
26
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
26
27
|
Classifier: Programming Language :: SQL
|
|
27
28
|
Classifier: Topic :: Internet
|
|
@@ -34,30 +35,19 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
|
34
35
|
Classifier: Typing :: Typed
|
|
35
36
|
Provides-Extra: asyncpg
|
|
36
37
|
Provides-Extra: aws-rds-iam
|
|
38
|
+
Provides-Extra: psycopg2
|
|
39
|
+
Provides-Extra: pytest-plugin
|
|
37
40
|
Provides-Extra: sqlmodel
|
|
38
|
-
|
|
39
|
-
Requires-Dist:
|
|
40
|
-
Requires-Dist:
|
|
41
|
-
Requires-Dist:
|
|
42
|
-
Requires-Dist:
|
|
43
|
-
Requires-Dist:
|
|
44
|
-
Requires-Dist:
|
|
45
|
-
Requires-Dist:
|
|
46
|
-
Requires-Dist:
|
|
47
|
-
Requires-Dist:
|
|
48
|
-
Requires-Dist: isort (>=5.5.3,<6.0.0) ; extra == "tests"
|
|
49
|
-
Requires-Dist: mypy[tests] (>=1.0.0,<2.0.0) ; extra == "tests"
|
|
50
|
-
Requires-Dist: pdbpp (>=0.10.2,<0.11.0) ; extra == "tests"
|
|
51
|
-
Requires-Dist: psycopg2 (>=2.8.6,<3.0.0) ; extra == "tests"
|
|
52
|
-
Requires-Dist: pydantic (>=1)
|
|
53
|
-
Requires-Dist: pylama (>=8.4.1,<9.0.0) ; extra == "tests"
|
|
54
|
-
Requires-Dist: pytest (>=7.2.1,<8.0.0) ; extra == "tests"
|
|
55
|
-
Requires-Dist: pytest-asyncio (>=0.19.0,<0.20.0) ; extra == "tests"
|
|
56
|
-
Requires-Dist: pytest-cov (>=2.10.1,<3.0.0) ; extra == "tests"
|
|
57
|
-
Requires-Dist: sqlalchemy (>=1.3)
|
|
58
|
-
Requires-Dist: sqlmodel (>=0.0.14,<0.0.15) ; extra == "sqlmodel"
|
|
59
|
-
Requires-Dist: structlog (>=20)
|
|
60
|
-
Requires-Dist: tox (>=3.26.0,<4.0.0) ; extra == "tests"
|
|
41
|
+
Requires-Dist: alembic (>=1.4.3,<2) ; extra == "pytest-plugin"
|
|
42
|
+
Requires-Dist: asyncpg (>=0.28.0,<0.31.0) ; extra == "asyncpg"
|
|
43
|
+
Requires-Dist: boto3 (>=1.24.74,<2) ; extra == "aws-rds-iam"
|
|
44
|
+
Requires-Dist: deprecated (>=1.2,<2)
|
|
45
|
+
Requires-Dist: fastapi (>=0.95.1,<0.116)
|
|
46
|
+
Requires-Dist: psycopg2 (>=2.8.6,<3) ; extra == "psycopg2"
|
|
47
|
+
Requires-Dist: pydantic (>=1,<3)
|
|
48
|
+
Requires-Dist: sqlalchemy (>=1.3,<3)
|
|
49
|
+
Requires-Dist: sqlmodel (>=0.0.14,<0.0.23) ; extra == "sqlmodel"
|
|
50
|
+
Requires-Dist: structlog (>=20,<26)
|
|
61
51
|
Project-URL: Repository, https://github.com/dialoguemd/fastapi-sqla
|
|
62
52
|
Description-Content-Type: text/markdown
|
|
63
53
|
|
|
@@ -83,6 +73,8 @@ Using [pip](https://pip.pypa.io/):
|
|
|
83
73
|
pip install fastapi-sqla
|
|
84
74
|
```
|
|
85
75
|
|
|
76
|
+
Note that you need a [SQLAlchemy compatible engine](https://docs.sqlalchemy.org/en/20/core/engines.html) adapter. We test with `psycopg2` which you can install using the `psycopg2` extra.
|
|
77
|
+
|
|
86
78
|
# Quick Example
|
|
87
79
|
|
|
88
80
|
Assuming it runs against a DB with a table `user` with 3 columns, `id`, `name` and
|
|
@@ -90,15 +82,22 @@ unique `email`:
|
|
|
90
82
|
|
|
91
83
|
```python
|
|
92
84
|
# main.py
|
|
85
|
+
from contextlib import asynccontextmanager
|
|
93
86
|
from fastapi import FastAPI, HTTPException
|
|
94
|
-
from fastapi_sqla import Base, Item, Page, Paginate, Session,
|
|
87
|
+
from fastapi_sqla import Base, Item, Page, Paginate, Session, setup_middlewares, startup
|
|
95
88
|
from pydantic import BaseModel, EmailStr
|
|
96
89
|
from sqlalchemy import select
|
|
97
90
|
from sqlalchemy.exc import IntegrityError
|
|
98
91
|
|
|
99
|
-
app = FastAPI()
|
|
100
92
|
|
|
101
|
-
|
|
93
|
+
@asynccontextmanager
|
|
94
|
+
async def lifespan(app: FastAPI):
|
|
95
|
+
await startup()
|
|
96
|
+
yield
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
app = FastAPI(lifespan=lifespan)
|
|
100
|
+
setup_middlewares(app)
|
|
102
101
|
|
|
103
102
|
|
|
104
103
|
class User(Base):
|
|
@@ -203,7 +202,23 @@ And define the environment variable `sqlalchemy_url` with `postgres+asyncpg` sch
|
|
|
203
202
|
export sqlalchemy_url=postgresql+asyncpg://postgres@localhost
|
|
204
203
|
```
|
|
205
204
|
|
|
206
|
-
## Setup the app:
|
|
205
|
+
## Setup the app AsyncContextManager (recommended):
|
|
206
|
+
|
|
207
|
+
```python
|
|
208
|
+
import fastapi_sqla
|
|
209
|
+
from fastapi import FastAPI
|
|
210
|
+
|
|
211
|
+
@asynccontextmanager
|
|
212
|
+
async def lifespan(app: FastAPI):
|
|
213
|
+
await fastapi_sqla.startup()
|
|
214
|
+
yield
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
app = FastAPI(lifespan=lifespan)
|
|
218
|
+
fastapi_sqla.setup_middlewares(app)
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
## Setup the app using startup/shutdown events (deprecated):
|
|
207
222
|
|
|
208
223
|
```python
|
|
209
224
|
import fastapi_sqla
|
|
@@ -640,7 +655,7 @@ If your project uses [SQLModel], then `Session` dependency is an SQLModel sessio
|
|
|
640
655
|
# Pytest fixtures
|
|
641
656
|
|
|
642
657
|
This library provides a set of utility fixtures, through its PyTest plugin, which is
|
|
643
|
-
automatically installed with the library.
|
|
658
|
+
automatically installed with the library. Using the plugin requires the `pytest_plugin` extra.
|
|
644
659
|
|
|
645
660
|
By default, no records are actually written to the database when running tests.
|
|
646
661
|
There currently is no way to change this behaviour.
|
|
@@ -784,7 +799,7 @@ It returns the path of `alembic.ini` configuration file. By default, it returns
|
|
|
784
799
|
## Setup
|
|
785
800
|
|
|
786
801
|
```bash
|
|
787
|
-
$ poetry install --extras
|
|
802
|
+
$ poetry install --all-extras
|
|
788
803
|
```
|
|
789
804
|
|
|
790
805
|
## Running tests
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
fastapi_sqla/__init__.py,sha256=RRkwo9xZzidQ-k3BRXfqT0jtWm8d08w94XuOLteFCdU,1221
|
|
2
|
+
fastapi_sqla/_pytest_plugin.py,sha256=IQUlj-O874Sfuth254LBrEBGCrwH3rNHST9OMZl2pIk,5411
|
|
3
|
+
fastapi_sqla/async_pagination.py,sha256=3DHGUjvrpkbWMIc_BEX4GvM-_PTcn62K9z48ucTJlH0,3164
|
|
4
|
+
fastapi_sqla/async_sqla.py,sha256=L5tHuYgHMpmSdGAsgxs30K7joVmMX-06NYJ7E5WaoUo,6865
|
|
5
|
+
fastapi_sqla/aws_aurora_support.py,sha256=4dxLKOqDccgLwFqlz81L6f4HzrOXMZkY7Zuf4t_310U,838
|
|
6
|
+
fastapi_sqla/aws_rds_iam_support.py,sha256=Uw-XaiwShMMWYKCvlSqXoxvtKMblCAvbCZ1m6BYVpJk,1257
|
|
7
|
+
fastapi_sqla/base.py,sha256=40lov7wREct4q4rfXiKjlwsjFC9iFE0eDq-ghiJwGgY,2279
|
|
8
|
+
fastapi_sqla/models.py,sha256=-B1xwINpTc9rEQd3KYHEC1s5s7jdVQkJ6Gy6xpmT13c,1108
|
|
9
|
+
fastapi_sqla/pagination.py,sha256=1gfIGcmt1OFspbRgtJ8AZOZdFd14DGRc4FkDgyh5bJ8,4517
|
|
10
|
+
fastapi_sqla/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
|
+
fastapi_sqla/sqla.py,sha256=3mIGdYmyOUZaClJiHCoaZpUlzbJilH2lrMzoc2gjZN0,7335
|
|
12
|
+
fastapi_sqla-3.4.8.dist-info/LICENSE,sha256=8G0-nWLqi3xRYRrtRlTE8n1mkYJcnCRoZGUhv6ZE29c,1064
|
|
13
|
+
fastapi_sqla-3.4.8.dist-info/METADATA,sha256=rARChNcTpioOmQMmMGY33T4KvhC8_kD2fDz-16sch3o,20980
|
|
14
|
+
fastapi_sqla-3.4.8.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
|
15
|
+
fastapi_sqla-3.4.8.dist-info/entry_points.txt,sha256=haa0EueKcRo8-AlJTpHBMn08wMBiULNGA53nkvaDWj0,53
|
|
16
|
+
fastapi_sqla-3.4.8.dist-info/RECORD,,
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
fastapi_sqla/__init__.py,sha256=G9bKR6L9EnLgzelE_BLnHgc1cOm0Yu0wMzwLAdyA8iE,1153
|
|
2
|
-
fastapi_sqla/_pytest_plugin.py,sha256=bjPzOuwk32Zo4hoOa_bGjTGhtHGPImh3RyiORqz-J84,5430
|
|
3
|
-
fastapi_sqla/async_pagination.py,sha256=jYeZYtIi7mssM3MfwobOoaPYNZF0BE1EmCK8CqFnwOg,3164
|
|
4
|
-
fastapi_sqla/async_sqla.py,sha256=7SXRH3DMsQvpjCQdvCBEjQhDZ4-cPAg175MlgNVBnCg,5888
|
|
5
|
-
fastapi_sqla/aws_aurora_support.py,sha256=4dxLKOqDccgLwFqlz81L6f4HzrOXMZkY7Zuf4t_310U,838
|
|
6
|
-
fastapi_sqla/aws_rds_iam_support.py,sha256=YSJNhrxmhGN-GVk9PLMTmQSWTKZBvuorKkhc_XaoL44,1189
|
|
7
|
-
fastapi_sqla/base.py,sha256=0X7Gbt49rBHPiSFmNy5S2PT0dA4UBNnwrAesYSkaHBc,1606
|
|
8
|
-
fastapi_sqla/models.py,sha256=fesW7BqkwOA4iC345dbybzcV8Kz4-kLwREo38oqy_7A,1108
|
|
9
|
-
fastapi_sqla/pagination.py,sha256=IjbKFyUHly8jHyOO4pnyPK4RE_cFRDY03OOZeZEm3cY,4513
|
|
10
|
-
fastapi_sqla/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
|
-
fastapi_sqla/sqla.py,sha256=lOikCu_BjGQYfbbtcM59ZjwTL-66cM9L_yIbCEQasAQ,6356
|
|
12
|
-
fastapi_sqla-3.1.2.dist-info/LICENSE,sha256=8G0-nWLqi3xRYRrtRlTE8n1mkYJcnCRoZGUhv6ZE29c,1064
|
|
13
|
-
fastapi_sqla-3.1.2.dist-info/METADATA,sha256=xl5jfdozvDDEJGHW0fe9bFm_ITasdlzYb3NThMDveNk,20915
|
|
14
|
-
fastapi_sqla-3.1.2.dist-info/WHEEL,sha256=Zb28QaM1gQi8f4VCBhsUklF61CTlNYfs9YAZn-TOGFk,88
|
|
15
|
-
fastapi_sqla-3.1.2.dist-info/entry_points.txt,sha256=haa0EueKcRo8-AlJTpHBMn08wMBiULNGA53nkvaDWj0,53
|
|
16
|
-
fastapi_sqla-3.1.2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|