Skip to content

Commit f1f9675

Browse files
JoleVLFJoleVLF
andauthored
29 add background fastapi tasks for pdf generation (#30)
* Append background task to reporting * Add pdf file name which will be used in the future * Decoded JWT info update * Add user ID inside pdf name * Retrieve user PDF * Add response schema * Update reporting dir and test locally * Refactor usage uuid --------- Co-authored-by: JoleVLF <Jovan@DESKTOP-JTIACNO>
1 parent 017b134 commit f1f9675

11 files changed

Lines changed: 264 additions & 175 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ venv
44
.alembic
55
__pycache__
66
.env
7+
app/user_reports

README.md

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,25 @@ docker compose up
5151

5252
The application will be served on http://127.0.0.1:8009 (I.E. typing localhost/docs in your browser will load the swagger documentation)
5353

54-
Full list of APIs available you can check [here](https://editor-next.swagger.io/?url=https://gist.githubusercontent.com/JoleVLF/20645c6e8e3545d02c7c4570271bdc49/raw/6efafb74bd2bc33e4221ee2a202d0c830b867c70/reporting)
54+
Full list of APIs available you can check [here](https://editor-next.swagger.io/?url=https://gist.githubusercontent.com/JoleVLF/b1bcdd77ac82aeb115a6c94fb3dadbdc/raw/4ce7fea5b022142002d0dfebcd2156a3e408fbe4/api.json)
5555
# Documentation
56+
<h3>GET</h3>
57+
58+
```
59+
/api/v1/openagri-report/{report_id}/
60+
```
61+
62+
## Path Params
63+
64+
### report_id
65+
- **Type**: `uuid str`
66+
- **Description**: UUID of the generated PDF file..
67+
68+
69+
## Response
70+
71+
Response is generated PDF file.
72+
5673
<h3>POST</h3>
5774

5875
```
@@ -69,8 +86,7 @@ Full list of APIs available you can check [here](https://editor-next.swagger.io/
6986

7087
## Response
7188

72-
Response is generated PDF file.
73-
89+
Response is uuid of generated PDF file.
7490

7591
<h3>POST</h3>
7692

@@ -95,13 +111,13 @@ Response is generated PDF file.
95111

96112

97113
## Response
98-
Response is generated PDF file.
114+
Response is uuid of generated PDF file.
99115

100116
<h3> Example usage </h3>
101117

102118
Compost and Irrigation Report APIs can be used with and without Gatekeeper support.
103119
If Gatekeeper is used, data file param can be ignored.
104-
When service is run wihtout Gatekeeper data must be provided in .json file format.
120+
When service is run without Gatekeeper data must be provided in .json file format.
105121

106122

107123
```shell
@@ -142,7 +158,7 @@ If a valid JSON file is uploaded, the API processes the data directly to generat
142158

143159
## Response
144160

145-
Response is generated PDF file.
161+
Response is uuid of generated PDF file.
146162

147163
<h2>Pytest</h2>
148164
Pytest can be run on the same machine the service has been deployed to by moving into the tests dir and running:

app/api/api_v1/endpoints/report.py

Lines changed: 90 additions & 147 deletions
Original file line numberDiff line numberDiff line change
@@ -1,81 +1,95 @@
1-
import json
1+
import os
22
import uuid
3-
from json import JSONDecodeError
43
from typing import Optional
54

6-
from fastapi import APIRouter, File, Depends, HTTPException, UploadFile, Response
5+
from fastapi import (
6+
APIRouter,
7+
Depends,
8+
HTTPException,
9+
UploadFile,
10+
BackgroundTasks,
11+
)
712
from pydantic import UUID4
813

914
from api import deps
1015
from core import settings
16+
from schemas import PDF
17+
from utils import decode_jwt_token
1118
from utils.animals_report import process_animal_data
1219
from utils.farm_calendar_report import process_farm_calendar_data
1320
from utils.irrigation_report import process_irrigation_data
14-
from utils.json_handler import make_get_request
21+
from fastapi.responses import FileResponse
1522

1623
router = APIRouter()
1724

1825

19-
@router.post("/irrigation-report/")
20-
async def generate_irrigation_report(
26+
@router.get("{report_id}", response_class=FileResponse)
27+
def retrieve_generated_pdf(
28+
report_id: str,
2129
token=Depends(deps.get_current_user),
22-
data: UploadFile = None,
2330
):
2431
"""
25-
Generates Irrigation Report PDF file
32+
33+
Retrieve generated PDF file
2634
2735
"""
36+
user_id = (
37+
decode_jwt_token(token)["user_id"]
38+
if settings.REPORTING_USING_GATEKEEPER
39+
else token.id
40+
)
2841

29-
if not data and not settings.REPORTING_USING_GATEKEEPER:
30-
raise HTTPException(
31-
status_code=400,
32-
detail=f"Data file must be provided if gatekeeper is not used.",
33-
)
42+
file_path = f"{settings.PDF_DIRECTORY}{user_id}/{report_id}.pdf"
3443

35-
pdf = None
36-
if not data:
37-
params = {"format": "json"}
38-
farm_calendar_irrigation_response = make_get_request(
39-
url=f'{settings.REPORTING_FARMCALENDAR_BASE_URL}{settings.REPORTING_FARMCALENDAR_URLS["irrigations"]}',
40-
token=token,
41-
params=params,
44+
if not os.path.exists(file_path):
45+
raise HTTPException(
46+
status_code=404,
47+
detail=f"File with uuid {report_id} for logged user not found.",
4248
)
4349

44-
if not farm_calendar_irrigation_response:
45-
raise HTTPException(status_code=400, detail="No Irrigation data found.")
50+
return FileResponse(
51+
path=file_path, media_type="application/pdf", filename=f"{report_id}"
52+
)
4653

47-
pdf = process_irrigation_data(json_data=farm_calendar_irrigation_response)
4854

49-
else:
50-
try:
51-
pdf = process_irrigation_data(json_data=json.load(data.file))
55+
@router.post("/irrigation-report/", response_model=PDF)
56+
async def generate_irrigation_report(
57+
background_tasks: BackgroundTasks,
58+
token=Depends(deps.get_current_user),
59+
data: UploadFile = None,
60+
):
61+
"""
62+
Generates Irrigation Report PDF file
5263
53-
except JSONDecodeError as e:
54-
raise HTTPException(
55-
status_code=400,
56-
detail=f"Reporting service failed during PDF generation. File is not correct JSON.",
57-
)
64+
"""
65+
uuid_v4 = str(uuid.uuid4())
66+
user_id = (
67+
decode_jwt_token(token)["user_id"]
68+
if settings.REPORTING_USING_GATEKEEPER
69+
else token.id
70+
)
71+
uuid_of_pdf = f"{user_id}/{uuid_v4}"
5872

59-
if not pdf:
73+
if not data and not settings.REPORTING_USING_GATEKEEPER:
6074
raise HTTPException(
6175
status_code=400,
62-
detail=f"Reporting service failed during PDF generation.",
63-
)
64-
65-
headers = {
66-
"Content-Disposition": "attachment; filename=irrigation-report-{}.pdf".format(
67-
uuid.uuid4()
76+
detail=f"Data file must be provided if gatekeeper is not used.",
6877
)
69-
}
7078

71-
return Response(
72-
content=bytes(pdf.output()), media_type="application/pdf", headers=headers
79+
background_tasks.add_task(
80+
process_irrigation_data,
81+
data=data,
82+
token=token,
83+
pdf_file_name=uuid_of_pdf,
7384
)
7485

86+
return PDF(uuid=uuid_v4)
87+
7588

76-
@router.post("/compost-report/")
89+
@router.post("/compost-report/", response_model=PDF)
7790
async def generate_generic_observation_report(
7891
observation_type_name: str,
92+
background_tasks: BackgroundTasks,
7993
token=Depends(deps.get_current_user),
8094
data: UploadFile = None,
8195
):
@@ -90,86 +104,28 @@ async def generate_generic_observation_report(
90104

91105
if observation_type_name == "CropStressIndicator":
92106
observation_type_name = "Crop Stress Indicator"
93-
94-
pdf = None
95-
if not data:
96-
if not settings.REPORTING_USING_GATEKEEPER:
97-
raise HTTPException(
98-
status_code=400,
99-
detail=f"Data file must be provided if gatekeeper is not used.",
100-
)
101-
102-
params = {"format": "json", "name": observation_type_name}
103-
farm_activity_type_info = make_get_request(
104-
url=f'{settings.REPORTING_FARMCALENDAR_BASE_URL}{settings.REPORTING_FARMCALENDAR_URLS["activity_types"]}',
105-
token=token,
106-
params=params,
107-
)
108-
109-
if not farm_activity_type_info:
110-
raise HTTPException(status_code=400, detail="Activity Type API failed.")
111-
112-
del params["name"]
113-
params["activity_type"] = farm_activity_type_info[0]["@id"].split(":")[3]
114-
115-
observations = make_get_request(
116-
url=f'{settings.REPORTING_FARMCALENDAR_BASE_URL}{settings.REPORTING_FARMCALENDAR_URLS["observations"]}',
117-
token=token,
118-
params=params,
119-
)
120-
121-
if not observations:
122-
raise HTTPException(status_code=400, detail="Observations are empty.")
123-
124-
farm_activities = make_get_request(
125-
url=f'{settings.REPORTING_FARMCALENDAR_BASE_URL}{settings.REPORTING_FARMCALENDAR_URLS["activities"]}',
126-
token=token,
127-
params=params,
128-
)
129-
130-
if not farm_activities:
131-
raise HTTPException(status_code=400, detail="Farm Activities are empty.")
132-
133-
pdf = process_farm_calendar_data(
134-
activity_type_info=observation_type_name,
135-
observations=observations,
136-
farm_activities=farm_activities,
137-
)
138-
139-
else:
140-
try:
141-
dt = json.load(data.file)
142-
pdf = process_farm_calendar_data(
143-
activity_type_info=observation_type_name,
144-
observations=dt["observations"],
145-
farm_activities=dt["farm_activities"],
146-
)
147-
148-
except JSONDecodeError as e:
149-
raise HTTPException(
150-
status_code=400,
151-
detail=f"Reporting service failed during PDF generation. File is not correct JSON.",
152-
)
153-
154-
if not pdf:
155-
raise HTTPException(
156-
status_code=400,
157-
detail=f"Reporting service failed during PDF generation.",
158-
)
159-
160-
headers = {
161-
"Content-Disposition": "attachment; filename=compost-report-{}.pdf".format(
162-
uuid.uuid4()
163-
)
164-
}
165-
166-
return Response(
167-
content=bytes(pdf.output()), media_type="application/pdf", headers=headers
107+
uuid_v4 = str(uuid.uuid4())
108+
user_id = (
109+
decode_jwt_token(token)["user_id"]
110+
if settings.REPORTING_USING_GATEKEEPER
111+
else token.id
112+
)
113+
uuid_of_pdf = f"{user_id}/{uuid_v4}"
114+
115+
background_tasks.add_task(
116+
process_farm_calendar_data,
117+
observation_type_name=observation_type_name,
118+
token=token,
119+
data=data,
120+
pdf_file_name=uuid_of_pdf,
168121
)
169122

123+
return PDF(uuid=uuid_v4)
170124

171-
@router.post("/animal-report/")
125+
126+
@router.post("/animal-report/", response_model=PDF)
172127
async def generate_animal_report(
128+
background_tasks: BackgroundTasks,
173129
token=Depends(deps.get_current_user),
174130
animal_group: Optional[str] = None,
175131
name: Optional[str] = None,
@@ -180,8 +136,14 @@ async def generate_animal_report(
180136
"""
181137
Generates Animal Report PDF file
182138
"""
183-
184-
pdf = None
139+
uuid_v4 = str(uuid.uuid4())
140+
user_id = (
141+
decode_jwt_token(token)["user_id"]
142+
if settings.REPORTING_USING_GATEKEEPER
143+
else token.id
144+
)
145+
uuid_of_pdf = f"{user_id}/{uuid_v4}"
146+
params = None
185147
if not data:
186148
if not settings.REPORTING_USING_GATEKEEPER:
187149
raise HTTPException(
@@ -199,31 +161,12 @@ async def generate_animal_report(
199161
if status is not None:
200162
params["status"] = status
201163

202-
json_response = make_get_request(
203-
url=f'{settings.REPORTING_FARMCALENDAR_BASE_URL}{settings.REPORTING_FARMCALENDAR_URLS["animals"]}',
204-
token=token,
205-
params=params,
206-
)
207-
208-
if not json_response:
209-
raise HTTPException(status_code=400, detail="No animal data found.")
210-
211-
pdf = process_animal_data(json_data=json_response)
212-
if not pdf:
213-
raise HTTPException(
214-
status_code=400,
215-
detail="Reporting service failed during PDF generation.",
216-
)
217-
else:
218-
try:
219-
pdf = process_animal_data(json_data=json.load(data.file))
220-
221-
except JSONDecodeError as e:
222-
raise HTTPException(
223-
status_code=400,
224-
detail=f"Reporting service failed during PDF generation. File is not correct JSON.",
225-
)
226-
headers = {"Content-Disposition": "attachment; filename=animal-report.pdf"}
227-
return Response(
228-
content=bytes(pdf.output()), media_type="application/pdf", headers=headers
164+
background_tasks.add_task(
165+
process_animal_data,
166+
data=data,
167+
token=token,
168+
params=params,
169+
pdf_file_name=uuid_of_pdf,
229170
)
171+
172+
return PDF(uuid=uuid_v4)

app/core/config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ class Settings(BaseSettings):
3232
"animals": "/FarmAnimals/",
3333
}
3434

35+
PDF_DIRECTORY: str = "user_reports/"
3536
SQLALCHEMY_DATABASE_URI: Optional[str] = None
3637

3738
@field_validator("SQLALCHEMY_DATABASE_URI", mode="before")

app/schemas/animals.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ class Animal(BaseModel):
1818
description: str
1919
hasAgriParcel: Optional[HasAgriParcel] = None
2020
sex: int
21-
castrated: bool
21+
isCastrated: bool = False
2222
species: str
2323
breed: str
2424
birthdate: datetime

app/schemas/generic.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,7 @@
33

44
class Message(BaseModel):
55
message: str
6+
7+
8+
class PDF(BaseModel):
9+
uuid: str

0 commit comments

Comments
 (0)