├── .gitignore ├── README.md ├── adapter ├── __init__.py └── post_adapter.py ├── architecture_image.png ├── data ├── __init__.py └── in_memory_post_repository.py ├── domain ├── __init__.py ├── dto │ ├── __init__.py │ ├── read_post.py │ └── write_post.py ├── entity │ ├── __init__.py │ └── post.py ├── exception │ ├── __init__.py │ └── repository.py ├── interface │ ├── __init__.py │ └── post_repository.py └── use_case │ ├── __init__.py │ ├── read_post.py │ ├── read_posts.py │ └── write_post.py └── main.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | .vscode/ 132 | .idea/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # python-clean-architecture-example 2 | A example python code of robert martin' clean architecture 3 | 4 | ### Project Structure 5 | 6 | ``` 7 | . 8 | ├── README.md 9 | ├── adapter 10 | │   └── post_adapter.py 11 | ├── data 12 | │   └── in_memory_post_repository.py 13 | ├── domain 14 | │   ├── dto 15 | │   │   ├── read_post.py 16 | │   │   └── write_post.py 17 | │   ├── entity 18 | │   │   └── post.py 19 | │   ├── exception 20 | │   │   └── repository.py 21 | │   ├── interface 22 | │   │   └── post_repository.py 23 | │   └── use_case 24 | │   ├── read_post.py 25 | │   ├── read_posts.py 26 | │   └── write_post.py 27 | └── main.py 28 | ``` 29 | 30 | ### Project Architecture 31 | 32 | ![](architecture_image.png) 33 | -------------------------------------------------------------------------------- /adapter/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heumsi/python-clean-architecture-example/88209d7be2db03efbd3edf19da6e7f3b260d3e8b/adapter/__init__.py -------------------------------------------------------------------------------- /adapter/post_adapter.py: -------------------------------------------------------------------------------- 1 | import json 2 | from dataclasses import asdict, is_dataclass 3 | from datetime import datetime 4 | from typing import Any, List 5 | 6 | from domain.dto.read_post import ReadPostOutputDto 7 | from domain.dto.write_post import WritePostInputDto, WritePostOutputDto 8 | 9 | 10 | class PostAdapter: 11 | @classmethod 12 | def read_post_output_dto_to_response_body(cls, read_post_output_dto: ReadPostOutputDto) -> str: 13 | return json.dumps(asdict(read_post_output_dto), default=cls._default_json_encoder) 14 | 15 | @classmethod 16 | def read_post_output_dtos_to_response_body(cls, read_post_output_dtos: List[ReadPostOutputDto]) -> str: 17 | return json.dumps( 18 | [asdict(output_dtos) for output_dtos in read_post_output_dtos], default=cls._default_json_encoder 19 | ) 20 | 21 | @classmethod 22 | def request_body_to_write_post_input_dto(cls, request_body: dict) -> WritePostInputDto: 23 | return WritePostInputDto(**request_body) 24 | 25 | @classmethod 26 | def write_post_output_dto_to_response_body(cls, write_post_output_dto: WritePostOutputDto) -> str: 27 | return json.dumps(asdict(write_post_output_dto), default=cls._default_json_encoder) 28 | 29 | @staticmethod 30 | def _default_json_encoder(obj: object) -> Any: 31 | if is_dataclass(obj): 32 | return asdict(obj) 33 | elif isinstance(obj, datetime.datetime): 34 | return obj.timestamp() 35 | else: 36 | return repr(obj) 37 | -------------------------------------------------------------------------------- /architecture_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heumsi/python-clean-architecture-example/88209d7be2db03efbd3edf19da6e7f3b260d3e8b/architecture_image.png -------------------------------------------------------------------------------- /data/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heumsi/python-clean-architecture-example/88209d7be2db03efbd3edf19da6e7f3b260d3e8b/data/__init__.py -------------------------------------------------------------------------------- /data/in_memory_post_repository.py: -------------------------------------------------------------------------------- 1 | from typing import List, Union 2 | 3 | from domain.entity.post import Post 4 | from domain.exception.repository import NotExistedDataError 5 | from domain.interface.post_repository import PostRepository 6 | 7 | 8 | class InMemoryPostRepository(PostRepository): 9 | def __init__(self) -> None: 10 | self.posts: List[Post] = [] 11 | 12 | def get_by_id(self, id: int) -> Union[Post, None]: 13 | try: 14 | return next(post for post in self.posts if post.id == id) 15 | except StopIteration: 16 | raise NotExistedDataError 17 | 18 | def get_all(self) -> List[Post]: 19 | return self.posts 20 | 21 | def save(self, post: Post): 22 | if post.id is None: 23 | post.id = len(self.posts) 24 | self.posts.append(post) 25 | -------------------------------------------------------------------------------- /domain/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heumsi/python-clean-architecture-example/88209d7be2db03efbd3edf19da6e7f3b260d3e8b/domain/__init__.py -------------------------------------------------------------------------------- /domain/dto/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heumsi/python-clean-architecture-example/88209d7be2db03efbd3edf19da6e7f3b260d3e8b/domain/dto/__init__.py -------------------------------------------------------------------------------- /domain/dto/read_post.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from dataclasses import dataclass 3 | 4 | 5 | @dataclass 6 | class ReadPostOutputDto: 7 | id: int 8 | author: str 9 | category: str 10 | content: str 11 | created_at: datetime.datetime 12 | updated_at: datetime.datetime 13 | -------------------------------------------------------------------------------- /domain/dto/write_post.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | 4 | @dataclass 5 | class WritePostInputDto: 6 | author: str 7 | category: str 8 | content: str 9 | created_at: int 10 | updated_at: int 11 | 12 | 13 | @dataclass 14 | class WritePostOutputDto: 15 | is_success: bool 16 | error_message: str 17 | -------------------------------------------------------------------------------- /domain/entity/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heumsi/python-clean-architecture-example/88209d7be2db03efbd3edf19da6e7f3b260d3e8b/domain/entity/__init__.py -------------------------------------------------------------------------------- /domain/entity/post.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from dataclasses import dataclass 3 | 4 | 5 | @dataclass 6 | class Post: 7 | id: int 8 | author: str 9 | category: str 10 | content: str 11 | created_at: datetime.datetime 12 | updated_at: datetime.datetime 13 | -------------------------------------------------------------------------------- /domain/exception/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heumsi/python-clean-architecture-example/88209d7be2db03efbd3edf19da6e7f3b260d3e8b/domain/exception/__init__.py -------------------------------------------------------------------------------- /domain/exception/repository.py: -------------------------------------------------------------------------------- 1 | class NotExistedDataError(Exception): 2 | pass 3 | -------------------------------------------------------------------------------- /domain/interface/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heumsi/python-clean-architecture-example/88209d7be2db03efbd3edf19da6e7f3b260d3e8b/domain/interface/__init__.py -------------------------------------------------------------------------------- /domain/interface/post_repository.py: -------------------------------------------------------------------------------- 1 | from abc import ABCMeta, abstractmethod 2 | from typing import List, Union 3 | 4 | from domain.entity.post import Post 5 | 6 | 7 | class PostRepository(metaclass=ABCMeta): 8 | @abstractmethod 9 | def get_by_id(self, id: int) -> Union[Post, None]: 10 | pass 11 | 12 | @abstractmethod 13 | def get_all(self) -> List[Post]: 14 | pass 15 | 16 | @abstractmethod 17 | def save(self, post: Post) -> bool: 18 | pass 19 | -------------------------------------------------------------------------------- /domain/use_case/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heumsi/python-clean-architecture-example/88209d7be2db03efbd3edf19da6e7f3b260d3e8b/domain/use_case/__init__.py -------------------------------------------------------------------------------- /domain/use_case/read_post.py: -------------------------------------------------------------------------------- 1 | from typing import Union 2 | 3 | from domain.dto.read_post import ReadPostOutputDto 4 | from domain.entity.post import Post 5 | from domain.exception.repository import NotExistedDataError 6 | from domain.interface.post_repository import PostRepository 7 | 8 | 9 | class ReadPost: 10 | def __init__(self, repository: PostRepository) -> None: 11 | self._repository = repository 12 | 13 | def execute(self, post_id: int) -> Union[ReadPostOutputDto, None]: 14 | try: 15 | post: Post = self._repository.get_by_id(id=post_id) 16 | except NotExistedDataError: 17 | return None 18 | else: 19 | output_dto = ReadPostOutputDto( 20 | id=post.id, 21 | author=post.author, 22 | category=post.category, 23 | content=post.content, 24 | created_at=post.created_at, 25 | updated_at=post.updated_at, 26 | ) 27 | return output_dto 28 | -------------------------------------------------------------------------------- /domain/use_case/read_posts.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from domain.dto.read_post import ReadPostOutputDto 4 | from domain.entity.post import Post 5 | from domain.interface.post_repository import PostRepository 6 | 7 | 8 | class ReadPosts: 9 | def __init__(self, repository: PostRepository) -> None: 10 | self._repository = repository 11 | 12 | def execute(self) -> List[ReadPostOutputDto]: 13 | posts: List[Post] = self._repository.get_all() 14 | 15 | return [ 16 | ReadPostOutputDto( 17 | id=post.id, 18 | author=post.author, 19 | category=post.category, 20 | content=post.content, 21 | created_at=post.created_at, 22 | updated_at=post.updated_at, 23 | ) 24 | for post in posts 25 | ] 26 | -------------------------------------------------------------------------------- /domain/use_case/write_post.py: -------------------------------------------------------------------------------- 1 | from domain.dto.write_post import WritePostInputDto, WritePostOutputDto 2 | from domain.entity.post import Post 3 | from domain.interface.post_repository import PostRepository 4 | 5 | 6 | class WritePost: 7 | def __init__(self, repository: PostRepository) -> None: 8 | self._repository = repository 9 | 10 | def execute(self, input_dto: WritePostInputDto) -> WritePostOutputDto: 11 | post = Post( 12 | id=None, # this will be injected by repository. 13 | author=input_dto.author, 14 | category=input_dto.category, 15 | content=input_dto.content, 16 | created_at=input_dto.created_at, 17 | updated_at=input_dto.updated_at, 18 | ) 19 | try: 20 | self._repository.save(post) 21 | except Exception as err: 22 | return WritePostOutputDto(is_success=False, error_message=str(err)) 23 | else: 24 | return WritePostOutputDto(is_success=True, error_message="") 25 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | from typing import List, Union 2 | 3 | from flask import Flask, abort, jsonify, request 4 | 5 | from adapter.post_adapter import PostAdapter 6 | from data.in_memory_post_repository import InMemoryPostRepository 7 | from domain.dto.read_post import ReadPostOutputDto 8 | from domain.dto.write_post import WritePostOutputDto 9 | from domain.use_case.read_post import ReadPost 10 | from domain.use_case.read_posts import ReadPosts 11 | from domain.use_case.write_post import WritePost, WritePostInputDto 12 | 13 | app = Flask(__name__) 14 | repository = InMemoryPostRepository() 15 | 16 | 17 | @app.route("/read_post//", methods=["GET"]) 18 | def read_post(post_id: int): 19 | # execute use_case 20 | use_case = ReadPost(repository=repository) 21 | output_dto: Union[ReadPostOutputDto, None] = use_case.execute(post_id=post_id) 22 | if output_dto is None: 23 | return abort(404, f"post_id = {post_id}") 24 | 25 | # convert output_dto to response_body 26 | response_body = PostAdapter.read_post_output_dto_to_response_body(output_dto) 27 | return jsonify(response_body) 28 | 29 | 30 | @app.route("/read_posts/", methods=["GET"]) 31 | def read_posts(): 32 | # execute use_case 33 | use_case = ReadPosts(repository=repository) 34 | output_dtos: List[ReadPostOutputDto] = use_case.execute() 35 | 36 | # convert output_dtos to response_body 37 | response_body = PostAdapter.read_post_output_dtos_to_response_body(output_dtos) 38 | return jsonify(response_body) 39 | 40 | 41 | @app.route("/write_post/", methods=["POST"]) 42 | def write_post(): 43 | # convert request_body to input_dto 44 | input_dto: WritePostInputDto = PostAdapter.request_body_to_write_post_input_dto(request.json) 45 | 46 | # execute use_case 47 | use_case = WritePost(repository=repository) 48 | output_dto: WritePostOutputDto = use_case.execute(input_dto=input_dto) 49 | 50 | # convert output_dto to response_body 51 | response_body = PostAdapter.write_post_output_dto_to_response_body(output_dto) 52 | 53 | return jsonify(response_body) 54 | 55 | 56 | @app.errorhandler(404) 57 | def resource_not_found(e): 58 | return jsonify(error=str(e)), 404 59 | 60 | 61 | if __name__ == "__main__": 62 | app.run() 63 | --------------------------------------------------------------------------------