Skip to content

Commit 5b088d4

Browse files
⚡️Cached Sessions (#8)
1 parent 743cf86 commit 5b088d4

File tree

4 files changed

+119
-0
lines changed

4 files changed

+119
-0
lines changed

fastapi_utilities/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,5 @@
22
from .repeat.repeat_at import repeat_at
33

44
from .timer import add_timer_middleware
5+
6+
from .session import FastAPISessionMaker
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from .session import FastAPISessionMaker
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
from contextlib import contextmanager
2+
from sqlalchemy import create_engine
3+
from sqlalchemy.engine import Engine
4+
from sqlalchemy.orm import Session, sessionmaker
5+
6+
7+
class FastAPISessionMaker:
8+
"""
9+
This will allow us to create a cached session maker that can be used as context manager.
10+
"""
11+
12+
def __init__(self, db_url: str):
13+
"""
14+
`db_url` should be any sqlalchemy-compatible database URI.
15+
"""
16+
self.db_url = db_url
17+
self._cached_engine: Engine = None
18+
self._cached_session_maker: Session = None
19+
20+
def _get_db(self):
21+
"""
22+
This will return a cached db connection Session.
23+
"""
24+
if self._cached_engine is None:
25+
self._cached_engine = create_engine(self.db_url)
26+
if self._cached_session_maker is None:
27+
self._cached_session_maker = sessionmaker(
28+
autocommit=False, autoflush=False, bind=self._cached_engine
29+
)
30+
db = self._cached_session_maker()
31+
try:
32+
yield db
33+
db.commit()
34+
except Exception as e:
35+
db.rollback()
36+
raise e
37+
finally:
38+
db.close()
39+
40+
@contextmanager
41+
def context_session(self):
42+
"""
43+
A context manager that works for `get_db` dependency injection.
44+
45+
Usage:
46+
session_maker = FastAPISessionMaker(database_uri)
47+
with session_maker.context_session() as session:
48+
session.query(...)
49+
"""
50+
yield from self._get_db()
51+
52+
def reset_session(self):
53+
"""
54+
This will reset the sessionmaker and engine.
55+
"""
56+
self._cached_session_maker = None
57+
self._cached_engine = None

tests/test_session.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import pytest
2+
from pathlib import Path
3+
import random
4+
from _pytest.capture import CaptureFixture
5+
6+
from fastapi import FastAPI
7+
from fastapi.testclient import TestClient
8+
from fastapi_utilities import FastAPISessionMaker
9+
10+
# setup
11+
db_path = Path("./db.sqlite3")
12+
13+
from sqlalchemy import create_engine
14+
from sqlalchemy.orm import sessionmaker, declarative_base
15+
16+
DB_URL = f"sqlite:///{db_path}"
17+
engine = create_engine(DB_URL, connect_args={"check_same_thread": False})
18+
Base = declarative_base()
19+
20+
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
21+
22+
from sqlalchemy import Column, Integer
23+
24+
25+
class User(Base):
26+
__tablename__ = "users"
27+
28+
id = Column(Integer, primary_key=True)
29+
30+
31+
app = FastAPI()
32+
Base.metadata.create_all(bind=engine)
33+
34+
# done
35+
36+
37+
def test_session():
38+
sm = FastAPISessionMaker(DB_URL)
39+
with sm.context_session() as session:
40+
x = User(id=random.randint(0, 10000))
41+
session.add(x)
42+
43+
44+
def test_reset_session():
45+
sm = FastAPISessionMaker(DB_URL)
46+
sm.reset_session()
47+
48+
49+
def test_session_raise_error(capsys: CaptureFixture[str]) -> None:
50+
sm = FastAPISessionMaker(DB_URL)
51+
try:
52+
with sm.context_session() as session:
53+
x = User(id=1)
54+
session.add(x)
55+
x = User(id=1)
56+
session.add(x)
57+
except Exception:
58+
out, err = capsys.readouterr()
59+
assert out == ""

0 commit comments

Comments
 (0)