Skip to content

Commit f8e4bf4

Browse files
committed
add summarizer
1 parent 121bca9 commit f8e4bf4

6 files changed

Lines changed: 300 additions & 15 deletions

File tree

project/app/api/crud.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,7 @@
88

99

1010
async def post(payload: SummaryPayloadSchema) -> int:
11-
summary = TextSummary(
12-
url=payload.url,
13-
summary="dummy summary",
14-
)
11+
summary = TextSummary(url=payload.url, summary="")
1512
await summary.save()
1613
return summary.id
1714

project/app/api/summaries.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@
33

44
from typing import List
55

6-
from fastapi import APIRouter, HTTPException, Path
6+
from fastapi import APIRouter, BackgroundTasks, HTTPException, Path
77

88
from app.api import crud
99
from app.models.tortoise import SummarySchema
10+
from app.summarizer import generate_summary
1011

1112
from app.models.pydantic import ( # isort:skip
1213
SummaryPayloadSchema,
@@ -18,9 +19,13 @@
1819

1920

2021
@router.post("/", response_model=SummaryResponseSchema, status_code=201)
21-
async def create_summary(payload: SummaryPayloadSchema) -> SummaryResponseSchema:
22+
async def create_summary(
23+
payload: SummaryPayloadSchema, background_tasks: BackgroundTasks
24+
) -> SummaryResponseSchema:
2225
summary_id = await crud.post(payload)
2326

27+
background_tasks.add_task(generate_summary, summary_id, payload.url)
28+
2429
response_object = {"id": summary_id, "url": payload.url}
2530
return response_object
2631

project/app/summarizer.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# project/app/summarizer.py
2+
3+
4+
import nltk
5+
from newspaper import Article
6+
7+
from app.models.tortoise import TextSummary
8+
9+
10+
async def generate_summary(summary_id: int, url: str) -> None:
11+
article = Article(url)
12+
article.download()
13+
article.parse()
14+
15+
try:
16+
nltk.data.find("tokenizers/punkt")
17+
except LookupError:
18+
nltk.download("punkt")
19+
finally:
20+
article.nlp()
21+
22+
summary = article.summary
23+
24+
await TextSummary.filter(id=summary_id).update(summary=summary)

project/requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@ fastapi==0.65.3
55
flake8==3.9.2
66
gunicorn==20.1.0
77
isort==5.9.1
8+
newspaper3k==0.2.8
89
pytest==6.2.4
910
pytest-cov==2.12.1
11+
pytest-xdist==2.3.0
1012
requests==2.25.1
1113
tortoise-orm==0.17.4
1214
uvicorn==0.14.0

project/tests/test_summaries.py

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
1-
# project/tests/test_summaries.py
2-
3-
41
import json
52

63
import pytest
74

5+
from app.api import summaries
6+
7+
8+
def test_create_summary(test_app_with_db, monkeypatch):
9+
def mock_generate_summary(summary_id, url):
10+
return None
11+
12+
monkeypatch.setattr(summaries, "generate_summary", mock_generate_summary)
813

9-
def test_create_summary(test_app_with_db):
1014
response = test_app_with_db.post(
1115
"/summaries/", data=json.dumps({"url": "https://foo.bar"})
1216
)
@@ -33,7 +37,12 @@ def test_create_summaries_invalid_json(test_app):
3337
assert response.json()["detail"][0]["msg"] == "URL scheme not permitted"
3438

3539

36-
def test_read_summary(test_app_with_db):
40+
def test_read_summary(test_app_with_db, monkeypatch):
41+
def mock_generate_summary(summary_id, url):
42+
return None
43+
44+
monkeypatch.setattr(summaries, "generate_summary", mock_generate_summary)
45+
3746
response = test_app_with_db.post(
3847
"/summaries/", data=json.dumps({"url": "https://foo.bar"})
3948
)
@@ -45,7 +54,6 @@ def test_read_summary(test_app_with_db):
4554
response_dict = response.json()
4655
assert response_dict["id"] == summary_id
4756
assert response_dict["url"] == "https://foo.bar"
48-
assert response_dict["summary"]
4957
assert response_dict["created_at"]
5058

5159

@@ -68,7 +76,12 @@ def test_read_summary_incorrect_id(test_app_with_db):
6876
}
6977

7078

71-
def test_read_all_summaries(test_app_with_db):
79+
def test_read_all_summaries(test_app_with_db, monkeypatch):
80+
def mock_generate_summary(summary_id, url):
81+
return None
82+
83+
monkeypatch.setattr(summaries, "generate_summary", mock_generate_summary)
84+
7285
response = test_app_with_db.post(
7386
"/summaries/", data=json.dumps({"url": "https://foo.bar"})
7487
)
@@ -81,7 +94,12 @@ def test_read_all_summaries(test_app_with_db):
8194
assert len(list(filter(lambda d: d["id"] == summary_id, response_list))) == 1
8295

8396

84-
def test_remove_summary(test_app_with_db):
97+
def test_remove_summary(test_app_with_db, monkeypatch):
98+
def mock_generate_summary(summary_id, url):
99+
return None
100+
101+
monkeypatch.setattr(summaries, "generate_summary", mock_generate_summary)
102+
85103
response = test_app_with_db.post(
86104
"/summaries/", data=json.dumps({"url": "https://foo.bar"})
87105
)
@@ -111,7 +129,12 @@ def test_remove_summary_incorrect_id(test_app_with_db):
111129
}
112130

113131

114-
def test_update_summary(test_app_with_db):
132+
def test_update_summary(test_app_with_db, monkeypatch):
133+
def mock_generate_summary(summary_id, url):
134+
return None
135+
136+
monkeypatch.setattr(summaries, "generate_summary", mock_generate_summary)
137+
115138
response = test_app_with_db.post(
116139
"/summaries/", data=json.dumps({"url": "https://foo.bar"})
117140
)
Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
# project/tests/test_summaries_unit.py
2+
3+
4+
import json
5+
from datetime import datetime
6+
7+
import pytest
8+
9+
from app.api import crud, summaries
10+
11+
12+
def test_create_summary(test_app, monkeypatch):
13+
test_request_payload = {"url": "https://foo.bar"}
14+
test_response_payload = {"id": 1, "url": "https://foo.bar"}
15+
16+
async def mock_post(payload):
17+
return 1
18+
19+
monkeypatch.setattr(crud, "post", mock_post)
20+
21+
def mock_generate_summary(summary_id, url):
22+
return None
23+
24+
monkeypatch.setattr(summaries, "generate_summary", mock_generate_summary)
25+
26+
response = test_app.post(
27+
"/summaries/",
28+
data=json.dumps(test_request_payload),
29+
)
30+
31+
assert response.status_code == 201
32+
assert response.json() == test_response_payload
33+
34+
35+
def test_create_summaries_invalid_json(test_app):
36+
response = test_app.post("/summaries/", data=json.dumps({}))
37+
assert response.status_code == 422
38+
assert response.json() == {
39+
"detail": [
40+
{
41+
"loc": ["body", "url"],
42+
"msg": "field required",
43+
"type": "value_error.missing",
44+
}
45+
]
46+
}
47+
48+
response = test_app.post("/summaries/", data=json.dumps({"url": "invalid://url"}))
49+
assert response.status_code == 422
50+
assert response.json()["detail"][0]["msg"] == "URL scheme not permitted"
51+
52+
53+
def test_read_summary(test_app, monkeypatch):
54+
test_data = {
55+
"id": 1,
56+
"url": "https://foo.bar",
57+
"summary": "summary",
58+
"created_at": datetime.utcnow().isoformat(),
59+
}
60+
61+
async def mock_get(id):
62+
return test_data
63+
64+
monkeypatch.setattr(crud, "get", mock_get)
65+
66+
response = test_app.get("/summaries/1/")
67+
assert response.status_code == 200
68+
assert response.json() == test_data
69+
70+
71+
def test_read_summary_incorrect_id(test_app, monkeypatch):
72+
async def mock_get(id):
73+
return None
74+
75+
monkeypatch.setattr(crud, "get", mock_get)
76+
77+
response = test_app.get("/summaries/999/")
78+
assert response.status_code == 404
79+
assert response.json()["detail"] == "Summary not found"
80+
81+
82+
def test_read_all_summaries(test_app, monkeypatch):
83+
test_data = [
84+
{
85+
"id": 1,
86+
"url": "https://foo.bar",
87+
"summary": "summary",
88+
"created_at": datetime.utcnow().isoformat(),
89+
},
90+
{
91+
"id": 2,
92+
"url": "https://testdrivenn.io",
93+
"summary": "summary",
94+
"created_at": datetime.utcnow().isoformat(),
95+
},
96+
]
97+
98+
async def mock_get_all():
99+
return test_data
100+
101+
monkeypatch.setattr(crud, "get_all", mock_get_all)
102+
103+
response = test_app.get("/summaries/")
104+
assert response.status_code == 200
105+
assert response.json() == test_data
106+
107+
108+
def test_remove_summary(test_app, monkeypatch):
109+
async def mock_get(id):
110+
return {
111+
"id": 1,
112+
"url": "https://foo.bar",
113+
"summary": "summary",
114+
"created_at": datetime.utcnow().isoformat(),
115+
}
116+
117+
monkeypatch.setattr(crud, "get", mock_get)
118+
119+
async def mock_delete(id):
120+
return id
121+
122+
monkeypatch.setattr(crud, "delete", mock_delete)
123+
124+
response = test_app.delete("/summaries/1/")
125+
assert response.status_code == 200
126+
assert response.json() == {"id": 1, "url": "https://foo.bar"}
127+
128+
129+
def test_remove_summary_incorrect_id(test_app, monkeypatch):
130+
async def mock_get(id):
131+
return None
132+
133+
monkeypatch.setattr(crud, "get", mock_get)
134+
135+
response = test_app.delete("/summaries/999/")
136+
assert response.status_code == 404
137+
assert response.json()["detail"] == "Summary not found"
138+
139+
140+
def test_update_summary(test_app, monkeypatch):
141+
test_request_payload = {"url": "https://foo.bar", "summary": "updated"}
142+
test_response_payload = {
143+
"id": 1,
144+
"url": "https://foo.bar",
145+
"summary": "summary",
146+
"created_at": datetime.utcnow().isoformat(),
147+
}
148+
149+
async def mock_put(id, payload):
150+
return test_response_payload
151+
152+
monkeypatch.setattr(crud, "put", mock_put)
153+
154+
response = test_app.put(
155+
"/summaries/1/",
156+
data=json.dumps(test_request_payload),
157+
)
158+
assert response.status_code == 200
159+
assert response.json() == test_response_payload
160+
161+
162+
@pytest.mark.parametrize(
163+
"summary_id, payload, status_code, detail",
164+
[
165+
[
166+
999,
167+
{"url": "https://foo.bar", "summary": "updated!"},
168+
404,
169+
"Summary not found",
170+
],
171+
[
172+
0,
173+
{"url": "https://foo.bar", "summary": "updated!"},
174+
422,
175+
[
176+
{
177+
"loc": ["path", "id"],
178+
"msg": "ensure this value is greater than 0",
179+
"type": "value_error.number.not_gt",
180+
"ctx": {"limit_value": 0},
181+
}
182+
],
183+
],
184+
[
185+
1,
186+
{},
187+
422,
188+
[
189+
{
190+
"loc": ["body", "url"],
191+
"msg": "field required",
192+
"type": "value_error.missing",
193+
},
194+
{
195+
"loc": ["body", "summary"],
196+
"msg": "field required",
197+
"type": "value_error.missing",
198+
},
199+
],
200+
],
201+
[
202+
1,
203+
{"url": "https://foo.bar"},
204+
422,
205+
[
206+
{
207+
"loc": ["body", "summary"],
208+
"msg": "field required",
209+
"type": "value_error.missing",
210+
}
211+
],
212+
],
213+
],
214+
)
215+
def test_update_summary_invalid(
216+
test_app, monkeypatch, summary_id, payload, status_code, detail
217+
):
218+
async def mock_put(id, payload):
219+
return None
220+
221+
monkeypatch.setattr(crud, "put", mock_put)
222+
223+
response = test_app.put(f"/summaries/{summary_id}/", data=json.dumps(payload))
224+
assert response.status_code == status_code
225+
assert response.json()["detail"] == detail
226+
227+
228+
def test_update_summary_invalid_url(test_app):
229+
response = test_app.put(
230+
"/summaries/1/",
231+
data=json.dumps({"url": "invalid://url", "summary": "updated!"}),
232+
)
233+
assert response.status_code == 422
234+
assert response.json()["detail"][0]["msg"] == "URL scheme not permitted"

0 commit comments

Comments
 (0)