mirror of
				https://github.com/ets-labs/python-dependency-injector.git
				synced 2025-11-04 18:07:44 +03:00 
			
		
		
		
	Fastapi sqlalchemy example (#389)
* Add application * Dockerize the app * Fix 204 content-leength error * Rename database file * Add tests * Add README * Fix a typo in FastAPI example * Add docs on FastAPI + SQLAlchemy example * Update changelog * Add link to the example to README and other docs pages * Add EOF to the config.yml
This commit is contained in:
		
							parent
							
								
									a1f779a9f3
								
							
						
					
					
						commit
						d45d98e300
					
				| 
						 | 
				
			
			@ -161,6 +161,7 @@ Choose one of the following:
 | 
			
		|||
- `Sanic example <https://python-dependency-injector.ets-labs.org/examples/sanic.html>`_
 | 
			
		||||
- `FastAPI example <https://python-dependency-injector.ets-labs.org/examples/fastapi.html>`_
 | 
			
		||||
- `FastAPI + Redis example <https://python-dependency-injector.ets-labs.org/examples/fastapi-redis.html>`_
 | 
			
		||||
- `FastAPI + SQLAlchemy example <https://python-dependency-injector.ets-labs.org/examples/fastapi-sqlalchemy.html>`_
 | 
			
		||||
 | 
			
		||||
Tutorials
 | 
			
		||||
---------
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										119
									
								
								docs/examples/fastapi-sqlalchemy.rst
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										119
									
								
								docs/examples/fastapi-sqlalchemy.rst
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,119 @@
 | 
			
		|||
.. _fastapi-sqlalchemy-example:
 | 
			
		||||
 | 
			
		||||
FastAPI + SQLAlchemy example
 | 
			
		||||
============================
 | 
			
		||||
 | 
			
		||||
.. meta::
 | 
			
		||||
   :keywords: Python,Dependency Injection,FastAPI,SQLAlchemy,Example
 | 
			
		||||
   :description: This example demonstrates a usage of the FastAPI, SQLAlchemy, and Dependency Injector.
 | 
			
		||||
 | 
			
		||||
This example shows how to use ``Dependency Injector`` with `FastAPI <https://fastapi.tiangolo.com/>`_ and
 | 
			
		||||
`SQLAlchemy <https://www.sqlalchemy.org/>`_.
 | 
			
		||||
 | 
			
		||||
The source code is available on the `Github <https://github.com/ets-labs/python-dependency-injector/tree/master/examples/miniapps/fastapi-sqlalchemy>`_.
 | 
			
		||||
 | 
			
		||||
Thanks to `@ShvetsovYura <https://github.com/ShvetsovYura>`_ for providing initial example:
 | 
			
		||||
`FastAPI_DI_SqlAlchemy <https://github.com/ShvetsovYura/FastAPI_DI_SqlAlchemy>`_.
 | 
			
		||||
 | 
			
		||||
Application structure
 | 
			
		||||
---------------------
 | 
			
		||||
 | 
			
		||||
Application has next structure:
 | 
			
		||||
 | 
			
		||||
.. code-block:: bash
 | 
			
		||||
 | 
			
		||||
   ./
 | 
			
		||||
   ├── webapp/
 | 
			
		||||
   │   ├── __init__.py
 | 
			
		||||
   │   ├── application.py
 | 
			
		||||
   │   ├── containers.py
 | 
			
		||||
   │   ├── database.py
 | 
			
		||||
   │   ├── endpoints.py
 | 
			
		||||
   │   ├── models.py
 | 
			
		||||
   │   ├── repositories.py
 | 
			
		||||
   │   ├── services.py
 | 
			
		||||
   │   └── tests.py
 | 
			
		||||
   ├── config.yml
 | 
			
		||||
   ├── docker-compose.yml
 | 
			
		||||
   ├── Dockerfile
 | 
			
		||||
   └── requirements.txt
 | 
			
		||||
 | 
			
		||||
Application factory
 | 
			
		||||
-------------------
 | 
			
		||||
 | 
			
		||||
Application factory creates container, wires it with the ``endpoints`` module, creates
 | 
			
		||||
``FastAPI`` app, and setup routes.
 | 
			
		||||
 | 
			
		||||
Application factory also creates database if it does not exist.
 | 
			
		||||
 | 
			
		||||
Listing of ``webapp/application.py``:
 | 
			
		||||
 | 
			
		||||
.. literalinclude:: ../../examples/miniapps/fastapi-sqlalchemy/webapp/application.py
 | 
			
		||||
   :language: python
 | 
			
		||||
 | 
			
		||||
Endpoints
 | 
			
		||||
---------
 | 
			
		||||
 | 
			
		||||
Module ``endpoints`` contains example endpoints. Endpoints have a dependency on user service.
 | 
			
		||||
User service is injected using :ref:`wiring` feature. See ``webapp/endpoints.py``:
 | 
			
		||||
 | 
			
		||||
.. literalinclude:: ../../examples/miniapps/fastapi-sqlalchemy/webapp/endpoints.py
 | 
			
		||||
   :language: python
 | 
			
		||||
 | 
			
		||||
Container
 | 
			
		||||
---------
 | 
			
		||||
 | 
			
		||||
Declarative container wires example user service, user repository, and utility database class.
 | 
			
		||||
See ``webapp/containers.py``:
 | 
			
		||||
 | 
			
		||||
.. literalinclude:: ../../examples/miniapps/fastapi-sqlalchemy/webapp/containers.py
 | 
			
		||||
   :language: python
 | 
			
		||||
 | 
			
		||||
Services
 | 
			
		||||
--------
 | 
			
		||||
 | 
			
		||||
Module ``services`` contains example user service. See ``webapp/services.py``:
 | 
			
		||||
 | 
			
		||||
.. literalinclude:: ../../examples/miniapps/fastapi-sqlalchemy/webapp/services.py
 | 
			
		||||
   :language: python
 | 
			
		||||
 | 
			
		||||
Repositories
 | 
			
		||||
------------
 | 
			
		||||
 | 
			
		||||
Module ``repositories`` contains example user repository. See ``webapp/repositories.py``:
 | 
			
		||||
 | 
			
		||||
.. literalinclude:: ../../examples/miniapps/fastapi-sqlalchemy/webapp/repositories.py
 | 
			
		||||
   :language: python
 | 
			
		||||
 | 
			
		||||
Models
 | 
			
		||||
------
 | 
			
		||||
 | 
			
		||||
Module ``models`` contains example SQLAlchemy user model. See ``webapp/models.py``:
 | 
			
		||||
 | 
			
		||||
.. literalinclude:: ../../examples/miniapps/fastapi-sqlalchemy/webapp/models.py
 | 
			
		||||
   :language: python
 | 
			
		||||
 | 
			
		||||
Database
 | 
			
		||||
-----
 | 
			
		||||
 | 
			
		||||
Module ``database`` defines declarative base and utility class with engine and session factory.
 | 
			
		||||
See ``webapp/database.py``:
 | 
			
		||||
 | 
			
		||||
.. literalinclude:: ../../examples/miniapps/fastapi-sqlalchemy/webapp/database.py
 | 
			
		||||
   :language: python
 | 
			
		||||
 | 
			
		||||
Tests
 | 
			
		||||
-----
 | 
			
		||||
 | 
			
		||||
Tests use :ref:`provider-overriding` feature to replace repository with a mock. See ``webapp/tests.py``:
 | 
			
		||||
 | 
			
		||||
.. literalinclude:: ../../examples/miniapps/fastapi-sqlalchemy/webapp/tests.py
 | 
			
		||||
   :language: python
 | 
			
		||||
   :emphasize-lines: 25, 45, 58, 74, 86, 97
 | 
			
		||||
 | 
			
		||||
Sources
 | 
			
		||||
-------
 | 
			
		||||
 | 
			
		||||
The source code is available on the `Github <https://github.com/ets-labs/python-dependency-injector/tree/master/examples/miniapps/fastapi-sqlalchemy>`_.
 | 
			
		||||
 | 
			
		||||
.. disqus::
 | 
			
		||||
| 
						 | 
				
			
			@ -20,5 +20,6 @@ Explore the examples to see the ``Dependency Injector`` in action.
 | 
			
		|||
    sanic
 | 
			
		||||
    fastapi
 | 
			
		||||
    fastapi-redis
 | 
			
		||||
    fastapi-sqlalchemy
 | 
			
		||||
 | 
			
		||||
.. disqus::
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -288,6 +288,7 @@ Choose one of the following as a next step:
 | 
			
		|||
    - :ref:`sanic-example`
 | 
			
		||||
    - :ref:`fastapi-example`
 | 
			
		||||
    - :ref:`fastapi-redis-example`
 | 
			
		||||
    - :ref:`fastapi-sqlalchemy-example`
 | 
			
		||||
- Pass the tutorials:
 | 
			
		||||
    - :ref:`flask-tutorial`
 | 
			
		||||
    - :ref:`aiohttp-tutorial`
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -7,6 +7,12 @@ that were made in every particular version.
 | 
			
		|||
From version 0.7.6 *Dependency Injector* framework strictly 
 | 
			
		||||
follows `Semantic versioning`_
 | 
			
		||||
 | 
			
		||||
Development version
 | 
			
		||||
-------------------
 | 
			
		||||
- Add ``FastAPI`` + ``SQLAlchemy`` example.
 | 
			
		||||
  Thanks to `@ShvetsovYura <https://github.com/ShvetsovYura>`_ for providing initial example:
 | 
			
		||||
  `FastAPI_DI_SqlAlchemy <https://github.com/ShvetsovYura/FastAPI_DI_SqlAlchemy>`_.
 | 
			
		||||
 | 
			
		||||
4.16.0
 | 
			
		||||
------
 | 
			
		||||
- Add container base class ``containers.Container``. ``DynamicContainer``
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -336,5 +336,6 @@ Take a look at other application examples:
 | 
			
		|||
- :ref:`sanic-example`
 | 
			
		||||
- :ref:`fastapi-example`
 | 
			
		||||
- :ref:`fastapi-redis-example`
 | 
			
		||||
- :ref:`fastapi-sqlalchemy-example`
 | 
			
		||||
 | 
			
		||||
.. disqus::
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										13
									
								
								examples/miniapps/fastapi-sqlalchemy/Dockerfile
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								examples/miniapps/fastapi-sqlalchemy/Dockerfile
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,13 @@
 | 
			
		|||
FROM python:3.9-buster
 | 
			
		||||
 | 
			
		||||
ENV PYTHONUNBUFFERED=1
 | 
			
		||||
ENV HOST=0.0.0.0
 | 
			
		||||
ENV PORT=8000
 | 
			
		||||
 | 
			
		||||
WORKDIR /code
 | 
			
		||||
COPY . /code/
 | 
			
		||||
 | 
			
		||||
RUN pip install --upgrade pip \
 | 
			
		||||
 && pip install -r requirements.txt
 | 
			
		||||
 | 
			
		||||
CMD uvicorn webapp.application:app --host ${HOST} --port ${PORT}
 | 
			
		||||
							
								
								
									
										96
									
								
								examples/miniapps/fastapi-sqlalchemy/README.rst
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										96
									
								
								examples/miniapps/fastapi-sqlalchemy/README.rst
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,96 @@
 | 
			
		|||
FastAPI + SQLAlchemy + Dependency Injector Example
 | 
			
		||||
==================================================
 | 
			
		||||
 | 
			
		||||
This is a `FastAPI <https://fastapi.tiangolo.com/>`_ +
 | 
			
		||||
`SQLAlchemy <https://www.sqlalchemy.org/>`_ +
 | 
			
		||||
`Dependency Injector <https://python-dependency-injector.ets-labs.org/>`_ example application.
 | 
			
		||||
 | 
			
		||||
Thanks to `@ShvetsovYura <https://github.com/ShvetsovYura>`_ for providing initial example:
 | 
			
		||||
`FastAPI_DI_SqlAlchemy <https://github.com/ShvetsovYura/FastAPI_DI_SqlAlchemy>`_.
 | 
			
		||||
 | 
			
		||||
Run
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
Build the Docker image:
 | 
			
		||||
 | 
			
		||||
.. code-block:: bash
 | 
			
		||||
 | 
			
		||||
   docker-compose build
 | 
			
		||||
 | 
			
		||||
Run the docker-compose environment:
 | 
			
		||||
 | 
			
		||||
.. code-block:: bash
 | 
			
		||||
 | 
			
		||||
    docker-compose up
 | 
			
		||||
 | 
			
		||||
The output should be something like:
 | 
			
		||||
 | 
			
		||||
.. code-block::
 | 
			
		||||
 | 
			
		||||
   Starting fastapi-sqlalchemy_webapp_1 ... done
 | 
			
		||||
   Attaching to fastapi-sqlalchemy_webapp_1
 | 
			
		||||
   webapp_1  | 2021-02-04 22:07:19,804 INFO sqlalchemy.engine.base.Engine SELECT CAST('test plain returns' AS VARCHAR(60)) AS anon_1
 | 
			
		||||
   webapp_1  | 2021-02-04 22:07:19,804 INFO sqlalchemy.engine.base.Engine ()
 | 
			
		||||
   webapp_1  | 2021-02-04 22:07:19,804 INFO sqlalchemy.engine.base.Engine SELECT CAST('test unicode returns' AS VARCHAR(60)) AS anon_1
 | 
			
		||||
   webapp_1  | 2021-02-04 22:07:19,804 INFO sqlalchemy.engine.base.Engine ()
 | 
			
		||||
   webapp_1  | 2021-02-04 22:07:19,805 INFO sqlalchemy.engine.base.Engine PRAGMA main.table_info("users")
 | 
			
		||||
   webapp_1  | 2021-02-04 22:07:19,805 INFO sqlalchemy.engine.base.Engine ()
 | 
			
		||||
   webapp_1  | 2021-02-04 22:07:19,808 INFO sqlalchemy.engine.base.Engine PRAGMA temp.table_info("users")
 | 
			
		||||
   webapp_1  | 2021-02-04 22:07:19,808 INFO sqlalchemy.engine.base.Engine ()
 | 
			
		||||
   webapp_1  | 2021-02-04 22:07:19,809 INFO sqlalchemy.engine.base.Engine
 | 
			
		||||
   webapp_1  | CREATE TABLE users (
 | 
			
		||||
   webapp_1  | 	id INTEGER NOT NULL,
 | 
			
		||||
   webapp_1  | 	email VARCHAR,
 | 
			
		||||
   webapp_1  | 	hashed_password VARCHAR,
 | 
			
		||||
   webapp_1  | 	is_active BOOLEAN,
 | 
			
		||||
   webapp_1  | 	PRIMARY KEY (id),
 | 
			
		||||
   webapp_1  | 	UNIQUE (email),
 | 
			
		||||
   webapp_1  | 	CHECK (is_active IN (0, 1))
 | 
			
		||||
   webapp_1  | )
 | 
			
		||||
   webapp_1  |
 | 
			
		||||
   webapp_1  |
 | 
			
		||||
   webapp_1  | 2021-02-04 22:07:19,810 INFO sqlalchemy.engine.base.Engine ()
 | 
			
		||||
   webapp_1  | 2021-02-04 22:07:19,821 INFO sqlalchemy.engine.base.Engine COMMIT
 | 
			
		||||
   webapp_1  | INFO:     Started server process [8]
 | 
			
		||||
   webapp_1  | INFO:     Waiting for application startup.
 | 
			
		||||
   webapp_1  | INFO:     Application startup complete.
 | 
			
		||||
   webapp_1  | INFO:     Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)
 | 
			
		||||
 | 
			
		||||
After that visit http://127.0.0.1:8000/docs in your browser.
 | 
			
		||||
 | 
			
		||||
Test
 | 
			
		||||
----
 | 
			
		||||
 | 
			
		||||
This application comes with the unit tests.
 | 
			
		||||
 | 
			
		||||
To run the tests do:
 | 
			
		||||
 | 
			
		||||
.. code-block:: bash
 | 
			
		||||
 | 
			
		||||
   docker-compose run --rm webapp py.test webapp/tests.py --cov=webapp
 | 
			
		||||
 | 
			
		||||
The output should be something like:
 | 
			
		||||
 | 
			
		||||
.. code-block::
 | 
			
		||||
 | 
			
		||||
   platform linux -- Python 3.9.1, pytest-6.2.2, py-1.10.0, pluggy-0.13.1
 | 
			
		||||
   rootdir: /code
 | 
			
		||||
   plugins: cov-2.11.1
 | 
			
		||||
   collected 7 items
 | 
			
		||||
 | 
			
		||||
   webapp/tests.py .......                                         [100%]
 | 
			
		||||
 | 
			
		||||
   ----------- coverage: platform linux, python 3.9.1-final-0 -----------
 | 
			
		||||
   Name                     Stmts   Miss  Cover
 | 
			
		||||
   --------------------------------------------
 | 
			
		||||
   webapp/__init__.py           0      0   100%
 | 
			
		||||
   webapp/application.py       14      0   100%
 | 
			
		||||
   webapp/containers.py         9      0   100%
 | 
			
		||||
   webapp/database.py          24      8    67%
 | 
			
		||||
   webapp/endpoints.py         32      0   100%
 | 
			
		||||
   webapp/models.py            10      1    90%
 | 
			
		||||
   webapp/repositories.py      36     20    44%
 | 
			
		||||
   webapp/services.py          16      0   100%
 | 
			
		||||
   webapp/tests.py             59      0   100%
 | 
			
		||||
   --------------------------------------------
 | 
			
		||||
   TOTAL                      200     29    86%
 | 
			
		||||
							
								
								
									
										2
									
								
								examples/miniapps/fastapi-sqlalchemy/config.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								examples/miniapps/fastapi-sqlalchemy/config.yml
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,2 @@
 | 
			
		|||
db:
 | 
			
		||||
  url: "sqlite:///./webapp.db"
 | 
			
		||||
							
								
								
									
										11
									
								
								examples/miniapps/fastapi-sqlalchemy/docker-compose.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								examples/miniapps/fastapi-sqlalchemy/docker-compose.yml
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,11 @@
 | 
			
		|||
version: "3.7"
 | 
			
		||||
 | 
			
		||||
services:
 | 
			
		||||
 | 
			
		||||
  webapp:
 | 
			
		||||
    build: ./
 | 
			
		||||
    image: webapp
 | 
			
		||||
    ports:
 | 
			
		||||
      - "8000:8000"
 | 
			
		||||
    volumes:
 | 
			
		||||
      - "./:/code"
 | 
			
		||||
							
								
								
									
										8
									
								
								examples/miniapps/fastapi-sqlalchemy/requirements.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								examples/miniapps/fastapi-sqlalchemy/requirements.txt
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,8 @@
 | 
			
		|||
dependency-injector
 | 
			
		||||
fastapi
 | 
			
		||||
uvicorn
 | 
			
		||||
pyyaml
 | 
			
		||||
sqlalchemy
 | 
			
		||||
pytest
 | 
			
		||||
requests
 | 
			
		||||
pytest-cov
 | 
			
		||||
							
								
								
									
										1
									
								
								examples/miniapps/fastapi-sqlalchemy/webapp/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								examples/miniapps/fastapi-sqlalchemy/webapp/__init__.py
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1 @@
 | 
			
		|||
"""Top-level package."""
 | 
			
		||||
							
								
								
									
										23
									
								
								examples/miniapps/fastapi-sqlalchemy/webapp/application.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								examples/miniapps/fastapi-sqlalchemy/webapp/application.py
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,23 @@
 | 
			
		|||
"""Application module."""
 | 
			
		||||
 | 
			
		||||
from fastapi import FastAPI
 | 
			
		||||
 | 
			
		||||
from .containers import Container
 | 
			
		||||
from . import endpoints
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def create_app() -> FastAPI:
 | 
			
		||||
    container = Container()
 | 
			
		||||
    container.config.from_yaml('config.yml')
 | 
			
		||||
    container.wire(modules=[endpoints])
 | 
			
		||||
 | 
			
		||||
    db = container.db()
 | 
			
		||||
    db.create_database()
 | 
			
		||||
 | 
			
		||||
    app = FastAPI()
 | 
			
		||||
    app.container = container
 | 
			
		||||
    app.include_router(endpoints.router)
 | 
			
		||||
    return app
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
app = create_app()
 | 
			
		||||
							
								
								
									
										24
									
								
								examples/miniapps/fastapi-sqlalchemy/webapp/containers.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								examples/miniapps/fastapi-sqlalchemy/webapp/containers.py
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,24 @@
 | 
			
		|||
"""Containers module."""
 | 
			
		||||
 | 
			
		||||
from dependency_injector import containers, providers
 | 
			
		||||
 | 
			
		||||
from .database import Database
 | 
			
		||||
from .repositories import UserRepository
 | 
			
		||||
from .services import UserService
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Container(containers.DeclarativeContainer):
 | 
			
		||||
 | 
			
		||||
    config = providers.Configuration()
 | 
			
		||||
 | 
			
		||||
    db = providers.Singleton(Database, db_url=config.db.url)
 | 
			
		||||
 | 
			
		||||
    user_repository = providers.Factory(
 | 
			
		||||
        UserRepository,
 | 
			
		||||
        session_factory=db.provided.session,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    user_service = providers.Factory(
 | 
			
		||||
        UserService,
 | 
			
		||||
        user_repository=user_repository,
 | 
			
		||||
    )
 | 
			
		||||
							
								
								
									
										41
									
								
								examples/miniapps/fastapi-sqlalchemy/webapp/database.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								examples/miniapps/fastapi-sqlalchemy/webapp/database.py
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,41 @@
 | 
			
		|||
"""Database module."""
 | 
			
		||||
 | 
			
		||||
from contextlib import contextmanager, AbstractContextManager
 | 
			
		||||
from typing import Callable
 | 
			
		||||
import logging
 | 
			
		||||
 | 
			
		||||
from sqlalchemy import create_engine, orm
 | 
			
		||||
from sqlalchemy.ext.declarative import declarative_base
 | 
			
		||||
from sqlalchemy.orm import Session
 | 
			
		||||
 | 
			
		||||
logger = logging.getLogger(__name__)
 | 
			
		||||
 | 
			
		||||
Base = declarative_base()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Database:
 | 
			
		||||
 | 
			
		||||
    def __init__(self, db_url: str) -> None:
 | 
			
		||||
        self._engine = create_engine(db_url, echo=True)
 | 
			
		||||
        self._session_factory = orm.scoped_session(
 | 
			
		||||
            orm.sessionmaker(
 | 
			
		||||
                autocommit=False,
 | 
			
		||||
                autoflush=False,
 | 
			
		||||
                bind=self._engine,
 | 
			
		||||
            ),
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def create_database(self) -> None:
 | 
			
		||||
        Base.metadata.create_all(self._engine)
 | 
			
		||||
 | 
			
		||||
    @contextmanager
 | 
			
		||||
    def session(self) -> Callable[..., AbstractContextManager[Session]]:
 | 
			
		||||
        session: Session = self._session_factory()
 | 
			
		||||
        try:
 | 
			
		||||
            yield session
 | 
			
		||||
        except Exception:
 | 
			
		||||
            logger.exception('Session rollback because of exception')
 | 
			
		||||
            session.rollback()
 | 
			
		||||
            raise
 | 
			
		||||
        finally:
 | 
			
		||||
            session.close()
 | 
			
		||||
							
								
								
									
										57
									
								
								examples/miniapps/fastapi-sqlalchemy/webapp/endpoints.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								examples/miniapps/fastapi-sqlalchemy/webapp/endpoints.py
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,57 @@
 | 
			
		|||
"""Endpoints module."""
 | 
			
		||||
 | 
			
		||||
from fastapi import APIRouter, Depends, Response, status
 | 
			
		||||
from dependency_injector.wiring import inject, Provide
 | 
			
		||||
 | 
			
		||||
from .containers import Container
 | 
			
		||||
from .services import UserService
 | 
			
		||||
from .repositories import NotFoundError
 | 
			
		||||
 | 
			
		||||
router = APIRouter()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@router.get('/users')
 | 
			
		||||
@inject
 | 
			
		||||
def get_list(
 | 
			
		||||
        user_service: UserService = Depends(Provide[Container.user_service]),
 | 
			
		||||
):
 | 
			
		||||
    return user_service.get_users()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@router.get('/users/{user_id}')
 | 
			
		||||
@inject
 | 
			
		||||
def get_by_id(
 | 
			
		||||
        user_id: int,
 | 
			
		||||
        user_service: UserService = Depends(Provide[Container.user_service]),
 | 
			
		||||
):
 | 
			
		||||
    try:
 | 
			
		||||
        return user_service.get_user_by_id(user_id)
 | 
			
		||||
    except NotFoundError:
 | 
			
		||||
        return Response(status_code=status.HTTP_404_NOT_FOUND)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@router.post('/users', status_code=status.HTTP_201_CREATED)
 | 
			
		||||
@inject
 | 
			
		||||
def add(
 | 
			
		||||
        user_service: UserService = Depends(Provide[Container.user_service]),
 | 
			
		||||
):
 | 
			
		||||
    return user_service.create_user()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@router.delete('/users/{user_id}', status_code=status.HTTP_204_NO_CONTENT)
 | 
			
		||||
@inject
 | 
			
		||||
def remove(
 | 
			
		||||
        user_id: int,
 | 
			
		||||
        user_service: UserService = Depends(Provide[Container.user_service]),
 | 
			
		||||
):
 | 
			
		||||
    try:
 | 
			
		||||
        user_service.delete_user_by_id(user_id)
 | 
			
		||||
    except NotFoundError:
 | 
			
		||||
        return Response(status_code=status.HTTP_404_NOT_FOUND)
 | 
			
		||||
    else:
 | 
			
		||||
        return Response(status_code=status.HTTP_204_NO_CONTENT)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@router.get('/status')
 | 
			
		||||
def get_status():
 | 
			
		||||
    return {'status': 'OK'}
 | 
			
		||||
							
								
								
									
										21
									
								
								examples/miniapps/fastapi-sqlalchemy/webapp/models.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								examples/miniapps/fastapi-sqlalchemy/webapp/models.py
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,21 @@
 | 
			
		|||
"""Models module."""
 | 
			
		||||
 | 
			
		||||
from sqlalchemy import Column, String, Boolean, Integer
 | 
			
		||||
 | 
			
		||||
from .database import Base
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class User(Base):
 | 
			
		||||
 | 
			
		||||
    __tablename__ = 'users'
 | 
			
		||||
 | 
			
		||||
    id = Column(Integer, primary_key=True)
 | 
			
		||||
    email = Column(String, unique=True)
 | 
			
		||||
    hashed_password = Column(String)
 | 
			
		||||
    is_active = Column(Boolean, default=True)
 | 
			
		||||
 | 
			
		||||
    def __repr__(self):
 | 
			
		||||
        return f'<User(id="{self.id}", ' \
 | 
			
		||||
               f'email="{self.email}", ' \
 | 
			
		||||
               f'hashed_password="{self.hashed_password}", ' \
 | 
			
		||||
               f'is_active="{self.is_active}")>'
 | 
			
		||||
							
								
								
									
										54
									
								
								examples/miniapps/fastapi-sqlalchemy/webapp/repositories.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								examples/miniapps/fastapi-sqlalchemy/webapp/repositories.py
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,54 @@
 | 
			
		|||
"""Repositories module."""
 | 
			
		||||
 | 
			
		||||
from contextlib import AbstractContextManager
 | 
			
		||||
from typing import Callable, Iterator
 | 
			
		||||
 | 
			
		||||
from sqlalchemy.orm import Session
 | 
			
		||||
 | 
			
		||||
from .models import User
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class UserRepository:
 | 
			
		||||
 | 
			
		||||
    def __init__(self, session_factory: Callable[..., AbstractContextManager[Session]]) -> None:
 | 
			
		||||
        self.session_factory = session_factory
 | 
			
		||||
 | 
			
		||||
    def get_all(self) -> Iterator[User]:
 | 
			
		||||
        with self.session_factory() as session:
 | 
			
		||||
            return session.query(User).all()
 | 
			
		||||
 | 
			
		||||
    def get_by_id(self, user_id: int) -> User:
 | 
			
		||||
        with self.session_factory() as session:
 | 
			
		||||
            user = session.query(User).filter(User.id == user_id).first()
 | 
			
		||||
            if not user:
 | 
			
		||||
                raise UserNotFoundError(user_id)
 | 
			
		||||
            return user
 | 
			
		||||
 | 
			
		||||
    def add(self, email: str, password: str, is_active: bool = True) -> User:
 | 
			
		||||
        with self.session_factory() as session:
 | 
			
		||||
            user = User(email=email, hashed_password=password, is_active=is_active)
 | 
			
		||||
            session.add(user)
 | 
			
		||||
            session.commit()
 | 
			
		||||
            session.refresh(user)
 | 
			
		||||
            return user
 | 
			
		||||
 | 
			
		||||
    def delete_by_id(self, user_id: int) -> None:
 | 
			
		||||
        with self.session_factory() as session:
 | 
			
		||||
            entity: User = session.query(User).filter(User.id == user_id).first()
 | 
			
		||||
            if not entity:
 | 
			
		||||
                raise UserNotFoundError(user_id)
 | 
			
		||||
            session.delete(entity)
 | 
			
		||||
            session.commit()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class NotFoundError(Exception):
 | 
			
		||||
 | 
			
		||||
    entity_name: str
 | 
			
		||||
 | 
			
		||||
    def __init__(self, entity_id):
 | 
			
		||||
        super().__init__(f'{self.entity_name} not found, id: {entity_id}')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class UserNotFoundError(NotFoundError):
 | 
			
		||||
 | 
			
		||||
    entity_name: str = 'User'
 | 
			
		||||
							
								
								
									
										26
									
								
								examples/miniapps/fastapi-sqlalchemy/webapp/services.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								examples/miniapps/fastapi-sqlalchemy/webapp/services.py
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,26 @@
 | 
			
		|||
"""Services module."""
 | 
			
		||||
 | 
			
		||||
from uuid import uuid4
 | 
			
		||||
from typing import Iterator
 | 
			
		||||
 | 
			
		||||
from .repositories import UserRepository
 | 
			
		||||
from .models import User
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class UserService:
 | 
			
		||||
 | 
			
		||||
    def __init__(self, user_repository: UserRepository) -> None:
 | 
			
		||||
        self._repository: UserRepository = user_repository
 | 
			
		||||
 | 
			
		||||
    def get_users(self) -> Iterator[User]:
 | 
			
		||||
        return self._repository.get_all()
 | 
			
		||||
 | 
			
		||||
    def get_user_by_id(self, user_id: int) -> User:
 | 
			
		||||
        return self._repository.get_by_id(user_id)
 | 
			
		||||
 | 
			
		||||
    def create_user(self) -> User:
 | 
			
		||||
        uid = uuid4()
 | 
			
		||||
        return self._repository.add(email=f'{uid}@email.com', password='pwd')
 | 
			
		||||
 | 
			
		||||
    def delete_user_by_id(self, user_id: int) -> None:
 | 
			
		||||
        return self._repository.delete_by_id(user_id)
 | 
			
		||||
							
								
								
									
										107
									
								
								examples/miniapps/fastapi-sqlalchemy/webapp/tests.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										107
									
								
								examples/miniapps/fastapi-sqlalchemy/webapp/tests.py
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,107 @@
 | 
			
		|||
"""Tests module."""
 | 
			
		||||
 | 
			
		||||
from unittest import mock
 | 
			
		||||
 | 
			
		||||
import pytest
 | 
			
		||||
from fastapi.testclient import TestClient
 | 
			
		||||
 | 
			
		||||
from .repositories import UserRepository, UserNotFoundError
 | 
			
		||||
from .models import User
 | 
			
		||||
from .application import app
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.fixture
 | 
			
		||||
def client():
 | 
			
		||||
    yield TestClient(app)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_get_list(client):
 | 
			
		||||
    repository_mock = mock.Mock(spec=UserRepository)
 | 
			
		||||
    repository_mock.get_all.return_value = [
 | 
			
		||||
        User(id=1, email='test1@email.com', hashed_password='pwd', is_active=True),
 | 
			
		||||
        User(id=2, email='test2@email.com', hashed_password='pwd', is_active=False),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    with app.container.user_repository.override(repository_mock):
 | 
			
		||||
        response = client.get('/users')
 | 
			
		||||
 | 
			
		||||
    assert response.status_code == 200
 | 
			
		||||
    data = response.json()
 | 
			
		||||
    assert data == [
 | 
			
		||||
        {'id': 1, 'email': 'test1@email.com', 'hashed_password': 'pwd', 'is_active': True},
 | 
			
		||||
        {'id': 2, 'email': 'test2@email.com', 'hashed_password': 'pwd', 'is_active': False},
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_get_by_id(client):
 | 
			
		||||
    repository_mock = mock.Mock(spec=UserRepository)
 | 
			
		||||
    repository_mock.get_by_id.return_value = User(
 | 
			
		||||
        id=1,
 | 
			
		||||
        email='xyz@email.com',
 | 
			
		||||
        hashed_password='pwd',
 | 
			
		||||
        is_active=True,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    with app.container.user_repository.override(repository_mock):
 | 
			
		||||
        response = client.get('/users/1')
 | 
			
		||||
 | 
			
		||||
    assert response.status_code == 200
 | 
			
		||||
    data = response.json()
 | 
			
		||||
    assert data == {'id': 1, 'email': 'xyz@email.com', 'hashed_password': 'pwd', 'is_active': True}
 | 
			
		||||
    repository_mock.get_by_id.assert_called_once_with(1)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_get_by_id_404(client):
 | 
			
		||||
    repository_mock = mock.Mock(spec=UserRepository)
 | 
			
		||||
    repository_mock.get_by_id.side_effect = UserNotFoundError(1)
 | 
			
		||||
 | 
			
		||||
    with app.container.user_repository.override(repository_mock):
 | 
			
		||||
        response = client.get('/users/1')
 | 
			
		||||
 | 
			
		||||
    assert response.status_code == 404
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@mock.patch('webapp.services.uuid4', return_value='xyz')
 | 
			
		||||
def test_add(_, client):
 | 
			
		||||
    repository_mock = mock.Mock(spec=UserRepository)
 | 
			
		||||
    repository_mock.add.return_value = User(
 | 
			
		||||
        id=1,
 | 
			
		||||
        email='xyz@email.com',
 | 
			
		||||
        hashed_password='pwd',
 | 
			
		||||
        is_active=True,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    with app.container.user_repository.override(repository_mock):
 | 
			
		||||
        response = client.post('/users')
 | 
			
		||||
 | 
			
		||||
    assert response.status_code == 201
 | 
			
		||||
    data = response.json()
 | 
			
		||||
    assert data == {'id': 1, 'email': 'xyz@email.com', 'hashed_password': 'pwd', 'is_active': True}
 | 
			
		||||
    repository_mock.add.assert_called_once_with(email='xyz@email.com', password='pwd')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_remove(client):
 | 
			
		||||
    repository_mock = mock.Mock(spec=UserRepository)
 | 
			
		||||
 | 
			
		||||
    with app.container.user_repository.override(repository_mock):
 | 
			
		||||
        response = client.delete('/users/1')
 | 
			
		||||
 | 
			
		||||
    assert response.status_code == 204
 | 
			
		||||
    repository_mock.delete_by_id.assert_called_once_with(1)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_remove_404(client):
 | 
			
		||||
    repository_mock = mock.Mock(spec=UserRepository)
 | 
			
		||||
    repository_mock.delete_by_id.side_effect = UserNotFoundError(1)
 | 
			
		||||
 | 
			
		||||
    with app.container.user_repository.override(repository_mock):
 | 
			
		||||
        response = client.delete('/users/1')
 | 
			
		||||
 | 
			
		||||
    assert response.status_code == 404
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_status(client):
 | 
			
		||||
    response = client.get('/status')
 | 
			
		||||
    assert response.status_code == 200
 | 
			
		||||
    data = response.json()
 | 
			
		||||
    assert data == {'status': 'OK'}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,7 +1,7 @@
 | 
			
		|||
FastAPI + Dependency Injector Example
 | 
			
		||||
=====================================
 | 
			
		||||
 | 
			
		||||
This is an `FastAPI <https://fastapi.tiangolo.com/>`_ +
 | 
			
		||||
This is a `FastAPI <https://fastapi.tiangolo.com/>`_ +
 | 
			
		||||
`Dependency Injector <https://python-dependency-injector.ets-labs.org/>`_ example application.
 | 
			
		||||
 | 
			
		||||
The example application is a REST API that searches for funny GIFs on the `Giphy <https://giphy.com/>`_.
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue
	
	Block a user