ok, good to know. Thanks for the info.
Security concerns with session: definitely. I’d be managing that carefully and have a lot of experience with SQLAlchemy.
I’m experimenting with matching routes to a data loader so that I can move away from only singular HTTP Request-Response to a composable system that supports singular and batch routes. That way I’d have the flexibility to use common data loaders across:
- Single route HTTP request-response
- Batch route HTTP request-response (similar to how GraphQL can run multiple queries in a single request)
- Websocket multiplexing
So if one had routes like /grommets
and /widgets
then a request to /batch
with a JSON POST request of {"load": ["/grommets", "/widgets"]}
would return the exact same thing as the non-batch routes, just nested in a batch JSON object
.
Here is a simple (-ish) example of the system:
from sqlalchemy import DeclarativeBase, Session, select, create_engine
from sqlalchemy.orm import Mapped, mapped_column
# SQLAlchemy 2.0 Models
class Grommet(DeclarativeBase):
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str]
class Widget(DeclarativeBase):
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str]
# Queries
def all_grommet_query():
return select(Grommet)
def all_widget_query():
return select(Widget)
# Loaders
def load_grommets(session):
query = all_grommet_query()
return session.execute(query)
def load_widgets(session):
query = all_widget_query()
return session.execute(query)
# Simple Route Lookup
routes = {
"/grommets": load_grommets,
"/widgets": load_widgets
}
# Handler - Load all
def route_handler(session, request):
response = {}
for route in request["load"]:
loader = routes[route]
response[route] = loader(session)
return response
# SQLAlchemy Engine
engine = create_engine("sqlite+pysqlite:///:memory:")
# -------------------------------
# EXAMPLE REQUESTS
# -------------------------------
# Simulate singular request for grommets
def single_grommets_request():
request = {
"load": ["/grommets"]
}
with Session(engine) as session:
return route_handler(session, request). # Respond with load_grommets
# Simulate singular request for widgets
def single_widgets_request():
request = {
"load": ["/widgets"]
}
with Session(engine) as session:
return route_handler(session, request). # Respond with load_widgets
# Simulate batch request for grommets and widgets
def batch_grommets_and_widgets_request():
request = {
"load": ["/grommets", "/widgets"]
}
with Session(engine) as session:
return route_handler(session, request). # Respond with load_grommets and load_widgets batched into single response
Lots of code, but basically it’s just mapping a URI Path (“route”) to database loader function of some kind. It’s so similar to the Sanic routing system, that all I’m really trying to do is avoid re-inventing the wheel by using (abusing?) the test client as a way to compose “sub requests” into a batch request, while also keeping those sub-requests accessible as well – a.k.a composability: I can add singular parts, or any sum of the parts, and it’s all consistent.
If I understand, it’s probably better to layer this in a way where the loaders are mapped in my own decorator system and then map that on to Sanic routes with app.add_route(...)
, so the database loader layer and the HTTP routing layer don’t get mixed up.
Thanks for the guidance. It helped clarify my thinking on this.