Skip to content

Commit be47fc4

Browse files
committedNov 10, 2019
create project
0 parents  commit be47fc4

19 files changed

+234
-0
lines changed
 

‎.gitignore

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
.idea
2+
tmp
3+
venv
4+
__pycache__

‎Makefile

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
test:
2+
python -m unittest

‎README.md

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Python Architecture Example
2+
3+
This project is a sample Python web application adapting Clean Architecture. By using typing module, development experience similar to static type language is achieved.
4+
5+
Dependency injection and Dependency inversion principle, which are representative feature in Clean Architecture, can be implemented without any obstacles.
6+
7+
In order to experience the separation of concerning and improvement of testability by Clean Architecture along with the high productivity by Python, I've published this sample project.
8+
9+
Also in `tests` directory, there are some test samples with partial mock. If you are interested, please take a look this.
10+
11+
# How to run
12+
13+
```bash
14+
$ pip install -r requirements/requirements
15+
$ python -m flask run
16+
```
17+
18+
Then open `http://localhost:5000` in your browser.

‎app.py

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
from flask import Flask
2+
3+
from src.driver.article_driver import ArticleDriverImpl
4+
from src.interactor.article_interactor import ArticleInteractor
5+
from src.repository.article_repository import ArticleRepositoryImpl
6+
from src.rest.article_resource import ArticleResource
7+
8+
app = Flask(__name__)
9+
10+
article_resource = ArticleResource(
11+
article_usecase=ArticleInteractor(
12+
article_repository=ArticleRepositoryImpl(
13+
article_driver=ArticleDriverImpl()
14+
)
15+
)
16+
)
17+
18+
app.add_url_rule('/', view_func=article_resource.index)

‎requirements/requirements.txt

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
flask
2+
aiohttp

‎src/domain/article.py

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
from dataclasses import dataclass
2+
3+
from src.domain.collection import Collection
4+
5+
6+
@dataclass(frozen=True)
7+
class Article:
8+
id: str
9+
body: str
10+
11+
12+
@dataclass(frozen=True)
13+
class Articles(Collection[Article]):
14+
values: [Article]

‎src/domain/collection.py

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
from typing import Generic, TypeVar
2+
3+
from attr import dataclass
4+
5+
T = TypeVar('T')
6+
7+
8+
@dataclass(frozen=True)
9+
class Collection(Generic[T]):
10+
values: [T]
11+
12+
def map(self, func) -> map:
13+
return map(func, self.values)

‎src/driver/article_driver.py

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import json
2+
import aiohttp
3+
from src.interface.driver.article_driver import ArticleDriver
4+
5+
6+
class ArticleDriverImpl(ArticleDriver):
7+
async def get_articles(self, page: int) -> dict:
8+
response = await aiohttp.request('GET', 'https://qiita.com/api/v2/items?page=1&per_page=20').coro
9+
articles = json.loads(await response.text())
10+
return {
11+
"articles": [{
12+
"id": a["id"],
13+
"body": a["body"]
14+
} for a in articles]
15+
}

‎src/interactor/article_interactor.py

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
from src.domain.article import Articles
2+
from src.interface.repository.article_repository import ArticleRepository
3+
from src.interface.usecase.article_usecase import ArticleUsecase
4+
5+
6+
class ArticleInteractor(ArticleUsecase):
7+
article_repository: ArticleRepository
8+
9+
def __init__(self, article_repository: ArticleRepository):
10+
self.article_repository = article_repository
11+
12+
async def get_list(self, page: int) -> Articles:
13+
return await self.article_repository.get_list(page)
+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
from abc import ABCMeta, abstractmethod
2+
3+
4+
class ArticleDriver(metaclass=ABCMeta):
5+
@abstractmethod
6+
async def get_articles(self, page: int) -> dict:
7+
raise NotImplementedError
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from abc import ABCMeta, abstractmethod
2+
3+
from src.domain.article import Articles
4+
5+
6+
class ArticleRepository(metaclass=ABCMeta):
7+
@abstractmethod
8+
async def get_list(self, page: int) -> Articles:
9+
raise NotImplementedError
+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from abc import ABCMeta, abstractmethod
2+
3+
from src.domain.article import Articles
4+
5+
6+
class ArticleUsecase(metaclass=ABCMeta):
7+
@abstractmethod
8+
async def get_list(self, page: int) -> Articles:
9+
raise NotImplementedError

‎src/repository/article_repository.py

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
from src.domain.article import Article, Articles
2+
from src.interface.driver.article_driver import ArticleDriver
3+
from src.interface.repository.article_repository import ArticleRepository
4+
5+
6+
class ArticleRepositoryImpl(ArticleRepository):
7+
article_driver: ArticleDriver
8+
9+
def __init__(self, article_driver: ArticleDriver):
10+
self.article_driver = article_driver
11+
12+
async def get_list(self, page: int) -> Articles:
13+
res = await self.article_driver.get_articles(page)
14+
return Articles(
15+
values=[
16+
Article(id=a["id"], body=a["body"])
17+
for a in res["articles"]
18+
]
19+
)

‎src/rest/article_resource.py

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import asyncio
2+
3+
from flask import jsonify
4+
5+
from src.domain.article import Articles
6+
from src.interface.usecase.article_usecase import ArticleUsecase
7+
8+
9+
class ArticleResource:
10+
article_usecase: ArticleUsecase
11+
12+
def __init__(self, article_usecase: ArticleUsecase):
13+
self.article_usecase = article_usecase
14+
15+
def index(self):
16+
main_group: Articles = asyncio.run(self.article_usecase.get_list(0))
17+
return jsonify(articles_response(main_group))
18+
19+
20+
def articles_response(articles: Articles) -> dict:
21+
return {
22+
"items": [{
23+
"id": article.id,
24+
"body": article.body
25+
} for article in articles.values]
26+
}

‎tests/__init__.py

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import asyncio
2+
3+
4+
def async_test(coro):
5+
def wrapper(*args, **kwargs):
6+
loop = asyncio.new_event_loop()
7+
return loop.run_until_complete(coro(*args, **kwargs))
8+
return wrapper

‎tests/repository/__init__.py

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
from unittest import TestCase
2+
3+
from asyncmock import AsyncMock
4+
5+
from src.domain.article import Article, Articles
6+
from src.interface.driver.article_driver import ArticleDriver
7+
from src.repository.article_repository import ArticleRepositoryImpl
8+
from tests import async_test
9+
10+
11+
class DriverMock(ArticleDriver):
12+
async def get_articles(self, page: int) -> dict:
13+
raise NotImplementedError
14+
15+
16+
class TestArticleRepository(TestCase):
17+
@async_test
18+
async def test_get(self):
19+
get_main_group_mock = AsyncMock(return_value={"articles": [
20+
{"id": "1", "body": "test"}
21+
]})
22+
23+
driver = DriverMock()
24+
driver.get_articles = get_main_group_mock
25+
repository = ArticleRepositoryImpl(article_driver=driver)
26+
27+
self.assertEqual(
28+
await repository.get_list(1),
29+
Articles(values=[Article(id="1", body="test")])
30+
)
31+
get_main_group_mock.assert_called_with(1)

‎tests/usecase/__init__.py

Whitespace-only changes.
+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
from unittest import TestCase
2+
3+
from asyncmock import AsyncMock
4+
5+
from src.domain.article import Article, Articles
6+
from src.interactor.article_interactor import ArticleInteractor
7+
from src.interface.repository.article_repository import ArticleRepository
8+
from tests import async_test
9+
10+
11+
class RepositoryMock(ArticleRepository):
12+
async def get_list(self, page: str) -> Articles:
13+
raise NotImplementedError
14+
15+
16+
class TestArticleInteractor(TestCase):
17+
@async_test
18+
async def test_get(self):
19+
get_mock = AsyncMock(return_value=Article(id="1", body='test'))
20+
21+
repo = RepositoryMock()
22+
repo.get_list = get_mock
23+
usecase = ArticleInteractor(article_repository=repo)
24+
25+
self.assertEqual(await usecase.get_list(1), Article(id="1", body="test"))
26+
get_mock.assert_called_with(1)

0 commit comments

Comments
 (0)
Please sign in to comment.