Skip to content

Commit f5fa677

Browse files
refactor and add automated tests: reorganize nfgda_service directory structure by migrating algorithm components and cleaning up legacy files.
1 parent b835d7d commit f5fa677

90 files changed

Lines changed: 334 additions & 34 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
name: Integration Tests
2+
3+
on:
4+
pull_request:
5+
6+
jobs:
7+
integration-tests:
8+
runs-on: ubuntu-latest
9+
10+
steps:
11+
- uses: actions/checkout@v4
12+
13+
- name: Set up Python
14+
uses: actions/setup-python@v5
15+
with:
16+
python-version: '3.11'
17+
18+
- name: Install test dependencies
19+
run: pip install pytest requests
20+
21+
- name: Start Docker Compose stack
22+
run: docker compose up -d --build
23+
24+
- name: Wait for backend to be ready
25+
run: |
26+
echo "Waiting for backend..."
27+
for i in $(seq 1 30); do
28+
if curl -sf http://localhost:8001/apis/stations > /dev/null; then
29+
echo "Backend is up"
30+
exit 0
31+
fi
32+
echo " attempt $i/30..."
33+
sleep 5
34+
done
35+
echo "Backend did not become ready in time"
36+
docker compose logs backend
37+
exit 1
38+
39+
- name: Run integration tests (non-slow)
40+
run: pytest test_endpoints.py -v -m "not slow"
41+
42+
- name: Dump logs on failure
43+
if: failure()
44+
run: docker compose logs
45+
46+
- name: Tear down stack
47+
if: always()
48+
run: docker compose down

backend/apis/retrieve_frames.py

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,23 @@
11
"""
2-
Frame Data API: returns the list of rendered frames for a completed job.
2+
Frame Data API: returns a single rendered GeoTIFF frame for a completed job.
33
"""
44

5-
def get_frames(job_id: str):
6-
7-
pass
5+
import os
6+
from flask import send_file, abort
7+
8+
9+
def get_frame(job_id: str, index: int):
10+
"""Return a single GeoTIFF frame file for the given job and frame index."""
11+
job_dir = "/processed_data/" + job_id
12+
if not os.path.exists(job_dir):
13+
abort(404, description="Job not found")
14+
15+
frame_path = job_dir + f"/frame_{index}.tif"
16+
if not os.path.exists(frame_path):
17+
abort(404, description="Frame not found")
18+
19+
return send_file(
20+
frame_path,
21+
mimetype="image/tiff",
22+
as_attachment=False
23+
)

backend/apis/run_request.py

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -68,13 +68,14 @@ def send_job_to_redis_queue(redis_client, request_fields: dict):
6868
def validate_time_parameters(request_fields: dict):
6969
"""Validate the time parameters recieved via the request."""
7070

71-
# Default timebox when not provided: look back 15 minutes from now
72-
# so the algorithm captures 2-3 recent NEXRAD scans for detection + forecast
73-
# (2 scan minimum needed for forcasting)
71+
# Default timebox when not provided: look back over the last ~25 minutes, ending
72+
# 10 minutes ago. The 10-minute buffer ensures the algorithm's end time is always
73+
# fully in the past — if endUtc is too close to "now" the algorithm enters live
74+
# polling mode and runs indefinitely.
7475
now = datetime.now(timezone.utc)
7576
if not request_fields.get("startUtc") and not request_fields.get("endUtc"):
76-
request_fields["startUtc"] = (now - timedelta(minutes=15)).strftime("%Y-%m-%dT%H:%M:%SZ")
77-
request_fields["endUtc"] = now.strftime("%Y-%m-%dT%H:%M:%SZ")
77+
request_fields["startUtc"] = (now - timedelta(minutes=35)).strftime("%Y-%m-%dT%H:%M:%SZ")
78+
request_fields["endUtc"] = (now - timedelta(minutes=10)).strftime("%Y-%m-%dT%H:%M:%SZ")
7879
elif not request_fields.get("startUtc") or not request_fields.get("endUtc"):
7980
return jsonify({"error": "Must provide both startUtc and endUtc, or neither"})
8081

@@ -102,8 +103,10 @@ def validate_time_parameters(request_fields: dict):
102103
if duration > max_duration:
103104
return jsonify({"error": f"Timebox duration must not exceed {max_hours:.0f} hours"})
104105

105-
# endUtc must not be in the future
106-
if end_utc > now:
107-
return jsonify({"error": "endUtc must not be later than the current time"})
106+
# endUtc must be at least 5 minutes in the past — the algorithm enters a live
107+
# polling loop if endUtc is too close to the current time, causing jobs to run
108+
# indefinitely instead of processing a closed historical window.
109+
if end_utc > now - timedelta(minutes=5):
110+
return jsonify({"error": "endUtc must be at least 5 minutes in the past"})
108111

109112
return None

backend/app.py

Lines changed: 4 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
1-
import os
21
import redis
3-
from flask import Flask, jsonify, request, send_file, abort
2+
from flask import Flask, jsonify, request
43
from apis.stations import list_stations_api
54
from apis.run_request import send_job_to_redis_queue
65
from apis.status import get_job_status
7-
from apis.retrieve_frames import get_frames
6+
from apis.retrieve_frames import get_frame
87

98
app = Flask(__name__)
109

@@ -38,22 +37,9 @@ def run_endpoint():
3837

3938
# Frame Data API
4039
@app.route("/apis/jobs/<job_id>/frames/<int:index>", methods=["GET"])
41-
def get_frame(job_id, index):
40+
def get_frame_endpoint(job_id, index):
4241
"""Takes job ID and frame index, returns a single GeoTIFF file."""
43-
44-
job_dir = "/processed_data/" + job_id
45-
if not os.path.exists(job_dir):
46-
abort(404, description="Job not found")
47-
48-
frame_path = job_dir + f"/frame_{index}.tif"
49-
if not os.path.exists(frame_path):
50-
abort(404, description="Frame not found")
51-
52-
return send_file(
53-
frame_path,
54-
mimetype="image/tiff",
55-
as_attachment=False
56-
)
42+
return get_frame(job_id, index)
5743

5844

5945
# Job Status API

nfgda_service/Dockerfile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,7 @@ RUN pip install --no-cache-dir -r requirements.txt
2323

2424
COPY . /app
2525

26+
# algorithm modules (nfgda/, python/, scripts/) now live under /app/algorithm
27+
ENV PYTHONPATH="/app/algorithm"
28+
2629
CMD ["python", "main.py"]
File renamed without changes.
File renamed without changes.
File renamed without changes.

nfgda_service/matlab/NF08_making_training_dataset.m renamed to nfgda_service/algorithm/matlab/NF08_making_training_dataset.m

File renamed without changes.
File renamed without changes.

0 commit comments

Comments
 (0)