Spaces:
Running
Running
fix(backend): use asyncio.to_thread for stats IO, guard malformed JSON, isolate test side-effects
Browse files- main.py +14 -6
- tests/test_main.py +1 -0
main.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
|
|
| 1 |
import json
|
| 2 |
import threading
|
| 3 |
import uuid
|
|
@@ -21,8 +22,11 @@ _stats_lock = threading.Lock()
|
|
| 21 |
def _read_stats() -> dict:
|
| 22 |
if not STATS_FILE.exists():
|
| 23 |
return {"total_visits": 0, "total_downloads": 0}
|
| 24 |
-
|
| 25 |
-
|
|
|
|
|
|
|
|
|
|
| 26 |
|
| 27 |
|
| 28 |
def _write_stats(stats: dict) -> None:
|
|
@@ -70,15 +74,19 @@ async def health():
|
|
| 70 |
return {"status": "ok"}
|
| 71 |
|
| 72 |
|
| 73 |
-
|
| 74 |
-
async def get_stats():
|
| 75 |
with _stats_lock:
|
| 76 |
return _read_stats()
|
| 77 |
|
| 78 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 79 |
@app.post("/stats/visit", status_code=204)
|
| 80 |
async def record_visit():
|
| 81 |
-
|
| 82 |
|
| 83 |
|
| 84 |
@app.get("/status/{job_id}", response_model=JobStatus)
|
|
@@ -135,7 +143,7 @@ async def download(job_id: str):
|
|
| 135 |
if not output_file.exists():
|
| 136 |
raise HTTPException(status_code=404, detail="Output file not found")
|
| 137 |
|
| 138 |
-
|
| 139 |
|
| 140 |
return FileResponse(
|
| 141 |
path=output_file,
|
|
|
|
| 1 |
+
import asyncio
|
| 2 |
import json
|
| 3 |
import threading
|
| 4 |
import uuid
|
|
|
|
| 22 |
def _read_stats() -> dict:
|
| 23 |
if not STATS_FILE.exists():
|
| 24 |
return {"total_visits": 0, "total_downloads": 0}
|
| 25 |
+
try:
|
| 26 |
+
with open(STATS_FILE, "r") as f:
|
| 27 |
+
return json.load(f)
|
| 28 |
+
except (json.JSONDecodeError, OSError):
|
| 29 |
+
return {"total_visits": 0, "total_downloads": 0}
|
| 30 |
|
| 31 |
|
| 32 |
def _write_stats(stats: dict) -> None:
|
|
|
|
| 74 |
return {"status": "ok"}
|
| 75 |
|
| 76 |
|
| 77 |
+
def _read_stats_locked() -> dict:
|
|
|
|
| 78 |
with _stats_lock:
|
| 79 |
return _read_stats()
|
| 80 |
|
| 81 |
|
| 82 |
+
@app.get("/stats")
|
| 83 |
+
async def get_stats():
|
| 84 |
+
return await asyncio.to_thread(_read_stats_locked)
|
| 85 |
+
|
| 86 |
+
|
| 87 |
@app.post("/stats/visit", status_code=204)
|
| 88 |
async def record_visit():
|
| 89 |
+
await asyncio.to_thread(_increment_stat, "total_visits")
|
| 90 |
|
| 91 |
|
| 92 |
@app.get("/status/{job_id}", response_model=JobStatus)
|
|
|
|
| 143 |
if not output_file.exists():
|
| 144 |
raise HTTPException(status_code=404, detail="Output file not found")
|
| 145 |
|
| 146 |
+
await asyncio.to_thread(_increment_stat, "total_downloads")
|
| 147 |
|
| 148 |
return FileResponse(
|
| 149 |
path=output_file,
|
tests/test_main.py
CHANGED
|
@@ -97,6 +97,7 @@ async def test_download_done_job(client, tmp_path, monkeypatch):
|
|
| 97 |
fake_status = JobStatus(job_id=job_id, state=JobState.done, progress=100)
|
| 98 |
monkeypatch.setitem(processor._jobs, job_id, fake_status)
|
| 99 |
monkeypatch.setattr(main, "OUTPUT_DIR", tmp_path)
|
|
|
|
| 100 |
|
| 101 |
response = await client.get(f"/download/{job_id}")
|
| 102 |
assert response.status_code == 200
|
|
|
|
| 97 |
fake_status = JobStatus(job_id=job_id, state=JobState.done, progress=100)
|
| 98 |
monkeypatch.setitem(processor._jobs, job_id, fake_status)
|
| 99 |
monkeypatch.setattr(main, "OUTPUT_DIR", tmp_path)
|
| 100 |
+
monkeypatch.setattr(main, "STATS_FILE", tmp_path / "stats.json")
|
| 101 |
|
| 102 |
response = await client.get(f"/download/{job_id}")
|
| 103 |
assert response.status_code == 200
|