Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 15 additions & 5 deletions src/fastsqla.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import os
from collections.abc import AsyncGenerator, Awaitable, Callable, Iterable
from contextlib import _AsyncGeneratorContextManager, asynccontextmanager
from typing import Annotated, Generic, TypedDict, TypeVar
from typing import Annotated, TypedDict, TypeVar

from fastapi import Depends as BaseDepends
from fastapi import FastAPI, Query
Expand Down Expand Up @@ -31,6 +31,7 @@
"Base",
"Collection",
"Item",
"MissingConfigurationError",
"Page",
"Paginate",
"PaginateType",
Expand Down Expand Up @@ -88,6 +89,10 @@ class State(TypedDict):
fastsqla_engine: AsyncEngine


class MissingConfigurationError(RuntimeError):
"""Raised when a required SQLAlchemy setting is missing."""


def new_lifespan(
url: str | None = None, **kw
) -> Callable[[FastAPI | None], _AsyncGeneratorContextManager[State, None]]:
Expand All @@ -112,6 +117,9 @@ def new_lifespan(
Args:
url (str): Database url.
kw (dict): Configuration parameters as expected by [`sqlalchemy.ext.asyncio.create_async_engine`][sqlalchemy.ext.asyncio.create_async_engine]

Raises:
MissingConfigurationError: If a required SQLAlchemy setting is missing.
"""

has_config = url is not None
Expand All @@ -120,7 +128,7 @@ def new_lifespan(
async def lifespan(app: FastAPI | None) -> AsyncGenerator[State, None]:
if has_config:
prefix = ""
sqla_config = {**kw, **{"url": url}}
sqla_config = {**kw, "url": url}

else:
prefix = "sqlalchemy_"
Expand All @@ -130,7 +138,9 @@ async def lifespan(app: FastAPI | None) -> AsyncGenerator[State, None]:
engine = async_engine_from_config(sqla_config, prefix=prefix)

except KeyError as exc:
raise Exception(f"Missing {prefix}{exc.args[0]} in environ.") from exc
raise MissingConfigurationError(
f"Missing {prefix}{exc.args[0]} in environ."
) from exc

async with engine.begin() as conn:
await conn.run_sync(Base.prepare)
Expand Down Expand Up @@ -330,11 +340,11 @@ class Meta(BaseModel):
T = TypeVar("T")


class Item(BaseModel, Generic[T]):
class Item[T](BaseModel):
data: T


class Collection(BaseModel, Generic[T]):
class Collection[T](BaseModel):
data: list[T]


Expand Down
4 changes: 2 additions & 2 deletions tests/integration/conftest.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from asgi_lifespan import LifespanManager
from httpx import AsyncClient, ASGITransport
from pytest import fixture
from fastapi import FastAPI
from httpx import ASGITransport, AsyncClient
from pytest import fixture


@fixture
Expand Down
4 changes: 3 additions & 1 deletion tests/integration/test_pagination.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from typing import Annotated, cast

from fastapi import Depends
from pydantic import EmailStr
from pytest import fixture
Expand Down Expand Up @@ -57,10 +58,11 @@ async def setup_tear_down(engine, faker):

@fixture
def app(app):
from fastsqla import Base, Page, Paginate, PaginateType, Session, new_pagination
from pydantic import BaseModel
from sqlalchemy.orm import Mapped, mapped_column

from fastsqla import Base, Page, Paginate, PaginateType, Session, new_pagination

class User(Base):
__tablename__ = "user"
id: Mapped[int] = mapped_column(primary_key=True)
Expand Down
1 change: 0 additions & 1 deletion tests/integration/test_session_dependency.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ async def get_session(session: Session):
@app.post("/users", response_model=Item[UserModel], status_code=HTTPStatus.CREATED)
async def create_user(user_in: UserIn, session: Session):
user = User(**user_in.model_dump())
user_in.model_dump
session.add(user)
try:
await session.flush()
Expand Down
6 changes: 3 additions & 3 deletions tests/integration/test_sqlmodel.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
from sqlalchemy.exc import IntegrityError
from sqlalchemy.ext.automap import automap_base


pytestmark = mark.require_sqlmodel


Expand Down Expand Up @@ -49,7 +48,7 @@ async def setup_tear_down(engine, heros_data):

stmt = insert(Hero).values(
[
dict(name=name, secret_identity=secret_identity, age=age)
{"name": name, "secret_identity": secret_identity, "age": age}
for name, secret_identity, age in heros_data
]
)
Expand All @@ -61,9 +60,10 @@ async def setup_tear_down(engine, heros_data):

@fixture
async def app(setup_tear_down, app):
from fastsqla import Item, Page, Paginate, Session
from sqlmodel import Field, SQLModel

from fastsqla import Item, Page, Paginate, Session

class Hero(SQLModel, table=True):
__table_args__ = {"extend_existing": True}
id: int | None = Field(default=None, primary_key=True)
Expand Down
10 changes: 5 additions & 5 deletions tests/unit/test_lifespan.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from fastapi import FastAPI
from pytest import raises, fixture
from pytest import fixture, raises

_app = FastAPI()

Expand Down Expand Up @@ -31,15 +31,15 @@ async def test_it_binds_an_sqla_engine_to_sessionmaker(environ, app):


async def test_it_fails_on_a_missing_sqlalchemy_url(monkeypatch, app):
from fastsqla import lifespan
from fastsqla import MissingConfigurationError, lifespan

monkeypatch.delenv("SQLALCHEMY_URL", raising=False)
with raises(Exception) as raise_info:
with raises(
MissingConfigurationError, match=r"Missing sqlalchemy_url in environ\."
):
async with lifespan(app):
pass

assert raise_info.value.args[0] == "Missing sqlalchemy_url in environ."


async def test_it_fails_on_not_async_engine(monkeypatch, app):
from fastsqla import lifespan
Expand Down
14 changes: 8 additions & 6 deletions tests/unit/test_open_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
from sqlalchemy import text


class SimulatedError(RuntimeError):
pass


@fixture
def tablename(request):
return request.node.name
Expand Down Expand Up @@ -38,22 +42,20 @@ async def test_it_re_raises_when_committing_fails():

with patch("fastsqla.SessionFactory") as SessionFactory:
session = AsyncMock()
session.commit.side_effect = Exception("Simulating a failure.")
session.commit.side_effect = SimulatedError("Simulating a failure.")
SessionFactory.return_value = session
with raises(Exception) as raise_info:
with raises(SimulatedError, match=r"Simulating a failure\."):
async with open_session():
pass

assert "Simulating a failure." in raise_info.value.args[0]


async def test_it_rollback_on_failure(engine, tablename):
from fastsqla import open_session

with raises(Exception):
with raises(SimulatedError, match=r"Simulating a failure\."):
async with open_session() as session:
await session.execute(text(f"insert into {tablename} values ('OK')"))
raise Exception("Simulating a failure.")
raise SimulatedError("Simulating a failure.")

async with engine.connect() as conn:
res = await conn.execute(text(f"select * from {tablename}"))
Expand Down
42 changes: 21 additions & 21 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading