├── .coveragerc ├── .coveralls.yml ├── .env ├── .github └── workflows │ └── main.yml ├── .gitignore ├── Dockerfile ├── README.md ├── app ├── __init__.py ├── application.py ├── data │ ├── __init__.py │ └── todo │ │ ├── __init__.py │ │ └── usecases │ │ ├── __init__.py │ │ ├── add_todo_data.py │ │ ├── delete_todo_by_id_data.py │ │ ├── get_todo_all_data.py │ │ ├── get_todo_by_id_data.py │ │ └── update_todo_data.py ├── db │ ├── __init__.py │ └── fake_db.py ├── notification │ ├── __init__.py │ └── todo_transaction │ │ ├── __init__.py │ │ ├── remove_cache_subscriber.py │ │ ├── transaction_log_subscriber.py │ │ └── transaction_notification.py ├── resources │ ├── __init__.py │ ├── health_check │ │ ├── __init__.py │ │ └── health_check_resource.py │ └── todo │ │ ├── __init__.py │ │ ├── todo_resource.py │ │ └── usecases │ │ ├── __init__.py │ │ ├── add_todo.py │ │ ├── delete_todo_by_id.py │ │ ├── get_todo_all.py │ │ ├── get_todo_by_id.py │ │ └── update_todo.py └── utils │ ├── __init__.py │ ├── cache_provider.py │ ├── config.py │ ├── error │ ├── __init__.py │ ├── error_models.py │ └── error_response.py │ ├── exception │ ├── __init__.py │ ├── exception_handlers.py │ └── exception_types.py │ ├── pydiator │ ├── __init__.py │ └── pydiator_core_config.py │ └── tracer_config.py ├── docker-compose.yml ├── docs └── assets │ ├── jaeger.png │ ├── jaeger_pipeline_is_not_enabled.png │ └── mediatr_flow.png ├── main.py ├── pytest.ini ├── requirements.txt └── tests ├── .env ├── __init__.py ├── conftest.py ├── integration ├── __init__.py └── resources │ ├── .DS_Store │ ├── __init__.py │ ├── healt_check │ ├── __init__.py │ └── test_health_check_resource.py │ └── todo │ ├── .DS_Store │ ├── __init__.py │ └── test_todo_resource.py └── unit ├── __init__.py ├── base_test_case.py ├── data ├── __init__.py └── todo │ ├── __init__.py │ └── usecases │ ├── __init__.py │ ├── test_add_todo_data.py │ ├── test_delete_todo_by_id_data.py │ ├── test_get_todo_all_data.py │ ├── test_get_todo_by_id.py │ └── test_update_todo_data.py ├── notification ├── __init__.py └── todo_transaction │ ├── __init__.py │ ├── test_remove_cache_subscriber.py │ └── test_transaction_log_subscriber.py ├── resources ├── __init__.py └── todo │ ├── __init__.py │ └── usecases │ ├── __init__.py │ ├── test_add_todo.py │ ├── test_delete_todo_by_id.py │ ├── test_get_todo_all.py │ ├── test_get_todo_by_id.py │ └── test_update_todo.py └── utils ├── __init__.py ├── pydiator └── __init__.py └── test_cache_provider.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | relative_files = True 3 | 4 | [report] 5 | exclude_lines = 6 | pragma: no cover 7 | @abstract 8 | def __init__ 9 | -------------------------------------------------------------------------------- /.coveralls.yml: -------------------------------------------------------------------------------- 1 | repo_token: {{ secrets.COVERALLS_TOKEN }} 2 | service_name: fastapi-pydiator-service 3 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | #REDIS_HOST=redis 2 | #REDIS_PORT=6379 3 | #REDIS_DB=0 4 | #REDIS_DB=REDIS_KEY_PREFIX -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: CI 4 | 5 | # Controls when the action will run. 6 | on: 7 | # Triggers the workflow on push or pull request events but only for the master branch 8 | push: 9 | branches: [ master ] 10 | pull_request: 11 | branches: [ master ] 12 | 13 | # Allows you to run this workflow manually from the Actions tab 14 | workflow_dispatch: 15 | 16 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 17 | jobs: 18 | # This workflow contains a single job called "build" 19 | build: 20 | # The type of runner that the job will run on 21 | runs-on: ubuntu-latest 22 | strategy: 23 | # You can use PyPy versions in python-version. 24 | # For example, pypy2 and pypy3 25 | matrix: 26 | python-version: [3.8] 27 | 28 | # Steps represent a sequence of tasks that will be executed as part of the job 29 | steps: 30 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 31 | - uses: actions/checkout@v2 32 | - uses: actions/setup-python@v2 33 | with: 34 | python-version: ${{ matrix.python-version }} 35 | 36 | - name: Install dependencies 37 | run: | 38 | python -m pip install --upgrade pip 39 | pip install -r requirements.txt 40 | 41 | - name: Tests 42 | run: pytest tests/ 43 | 44 | - name: Coverage 45 | run: | 46 | coverage run --source app/ -m pytest 47 | coverage xml 48 | 49 | - name: Coveralls 50 | uses: AndreMiras/coveralls-python-action@develop 51 | with: 52 | flag-name: Tests 53 | 54 | coveralls_finish: 55 | needs: build 56 | runs-on: ubuntu-latest 57 | steps: 58 | - name: Coveralls Finished 59 | uses: AndreMiras/coveralls-python-action@develop 60 | with: 61 | parallel-finished: true 62 | github-token: ${{ secrets.GITHUB_TOKEN }} 63 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | venv 2 | .data 3 | .pytest_cache 4 | htmlcov -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.8 2 | 3 | RUN mkdir src 4 | 5 | COPY requirements.txt src 6 | COPY app src/app 7 | COPY .env src 8 | COPY main.py src 9 | 10 | WORKDIR src 11 | RUN pip install -r requirements.txt 12 | 13 | EXPOSE 8080 14 | CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8080"] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![example event parameter](https://github.com/ozgurkara/fastapi-pydiator/workflows/CI/badge.svg) [![Coverage Status](https://coveralls.io/repos/github/ozgurkara/fastapi-pydiator/badge.svg?branch=master)](https://coveralls.io/github/ozgurkara/fastapi-pydiator?branch=master) 2 | 3 | # What is the purpose of this repository 4 | This project is an example that how to implement FastAPI and the pydiator-core. You can see the detail of the pydiator-core on this link https://github.com/ozgurkara/pydiator-core 5 | 6 | # How to run app 7 | `uvicorn main:app --reload` 8 | or `docker-compose up` 9 | 10 | # How to run Tests 11 | `coverage run --source app/ -m pytest` 12 | 13 | `coverage report -m` 14 | 15 | `coverage html` 16 | 17 | 18 | # What is the pydiator? 19 | You can see details here https://github.com/ozgurkara/pydiator-core 20 | ![pydiator](https://raw.githubusercontent.com/ozgurkara/pydiator-core/master/assets/pydiator_flow.png) 21 | 22 | This architecture; 23 | * Testable 24 | * Use case oriented 25 | * Has aspect programming (Authorization, Validation, Cache, Logging, Tracer etc.) support 26 | * Clean architecture 27 | * SOLID principles 28 | * Has publisher subscriber infrastructure 29 | 30 | There are ready implementations; 31 | * Redis cache 32 | * Swagger (http://0.0.0.0:8080) 33 | 34 | 35 | # How to add the new use case? 36 | 37 | **Add New Use Case** 38 | 39 | ```python 40 | #resources/sample/get_sample_by_id.py 41 | 42 | class GetSampleByIdRequest(BaseRequest): 43 | def __init__(self, id: int): 44 | self.id = id 45 | 46 | class GetSampleByIdResponse(BaseResponse): 47 | def __init__(self, id: int, title: str): 48 | self.id = id 49 | self.title = title 50 | 51 | class GetSampleByIdUseCase(BaseHandler): 52 | async def handle(self, req: GetSampleByIdRequest): 53 | # related codes are here such as business 54 | return GetSampleByIdResponse(id=req.id, title="hello pydiatr") 55 | ``` 56 | 57 | **Register Use Case** 58 | ```python 59 | # utils/pydiator/pydiator_core_config.py set_up_pydiator 60 | container.register_request(GetSampleByIdRequest, GetSampleByIdUseCase()) 61 | ``` 62 | 63 | Calling Use Case; 64 | ```python 65 | await pydiator.send(GetSampleByIdRequest(id=1)) 66 | ``` 67 |
68 | 69 | # What is the pipeline? 70 | 71 | You can think that the pipeline is middleware for use cases. So, all pipelines are called with the sequence for every use case. 72 | You can obtain more features via pipeline such as cache, tracing, log, retry mechanism, authorization. 73 | You should know and be careful that if you register a pipeline, it runs for every use case calling. 74 | 75 | **Add New Pipeline** 76 | ```python 77 | class SamplePipeline(BasePipeline): 78 | def __init__(self): 79 | pass 80 | 81 | async def handle(self, req: BaseRequest) -> object: 82 | 83 | # before executed pipeline and use case 84 | 85 | response = await self.next().handle(req) 86 | 87 | # after executed next pipeline and use case 88 | 89 | return response 90 | ``` 91 | 92 | **Register Pipeline** 93 | ```python 94 | # utils/pydiator/pydiator_core_config.py set_up_pydiator 95 | container.register_pipeline(SamplePipeline()) 96 | ``` 97 | 98 | 99 | # What is the notification? 100 | The notification feature provides you the pub-sub pattern as ready. 101 | 102 | The notification is being used for example in this repository. 103 | We want to trigger 2 things if the todo item is added or deleted or updated; 104 | 105 | 1- We want to clear the to-do list cache. 106 | 107 | 2- We want to write the id information of the to-do item to console 108 | 109 | 110 | **Add New Notification** 111 | ```python 112 | class TodoTransactionNotification(BaseModel, BaseNotification): 113 | id: int = Field(0, gt=0, title="todo id") 114 | ``` 115 | 116 | **Add Subscriber** 117 | ```python 118 | class TodoClearCacheSubscriber(BaseNotificationHandler): 119 | def __init__(self): 120 | self.cache_provider = get_cache_provider() 121 | 122 | async def handle(self, notification: TodoTransactionNotification): 123 | self.cache_provider.delete(GetTodoAllRequest().get_cache_key()) 124 | 125 | 126 | class TransactionLogSubscriber(BaseNotificationHandler): 127 | def __init__(self): 128 | self.cache_provider = get_cache_provider() 129 | 130 | async def handle(self, notification: TodoTransactionNotification): 131 | print(f'the transaction completed. its id {notification.id}') 132 | ``` 133 | 134 | **Register Notification** 135 | ```python 136 | container.register_notification(TodoTransactionNotification, 137 | [TodoClearCacheSubscriber(), TransactionLogSubscriber()]) 138 | ``` 139 | 140 | **Calling Notification** 141 | ```python 142 | await pydiator.publish(TodoTransactionNotification(id=1)) 143 | ``` 144 | 145 |
146 | 147 | # How to use the cache? 148 | The cache pipeline decides that put to cache or not via the request model. If the request model inherits from the BaseCacheable object, this use case response can be cacheable. 149 |
150 | If the cache already exists, the cache pipeline returns with cache data so, the use case is not called. Otherwise, the use case is called and the response of the use case is added to cache on the cache pipeline. 151 | 152 | ```python 153 | class GetTodoAllRequest(BaseModel, BaseRequest, BaseCacheable): 154 | # cache key. 155 | def get_cache_key(self) -> str: 156 | return type(self).__name__ # it is cache key 157 | 158 | # cache duration value as second 159 | def get_cache_duration(self) -> int: 160 | return 600 161 | 162 | # cache location type 163 | def get_cache_type(self) -> CacheType: 164 | return CacheType.DISTRIBUTED 165 | ``` 166 | 167 | Requirements; 168 | 169 | 1- Must have a redis and should be set the below environment variables 170 | 171 | REDIS_HOST = 'redis ip' 172 | 173 | 2- Must be activated the below environment variables on the config for using the cache; 174 | 175 | DISTRIBUTED_CACHE_IS_ENABLED=True 176 | CACHE_PIPELINE_IS_ENABLED=True 177 | 178 |
179 | 180 | 181 | 182 | -------------------------------------------------------------------------------- /app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozgurkara/fastapi-pydiator/7d70a90aec5393eac96639d2c0b7d652818f172f/app/__init__.py -------------------------------------------------------------------------------- /app/application.py: -------------------------------------------------------------------------------- 1 | from pydantic import ValidationError 2 | from starlette.exceptions import HTTPException 3 | 4 | from app.resources.health_check import health_check_resource 5 | from app.resources.todo import todo_resource 6 | from fastapi import FastAPI 7 | 8 | from fastapi_contrib.common.middlewares import StateRequestIDMiddleware 9 | from app.utils.exception.exception_handlers import ExceptionHandlers 10 | from app.utils.pydiator.pydiator_core_config import set_up_pydiator 11 | from app.utils.exception.exception_types import DataException, ServiceException 12 | 13 | 14 | def create_app(): 15 | app = FastAPI( 16 | title="FastAPI Pydiator", 17 | description="FastAPI pydiator integration project", 18 | version="1.0.0", 19 | openapi_url="/openapi.json", 20 | docs_url="/", 21 | redoc_url="/redoc" 22 | ) 23 | 24 | app.add_exception_handler(Exception, ExceptionHandlers.unhandled_exception) 25 | app.add_exception_handler(DataException, ExceptionHandlers.data_exception) 26 | app.add_exception_handler(ServiceException, ExceptionHandlers.service_exception) 27 | app.add_exception_handler(HTTPException, ExceptionHandlers.http_exception) 28 | app.add_exception_handler(ValidationError, ExceptionHandlers.validation_exception) 29 | 30 | app.include_router( 31 | health_check_resource.router, 32 | prefix="/health-check", 33 | tags=["health check"] 34 | ) 35 | 36 | app.include_router( 37 | todo_resource.router, 38 | prefix="/v1/todos", 39 | tags=["todo"] 40 | ) 41 | 42 | set_up_pydiator() 43 | 44 | return app 45 | -------------------------------------------------------------------------------- /app/data/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozgurkara/fastapi-pydiator/7d70a90aec5393eac96639d2c0b7d652818f172f/app/data/__init__.py -------------------------------------------------------------------------------- /app/data/todo/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozgurkara/fastapi-pydiator/7d70a90aec5393eac96639d2c0b7d652818f172f/app/data/todo/__init__.py -------------------------------------------------------------------------------- /app/data/todo/usecases/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozgurkara/fastapi-pydiator/7d70a90aec5393eac96639d2c0b7d652818f172f/app/data/todo/usecases/__init__.py -------------------------------------------------------------------------------- /app/data/todo/usecases/add_todo_data.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, Field 2 | from pydiator_core.interfaces import BaseRequest, BaseResponse, BaseHandler 3 | from app.db.fake_db import fake_todo_db 4 | 5 | 6 | class AddTodoDataRequest(BaseModel, BaseRequest): 7 | title: str = Field("", title="The title of the item", max_length=300, min_length=1) 8 | 9 | 10 | class AddTodoDataResponse(BaseModel, BaseResponse): 11 | success: bool = Field(...) 12 | id: int = Field(0, title="todo id") 13 | 14 | 15 | class AddTodoDataUseCase(BaseHandler): 16 | 17 | async def handle(self, req: AddTodoDataRequest) -> AddTodoDataResponse: 18 | id = 1 19 | last_item = fake_todo_db[len(fake_todo_db) - 1] 20 | if last_item is not None: 21 | id = last_item["id"] + 1 22 | 23 | fake_todo_db.append({ 24 | "id": id, 25 | "title": f"title {id}" 26 | }) 27 | 28 | return AddTodoDataResponse(success=True, id=id) 29 | -------------------------------------------------------------------------------- /app/data/todo/usecases/delete_todo_by_id_data.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, Field 2 | from pydiator_core.interfaces import BaseRequest, BaseResponse, BaseHandler 3 | from app.db.fake_db import fake_todo_db 4 | 5 | 6 | class DeleteTodoByIdDataRequest(BaseModel, BaseRequest): 7 | id: int = Field(0, gt=0, title="item id") 8 | 9 | 10 | class DeleteTodoByIdDataResponse(BaseModel, BaseResponse): 11 | success: bool = Field(...) 12 | 13 | 14 | class DeleteTodoByIdDataUseCase(BaseHandler): 15 | 16 | async def handle(self, req: DeleteTodoByIdDataRequest) -> DeleteTodoByIdDataResponse: 17 | for it in fake_todo_db: 18 | if it["id"] == req.id: 19 | fake_todo_db.remove(it) 20 | return DeleteTodoByIdDataResponse(success=True) 21 | 22 | return DeleteTodoByIdDataResponse(success=False) 23 | -------------------------------------------------------------------------------- /app/data/todo/usecases/get_todo_all_data.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, Field 2 | from pydiator_core.interfaces import BaseRequest, BaseResponse, BaseHandler 3 | from app.db.fake_db import fake_todo_db 4 | from typing import List 5 | 6 | 7 | class GetTodoAllDataRequest(BaseModel, BaseRequest): 8 | def __init__(self): 9 | pass 10 | 11 | 12 | class GetTodoAllDataResponse(BaseModel, BaseResponse): 13 | id: int = Field(...) 14 | title: str = Field(...) 15 | 16 | 17 | class GetTodoAllDataUseCase(BaseHandler): 18 | 19 | async def handle(self, req: GetTodoAllDataRequest) -> List[GetTodoAllDataResponse]: 20 | response = [] 21 | for it in fake_todo_db: 22 | response.append(GetTodoAllDataResponse(id=it["id"], title=it["title"])) 23 | 24 | return response 25 | -------------------------------------------------------------------------------- /app/data/todo/usecases/get_todo_by_id_data.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, Field 2 | from pydiator_core.interfaces import BaseRequest, BaseResponse, BaseHandler 3 | from app.db.fake_db import fake_todo_db 4 | 5 | 6 | class GetTodoByIdDataRequest(BaseModel, BaseRequest): 7 | id: int = Field(0, gt=0, description="The item id be greater than zero") 8 | 9 | 10 | class GetTodoByIdDataResponse(BaseModel, BaseResponse): 11 | id: int = Field(...) 12 | title: str = Field(...) 13 | 14 | 15 | class GetTodoByIdDataUseCase(BaseHandler): 16 | 17 | async def handle(self, req: GetTodoByIdDataRequest) -> GetTodoByIdDataResponse: 18 | for it in fake_todo_db: 19 | if it["id"] == req.id: 20 | return GetTodoByIdDataResponse(id=it["id"], title=it["title"]) 21 | 22 | return None 23 | -------------------------------------------------------------------------------- /app/data/todo/usecases/update_todo_data.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, Field, Extra 2 | from pydiator_core.interfaces import BaseRequest, BaseResponse, BaseHandler 3 | from app.db.fake_db import fake_todo_db 4 | from app.utils.error.error_models import ErrorInfoContainer 5 | from app.utils.exception.exception_types import DataException 6 | 7 | 8 | class UpdateTodoDataRequest(BaseModel, BaseRequest): 9 | title: str = Field("", title="The title of the item", max_length=300, min_length=1) 10 | id: int = Field(0, title="", gt=0) 11 | 12 | 13 | class UpdateTodoDataResponse(BaseModel, BaseResponse): 14 | success: bool = Field(...) 15 | 16 | 17 | class UpdateTodoDataUseCase(BaseHandler): 18 | 19 | async def handle(self, req: UpdateTodoDataRequest) -> UpdateTodoDataResponse: 20 | for it in fake_todo_db: 21 | if it["id"] == req.id: 22 | it["title"] = req.title 23 | return UpdateTodoDataResponse(success=True) 24 | 25 | raise DataException(error_info=ErrorInfoContainer.todo_not_found_error) 26 | 27 | -------------------------------------------------------------------------------- /app/db/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozgurkara/fastapi-pydiator/7d70a90aec5393eac96639d2c0b7d652818f172f/app/db/__init__.py -------------------------------------------------------------------------------- /app/db/fake_db.py: -------------------------------------------------------------------------------- 1 | import string 2 | import random 3 | 4 | fake_todo_db = [ 5 | {"id": 1, "title": "title 1"}, 6 | {"id": 2, "title": "title 2"} 7 | ] 8 | -------------------------------------------------------------------------------- /app/notification/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozgurkara/fastapi-pydiator/7d70a90aec5393eac96639d2c0b7d652818f172f/app/notification/__init__.py -------------------------------------------------------------------------------- /app/notification/todo_transaction/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozgurkara/fastapi-pydiator/7d70a90aec5393eac96639d2c0b7d652818f172f/app/notification/todo_transaction/__init__.py -------------------------------------------------------------------------------- /app/notification/todo_transaction/remove_cache_subscriber.py: -------------------------------------------------------------------------------- 1 | from pydiator_core.interfaces import BaseNotificationHandler 2 | 3 | from app.notification.todo_transaction.transaction_notification import TodoTransactionNotification 4 | from app.resources.todo.usecases.get_todo_all import GetTodoAllRequest 5 | from app.utils.cache_provider import get_cache_provider 6 | 7 | 8 | class TodoRemoveCacheSubscriber(BaseNotificationHandler): 9 | def __init__(self): 10 | self.cache_provider = get_cache_provider() 11 | 12 | async def handle(self, notification: TodoTransactionNotification): 13 | self.cache_provider.delete(GetTodoAllRequest().get_cache_key()) 14 | -------------------------------------------------------------------------------- /app/notification/todo_transaction/transaction_log_subscriber.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from pydiator_core.interfaces import BaseNotificationHandler 4 | from app.notification.todo_transaction.transaction_notification import TodoTransactionNotification 5 | 6 | 7 | class TransactionLogSubscriber(BaseNotificationHandler): 8 | def __init__(self): 9 | pass 10 | 11 | async def handle(self, notification: TodoTransactionNotification): 12 | logging.info(f'the transaction completed. its id {notification.id}') 13 | -------------------------------------------------------------------------------- /app/notification/todo_transaction/transaction_notification.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, Field 2 | from pydiator_core.interfaces import BaseNotification 3 | 4 | 5 | class TodoTransactionNotification(BaseModel, BaseNotification): 6 | id: int = Field(0, gt=0, title="todo id") 7 | -------------------------------------------------------------------------------- /app/resources/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozgurkara/fastapi-pydiator/7d70a90aec5393eac96639d2c0b7d652818f172f/app/resources/__init__.py -------------------------------------------------------------------------------- /app/resources/health_check/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozgurkara/fastapi-pydiator/7d70a90aec5393eac96639d2c0b7d652818f172f/app/resources/health_check/__init__.py -------------------------------------------------------------------------------- /app/resources/health_check/health_check_resource.py: -------------------------------------------------------------------------------- 1 | from fastapi import status, APIRouter 2 | from fastapi.responses import PlainTextResponse 3 | 4 | router = APIRouter() 5 | 6 | 7 | @router.get("", 8 | status_code=status.HTTP_200_OK, 9 | responses={ 10 | status.HTTP_200_OK: { 11 | "model": str, 12 | "content": { 13 | "text/plain": { 14 | "example": "OK" 15 | } 16 | } 17 | } 18 | }, 19 | response_class=PlainTextResponse) 20 | async def get(): 21 | return "OK" 22 | -------------------------------------------------------------------------------- /app/resources/todo/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozgurkara/fastapi-pydiator/7d70a90aec5393eac96639d2c0b7d652818f172f/app/resources/todo/__init__.py -------------------------------------------------------------------------------- /app/resources/todo/todo_resource.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | from fastapi import status, APIRouter, Response 3 | 4 | from pydiator_core.mediatr import pydiator 5 | from app.resources.todo.usecases.get_todo_all import GetTodoAllRequest, GetTodoAllResponse 6 | from app.resources.todo.usecases.get_todo_by_id import GetTodoByIdRequest, GetTodoByIdResponse 7 | from app.resources.todo.usecases.add_todo import AddTodoRequest, AddTodoResponse 8 | from app.resources.todo.usecases.update_todo import UpdateTodoRequest, UpdateTodoResponse 9 | from app.resources.todo.usecases.delete_todo_by_id import DeleteTodoByIdRequest, DeleteTodoByIdResponse 10 | from app.utils.error.error_response import ErrorResponseModel, ErrorResponseExample 11 | 12 | router = APIRouter() 13 | 14 | 15 | @router.get("", 16 | status_code=status.HTTP_200_OK, 17 | responses={ 18 | status.HTTP_200_OK: {"model": List[GetTodoAllResponse]}, 19 | status.HTTP_400_BAD_REQUEST: { 20 | "model": ErrorResponseModel, 21 | "content": ErrorResponseExample.get_error_response(), 22 | }, 23 | }) 24 | async def get_todo_all(): 25 | return await pydiator.send(req=GetTodoAllRequest()) 26 | 27 | 28 | @router.get("/{id}", 29 | status_code=status.HTTP_200_OK, 30 | responses={ 31 | status.HTTP_200_OK: {"model": GetTodoByIdResponse}, 32 | status.HTTP_400_BAD_REQUEST: { 33 | "model": ErrorResponseModel, 34 | "content": ErrorResponseExample.get_error_response(), 35 | }, 36 | status.HTTP_422_UNPROCESSABLE_ENTITY: { 37 | "model": ErrorResponseModel, 38 | "content": ErrorResponseExample.get_validation_error_response( 39 | invalid_field_location=["path", "id"] 40 | ), 41 | }, 42 | }) 43 | async def get_todo_by_id(id: int, response: Response): 44 | return await pydiator.send(req=GetTodoByIdRequest(id=id), response=response) 45 | 46 | 47 | @router.post("", 48 | status_code=status.HTTP_200_OK, 49 | responses={ 50 | status.HTTP_200_OK: {"model": AddTodoResponse}, 51 | status.HTTP_400_BAD_REQUEST: { 52 | "model": ErrorResponseModel, 53 | "content": ErrorResponseExample.get_error_response(), 54 | }, 55 | status.HTTP_422_UNPROCESSABLE_ENTITY: { 56 | "model": ErrorResponseModel, 57 | "content": ErrorResponseExample.get_validation_error_response( 58 | invalid_field_location=["body", "title"] 59 | ), 60 | }, 61 | }) 62 | async def add_todo(req: AddTodoRequest): 63 | return await pydiator.send(req=req) 64 | 65 | 66 | @router.put("/{id}", 67 | responses={ 68 | status.HTTP_200_OK: {"model": UpdateTodoResponse}, 69 | status.HTTP_400_BAD_REQUEST: { 70 | "model": ErrorResponseModel, 71 | "content": ErrorResponseExample.get_error_response(), 72 | }, 73 | status.HTTP_422_UNPROCESSABLE_ENTITY: { 74 | "model": ErrorResponseModel, 75 | "content": ErrorResponseExample.get_validation_error_response( 76 | invalid_field_location=["path", "id"] 77 | ), 78 | }, 79 | }) 80 | async def update_todo(id: int, req: UpdateTodoRequest): 81 | req.CustomFields.id = id 82 | return await pydiator.send(req=req) 83 | 84 | 85 | @router.delete("/{id}", 86 | responses={ 87 | status.HTTP_200_OK: {"model": DeleteTodoByIdResponse}, 88 | status.HTTP_400_BAD_REQUEST: { 89 | "model": ErrorResponseModel, 90 | "content": ErrorResponseExample.get_error_response(), 91 | }, 92 | status.HTTP_422_UNPROCESSABLE_ENTITY: { 93 | "model": ErrorResponseModel, 94 | "content": ErrorResponseExample.get_validation_error_response( 95 | invalid_field_location=["path", "id"] 96 | ), 97 | }, 98 | }) 99 | async def delete_todo(id: int): 100 | return await pydiator.send(req=DeleteTodoByIdRequest(id=id)) 101 | -------------------------------------------------------------------------------- /app/resources/todo/usecases/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozgurkara/fastapi-pydiator/7d70a90aec5393eac96639d2c0b7d652818f172f/app/resources/todo/usecases/__init__.py -------------------------------------------------------------------------------- /app/resources/todo/usecases/add_todo.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, Field 2 | 3 | from app.data.todo.usecases.add_todo_data import AddTodoDataRequest, AddTodoDataResponse 4 | from pydiator_core.interfaces import BaseRequest, BaseResponse, BaseHandler 5 | from pydiator_core.mediatr import pydiator 6 | from app.notification.todo_transaction.transaction_notification import TodoTransactionNotification 7 | 8 | 9 | class AddTodoRequest(BaseModel, BaseRequest): 10 | title: str = Field("title", title="The title of the item", max_length=300, min_length=1) 11 | 12 | 13 | class AddTodoResponse(BaseModel, BaseResponse): 14 | success: bool = Field(...) 15 | 16 | 17 | class AddTodoUseCase(BaseHandler): 18 | 19 | async def handle(self, req: AddTodoRequest) -> AddTodoResponse: 20 | data_response: AddTodoDataResponse = await pydiator.send(AddTodoDataRequest(title=req.title)) 21 | if data_response.success: 22 | await pydiator.publish(TodoTransactionNotification(id=data_response.id)) 23 | return AddTodoResponse(success=True) 24 | 25 | return AddTodoResponse(success=False) 26 | -------------------------------------------------------------------------------- /app/resources/todo/usecases/delete_todo_by_id.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, Field 2 | 3 | from app.data.todo.usecases.delete_todo_by_id_data import DeleteTodoByIdDataRequest 4 | from pydiator_core.mediatr import pydiator 5 | from pydiator_core.interfaces import BaseRequest, BaseResponse, BaseHandler 6 | from app.notification.todo_transaction.transaction_notification import TodoTransactionNotification 7 | 8 | 9 | class DeleteTodoByIdRequest(BaseModel, BaseRequest): 10 | id: int = Field(0, gt=0, title="item id") 11 | 12 | 13 | class DeleteTodoByIdResponse(BaseModel, BaseResponse): 14 | success: bool = Field(...) 15 | 16 | 17 | class DeleteTodoByIdUseCase(BaseHandler): 18 | 19 | async def handle(self, req: DeleteTodoByIdRequest) -> DeleteTodoByIdResponse: 20 | data_response = await pydiator.send(DeleteTodoByIdDataRequest(id=req.id)) 21 | if data_response.success: 22 | await pydiator.publish(TodoTransactionNotification(id=req.id)) 23 | return DeleteTodoByIdResponse(success=True) 24 | 25 | return DeleteTodoByIdResponse(success=False) 26 | -------------------------------------------------------------------------------- /app/resources/todo/usecases/get_todo_all.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, Field 2 | from pydiator_core.interfaces import BaseRequest, BaseResponse, BaseHandler, BaseCacheable, CacheType 3 | from typing import List 4 | from pydiator_core.mediatr import pydiator 5 | 6 | from app.data.todo.usecases.get_todo_all_data import GetTodoAllDataRequest 7 | 8 | 9 | class GetTodoAllRequest(BaseModel, BaseRequest, BaseCacheable): 10 | def get_cache_key(self) -> str: 11 | return type(self).__name__ 12 | 13 | def get_cache_duration(self) -> int: 14 | return 600 15 | 16 | def get_cache_type(self) -> CacheType: 17 | return CacheType.DISTRIBUTED 18 | 19 | 20 | class Todo(BaseModel): 21 | id: int = Field(...) 22 | title: str = Field(...) 23 | 24 | 25 | class GetTodoAllResponse(BaseModel, BaseResponse): 26 | items: List[Todo] = [] 27 | 28 | 29 | class GetTodoAllUseCase(BaseHandler): 30 | 31 | async def handle(self, req: GetTodoAllRequest) -> GetTodoAllResponse: 32 | response = GetTodoAllResponse() 33 | todo_data = await pydiator.send(GetTodoAllDataRequest()) 34 | for item in todo_data: 35 | response.items.append(Todo(id=item.id, title=item.title)) 36 | 37 | return response 38 | -------------------------------------------------------------------------------- /app/resources/todo/usecases/get_todo_by_id.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, Field 2 | from pydiator_core.interfaces import BaseRequest, BaseResponse, BaseHandler 3 | from pydiator_core.mediatr import pydiator 4 | 5 | from app.data.todo.usecases.get_todo_by_id_data import GetTodoByIdDataRequest 6 | from app.utils.error.error_models import ErrorInfoContainer 7 | from app.utils.exception.exception_types import ServiceException 8 | 9 | 10 | class GetTodoByIdRequest(BaseModel, BaseRequest): 11 | id: int = Field(0, gt=0, description="The item id be greater than zero") 12 | 13 | 14 | class GetTodoByIdResponse(BaseModel, BaseResponse): 15 | id: int = Field(...) 16 | title: str = Field(...) 17 | 18 | 19 | class GetTodoByIdUseCase(BaseHandler): 20 | 21 | async def handle(self, req: GetTodoByIdRequest) -> GetTodoByIdResponse: 22 | todo_data = await pydiator.send(GetTodoByIdDataRequest(id=req.id)) 23 | if todo_data is not None: 24 | return GetTodoByIdResponse(id=todo_data.id, title=todo_data.title) 25 | 26 | raise ServiceException(error_info=ErrorInfoContainer.todo_not_found_error) 27 | -------------------------------------------------------------------------------- /app/resources/todo/usecases/update_todo.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, Field, Extra 2 | 3 | from app.data.todo.usecases.update_todo_data import UpdateTodoDataRequest 4 | from pydiator_core.interfaces import BaseRequest, BaseResponse, BaseHandler 5 | from pydiator_core.mediatr import pydiator 6 | from app.notification.todo_transaction.transaction_notification import TodoTransactionNotification 7 | 8 | 9 | class UpdateTodoRequest(BaseModel, BaseRequest): 10 | title: str = Field("", title="The title of the item", max_length=300, min_length=1) 11 | 12 | class CustomFields: 13 | id: int = Extra.allow 14 | 15 | 16 | class UpdateTodoResponse(BaseModel, BaseResponse): 17 | success: bool = Field(...) 18 | 19 | 20 | class UpdateTodoUseCase(BaseHandler): 21 | 22 | async def handle(self, req: UpdateTodoRequest) -> UpdateTodoResponse: 23 | data_response = await pydiator.send(UpdateTodoDataRequest(id=req.CustomFields.id, title=req.title)) 24 | if data_response.success: 25 | await pydiator.publish(TodoTransactionNotification(id=req.CustomFields.id)) 26 | return UpdateTodoResponse(success=True) 27 | 28 | return UpdateTodoResponse(success=False) 29 | -------------------------------------------------------------------------------- /app/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozgurkara/fastapi-pydiator/7d70a90aec5393eac96639d2c0b7d652818f172f/app/utils/__init__.py -------------------------------------------------------------------------------- /app/utils/cache_provider.py: -------------------------------------------------------------------------------- 1 | import redis 2 | from pydiator_core.interfaces import BaseCacheProvider 3 | from app.utils import config 4 | from app.utils.config import REDIS_HOST, REDIS_PORT, REDIS_DB, REDIS_KEY_PREFIX 5 | 6 | 7 | class CacheProvider(BaseCacheProvider): 8 | key_prefix = "pydiator" 9 | 10 | def __init__(self, client, key_prefix: str = None): 11 | self.client = client 12 | self.key_prefix = key_prefix 13 | 14 | def add(self, key: str, value, expires): 15 | return self.__get_client().set(self.__get_formatted_key(key), value, ex=expires) 16 | 17 | def get(self, key): 18 | return self.__get_client().get(self.__get_formatted_key(key)) 19 | 20 | def exist(self, key): 21 | return self.__get_client().exists(self.__get_formatted_key(key)) 22 | 23 | def delete(self, key): 24 | self.__get_client().delete(self.__get_formatted_key(key)) 25 | 26 | def check_connection(self): 27 | result = self.__get_client().echo("echo") 28 | if result == b'echo': 29 | return True 30 | return False 31 | 32 | def __get_formatted_key(self, key) -> str: 33 | return '{}:{}'.format(self.key_prefix, key) 34 | 35 | def __get_client(self): 36 | if self.client is None: 37 | raise Exception('CacheProvider:client is None') 38 | 39 | return self.client 40 | 41 | 42 | def get_cache_provider(): 43 | if config.DISTRIBUTED_CACHE_IS_ENABLED: 44 | client = redis.Redis(host=REDIS_HOST, port=REDIS_PORT, db=REDIS_DB) 45 | return CacheProvider(client=client, key_prefix=REDIS_KEY_PREFIX) 46 | return None 47 | -------------------------------------------------------------------------------- /app/utils/config.py: -------------------------------------------------------------------------------- 1 | from starlette.config import Config 2 | 3 | from dotenv import load_dotenv 4 | 5 | load_dotenv() 6 | config = Config(".env") 7 | 8 | REDIS_HOST = config('REDIS_HOST', str, '0.0.0.0') 9 | REDIS_PORT = config('REDIS_PORT', int, 6379) 10 | REDIS_DB = config('REDIS_DB', int, 0) 11 | REDIS_KEY_PREFIX = config('REDIS_KEY_PREFIX', str, 'fastapi_pydiator:') 12 | 13 | DISTRIBUTED_CACHE_IS_ENABLED = config("DISTRIBUTED_CACHE_IS_ENABLED", bool, True) 14 | CACHE_PIPELINE_IS_ENABLED = config("CACHE_PIPELINE_IS_ENABLED", bool, False) 15 | LOG_PIPELINE_IS_ENABLED = config("LOG_PIPELINE_IS_ENABLED", bool, True) 16 | 17 | 18 | -------------------------------------------------------------------------------- /app/utils/error/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozgurkara/fastapi-pydiator/7d70a90aec5393eac96639d2c0b7d652818f172f/app/utils/error/__init__.py -------------------------------------------------------------------------------- /app/utils/error/error_models.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | 3 | 4 | class ErrorInfoModel: 5 | def __init__(self, code: int, message: str): 6 | self.code = code 7 | self.message = message 8 | 9 | def __repr__(self): 10 | return f'code:{self.code},message:{self.message}' 11 | 12 | 13 | class ErrorInfoContainer: 14 | # General errors 15 | unhandled_error = ErrorInfoModel(code=1, message='Internal server error') 16 | could_not_get_excepted_response = ErrorInfoModel(code=2, message='Could not get expected response') 17 | model_validation_error = ErrorInfoModel(code=3, message='Model validation error') 18 | not_found_error = ErrorInfoModel(code=4, message='Not found') 19 | 20 | # Custom errors 21 | todo_not_found_error = ErrorInfoModel(code=101, message='Todo not found') 22 | 23 | 24 | class ErrorResponseModel(BaseModel): 25 | error_code: int = None 26 | error_message: str = None 27 | error_detail: list = None 28 | -------------------------------------------------------------------------------- /app/utils/error/error_response.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | from app.utils.error.error_models import ErrorInfoContainer, ErrorResponseModel 3 | 4 | 5 | class ErrorResponseExample: 6 | 7 | @staticmethod 8 | def get_error_response(): 9 | return { 10 | "application/json": { 11 | "example": ErrorResponseModel( 12 | error_code=ErrorInfoContainer.could_not_get_excepted_response.code, 13 | error_message=ErrorInfoContainer.could_not_get_excepted_response.message 14 | ).dict() 15 | } 16 | } 17 | 18 | @staticmethod 19 | def get_validation_error_response(invalid_field_location: List[str]): 20 | return { 21 | "application/json": { 22 | "example": ErrorResponseModel( 23 | error_code=ErrorInfoContainer.model_validation_error.code, 24 | error_message=ErrorInfoContainer.model_validation_error.message, 25 | error_detail=[{ 26 | "loc": invalid_field_location, 27 | "msg": "field required", 28 | "type": "value_error.missing", 29 | }] 30 | ).dict() 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/utils/exception/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozgurkara/fastapi-pydiator/7d70a90aec5393eac96639d2c0b7d652818f172f/app/utils/exception/__init__.py -------------------------------------------------------------------------------- /app/utils/exception/exception_handlers.py: -------------------------------------------------------------------------------- 1 | import traceback 2 | from typing import Optional, List 3 | 4 | from fastapi import HTTPException 5 | from fastapi.encoders import jsonable_encoder 6 | from starlette import status 7 | from starlette.responses import JSONResponse 8 | 9 | from app.utils.error.error_models import ErrorInfoModel 10 | from app.utils.error.error_response import ErrorResponseModel, ErrorInfoContainer 11 | from app.utils.exception.exception_types import DataException, ServiceException 12 | 13 | 14 | class ExceptionHandlers: 15 | 16 | @staticmethod 17 | def unhandled_exception(request, exc: Exception): 18 | return JSONResponse( 19 | status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, 20 | content=ExceptionHandlers.__get_error_content( 21 | error_info=ErrorInfoContainer.unhandled_error, 22 | error_detail=[ExceptionHandlers.__get_stack_trace(exc)] 23 | ), 24 | ) 25 | 26 | @staticmethod 27 | def data_exception(request, exc: DataException): 28 | return JSONResponse( 29 | status_code=status.HTTP_400_BAD_REQUEST, 30 | content=ExceptionHandlers.__get_error_content( 31 | error_info=exc.error_info 32 | ), 33 | ) 34 | 35 | @staticmethod 36 | def service_exception(request, exc: ServiceException): 37 | return JSONResponse( 38 | status_code=status.HTTP_400_BAD_REQUEST, 39 | content=ExceptionHandlers.__get_error_content( 40 | error_info=exc.error_info 41 | ), 42 | ) 43 | 44 | @staticmethod 45 | def http_exception(request, exc: HTTPException): 46 | return JSONResponse( 47 | status_code=exc.status_code, 48 | content=ExceptionHandlers.__get_error_content( 49 | error_info=ErrorInfoContainer.unhandled_error, 50 | error_detail=[exc.detail] 51 | ), 52 | ) 53 | 54 | @staticmethod 55 | def validation_exception(request, exc): 56 | return JSONResponse( 57 | status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, 58 | content=ExceptionHandlers.__get_error_content( 59 | error_info=ErrorInfoContainer.model_validation_error, 60 | error_detail=exc.errors() 61 | ), 62 | ) 63 | 64 | @staticmethod 65 | def __get_error_content(error_info: ErrorInfoModel, error_detail: Optional[List] = None): 66 | return jsonable_encoder( 67 | ErrorResponseModel( 68 | error_code=error_info.code, 69 | error_message=error_info.message, 70 | error_detail=error_detail, 71 | ).dict() 72 | ) 73 | 74 | @staticmethod 75 | def __get_stack_trace(exc: Exception) -> str: 76 | return "".join(traceback.TracebackException.from_exception(exc).format()) 77 | -------------------------------------------------------------------------------- /app/utils/exception/exception_types.py: -------------------------------------------------------------------------------- 1 | from app.utils.error.error_models import ErrorInfoModel 2 | 3 | 4 | class ApplicationException(Exception): 5 | def __init__(self, error_info: ErrorInfoModel, exception: Exception = None) -> None: 6 | super().__init__() 7 | self.exception = exception 8 | self.error_info = error_info 9 | 10 | 11 | class DataException(ApplicationException): 12 | pass 13 | 14 | 15 | class ServiceException(ApplicationException): 16 | pass 17 | -------------------------------------------------------------------------------- /app/utils/pydiator/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozgurkara/fastapi-pydiator/7d70a90aec5393eac96639d2c0b7d652818f172f/app/utils/pydiator/__init__.py -------------------------------------------------------------------------------- /app/utils/pydiator/pydiator_core_config.py: -------------------------------------------------------------------------------- 1 | from app.data.todo.usecases.delete_todo_by_id_data import DeleteTodoByIdDataUseCase, DeleteTodoByIdDataRequest 2 | from app.notification.todo_transaction.transaction_log_subscriber import TransactionLogSubscriber 3 | from app.utils.config import CACHE_PIPELINE_IS_ENABLED, LOG_PIPELINE_IS_ENABLED 4 | from app.utils.cache_provider import get_cache_provider 5 | 6 | from pydiator_core.mediatr import pydiator 7 | from pydiator_core.mediatr_container import MediatrContainer 8 | from pydiator_core.pipelines.cache_pipeline import CachePipeline 9 | from pydiator_core.pipelines.log_pipeline import LogPipeline 10 | 11 | from app.resources.todo.usecases.get_todo_all import GetTodoAllRequest, GetTodoAllUseCase 12 | from app.resources.todo.usecases.get_todo_by_id import GetTodoByIdRequest, GetTodoByIdUseCase 13 | from app.resources.todo.usecases.add_todo import AddTodoRequest, AddTodoUseCase 14 | from app.resources.todo.usecases.update_todo import UpdateTodoRequest, UpdateTodoUseCase 15 | from app.resources.todo.usecases.delete_todo_by_id import DeleteTodoByIdRequest, DeleteTodoByIdUseCase 16 | from app.notification.todo_transaction.transaction_notification import TodoTransactionNotification 17 | from app.notification.todo_transaction.remove_cache_subscriber import TodoRemoveCacheSubscriber 18 | 19 | from app.data.todo.usecases.get_todo_all_data import GetTodoAllDataRequest, GetTodoAllDataUseCase 20 | from app.data.todo.usecases.get_todo_by_id_data import GetTodoByIdDataRequest, GetTodoByIdDataUseCase 21 | from app.data.todo.usecases.add_todo_data import AddTodoDataUseCase, AddTodoDataRequest 22 | from app.data.todo.usecases.update_todo_data import UpdateTodoDataRequest, UpdateTodoDataUseCase 23 | 24 | 25 | def set_up_pydiator(): 26 | container = MediatrContainer() 27 | 28 | 29 | if LOG_PIPELINE_IS_ENABLED: 30 | container.register_pipeline(LogPipeline()) 31 | 32 | if CACHE_PIPELINE_IS_ENABLED: 33 | cache_pipeline = CachePipeline(get_cache_provider()) 34 | container.register_pipeline(cache_pipeline) 35 | 36 | # Service usecases mapping 37 | container.register_request(GetTodoAllRequest, GetTodoAllUseCase()) 38 | container.register_request(GetTodoByIdRequest, GetTodoByIdUseCase()) 39 | container.register_request(AddTodoRequest, AddTodoUseCase()) 40 | container.register_request(UpdateTodoRequest, UpdateTodoUseCase()) 41 | container.register_request(DeleteTodoByIdRequest, DeleteTodoByIdUseCase()) 42 | 43 | # Data usecases mapping 44 | container.register_request(GetTodoAllDataRequest, GetTodoAllDataUseCase()) 45 | container.register_request(GetTodoByIdDataRequest, GetTodoByIdDataUseCase()) 46 | container.register_request(AddTodoDataRequest, AddTodoDataUseCase()) 47 | container.register_request(DeleteTodoByIdDataRequest, DeleteTodoByIdDataUseCase()) 48 | container.register_request(UpdateTodoDataRequest, UpdateTodoDataUseCase()) 49 | 50 | # Notification mapping 51 | container.register_notification(TodoTransactionNotification, 52 | [TodoRemoveCacheSubscriber(), TransactionLogSubscriber()]) 53 | 54 | # Start 55 | pydiator.ready(container=container) 56 | -------------------------------------------------------------------------------- /app/utils/tracer_config.py: -------------------------------------------------------------------------------- 1 | from jaeger_client import Config 2 | from opentracing.scope_managers.asyncio import AsyncioScopeManager 3 | 4 | from app.utils.config import JAEGER_HOST, JAEGER_PORT, JAEGER_SAMPLER_RATE, JAEGER_SAMPLER_TYPE, JAEGER_TRACE_ID_HEADER, \ 5 | JAEGER_SERVICE_NAME 6 | 7 | 8 | def init_tracer(service_name: str): 9 | config = Config( 10 | config={ 11 | "local_agent": { 12 | "reporting_host": JAEGER_HOST, 13 | "reporting_port": JAEGER_PORT, 14 | }, 15 | "sampler": { 16 | "type": JAEGER_SAMPLER_TYPE, 17 | "param": JAEGER_SAMPLER_RATE 18 | }, 19 | "trace_id_header": JAEGER_TRACE_ID_HEADER, 20 | }, 21 | scope_manager=AsyncioScopeManager(), 22 | service_name=service_name, 23 | validate=True, 24 | ) 25 | return config.initialize_tracer() 26 | 27 | 28 | tracer = init_tracer(JAEGER_SERVICE_NAME) 29 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | application: 4 | build: 5 | context: . 6 | dockerfile: Dockerfile 7 | restart: always 8 | ports: 9 | - "8080:8080" 10 | volumes: 11 | - .:/src 12 | env_file: 13 | - .env 14 | depends_on: 15 | - redis 16 | command: uvicorn main:app --host 0.0.0.0 --port 8080 --reload 17 | environment: 18 | REDIS_HOST: redis 19 | redis: 20 | restart: always 21 | image: redis:5.0.7 22 | ports: 23 | - "6379:6379" 24 | volumes: 25 | - .data/db:/data 26 | -------------------------------------------------------------------------------- /docs/assets/jaeger.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozgurkara/fastapi-pydiator/7d70a90aec5393eac96639d2c0b7d652818f172f/docs/assets/jaeger.png -------------------------------------------------------------------------------- /docs/assets/jaeger_pipeline_is_not_enabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozgurkara/fastapi-pydiator/7d70a90aec5393eac96639d2c0b7d652818f172f/docs/assets/jaeger_pipeline_is_not_enabled.png -------------------------------------------------------------------------------- /docs/assets/mediatr_flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozgurkara/fastapi-pydiator/7d70a90aec5393eac96639d2c0b7d652818f172f/docs/assets/mediatr_flow.png -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import uvicorn 2 | 3 | from app.application import create_app 4 | 5 | app = create_app() 6 | 7 | if __name__ == "__main__": 8 | uvicorn.run(app) 9 | #uvicorn.run("main:app", host="0.0.0.0", port=8000, log_level="info", use_colors=True) 10 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | env = 3 | DISTRIBUTED_CACHE_IS_ENABLED=False 4 | CACHE_PIPELINE_IS_ENABLED=False 5 | LOG_PIPELINE_IS_ENABLED=False 6 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | uvicorn==0.27.0.post1 2 | fastapi==0.109.1 3 | fastapi-contrib==0.2.11 4 | pydiator-core==1.0.12 5 | pydantic==1.10.14 6 | redis==5.0.1 7 | 8 | # test 9 | pytest==8.0.0 10 | requests==2.31.0 11 | coverage==7.4.1 12 | pytest-env==1.1.3 13 | python-dotenv==1.0.1 14 | httpx==0.26.0 15 | -------------------------------------------------------------------------------- /tests/.env: -------------------------------------------------------------------------------- 1 | #RedisHost=redis 2 | #RedisPort=6379 3 | #RedisDb=0 -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozgurkara/fastapi-pydiator/7d70a90aec5393eac96639d2c0b7d652818f172f/tests/__init__.py -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | # Global Fixtures can be defined in this file 2 | import pytest 3 | from starlette.testclient import TestClient 4 | 5 | from main import app 6 | 7 | 8 | @pytest.fixture(scope="module") 9 | def test_app(): 10 | client = TestClient(app=app) 11 | yield client # testing happens here 12 | -------------------------------------------------------------------------------- /tests/integration/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozgurkara/fastapi-pydiator/7d70a90aec5393eac96639d2c0b7d652818f172f/tests/integration/__init__.py -------------------------------------------------------------------------------- /tests/integration/resources/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozgurkara/fastapi-pydiator/7d70a90aec5393eac96639d2c0b7d652818f172f/tests/integration/resources/.DS_Store -------------------------------------------------------------------------------- /tests/integration/resources/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozgurkara/fastapi-pydiator/7d70a90aec5393eac96639d2c0b7d652818f172f/tests/integration/resources/__init__.py -------------------------------------------------------------------------------- /tests/integration/resources/healt_check/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozgurkara/fastapi-pydiator/7d70a90aec5393eac96639d2c0b7d652818f172f/tests/integration/resources/healt_check/__init__.py -------------------------------------------------------------------------------- /tests/integration/resources/healt_check/test_health_check_resource.py: -------------------------------------------------------------------------------- 1 | from starlette.status import HTTP_200_OK 2 | 3 | 4 | class TestTodo: 5 | 6 | def test_get(self, test_app): 7 | response = test_app.get("/health-check") 8 | 9 | assert response.status_code == HTTP_200_OK 10 | assert response.content == b"OK" 11 | -------------------------------------------------------------------------------- /tests/integration/resources/todo/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozgurkara/fastapi-pydiator/7d70a90aec5393eac96639d2c0b7d652818f172f/tests/integration/resources/todo/.DS_Store -------------------------------------------------------------------------------- /tests/integration/resources/todo/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozgurkara/fastapi-pydiator/7d70a90aec5393eac96639d2c0b7d652818f172f/tests/integration/resources/todo/__init__.py -------------------------------------------------------------------------------- /tests/integration/resources/todo/test_todo_resource.py: -------------------------------------------------------------------------------- 1 | from starlette.status import HTTP_422_UNPROCESSABLE_ENTITY, HTTP_200_OK 2 | 3 | 4 | class TestTodo: 5 | 6 | def test_get_todo_all(self, test_app): 7 | response = test_app.get("/v1/todos") 8 | items = response.json()["items"] 9 | 10 | assert response.status_code == HTTP_200_OK 11 | assert len(items) == 2 12 | assert items[0]["id"] == 1 13 | assert items[0]["title"] == "title 1" 14 | assert items[1]["id"] == 2 15 | assert items[1]["title"] == "title 2" 16 | 17 | def test_get_todo_by_id(self, test_app): 18 | response = test_app.get("/v1/todos/1") 19 | 20 | assert response.status_code == HTTP_200_OK 21 | assert response.json()["id"] == 1 22 | assert response.json()["title"] == "title 1" 23 | 24 | def test_add_todo(self, test_app): 25 | response = test_app.post("/v1/todos", json={ 26 | "title": "title 3" 27 | }) 28 | 29 | assert response.status_code == HTTP_200_OK 30 | assert response.json()["success"] 31 | 32 | def test_add_todo_should_return_unprocessable_when_invalid_entity(self, test_app): 33 | response = test_app.post("/v1/todos", json=None) 34 | 35 | assert response.status_code == HTTP_422_UNPROCESSABLE_ENTITY 36 | 37 | def test_update_todo(self, test_app): 38 | response = test_app.put("/v1/todos/1", json={ 39 | "title": "title 1 updated" 40 | }) 41 | 42 | assert response.status_code == HTTP_200_OK 43 | assert response.json()["success"] 44 | 45 | def test_update_todo_should_return_unprocessable_when_invalid_entity(self, test_app): 46 | response = test_app.put("/v1/todos/1", json={ 47 | 48 | }) 49 | 50 | assert response.status_code == HTTP_422_UNPROCESSABLE_ENTITY 51 | 52 | def test_delete_todo(self, test_app): 53 | response = test_app.delete("/v1/todos/1") 54 | 55 | assert response.status_code == HTTP_200_OK 56 | assert response.json()["success"] 57 | -------------------------------------------------------------------------------- /tests/unit/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozgurkara/fastapi-pydiator/7d70a90aec5393eac96639d2c0b7d652818f172f/tests/unit/__init__.py -------------------------------------------------------------------------------- /tests/unit/base_test_case.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from unittest import TestCase 3 | 4 | from pydiator_core.mediatr_container import MediatrContainer 5 | from pydiator_core.mediatr import pydiator 6 | 7 | 8 | class BaseTestCase(TestCase): 9 | @staticmethod 10 | def async_return(result): 11 | f = asyncio.Future() 12 | f.set_result(result) 13 | return f 14 | 15 | @staticmethod 16 | def async_loop(func): 17 | loop = asyncio.new_event_loop() 18 | response = loop.run_until_complete(func) 19 | loop.close() 20 | return response 21 | 22 | @staticmethod 23 | def register_request(req, handler): 24 | container = MediatrContainer() 25 | container.register_request(req, handler) 26 | pydiator.ready(container=container) 27 | -------------------------------------------------------------------------------- /tests/unit/data/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozgurkara/fastapi-pydiator/7d70a90aec5393eac96639d2c0b7d652818f172f/tests/unit/data/__init__.py -------------------------------------------------------------------------------- /tests/unit/data/todo/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozgurkara/fastapi-pydiator/7d70a90aec5393eac96639d2c0b7d652818f172f/tests/unit/data/todo/__init__.py -------------------------------------------------------------------------------- /tests/unit/data/todo/usecases/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozgurkara/fastapi-pydiator/7d70a90aec5393eac96639d2c0b7d652818f172f/tests/unit/data/todo/usecases/__init__.py -------------------------------------------------------------------------------- /tests/unit/data/todo/usecases/test_add_todo_data.py: -------------------------------------------------------------------------------- 1 | from unittest import mock 2 | 3 | from app.data.todo.usecases.add_todo_data import AddTodoDataUseCase, AddTodoDataRequest, AddTodoDataResponse 4 | from pydiator_core.mediatr import pydiator 5 | from tests.unit.base_test_case import BaseTestCase 6 | 7 | 8 | class TestAddTodoDataUseCase(BaseTestCase): 9 | def setUp(self): 10 | self.register_request(AddTodoDataRequest(), AddTodoDataUseCase()) 11 | 12 | @mock.patch("app.data.todo.usecases.add_todo_data.fake_todo_db") 13 | def test_handle_return_success(self, mock_fake_todo_db): 14 | # Given 15 | self.register_request(AddTodoDataRequest(), AddTodoDataUseCase()) 16 | mock_fake_todo_db.__iter__.return_value = [] 17 | 18 | title_val = "title" 19 | request = AddTodoDataRequest(title=title_val) 20 | expected_response = AddTodoDataResponse(success=True, id=1) 21 | 22 | # When 23 | response = self.async_loop(pydiator.send(request)) 24 | 25 | # Then 26 | assert response == expected_response 27 | assert mock_fake_todo_db.append.call_count == 1 28 | assert mock_fake_todo_db.append.called 29 | -------------------------------------------------------------------------------- /tests/unit/data/todo/usecases/test_delete_todo_by_id_data.py: -------------------------------------------------------------------------------- 1 | from unittest import mock 2 | 3 | from app.data.todo.usecases.delete_todo_by_id_data import DeleteTodoByIdDataRequest, DeleteTodoByIdDataUseCase, \ 4 | DeleteTodoByIdDataResponse 5 | from pydiator_core.mediatr import pydiator 6 | from app.resources.todo.usecases.delete_todo_by_id import DeleteTodoByIdResponse 7 | from tests.unit.base_test_case import BaseTestCase 8 | 9 | 10 | class TestDeleteTodoByIdDataUseCase(BaseTestCase): 11 | def setUp(self): 12 | self.register_request(DeleteTodoByIdDataRequest(), DeleteTodoByIdDataUseCase()) 13 | 14 | @mock.patch("app.data.todo.usecases.delete_todo_by_id_data.fake_todo_db") 15 | def test_handle_return_success(self, mock_fake_todo_db): 16 | # Given 17 | id_val = 1 18 | mock_fake_todo_db.__iter__.return_value = [{"id": id_val, "title": "title 1"}] 19 | request = DeleteTodoByIdDataRequest(id=id_val) 20 | expected_response = DeleteTodoByIdResponse(success=True) 21 | 22 | # When 23 | response = self.async_loop(pydiator.send(request)) 24 | 25 | # Then 26 | assert response == expected_response 27 | assert mock_fake_todo_db.remove.called 28 | assert mock_fake_todo_db.remove.call_count == 1 29 | 30 | @mock.patch("app.data.todo.usecases.delete_todo_by_id_data.fake_todo_db") 31 | def test_handle_return_success_false_when_todo_is_not_exist(self, mock_fake_todo_db): 32 | # Given 33 | mock_fake_todo_db.__iter__.return_value = [] 34 | request = DeleteTodoByIdDataRequest(id=1) 35 | expected_response = DeleteTodoByIdDataResponse(success=False) 36 | 37 | # When 38 | response = self.async_loop(pydiator.send(request)) 39 | 40 | # Then 41 | assert response == expected_response 42 | -------------------------------------------------------------------------------- /tests/unit/data/todo/usecases/test_get_todo_all_data.py: -------------------------------------------------------------------------------- 1 | from unittest import mock 2 | 3 | from app.data.todo.usecases.get_todo_all_data import GetTodoAllDataRequest, GetTodoAllDataResponse, \ 4 | GetTodoAllDataUseCase 5 | from pydiator_core.interfaces import CacheType 6 | from pydiator_core.mediatr import pydiator 7 | from app.resources.todo.usecases.get_todo_all import GetTodoAllRequest 8 | from tests.unit.base_test_case import BaseTestCase 9 | 10 | 11 | class TestGetTodoAllDataUseCase(BaseTestCase): 12 | def setUp(self): 13 | self.register_request(GetTodoAllDataRequest(), GetTodoAllDataUseCase()) 14 | 15 | def test_request_cache_parameter(self): 16 | # When 17 | request = GetTodoAllRequest() 18 | 19 | # Then 20 | assert request.get_cache_key() == "GetTodoAllRequest" 21 | assert request.get_cache_duration() == 600 22 | assert request.get_cache_type() == CacheType.DISTRIBUTED 23 | 24 | @mock.patch("app.data.todo.usecases.get_todo_all_data.fake_todo_db") 25 | def test_handle_return_list(self, mock_fake_todo_db): 26 | # Give 27 | id_val = 1 28 | title_val = "title 1" 29 | mock_fake_todo_db.__iter__.return_value = [{"id": id_val, "title": title_val}] 30 | 31 | request = GetTodoAllDataRequest() 32 | expected_response = [GetTodoAllDataResponse(id=id_val, title=title_val)] 33 | 34 | # When 35 | response = self.async_loop(pydiator.send(request)) 36 | 37 | # Then 38 | assert response == expected_response 39 | 40 | @mock.patch("app.data.todo.usecases.get_todo_all_data.fake_todo_db") 41 | def test_handle_return_empty_list(self, mock_fake_todo_db): 42 | # Given 43 | mock_fake_todo_db.__iter__.return_value = [] 44 | request = GetTodoAllDataRequest() 45 | expected_response = [] 46 | 47 | # When 48 | response = self.async_loop(pydiator.send(request)) 49 | 50 | # Then 51 | assert response == expected_response 52 | -------------------------------------------------------------------------------- /tests/unit/data/todo/usecases/test_get_todo_by_id.py: -------------------------------------------------------------------------------- 1 | from unittest import mock 2 | 3 | from app.data.todo.usecases.get_todo_by_id_data import GetTodoByIdDataUseCase, GetTodoByIdDataRequest, \ 4 | GetTodoByIdDataResponse 5 | from pydiator_core.mediatr import pydiator 6 | from tests.unit.base_test_case import BaseTestCase 7 | 8 | 9 | class TestGetTodoByIdDataUseCase(BaseTestCase): 10 | def setUp(self): 11 | self.register_request(GetTodoByIdDataRequest(), GetTodoByIdDataUseCase()) 12 | 13 | @mock.patch("app.data.todo.usecases.get_todo_by_id_data.fake_todo_db") 14 | def test_handle_return_todo(self, mock_fake_todo_db): 15 | # Given 16 | id_val = 1 17 | title_val = "title 1" 18 | mock_fake_todo_db.__iter__.return_value = [{"id": id_val, "title": title_val}] 19 | 20 | request = GetTodoByIdDataRequest(id=id_val) 21 | expected_response = GetTodoByIdDataResponse(id=id_val, title=title_val) 22 | 23 | # When 24 | response = self.async_loop(pydiator.send(request)) 25 | 26 | # Then 27 | assert response == expected_response 28 | 29 | @mock.patch("app.data.todo.usecases.get_todo_by_id_data.fake_todo_db") 30 | def test_handle_return_none_when_todo_is_not_exist(self, mock_fake_todo_db): 31 | # Given 32 | mock_fake_todo_db.__iter__.return_value = [] 33 | request = GetTodoByIdDataRequest(id=1) 34 | expected_response = None 35 | 36 | # When 37 | response = self.async_loop(pydiator.send(request)) 38 | 39 | # Then 40 | assert response == expected_response 41 | -------------------------------------------------------------------------------- /tests/unit/data/todo/usecases/test_update_todo_data.py: -------------------------------------------------------------------------------- 1 | from unittest import mock 2 | 3 | from pytest import raises 4 | 5 | from app.data.todo.usecases.update_todo_data import UpdateTodoDataUseCase, UpdateTodoDataRequest, \ 6 | UpdateTodoDataResponse 7 | from pydiator_core.mediatr import pydiator 8 | 9 | from app.utils.error.error_models import ErrorInfoContainer 10 | from app.utils.exception.exception_types import DataException 11 | from tests.unit.base_test_case import BaseTestCase 12 | 13 | 14 | class TestUpdateTodoDataUseCase(BaseTestCase): 15 | def setUp(self): 16 | self.register_request(UpdateTodoDataRequest(), UpdateTodoDataUseCase()) 17 | 18 | @mock.patch("app.data.todo.usecases.update_todo_data.fake_todo_db") 19 | def test_handle_return_success(self, mock_fake_todo_db): 20 | # Given 21 | id_val = 1 22 | mock_fake_todo_db.__iter__.return_value = [{"id": id_val, "title": "title 1"}] 23 | title_val = "title 1 updated" 24 | request = UpdateTodoDataRequest(title=title_val) 25 | request.id = id_val 26 | expected_response = UpdateTodoDataResponse(success=True) 27 | 28 | # When 29 | response = self.async_loop(pydiator.send(request)) 30 | 31 | # Then 32 | assert response == expected_response 33 | 34 | @mock.patch("app.data.todo.usecases.update_todo_data.fake_todo_db") 35 | def test_handle_return_exception_when_todo_not_found(self, mock_fake_todo_db): 36 | # Given 37 | mock_fake_todo_db.__iter__.return_value = [] 38 | title_val = "title 1 updated" 39 | request = UpdateTodoDataRequest(title=title_val) 40 | 41 | # When 42 | with raises(DataException) as exc: 43 | self.async_loop(pydiator.send(request)) 44 | 45 | # Then 46 | assert exc.value.error_info == ErrorInfoContainer.todo_not_found_error 47 | -------------------------------------------------------------------------------- /tests/unit/notification/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozgurkara/fastapi-pydiator/7d70a90aec5393eac96639d2c0b7d652818f172f/tests/unit/notification/__init__.py -------------------------------------------------------------------------------- /tests/unit/notification/todo_transaction/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozgurkara/fastapi-pydiator/7d70a90aec5393eac96639d2c0b7d652818f172f/tests/unit/notification/todo_transaction/__init__.py -------------------------------------------------------------------------------- /tests/unit/notification/todo_transaction/test_remove_cache_subscriber.py: -------------------------------------------------------------------------------- 1 | from unittest import mock 2 | 3 | from app.notification.todo_transaction.remove_cache_subscriber import TodoRemoveCacheSubscriber 4 | from app.notification.todo_transaction.transaction_notification import TodoTransactionNotification 5 | from app.resources.todo.usecases.get_todo_all import GetTodoAllRequest 6 | from tests.unit.base_test_case import BaseTestCase 7 | 8 | 9 | class TestTodoRemoveCacheSubscriber(BaseTestCase): 10 | 11 | def setUp(self) -> None: 12 | pass 13 | 14 | def tearDown(self) -> None: 15 | pass 16 | 17 | @mock.patch("app.notification.todo_transaction.remove_cache_subscriber.get_cache_provider") 18 | def test_handle(self, mock_get_cache_provider): 19 | # Given 20 | subscriber = TodoRemoveCacheSubscriber() 21 | notification = TodoTransactionNotification() 22 | 23 | # When 24 | self.async_loop(subscriber.handle(notification=notification)) 25 | 26 | # Then 27 | assert mock_get_cache_provider.return_value.delete.called 28 | assert mock_get_cache_provider.return_value.delete.call_count == 1 29 | assert mock_get_cache_provider.return_value.delete.call_args.args[0] == GetTodoAllRequest().get_cache_key() 30 | -------------------------------------------------------------------------------- /tests/unit/notification/todo_transaction/test_transaction_log_subscriber.py: -------------------------------------------------------------------------------- 1 | from unittest import mock 2 | from app.notification.todo_transaction.transaction_log_subscriber import TransactionLogSubscriber 3 | from app.notification.todo_transaction.transaction_notification import TodoTransactionNotification 4 | from tests.unit.base_test_case import BaseTestCase 5 | 6 | 7 | class TestTransactionLogSubscriber(BaseTestCase): 8 | 9 | def setUp(self) -> None: 10 | pass 11 | 12 | def tearDown(self) -> None: 13 | pass 14 | 15 | @mock.patch("app.notification.todo_transaction.transaction_log_subscriber.logging") 16 | def test_handle(self, mock_logging): 17 | # Given 18 | subscriber = TransactionLogSubscriber() 19 | notification = TodoTransactionNotification(id=1) 20 | 21 | # When 22 | self.async_loop(subscriber.handle(notification=notification)) 23 | 24 | # Then 25 | assert mock_logging.info.called 26 | assert mock_logging.info.call_count == 1 27 | assert mock_logging.info.call_args.args[0] == f'the transaction completed. its id {notification.id}' 28 | -------------------------------------------------------------------------------- /tests/unit/resources/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozgurkara/fastapi-pydiator/7d70a90aec5393eac96639d2c0b7d652818f172f/tests/unit/resources/__init__.py -------------------------------------------------------------------------------- /tests/unit/resources/todo/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozgurkara/fastapi-pydiator/7d70a90aec5393eac96639d2c0b7d652818f172f/tests/unit/resources/todo/__init__.py -------------------------------------------------------------------------------- /tests/unit/resources/todo/usecases/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozgurkara/fastapi-pydiator/7d70a90aec5393eac96639d2c0b7d652818f172f/tests/unit/resources/todo/usecases/__init__.py -------------------------------------------------------------------------------- /tests/unit/resources/todo/usecases/test_add_todo.py: -------------------------------------------------------------------------------- 1 | from unittest import mock 2 | from pydiator_core.mediatr import pydiator 3 | from app.data.todo.usecases.add_todo_data import AddTodoDataResponse 4 | from app.resources.todo.usecases.add_todo import AddTodoRequest, AddTodoResponse, AddTodoUseCase 5 | from tests.unit.base_test_case import BaseTestCase 6 | 7 | 8 | class TestAddTodoUseCase(BaseTestCase): 9 | 10 | def setUp(self): 11 | self.register_request(AddTodoRequest(), AddTodoUseCase()) 12 | 13 | @mock.patch("app.resources.todo.usecases.add_todo.pydiator") 14 | def test_handle_return_success(self, mock_pydiator): 15 | # Given 16 | mock_pydiator.send.side_effect = [self.async_return(AddTodoDataResponse(success=True, id=1))] 17 | mock_pydiator.publish.side_effect = [self.async_return(True)] 18 | 19 | title_val = "title" 20 | request = AddTodoRequest(title=title_val) 21 | expected_response = AddTodoResponse(success=True) 22 | 23 | # When 24 | response = self.async_loop(pydiator.send(request)) 25 | 26 | # Then 27 | assert response == expected_response 28 | assert mock_pydiator.send.called 29 | assert mock_pydiator.publish.called 30 | 31 | @mock.patch("app.resources.todo.usecases.add_todo.pydiator") 32 | def test_handle_return_success_false_when_data_response_is_not_successful(self, mock_pydiator): 33 | # Given 34 | mock_pydiator.send.side_effect = [self.async_return(AddTodoDataResponse(success=False, id=0))] 35 | 36 | title_val = "title" 37 | request = AddTodoRequest(title=title_val) 38 | expected_response = AddTodoResponse(success=False) 39 | 40 | # When 41 | response = self.async_loop(pydiator.send(request)) 42 | 43 | # Then 44 | assert response == expected_response 45 | assert mock_pydiator.send.called 46 | assert mock_pydiator.publish.called is False 47 | -------------------------------------------------------------------------------- /tests/unit/resources/todo/usecases/test_delete_todo_by_id.py: -------------------------------------------------------------------------------- 1 | from unittest import mock 2 | 3 | from app.data.todo.usecases.delete_todo_by_id_data import DeleteTodoByIdDataResponse 4 | from pydiator_core.mediatr import pydiator 5 | from app.resources.todo.usecases.delete_todo_by_id import \ 6 | DeleteTodoByIdRequest, DeleteTodoByIdResponse, DeleteTodoByIdUseCase 7 | from tests.unit.base_test_case import BaseTestCase 8 | 9 | 10 | class TestDeleteTodoByIdUseCase(BaseTestCase): 11 | 12 | def setUp(self): 13 | self.register_request(DeleteTodoByIdRequest(), DeleteTodoByIdUseCase()) 14 | 15 | @mock.patch("app.resources.todo.usecases.delete_todo_by_id.pydiator") 16 | def test_handle_return_success(self, mock_pydiator): 17 | # Given 18 | mock_pydiator.send.side_effect = [self.async_return(DeleteTodoByIdDataResponse(success=True))] 19 | mock_pydiator.publish.side_effect = [self.async_return(True)] 20 | 21 | id_val = 1 22 | request = DeleteTodoByIdRequest(id=id_val) 23 | expected_response = DeleteTodoByIdResponse(success=True) 24 | 25 | # When 26 | response = self.async_loop(pydiator.send(request)) 27 | 28 | # Then 29 | assert response == expected_response 30 | assert mock_pydiator.send.called 31 | assert mock_pydiator.publish.called 32 | 33 | @mock.patch("app.resources.todo.usecases.delete_todo_by_id.pydiator") 34 | def test_handle_return_false_when_data_response_is_not_successful(self, mock_pydiator): 35 | # Given 36 | mock_pydiator.send.side_effect = [self.async_return(DeleteTodoByIdDataResponse(success=False))] 37 | request = DeleteTodoByIdRequest(id=1) 38 | expected_response = DeleteTodoByIdResponse(success=False) 39 | 40 | # When 41 | response = self.async_loop(pydiator.send(request)) 42 | 43 | # Then 44 | assert response == expected_response 45 | assert mock_pydiator.send.called 46 | assert mock_pydiator.publish.called is False 47 | -------------------------------------------------------------------------------- /tests/unit/resources/todo/usecases/test_get_todo_all.py: -------------------------------------------------------------------------------- 1 | from unittest import mock 2 | 3 | from app.data.todo.usecases.get_todo_all_data import GetTodoAllDataResponse 4 | from pydiator_core.mediatr import pydiator 5 | from app.resources.todo.usecases.get_todo_all import GetTodoAllRequest, GetTodoAllUseCase, GetTodoAllResponse, Todo 6 | from tests.unit.base_test_case import BaseTestCase 7 | 8 | 9 | class TestGetTodoByIdUseCase(BaseTestCase): 10 | def setUp(self): 11 | self.register_request(GetTodoAllRequest, GetTodoAllUseCase()) 12 | 13 | def tearDown(self): 14 | pass 15 | 16 | @mock.patch("app.resources.todo.usecases.get_todo_all.pydiator") 17 | def test_handle_return_list(self, mock_pydiator): 18 | # Given 19 | id_val = 1 20 | title_val = "title 1" 21 | mock_pydiator.send.side_effect = [self.async_return([GetTodoAllDataResponse(id=id_val, title=title_val)])] 22 | 23 | request = GetTodoAllRequest(id=id_val) 24 | expected_response = GetTodoAllResponse(items=[Todo(id=id_val, title=title_val)]) 25 | 26 | # When 27 | response = self.async_loop(pydiator.send(request)) 28 | 29 | # Then 30 | assert response is not None 31 | assert expected_response == response 32 | 33 | @mock.patch("app.resources.todo.usecases.get_todo_all.pydiator") 34 | def test_handle_return_empty_list(self, mock_pydiator): 35 | # Given 36 | mock_pydiator.send.side_effect = [self.async_return([])] 37 | request = GetTodoAllRequest() 38 | expected_response = GetTodoAllResponse() 39 | 40 | # When 41 | response = self.async_loop(pydiator.send(request)) 42 | 43 | # Then 44 | assert response == expected_response 45 | -------------------------------------------------------------------------------- /tests/unit/resources/todo/usecases/test_get_todo_by_id.py: -------------------------------------------------------------------------------- 1 | from unittest import mock 2 | 3 | from pytest import raises 4 | 5 | from app.data.todo.usecases.get_todo_by_id_data import GetTodoByIdDataResponse 6 | from pydiator_core.mediatr import pydiator 7 | from app.resources.todo.usecases.get_todo_by_id import \ 8 | GetTodoByIdRequest, GetTodoByIdResponse, GetTodoByIdUseCase 9 | from app.utils.error.error_models import ErrorInfoContainer 10 | from app.utils.exception.exception_types import ServiceException 11 | from tests.unit.base_test_case import BaseTestCase 12 | 13 | 14 | class TestGetTodoByIdUseCase(BaseTestCase): 15 | def setUp(self): 16 | self.register_request(GetTodoByIdRequest(), GetTodoByIdUseCase()) 17 | 18 | @mock.patch("app.resources.todo.usecases.get_todo_by_id.pydiator") 19 | def test_handle_return_todo(self, mock_pydiator): 20 | # Given 21 | id_val = 1 22 | title_val = "title 1" 23 | mock_pydiator.send.side_effect = [self.async_return(GetTodoByIdDataResponse(id=id_val, title=title_val))] 24 | 25 | request = GetTodoByIdRequest(id=id_val) 26 | expected_response = GetTodoByIdResponse(id=id_val, title=title_val) 27 | 28 | # When 29 | response = self.async_loop(pydiator.send(request)) 30 | 31 | # Then 32 | assert response == expected_response 33 | 34 | @mock.patch("app.resources.todo.usecases.get_todo_by_id.pydiator") 35 | def test_handle_return_none_when_data_response_is_none(self, mock_pydiator): 36 | # Given 37 | mock_pydiator.send.side_effect = [self.async_return(None)] 38 | request = GetTodoByIdRequest(id=1) 39 | 40 | # When 41 | with raises(ServiceException) as exc: 42 | self.async_loop(pydiator.send(request)) 43 | 44 | # Then 45 | assert exc.value.error_info == ErrorInfoContainer.todo_not_found_error 46 | -------------------------------------------------------------------------------- /tests/unit/resources/todo/usecases/test_update_todo.py: -------------------------------------------------------------------------------- 1 | from unittest import mock 2 | 3 | from app.data.todo.usecases.update_todo_data import UpdateTodoDataResponse 4 | from app.resources.todo.usecases.update_todo import \ 5 | UpdateTodoRequest, UpdateTodoResponse, UpdateTodoUseCase 6 | from pydiator_core.mediatr import pydiator 7 | from tests.unit.base_test_case import BaseTestCase 8 | 9 | 10 | class TestAddTodoUseCase(BaseTestCase): 11 | def setUp(self): 12 | self.register_request(UpdateTodoRequest(), UpdateTodoUseCase()) 13 | 14 | @mock.patch("app.resources.todo.usecases.update_todo.pydiator") 15 | def test_handle_return_success(self, mock_pydiator): 16 | # Given 17 | mock_pydiator.send.side_effect = [self.async_return(UpdateTodoDataResponse(success=True))] 18 | mock_pydiator.publish.side_effect = [self.async_return(True)] 19 | 20 | id_val = 1 21 | title_val = "title 1 updated" 22 | request = UpdateTodoRequest(title=title_val) 23 | request.CustomFields.id = id_val 24 | expected_response = UpdateTodoResponse(success=True) 25 | 26 | # When 27 | response = self.async_loop(pydiator.send(request)) 28 | 29 | # Then 30 | assert response == expected_response 31 | assert mock_pydiator.send.called 32 | assert mock_pydiator.publish.called 33 | 34 | @mock.patch("app.resources.todo.usecases.update_todo.pydiator") 35 | def test_handle_return_false_when_data_response_is_not_successful(self, mock_pydiator): 36 | # Given 37 | self.register_request(UpdateTodoRequest(), UpdateTodoUseCase()) 38 | mock_pydiator.send.side_effect = [self.async_return(UpdateTodoDataResponse(success=False))] 39 | 40 | id_val = 1 41 | title_val = "title 1 updated" 42 | request = UpdateTodoRequest(title=title_val) 43 | request.CustomFields.id = id_val 44 | expected_response = UpdateTodoResponse(success=False) 45 | 46 | # When 47 | response = self.async_loop(pydiator.send(request)) 48 | 49 | # Then 50 | assert response == expected_response 51 | assert mock_pydiator.send.called 52 | assert mock_pydiator.publish.called is False 53 | -------------------------------------------------------------------------------- /tests/unit/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozgurkara/fastapi-pydiator/7d70a90aec5393eac96639d2c0b7d652818f172f/tests/unit/utils/__init__.py -------------------------------------------------------------------------------- /tests/unit/utils/pydiator/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozgurkara/fastapi-pydiator/7d70a90aec5393eac96639d2c0b7d652818f172f/tests/unit/utils/pydiator/__init__.py -------------------------------------------------------------------------------- /tests/unit/utils/test_cache_provider.py: -------------------------------------------------------------------------------- 1 | from unittest import mock 2 | 3 | from pytest import raises 4 | 5 | from app.utils.cache_provider import CacheProvider 6 | from tests.unit.base_test_case import BaseTestCase 7 | 8 | 9 | class TestCacheProvider(BaseTestCase): 10 | def setUp(self) -> None: 11 | pass 12 | 13 | def tearDown(self) -> None: 14 | pass 15 | 16 | def test_add(self): 17 | # Given 18 | mock_client = mock.MagicMock() 19 | prefix = "prefix" 20 | provider = CacheProvider(client=mock_client, key_prefix=prefix) 21 | 22 | # When 23 | provider.add(key="key", value="val", expires=10) 24 | 25 | # Then 26 | assert mock_client.set.called 27 | assert mock_client.set.call_count == 1 28 | assert mock_client.set.call_args.args[0] == "prefix:key" 29 | assert mock_client.set.call_args.args[1] == "val" 30 | assert mock_client.set.call_args.kwargs['ex'] == 10 31 | 32 | def test_get(self): 33 | # Given 34 | mock_client = mock.MagicMock() 35 | mock_client.get.return_value = "val" 36 | prefix = "prefix" 37 | provider = CacheProvider(client=mock_client, key_prefix=prefix) 38 | 39 | # When 40 | response = provider.get(key="key") 41 | 42 | # Then 43 | assert response == "val" 44 | assert mock_client.get.called 45 | assert mock_client.get.call_count == 1 46 | assert mock_client.get.call_args.args[0] == "prefix:key" 47 | 48 | def test_exist(self): 49 | # Given 50 | mock_client = mock.MagicMock() 51 | mock_client.exists.return_value = True 52 | prefix = "prefix" 53 | provider = CacheProvider(client=mock_client, key_prefix=prefix) 54 | 55 | # When 56 | response = provider.exist(key="key") 57 | 58 | # Then 59 | assert response 60 | assert mock_client.exists.called 61 | assert mock_client.exists.call_count == 1 62 | assert mock_client.exists.call_args.args[0] == "prefix:key" 63 | 64 | def test_delete(self): 65 | # Given 66 | mock_client = mock.MagicMock() 67 | mock_client.delete.return_value = True 68 | prefix = "prefix" 69 | provider = CacheProvider(client=mock_client, key_prefix=prefix) 70 | 71 | # When 72 | provider.delete(key="key") 73 | 74 | # Then 75 | assert mock_client.delete.called 76 | assert mock_client.delete.call_count == 1 77 | assert mock_client.delete.call_args.args[0] == "prefix:key" 78 | 79 | def test_check_connection_is_success(self): 80 | # Given 81 | mock_client = mock.MagicMock() 82 | mock_client.echo.return_value = b'echo' 83 | prefix = "prefix" 84 | provider = CacheProvider(client=mock_client, key_prefix=prefix) 85 | 86 | # When 87 | response = provider.check_connection() 88 | 89 | # Then 90 | assert response 91 | assert mock_client.echo.called 92 | assert mock_client.echo.call_count == 1 93 | 94 | def test_check_connection_is_not_success(self): 95 | # Given 96 | mock_client = mock.MagicMock() 97 | mock_client.echo.return_value = b'' 98 | prefix = "prefix" 99 | provider = CacheProvider(client=mock_client, key_prefix=prefix) 100 | 101 | # When 102 | response = provider.check_connection() 103 | 104 | # Then 105 | assert response is False 106 | assert mock_client.echo.called 107 | assert mock_client.echo.call_count == 1 108 | 109 | def test_get_client_throw_exception_when_client_is_none(self): 110 | # Given 111 | mock_client = mock.MagicMock() 112 | mock_client.echo.return_value = b'' 113 | prefix = "prefix" 114 | provider = CacheProvider(client=None, key_prefix=prefix) 115 | 116 | # When 117 | with raises(Exception) as exc: 118 | provider.check_connection() 119 | 120 | # Then 121 | assert exc.value.args[0] == 'CacheProvider:client is None' 122 | --------------------------------------------------------------------------------