Skip to content

Commit 14c2c25

Browse files
authored
Merge pull request #54 from openagri-eu/main
Implemented script and added example datasets (#12)
2 parents 2314990 + 0f3dd49 commit 14c2c25

File tree

6 files changed

+360
-0
lines changed

6 files changed

+360
-0
lines changed

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,11 @@ This will run the tests and return success values for each api tested in the ter
222222

223223
<h3>These tests will NOT result in generated .pdf files.</h3>
224224

225+
## Working examples
226+
You can find working examples for animal, irrigation as well as compost reports generation in the following pages:
227+
228+
[Script](scripts/report_client.py)
229+
225230
## Contributing
226231

227232
We welcome first-time contributions!

scripts/animal_data.json

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
{
2+
"@graph": [
3+
{
4+
"@id": "urn:ngsi-ld:FarmAnimal:bessie-001",
5+
"name": "Bessie",
6+
"description": "Holstein cow, record created upon purchase.",
7+
"species": "Bos taurus",
8+
"sex": 1,
9+
"birthdate": "2022-04-12",
10+
"isCastrated": false,
11+
"status": 1,
12+
"hasAgriParcel": {
13+
"@id": "urn:ngsi-ld:AgriParcel:parcel-uuid-001"
14+
},
15+
"isMemberOfAnimalGroup": {
16+
"hasName": "Main Herd"
17+
},
18+
"dateCreated": "2025-10-09T10:00:00Z",
19+
"invalidatedAtTime": null
20+
},
21+
{
22+
"@id": "urn:ngsi-ld:FarmAnimal:clover-002",
23+
"name": "Clover",
24+
"description": "Jersey cow, born on farm.",
25+
"species": "Bos taurus",
26+
"sex": 1,
27+
"birthdate": "2023-01-15",
28+
"isCastrated": false,
29+
"status": 1,
30+
"hasAgriParcel": {
31+
"@id": "urn:ngsi-ld:AgriParcel:parcel-uuid-002"
32+
},
33+
"isMemberOfAnimalGroup": {
34+
"hasName": "Main Herd"
35+
},
36+
"dateCreated": "2025-10-09T11:00:00Z",
37+
"invalidatedAtTime": null
38+
}
39+
]
40+
}

scripts/compost_data.json

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
{
2+
"@graph": [
3+
{
4+
"@id": "urn:ngsi-ld:CompostTurningOperation:op-b42-20251010",
5+
"@type": "CompostTurningOperation",
6+
"title": "Monthly Compost Check",
7+
"details": "Regular check and turning of compost pile B-42.",
8+
"hasStartDatetime": "2025-10-10T08:00:00Z",
9+
"hasEndDatetime": "2025-10-10T09:00:00Z",
10+
"responsibleAgent": "John Doe",
11+
"isOperatedOn": {
12+
"@id": "urn:ngsi-ld:CompostPile:cp-b42"
13+
},
14+
"hasAgriParcel": {
15+
"@id": "urn:ngsi-ld:AgriParcel:parcel-for-compost-b42"
16+
},
17+
"usesAgriculturalMachinery": [
18+
{
19+
"@id": "urn:ngsi-ld:AgriculturalMachinery:tractor-01"
20+
}
21+
],
22+
"hasMeasurement": [
23+
{
24+
"hasMember": [
25+
{
26+
"@id": "urn:ngsi-ld:CropObservation:obs-temp-001",
27+
"@type": "CropObservation",
28+
"type": "observed",
29+
"phenomenonTime": "2025-10-10T08:15:00Z",
30+
"observedProperty": "temperature",
31+
"hasResult": {
32+
"@id": "urn:ngsi-ld:Property:result-temp-001",
33+
"@type": "Property",
34+
"hasValue": "62",
35+
"unit": "CEL"
36+
},
37+
"details": "Core temperature reading."
38+
}
39+
]
40+
}
41+
],
42+
"hasNestedOperation": [
43+
{
44+
"@id": "urn:ngsi-ld:AddRawMaterialOperation:raw-mat-001",
45+
"@type": "AddRawMaterialOperation",
46+
"type": "raw",
47+
"hasStartDatetime": "2025-10-10T08:30:00Z",
48+
"details": "Added fresh green waste.",
49+
"hasCompostMaterial": [
50+
{
51+
"@type": "CompostMaterial",
52+
"typeName": "Grass Clippings",
53+
"quantityValue": {
54+
"@type": "QuantitativeValue",
55+
"numericValue": "150",
56+
"unit": "KGM"
57+
}
58+
}
59+
]
60+
}
61+
]
62+
}
63+
]
64+
}

scripts/irrigation_data.json

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
{
2+
"@graph": [
3+
{
4+
"@id": "urn:ngsi-ld:IrrigationOperation:op-001",
5+
"@type": "IrrigationOperation",
6+
"operatedOn": {
7+
"@id": "urn:ngsi-ld:AgriParcel:parcel-north-42"
8+
},
9+
"hasStartDatetime": "2025-10-11T09:00:00Z",
10+
"hasEndDatetime": "2025-10-11T12:30:00Z",
11+
"hasAppliedAmount": {
12+
"numericValue": 150,
13+
"unit": "Cubic Meter"
14+
},
15+
"usesIrrigationSystem": "Center Pivot Irrigation"
16+
},
17+
{
18+
"@id": "urn:ngsi-ld:IrrigationOperation:op-002",
19+
"@type": "IrrigationOperation",
20+
"operatedOn": {
21+
"@id": "urn:ngsi-ld:AgriParcel:parcel-south-15"
22+
},
23+
"hasStartDatetime": "2025-10-10T14:00:00Z",
24+
"hasEndDatetime": "2025-10-10T16:00:00Z",
25+
"hasAppliedAmount": {
26+
"numericValue": 75,
27+
"unit": "Cubic Meter"
28+
},
29+
"usesIrrigationSystem": "Drip Irrigation System"
30+
}
31+
]
32+
}

scripts/readme.md

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
This command-line script (report_client.py) provides a simple way to interact with the Reporting service. It allows you to request the generation of three different types of reports (animal, irrigation, or compost) by uploading a corresponding JSON data file.
2+
3+
The script handles the entire workflow:
4+
5+
It sends the initial request to start the PDF generation process.
6+
7+
It waits and periodically checks the server's status.
8+
9+
Once the PDF is ready, it downloads the file to the same directory where the script is run.
10+
11+
Prerequisites
12+
13+
Python 3.6+
14+
15+
The requests library. If you don't have it, install it via pip:
16+
17+
pip install requests
18+
19+
A valid authentication token from the reporting service.
20+
21+
A correctly formatted JSON data file for the report you wish to generate.
22+
23+
Usage
24+
25+
The script is run from your terminal and accepts four command-line arguments.
26+
```
27+
python3 report_client.py --type <REPORT_TYPE> --token <YOUR_TOKEN> --file <PATH_TO_JSON> [--url <SERVICE_URL>]
28+
```
29+
Command-Line Arguments
30+
31+
32+
| Argument | Required | Description |
33+
|----------|----------|----|
34+
| --type | Yes | The type of report to generate. Must be one of animal, irrigation, or compost. |
35+
| --token | Yes | Your personal authentication bearer token for the service. |
36+
| --file | Yes | The relative or absolute path to the JSON file containing the data for the report. |
37+
| --url | No | The base URL of the reporting service. If not provided, it defaults to http://127.0.0.1:8009. |
38+
39+
Example Commands
40+
Animal Report
41+
```
42+
python3 report_client.py --type animal --token "your-auth-token-here" --file "./animal_data.json"
43+
```
44+
Irrigation Report
45+
```
46+
python3 report_client.py --type irrigation --token "your-auth-token-here" --file "data/irrigation_data.json"
47+
```
48+
Compost Report (with a custom URL)
49+
```
50+
python3 report_client.py --type compost --token "your-auth-token-here" --file "compost.json" --url "http://api.myfarm.com"
51+
```
52+
How It Works
53+
54+
Generate Report: The script sends a POST request to the appropriate endpoint (e.g., /api/v1/openagri-report/animal-report/) with your token and JSON file. The server accepts the request and returns a unique uuid for the report job.
55+
56+
Download PDF: The script then enters a loop, sending GET requests to the retrieval endpoint (e.g., /api/v1/openagri-report/<uuid>/).
57+
58+
If the server responds with status 202 Accepted, it means the PDF is still being created, and the script waits a few seconds before trying again.
59+
60+
If the server responds with 200 OK, the PDF is ready. The script writes the content to a new file named <uuid>.pdf.
61+
62+
The script will retry up to 10 times before timing out.
63+
64+
Important Notes
65+
66+
The success of the report generation depends entirely on providing a correctly formatted JSON file. If the server's validation fails, the PDF generation will fail.
67+
68+
The final PDF file will be saved in the same directory from which you execute the script.

scripts/report_client.py

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
#!/usr/bin/env python3
2+
import requests
3+
import time
4+
import os
5+
import argparse
6+
7+
# --- Configuration ---
8+
# Maximum number of times to check for the PDF before giving up
9+
MAX_RETRIES = 10
10+
# Seconds to wait between check attempts
11+
RETRY_DELAY_SECONDS = 3
12+
13+
# USAGE EXAMPLES:
14+
15+
# python report_client.py --type animal --token "your-long-auth-token-here" --file "path/to/animal_data.json"
16+
# python report_client.py --type irrigation --token "your-long-auth-token-here" --file "data/irrigation_payload.json"
17+
# python report_client.py --type compost --token "your-long-auth-token-here" --file "compost.json" --url "http://localhost:5000"
18+
19+
# --- End Configuration ---
20+
21+
def generate_report(report_type: str, base_url: str, token: str, json_file: str) -> str | None:
22+
"""
23+
Calls the appropriate report endpoint and returns the report UUID.
24+
25+
Args:
26+
report_type: The type of report ('animal', 'irrigation', 'compost').
27+
base_url: The base URL of the reporting service.
28+
token: The authentication bearer token.
29+
json_file: The path to the JSON data file to upload.
30+
31+
Returns:
32+
The report UUID string if successful, otherwise None.
33+
"""
34+
# The API endpoint is dynamically constructed from the report_type
35+
report_url = f"{base_url}/api/v1/openagri-report/{report_type}-report/"
36+
print(f"➡️ Attempting to generate '{report_type}' report from '{json_file}'...")
37+
38+
headers = {
39+
"Authorization": f"Bearer {token}"
40+
}
41+
42+
try:
43+
with open(json_file, 'rb') as f:
44+
# The API expects the file in a multipart/form-data request
45+
files = {
46+
"data": (os.path.basename(json_file), f, "application/json")
47+
}
48+
response = requests.post(report_url, headers=headers, files=files)
49+
response.raise_for_status() # Raises an exception for 4XX/5XX errors
50+
51+
response_data = response.json()
52+
report_uuid = response_data.get("uuid")
53+
54+
if not report_uuid:
55+
print("❌ Report generation failed: 'uuid' not found in the server response.")
56+
return None
57+
58+
print(f"✅ Report generation started successfully. Report ID: {report_uuid}")
59+
return report_uuid
60+
61+
except FileNotFoundError:
62+
print(f"❌ Error: The data file '{json_file}' was not found.")
63+
return None
64+
except requests.exceptions.RequestException as e:
65+
print(f"❌ Report generation failed: {e}")
66+
if e.response is not None:
67+
print(f" Server Response ({e.response.status_code}): {e.response.text}")
68+
return None
69+
70+
71+
def download_pdf(report_uuid: str, base_url: str, token: str):
72+
"""
73+
Polls the retrieval endpoint and downloads the generated PDF.
74+
"""
75+
print(f"⏳ Attempting to download PDF for report ID: {report_uuid}...")
76+
pdf_url = f"{base_url}/api/v1/openagri-report/{report_uuid}/"
77+
78+
headers = {
79+
"Authorization": f"Bearer {token}"
80+
}
81+
82+
for attempt in range(MAX_RETRIES):
83+
try:
84+
print(f" Attempt {attempt + 1}/{MAX_RETRIES}... ", end="", flush=True)
85+
response = requests.get(pdf_url, headers=headers)
86+
87+
if response.status_code == 202:
88+
print(f"PDF is still being generated. Retrying in {RETRY_DELAY_SECONDS}s...")
89+
time.sleep(RETRY_DELAY_SECONDS)
90+
continue
91+
92+
response.raise_for_status()
93+
94+
output_filename = f"{report_uuid}.pdf"
95+
with open(output_filename, 'wb') as f:
96+
f.write(response.content)
97+
f.flush()
98+
os.fsync(f.fileno())
99+
100+
print(f"\n🎉 PDF downloaded successfully! Saved as '{output_filename}'")
101+
return
102+
103+
except requests.exceptions.RequestException as e:
104+
print(f"\n❌ Failed to download PDF: {e}")
105+
if e.response is not None:
106+
print(f" Server Response ({e.response.status_code}): {e.response.text}")
107+
return
108+
109+
print(
110+
f"\n❌ Failed to retrieve PDF after {MAX_RETRIES} attempts. The report might still be processing or an error occurred on the server.")
111+
112+
def main():
113+
"""
114+
Main function to parse arguments and run the reporting process.
115+
"""
116+
parser = argparse.ArgumentParser(
117+
description="A command-line client to generate and download PDF reports from the reporting service.",
118+
formatter_class=argparse.RawTextHelpFormatter
119+
)
120+
121+
parser.add_argument(
122+
"--type",
123+
required=True,
124+
choices=['animal', 'irrigation', 'compost'],
125+
help="The type of report to generate."
126+
)
127+
parser.add_argument(
128+
"--token",
129+
required=True,
130+
help="Your authentication bearer token."
131+
)
132+
parser.add_argument(
133+
"--file",
134+
required=True,
135+
help="Path to the JSON data file to upload."
136+
)
137+
parser.add_argument(
138+
"--url",
139+
default="http://127.0.0.1:8009",
140+
help="The base URL of the reporting service. (default: http://127.0.0.1:8009)"
141+
)
142+
143+
args = parser.parse_args()
144+
145+
report_id = generate_report(args.type, args.url, args.token, args.file)
146+
if report_id:
147+
download_pdf(report_id, args.url, args.token)
148+
149+
150+
if __name__ == "__main__":
151+
main()

0 commit comments

Comments
 (0)