-
-
Notifications
You must be signed in to change notification settings - Fork 954
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Rewrite databases page #1921
Comments
The I can't see it used anywhere else. https://github.com/encode/starlette/search?q=databases |
Ah that's unfortunate. Databases was a great concept- SQLAlchemy's documentation is still some of the most verbose and troublesome to navigate in the Python community, and I don't see that being addressed anytime soon- even if the new 1.4 (and 2.0 API) is a good option once you learn it and can treat the SQLAlchemy docs strictly as a reference manual rather than something one reads top to bottom. |
To your credit, @tomchristie all your documentation can be read from top to bottom, and that's underappreciated and lovely. |
Does this mean that |
Thank you for your comment, @gnat. |
I see SQLAlchemy has |
I imagine if SQLAlchemy had https://docs.sqlalchemy.org/en/20/changelog/migration_14.html#change-3414 tl;dr: SQLAlchemy makes use of I'm not terribly familiar with the |
Yes. |
To Starlette team:
I find that features of SQLAlchemy providing more consideration for real life. (For another example: horizontal partitioning https://docs.sqlalchemy.org/en/14/orm/examples.html#examples-sharding) I follow the above url of SQLAlchemy document that I can make a starlette program with multiple databases support.
We are engineers, we know the good quality of code in Starlette. In summary, I suggest your coming document to target more non-technical outsiders to know the strength of enterprise-ready Starlette . |
I've been thinking to document the SQLAlchemy docs with latest SQLAlchemy v2 API, which is a bit different from V1, I'm open to suggestions but I think doing it for v2 will avoid a redo in the future and is much nicer. |
For the For the person that will work on this, I've created this example to replace the one we have in that page: from contextlib import asynccontextmanager
from typing import AsyncIterator
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncAttrs, async_sessionmaker, create_async_engine
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
from starlette.applications import Starlette
from starlette.config import Config
from starlette.requests import Request
from starlette.responses import JSONResponse
from starlette.routing import Route
# Configuration from environment variables or '.env' file.
config = Config(".env")
DATABASE_URL = config("DATABASE_URL")
# SQLAlchemy setup
engine = create_async_engine(DATABASE_URL, echo=True)
async_session = async_sessionmaker(engine, expire_on_commit=False)
class Base(AsyncAttrs, DeclarativeBase):
pass
class Note(Base):
__tablename__ = "notes"
id: Mapped[int] = mapped_column(primary_key=True)
text: Mapped[str]
completed: Mapped[bool]
# Main application code
@asynccontextmanager
async def lifespan(app: Starlette) -> AsyncIterator[None]:
# Create tables
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
yield
await engine.dispose()
async def list_notes(request: Request):
async with async_session() as session:
query = await session.execute(select(Note))
results = query.scalars().all()
return JSONResponse(
[{"text": result.text, "completed": result.completed} for result in results]
)
async def add_note(request: Request):
data = await request.json()
new_note = Note(text=data["text"], completed=data["completed"])
async with async_session() as session:
async with session.begin():
session.add(new_note)
return JSONResponse({"text": new_note.text, "completed": new_note.completed})
routes = [
Route("/notes", endpoint=list_notes, methods=["GET"]),
Route("/notes", endpoint=add_note, methods=["POST"]),
]
app = Starlette(routes=routes, lifespan=lifespan) |
Are you still interested in working on this @aminalaee ? |
Yeah sure, I can give it a try. |
Whoever wants to work on this, go ahead. |
@Kludex , I think I can try working on it, but I have a few questions about how to set up the database for the tests. For the demonstration you showed above, and following the current approach in the databases documentation, I was thinking of something like an in-memory database # main.py
# ...
# Configuration from environment variables or '.env' file.
config = Config(".env")
TESTING = config("TESTING", cast=bool, default=False)
DATABASE_URL = config("DATABASE_URL")
TEST_DATABASE_URL = "sqlite+aiosqlite:///:memory:"
database_url = TEST_DATABASE_URL if TESTING else DATABASE_URL
# SQLAlchemy setup
engine = create_async_engine(database_url, echo=True)
async_session = async_sessionmaker(engine, expire_on_commit=False)
# ... And then, in the tests. from collections.abc import AsyncGenerator
from typing import Literal
import pytest
from sqlalchemy.ext.asyncio import (
AsyncEngine,
AsyncSession,
async_sessionmaker,
create_async_engine,
)
from starlette.config import environ
from starlette.testclient import TestClient
# This sets `os.environ`, but provides some additional protection.
# If we placed it below the application import, it would raise an error
# informing us that 'TESTING' had already been read from the environment.
environ["TESTING"] = "True"
from main import Base, app, database_url
@pytest.fixture(autouse=True)
async def _engine() -> AsyncGenerator:
engine = create_async_engine(database_url, echo=True)
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
yield
await conn.run_sync(Base.metadata.drop_all)
await engine.dispose() And should it also include a session feature? Something like this maybe from collections.abc import AsyncGenerator
from typing import Literal
import pytest
from sqlalchemy.ext.asyncio import (
AsyncEngine,
AsyncSession,
async_sessionmaker,
create_async_engine,
)
from starlette.config import environ
from starlette.testclient import TestClient
# This sets `os.environ`, but provides some additional protection.
# If we placed it below the application import, it would raise an error
# informing us that 'TESTING' had already been read from the environment.
environ["TESTING"] = "True"
from main import Base, app, database_url
@pytest.fixture(scope="session", autouse=True)
def anyio_backend() -> Literal["asyncio"]:
return "asyncio"
@pytest.fixture(autouse=True)
async def engine() -> AsyncGenerator[AsyncEngine]:
engine = create_async_engine(database_url, echo=True)
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
yield engine
await conn.run_sync(Base.metadata.drop_all)
await engine.dispose()
@pytest.fixture
async def session(engine: AsyncEngine) -> AsyncGenerator[AsyncSession]:
async_session = async_sessionmaker(engine, expire_on_commit=False)
async with async_session() as session:
yield session
@pytest.fixture
def client():
with TestClient(app) as client:
yield client
But in this case, it would not work with an in-memory database because of the lifespan. Is there a better way to "override" the app session than using this approach of setting the |
The idea is to rewrite the databases page on our documentation: https://www.starlette.io/database/ .
Instead of recommending
databases
, we should recommend pureSQLAlchemy
.cc @zzzeek (just pinging to let you know about our intentions, no need to react)
Important
The text was updated successfully, but these errors were encountered: