├── .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 |  [](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 | 
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 |
--------------------------------------------------------------------------------