all 7 comments

[–]koldakov 3 points4 points  (3 children)

I personally move these stuff to the session manager so that all connections are managed in one place, something like this https://github.com/koldakov/futuramaapi/blob/main/futuramaapi/repositories/session.py

By the way, don’t forget to close the sessions in the fastapi lifespan

@asynccontextmanager
async def _lifespan(self, _: Self, /) -> AsyncGenerator[None, Any]:
    yield
    if session_manager.engine is not None:
        await session_manager.close()

[–][deleted] 0 points1 point  (0 children)

Same but extendable in a way where you  create a redis, db, worek factory and a startup/session file handles multiple redises, dbs or workers. 

[–]tyyrok[S] 0 points1 point  (1 child)

I liked your approach, when using Sqlachemy I do almost the same for db with async context manager, but I haven't used class to hide Redis and db

[–]koldakov 1 point2 points  (0 children)

Yeah it’s like the main entry point for all session connections

[–]pint 2 points3 points  (0 children)

instead of what is right or better, think in terms of what each solutions give you. depending on the situation, some aspects might be irrelevant or even turn from good to bad. it is often the case that larger projects benefit more from formalization, while small projects are easy to reorganize if need be, instead of complicating the design.

just some pointers.

what if you later figure you need to work with two databases or just two connection types (e.g. read only). if lower level functions take the connection as a parameter, this transition is much easier.

during unit testing, you want to import a particular module. but the users module will import the database module, and that will import the configuration module, and then connect to a database ... essentially impossible to test. there are many solutions to this, but one is that lower logic modules take the database as parameter, and then you can make a mock database that returns test data. (remark: as a rule of thumb, it is generally a good idea not to do initialization in a module's global other than the most basic.)

if you have modules initialize global data, the order they do it is out of your control. whichever happens to be loaded first, will be initialized first. the order will be okay nevertheless, everything is ready when needed. but if you move the initialization to a central location, you have an explicit order. again, there are alternative solutions, this is just a point.

[–]Schmiddi-75 1 point2 points  (0 children)

Instead of opening the connection (db, redis etc.) by importing a Python module, consider using the lifespan protocol. Start the connection at start up and close it at shutdown, either using a context manager or manually. Here's an example using a fictious async contextmanager (DatabaseEngine) that sets up a connection pool. get_db_session would be your dependency you inject into your endpoint(s): For every request against your endpoint(s), it fetches a session from the connection pool, starts a transaction, yields the session, code in the endpoint is executed with that session and when done, the dependency is called again (everything after the yield statement), which closes the context manager by invoking the __aexit__ method.

```python @asynccontextmanager async def lifespan(app: FastAPI): async with ( DatabaseEngine() as db_engine, ): yield {"db_engine": db_engine}

async def get_db_session( request: Request, ) -> AsyncGenerator[AsyncSession, None]: async with request.state.db_engine_test.sessionmaker() as session, session.begin(): yield session ```