From d45d98e300db4d91d2bd2064c5dbc51043428d73 Mon Sep 17 00:00:00 2001 From: Roman Mogylatov Date: Thu, 4 Feb 2021 18:18:25 -0500 Subject: [PATCH 1/2] 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 --- README.rst | 1 + docs/examples/fastapi-sqlalchemy.rst | 119 ++++++++++++++++++ docs/examples/index.rst | 1 + docs/introduction/di_in_python.rst | 1 + docs/main/changelog.rst | 6 + docs/wiring.rst | 1 + .../miniapps/fastapi-sqlalchemy/Dockerfile | 13 ++ .../miniapps/fastapi-sqlalchemy/README.rst | 96 ++++++++++++++ .../miniapps/fastapi-sqlalchemy/config.yml | 2 + .../fastapi-sqlalchemy/docker-compose.yml | 11 ++ .../fastapi-sqlalchemy/requirements.txt | 8 ++ .../fastapi-sqlalchemy/webapp/__init__.py | 1 + .../fastapi-sqlalchemy/webapp/application.py | 23 ++++ .../fastapi-sqlalchemy/webapp/containers.py | 24 ++++ .../fastapi-sqlalchemy/webapp/database.py | 41 ++++++ .../fastapi-sqlalchemy/webapp/endpoints.py | 57 +++++++++ .../fastapi-sqlalchemy/webapp/models.py | 21 ++++ .../fastapi-sqlalchemy/webapp/repositories.py | 54 ++++++++ .../fastapi-sqlalchemy/webapp/services.py | 26 ++++ .../fastapi-sqlalchemy/webapp/tests.py | 107 ++++++++++++++++ examples/miniapps/fastapi/README.rst | 2 +- 21 files changed, 614 insertions(+), 1 deletion(-) create mode 100644 docs/examples/fastapi-sqlalchemy.rst create mode 100644 examples/miniapps/fastapi-sqlalchemy/Dockerfile create mode 100644 examples/miniapps/fastapi-sqlalchemy/README.rst create mode 100644 examples/miniapps/fastapi-sqlalchemy/config.yml create mode 100644 examples/miniapps/fastapi-sqlalchemy/docker-compose.yml create mode 100644 examples/miniapps/fastapi-sqlalchemy/requirements.txt create mode 100644 examples/miniapps/fastapi-sqlalchemy/webapp/__init__.py create mode 100644 examples/miniapps/fastapi-sqlalchemy/webapp/application.py create mode 100644 examples/miniapps/fastapi-sqlalchemy/webapp/containers.py create mode 100644 examples/miniapps/fastapi-sqlalchemy/webapp/database.py create mode 100644 examples/miniapps/fastapi-sqlalchemy/webapp/endpoints.py create mode 100644 examples/miniapps/fastapi-sqlalchemy/webapp/models.py create mode 100644 examples/miniapps/fastapi-sqlalchemy/webapp/repositories.py create mode 100644 examples/miniapps/fastapi-sqlalchemy/webapp/services.py create mode 100644 examples/miniapps/fastapi-sqlalchemy/webapp/tests.py diff --git a/README.rst b/README.rst index 1d5d4512..6eba4ce6 100644 --- a/README.rst +++ b/README.rst @@ -161,6 +161,7 @@ Choose one of the following: - `Sanic example `_ - `FastAPI example `_ - `FastAPI + Redis example `_ +- `FastAPI + SQLAlchemy example `_ Tutorials --------- diff --git a/docs/examples/fastapi-sqlalchemy.rst b/docs/examples/fastapi-sqlalchemy.rst new file mode 100644 index 00000000..5db1cf8f --- /dev/null +++ b/docs/examples/fastapi-sqlalchemy.rst @@ -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 `_ and +`SQLAlchemy `_. + +The source code is available on the `Github `_. + +Thanks to `@ShvetsovYura `_ for providing initial example: +`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 `_. + +.. disqus:: diff --git a/docs/examples/index.rst b/docs/examples/index.rst index 2898a5a0..6ec55c23 100644 --- a/docs/examples/index.rst +++ b/docs/examples/index.rst @@ -20,5 +20,6 @@ Explore the examples to see the ``Dependency Injector`` in action. sanic fastapi fastapi-redis + fastapi-sqlalchemy .. disqus:: diff --git a/docs/introduction/di_in_python.rst b/docs/introduction/di_in_python.rst index bf8d7f0c..23ba547e 100644 --- a/docs/introduction/di_in_python.rst +++ b/docs/introduction/di_in_python.rst @@ -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` diff --git a/docs/main/changelog.rst b/docs/main/changelog.rst index 14d002be..23c503f9 100644 --- a/docs/main/changelog.rst +++ b/docs/main/changelog.rst @@ -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 `_ for providing initial example: + `FastAPI_DI_SqlAlchemy `_. + 4.16.0 ------ - Add container base class ``containers.Container``. ``DynamicContainer`` diff --git a/docs/wiring.rst b/docs/wiring.rst index 2aed6396..16126de6 100644 --- a/docs/wiring.rst +++ b/docs/wiring.rst @@ -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:: diff --git a/examples/miniapps/fastapi-sqlalchemy/Dockerfile b/examples/miniapps/fastapi-sqlalchemy/Dockerfile new file mode 100644 index 00000000..f7c7c1d4 --- /dev/null +++ b/examples/miniapps/fastapi-sqlalchemy/Dockerfile @@ -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} diff --git a/examples/miniapps/fastapi-sqlalchemy/README.rst b/examples/miniapps/fastapi-sqlalchemy/README.rst new file mode 100644 index 00000000..2c8e0b66 --- /dev/null +++ b/examples/miniapps/fastapi-sqlalchemy/README.rst @@ -0,0 +1,96 @@ +FastAPI + SQLAlchemy + Dependency Injector Example +================================================== + +This is a `FastAPI `_ + +`SQLAlchemy `_ + +`Dependency Injector `_ example application. + +Thanks to `@ShvetsovYura `_ for providing initial example: +`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% diff --git a/examples/miniapps/fastapi-sqlalchemy/config.yml b/examples/miniapps/fastapi-sqlalchemy/config.yml new file mode 100644 index 00000000..80a03d3f --- /dev/null +++ b/examples/miniapps/fastapi-sqlalchemy/config.yml @@ -0,0 +1,2 @@ +db: + url: "sqlite:///./webapp.db" diff --git a/examples/miniapps/fastapi-sqlalchemy/docker-compose.yml b/examples/miniapps/fastapi-sqlalchemy/docker-compose.yml new file mode 100644 index 00000000..c42244f3 --- /dev/null +++ b/examples/miniapps/fastapi-sqlalchemy/docker-compose.yml @@ -0,0 +1,11 @@ +version: "3.7" + +services: + + webapp: + build: ./ + image: webapp + ports: + - "8000:8000" + volumes: + - "./:/code" diff --git a/examples/miniapps/fastapi-sqlalchemy/requirements.txt b/examples/miniapps/fastapi-sqlalchemy/requirements.txt new file mode 100644 index 00000000..f2c5ade5 --- /dev/null +++ b/examples/miniapps/fastapi-sqlalchemy/requirements.txt @@ -0,0 +1,8 @@ +dependency-injector +fastapi +uvicorn +pyyaml +sqlalchemy +pytest +requests +pytest-cov diff --git a/examples/miniapps/fastapi-sqlalchemy/webapp/__init__.py b/examples/miniapps/fastapi-sqlalchemy/webapp/__init__.py new file mode 100644 index 00000000..1c744ca5 --- /dev/null +++ b/examples/miniapps/fastapi-sqlalchemy/webapp/__init__.py @@ -0,0 +1 @@ +"""Top-level package.""" diff --git a/examples/miniapps/fastapi-sqlalchemy/webapp/application.py b/examples/miniapps/fastapi-sqlalchemy/webapp/application.py new file mode 100644 index 00000000..9e46af20 --- /dev/null +++ b/examples/miniapps/fastapi-sqlalchemy/webapp/application.py @@ -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() diff --git a/examples/miniapps/fastapi-sqlalchemy/webapp/containers.py b/examples/miniapps/fastapi-sqlalchemy/webapp/containers.py new file mode 100644 index 00000000..a5bf81ca --- /dev/null +++ b/examples/miniapps/fastapi-sqlalchemy/webapp/containers.py @@ -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, + ) diff --git a/examples/miniapps/fastapi-sqlalchemy/webapp/database.py b/examples/miniapps/fastapi-sqlalchemy/webapp/database.py new file mode 100644 index 00000000..763f4c37 --- /dev/null +++ b/examples/miniapps/fastapi-sqlalchemy/webapp/database.py @@ -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() diff --git a/examples/miniapps/fastapi-sqlalchemy/webapp/endpoints.py b/examples/miniapps/fastapi-sqlalchemy/webapp/endpoints.py new file mode 100644 index 00000000..799e6171 --- /dev/null +++ b/examples/miniapps/fastapi-sqlalchemy/webapp/endpoints.py @@ -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'} diff --git a/examples/miniapps/fastapi-sqlalchemy/webapp/models.py b/examples/miniapps/fastapi-sqlalchemy/webapp/models.py new file mode 100644 index 00000000..1f3afe49 --- /dev/null +++ b/examples/miniapps/fastapi-sqlalchemy/webapp/models.py @@ -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'' diff --git a/examples/miniapps/fastapi-sqlalchemy/webapp/repositories.py b/examples/miniapps/fastapi-sqlalchemy/webapp/repositories.py new file mode 100644 index 00000000..e76a107b --- /dev/null +++ b/examples/miniapps/fastapi-sqlalchemy/webapp/repositories.py @@ -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' diff --git a/examples/miniapps/fastapi-sqlalchemy/webapp/services.py b/examples/miniapps/fastapi-sqlalchemy/webapp/services.py new file mode 100644 index 00000000..c48cccbb --- /dev/null +++ b/examples/miniapps/fastapi-sqlalchemy/webapp/services.py @@ -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) diff --git a/examples/miniapps/fastapi-sqlalchemy/webapp/tests.py b/examples/miniapps/fastapi-sqlalchemy/webapp/tests.py new file mode 100644 index 00000000..fe2e6a12 --- /dev/null +++ b/examples/miniapps/fastapi-sqlalchemy/webapp/tests.py @@ -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'} diff --git a/examples/miniapps/fastapi/README.rst b/examples/miniapps/fastapi/README.rst index 96291ae9..eaaee20b 100644 --- a/examples/miniapps/fastapi/README.rst +++ b/examples/miniapps/fastapi/README.rst @@ -1,7 +1,7 @@ FastAPI + Dependency Injector Example ===================================== -This is an `FastAPI `_ + +This is a `FastAPI `_ + `Dependency Injector `_ example application. The example application is a REST API that searches for funny GIFs on the `Giphy `_. From 2c1eb9f95f65e062eb3eb0d08e53151dcd287570 Mon Sep 17 00:00:00 2001 From: Roman Mogylatov Date: Thu, 4 Feb 2021 18:19:40 -0500 Subject: [PATCH 2/2] Bump version to 4.17.0 --- docs/main/changelog.rst | 4 ++-- src/dependency_injector/__init__.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/main/changelog.rst b/docs/main/changelog.rst index 23c503f9..6e439e8d 100644 --- a/docs/main/changelog.rst +++ b/docs/main/changelog.rst @@ -7,8 +7,8 @@ that were made in every particular version. From version 0.7.6 *Dependency Injector* framework strictly follows `Semantic versioning`_ -Development version -------------------- +4.17.0 +------ - Add ``FastAPI`` + ``SQLAlchemy`` example. Thanks to `@ShvetsovYura `_ for providing initial example: `FastAPI_DI_SqlAlchemy `_. diff --git a/src/dependency_injector/__init__.py b/src/dependency_injector/__init__.py index 95c3b864..fb412079 100644 --- a/src/dependency_injector/__init__.py +++ b/src/dependency_injector/__init__.py @@ -1,6 +1,6 @@ """Top-level package.""" -__version__ = '4.16.0' +__version__ = '4.17.0' """Version number. :type: str