Skip to content

Commit d26cbab

Browse files
committed
updates
1 parent ba8d0c7 commit d26cbab

File tree

6 files changed

+305
-13
lines changed

6 files changed

+305
-13
lines changed

project/app/api/crud.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,7 @@ async def get_all() -> List:
2121

2222

2323
async def post(payload: SummaryPayloadSchema) -> int:
24-
summary = TextSummary(
25-
url=payload.url,
26-
summary="dummy summary",
27-
)
24+
summary = TextSummary(url=payload.url, summary="")
2825
await summary.save()
2926
return summary.id
3027

project/app/api/summaries.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
from typing import List
22

3-
from fastapi import APIRouter, HTTPException, Path
3+
from fastapi import APIRouter, BackgroundTasks, HTTPException, Path
44

55
from app.api import crud
66
from app.models.tortoise import SummarySchema
7+
from app.summarizer import generate_summary
78

89
from app.models.pydantic import ( # isort:skip
910
SummaryPayloadSchema,
@@ -29,9 +30,13 @@ async def read_all_summaries() -> List[SummarySchema]:
2930

3031

3132
@router.post("/", response_model=SummaryResponseSchema, status_code=201)
32-
async def create_summary(payload: SummaryPayloadSchema) -> SummaryResponseSchema:
33+
async def create_summary(
34+
payload: SummaryPayloadSchema, background_tasks: BackgroundTasks
35+
) -> SummaryResponseSchema:
3336
summary_id = await crud.post(payload)
3437

38+
background_tasks.add_task(generate_summary, summary_id, str(payload.url))
39+
3540
response_object = {"id": summary_id, "url": payload.url}
3641
return response_object
3742

project/app/summarizer.py

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

project/requirements.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,11 @@ flake8==7.2.0
66
gunicorn==22.0.0
77
httpx==0.28.1
88
isort==6.0.1
9+
lxml-html-clean==0.4.2
10+
newspaper3k==0.2.8
911
pydantic-settings==2.8.1
1012
pytest==8.3.5
1113
pytest-cov==6.1.1
14+
pytest-xdist==3.6.1
1215
tortoise-orm==0.25.0
1316
uvicorn==0.34.1

project/tests/test_summaries.py

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

33
import pytest
44

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)
513

6-
def test_create_summary(test_app_with_db):
714
response = test_app_with_db.post(
8-
"/summaries/", data=json.dumps({"url": "https://foo.bar/"})
15+
"/summaries/", data=json.dumps({"url": "https://foo.bar"})
916
)
1017

1118
assert response.status_code == 201
@@ -33,7 +40,12 @@ def test_create_summaries_invalid_json(test_app):
3340
)
3441

3542

36-
def test_read_summary(test_app_with_db):
43+
def test_read_summary(test_app_with_db, monkeypatch):
44+
def mock_generate_summary(summary_id, url):
45+
return None
46+
47+
monkeypatch.setattr(summaries, "generate_summary", mock_generate_summary)
48+
3749
response = test_app_with_db.post(
3850
"/summaries/", data=json.dumps({"url": "https://foo.bar/"})
3951
)
@@ -45,7 +57,6 @@ def test_read_summary(test_app_with_db):
4557
response_dict = response.json()
4658
assert response_dict["id"] == summary_id
4759
assert response_dict["url"] == "https://foo.bar/"
48-
assert response_dict["summary"]
4960
assert response_dict["created_at"]
5061

5162

@@ -69,7 +80,12 @@ def test_read_summary_incorrect_id(test_app_with_db):
6980
}
7081

7182

72-
def test_read_all_summaries(test_app_with_db):
83+
def test_read_all_summaries(test_app_with_db, monkeypatch):
84+
def mock_generate_summary(summary_id, url):
85+
return None
86+
87+
monkeypatch.setattr(summaries, "generate_summary", mock_generate_summary)
88+
7389
response = test_app_with_db.post(
7490
"/summaries/", data=json.dumps({"url": "https://foo.bar/"})
7591
)
@@ -82,7 +98,12 @@ def test_read_all_summaries(test_app_with_db):
8298
assert len(list(filter(lambda d: d["id"] == summary_id, response_list))) == 1
8399

84100

85-
def test_remove_summary(test_app_with_db):
101+
def test_remove_summary(test_app_with_db, monkeypatch):
102+
def mock_generate_summary(summary_id, url):
103+
return None
104+
105+
monkeypatch.setattr(summaries, "generate_summary", mock_generate_summary)
106+
86107
response = test_app_with_db.post(
87108
"/summaries/", data=json.dumps({"url": "https://foo.bar/"})
88109
)
@@ -113,7 +134,12 @@ def test_remove_summary_incorrect_id(test_app_with_db):
113134
}
114135

115136

116-
def test_update_summary(test_app_with_db):
137+
def test_update_summary(test_app_with_db, monkeypatch):
138+
def mock_generate_summary(summary_id, url):
139+
return None
140+
141+
monkeypatch.setattr(summaries, "generate_summary", mock_generate_summary)
142+
117143
response = test_app_with_db.post(
118144
"/summaries/", data=json.dumps({"url": "https://foo.bar/"})
119145
)
Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
import json
2+
from datetime import datetime
3+
4+
import pytest
5+
6+
from app.api import crud, summaries
7+
8+
9+
def test_create_summary(test_app, monkeypatch):
10+
def mock_generate_summary(summary_id, url):
11+
return None
12+
13+
monkeypatch.setattr(summaries, "generate_summary", mock_generate_summary)
14+
15+
test_request_payload = {"url": "https://foo.bar"}
16+
test_response_payload = {"id": 1, "url": "https://foo.bar/"}
17+
18+
async def mock_post(payload):
19+
return 1
20+
21+
monkeypatch.setattr(crud, "post", mock_post)
22+
23+
response = test_app.post(
24+
"/summaries/",
25+
data=json.dumps(test_request_payload),
26+
)
27+
28+
assert response.status_code == 201
29+
assert response.json() == test_response_payload
30+
31+
32+
def test_create_summaries_invalid_json(test_app):
33+
response = test_app.post("/summaries/", data=json.dumps({}))
34+
assert response.status_code == 422
35+
assert response.json() == {
36+
"detail": [
37+
{
38+
"type": "missing",
39+
"loc": ["body", "url"],
40+
"msg": "Field required",
41+
"input": {},
42+
}
43+
]
44+
}
45+
46+
response = test_app.post("/summaries/", data=json.dumps({"url": "invalid://url"}))
47+
assert response.status_code == 422
48+
assert (
49+
response.json()["detail"][0]["msg"] == "URL scheme should be 'http' or 'https'"
50+
)
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+
"type": "greater_than",
178+
"loc": ["path", "id"],
179+
"msg": "Input should be greater than 0",
180+
"input": "0",
181+
"ctx": {"gt": 0},
182+
}
183+
],
184+
],
185+
[
186+
1,
187+
{},
188+
422,
189+
[
190+
{
191+
"type": "missing",
192+
"loc": ["body", "url"],
193+
"msg": "Field required",
194+
"input": {},
195+
},
196+
{
197+
"type": "missing",
198+
"loc": ["body", "summary"],
199+
"msg": "Field required",
200+
"input": {},
201+
},
202+
],
203+
],
204+
[
205+
1,
206+
{"url": "https://foo.bar"},
207+
422,
208+
[
209+
{
210+
"type": "missing",
211+
"loc": ["body", "summary"],
212+
"msg": "Field required",
213+
"input": {"url": "https://foo.bar"},
214+
}
215+
],
216+
],
217+
],
218+
)
219+
def test_update_summary_invalid(
220+
test_app, monkeypatch, summary_id, payload, status_code, detail
221+
):
222+
async def mock_put(id, payload):
223+
return None
224+
225+
monkeypatch.setattr(crud, "put", mock_put)
226+
227+
response = test_app.put(f"/summaries/{summary_id}/", data=json.dumps(payload))
228+
assert response.status_code == status_code
229+
assert response.json()["detail"] == detail
230+
231+
232+
def test_update_summary_invalid_url(test_app):
233+
response = test_app.put(
234+
"/summaries/1/",
235+
data=json.dumps({"url": "invalid://url", "summary": "updated!"}),
236+
)
237+
assert response.status_code == 422
238+
assert (
239+
response.json()["detail"][0]["msg"] == "URL scheme should be 'http' or 'https'"
240+
)

0 commit comments

Comments
 (0)