Skip to content

Commit 611287f

Browse files
committed
feat: v1.6.0 skip already-processed WiGLE uploads
Daily --from-wigle re-downloaded the same upload every night; WiGLE regenerates each CSV server-side (minutes for a large upload), so this wasted time and quota. Now records each successfully pushed transid in ~/.config/wigle-to-wdgwars/processed-transids.json and skips re-pulling it. Recorded only on a real (non-dry-run) success; --reprocess forces. Adds tests/test_processed_skip.py (7 cases).
1 parent 6daaccf commit 611287f

3 files changed

Lines changed: 171 additions & 7 deletions

File tree

CHANGELOG.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,26 @@ All notable changes to wigle-to-wdgwars are documented here. Format
44
follows [Keep a Changelog](https://keepachangelog.com/) and the
55
project uses [Semantic Versioning](https://semver.org/).
66

7+
## [1.6.0] - 2026-06-15 - Skip already-processed uploads
8+
9+
Stops the daily `--from-wigle` pull from re-downloading uploads it has
10+
already pushed. WiGLE regenerates each CSV server-side (minutes for a
11+
large upload), so re-pulling one that's already on WDGoWars wasted both
12+
time and quota every night.
13+
14+
### Added
15+
16+
- Persistent processed-transid state at
17+
`~/.config/wigle-to-wdgwars/processed-transids.json`. Each WiGLE upload
18+
is recorded only after a real (non-dry-run) successful push, so failed
19+
uploads and dry-runs are retried.
20+
- `--reprocess` flag to re-pull uploads even if already recorded.
21+
22+
### Changed
23+
24+
- `--from-wigle` now skips already-processed uploads before downloading;
25+
if nothing is new it exits early without hitting WiGLE's slow CSV export.
26+
727
## [1.5.1] - 2026-06-15 - Survive slow WiGLE CSV exports
828

929
### Fixed

tests/test_processed_skip.py

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
"""Tests for the processed-transid skip gate introduced in v1.6.0.
2+
3+
The daily --from-wigle pull records each successfully pushed WiGLE upload
4+
in a state file and skips re-downloading it on the next run (WiGLE
5+
regenerates each CSV server-side, so re-pulling one already on WDGoWars
6+
wastes minutes). A transid is recorded only on a real, non-dry-run
7+
success; --reprocess ignores the state entirely.
8+
"""
9+
from __future__ import annotations
10+
11+
import json
12+
import tempfile
13+
import unittest
14+
from pathlib import Path
15+
from unittest import mock
16+
17+
import wigle_to_wdgwars as w2w
18+
19+
20+
class ProcessedTransidTests(unittest.TestCase):
21+
def setUp(self):
22+
self._tmp = tempfile.TemporaryDirectory()
23+
self.state = Path(self._tmp.name) / "processed-transids.json"
24+
self._patch = mock.patch.object(w2w, "PROCESSED_FILE", self.state)
25+
self._patch.start()
26+
27+
def tearDown(self):
28+
self._patch.stop()
29+
self._tmp.cleanup()
30+
31+
def test_load_empty_when_missing(self):
32+
self.assertEqual(w2w._load_processed_transids(), set())
33+
34+
def test_mark_then_load_roundtrip(self):
35+
w2w._mark_transid_processed("20260612-01443")
36+
w2w._mark_transid_processed("20260613-00001")
37+
self.assertEqual(w2w._load_processed_transids(),
38+
{"20260612-01443", "20260613-00001"})
39+
data = json.loads(self.state.read_text())
40+
self.assertEqual(data["processed"],
41+
["20260612-01443", "20260613-00001"])
42+
43+
def test_skips_already_processed_without_downloading(self):
44+
w2w._mark_transid_processed("T1")
45+
with mock.patch.object(w2w, "wigle_list_transactions", return_value=["T1"]), \
46+
mock.patch.object(w2w, "wigle_download_csv") as dl, \
47+
mock.patch.object(w2w, "upload_csv_bytes") as up:
48+
rc = w2w.pull_from_wigle_push_to_wdgwars(
49+
"tok", "key", "file", latest=1, dry_run=False,
50+
chunk_rows=10000, cooldown_sec=0)
51+
self.assertEqual(rc, 0)
52+
dl.assert_not_called()
53+
up.assert_not_called()
54+
55+
def test_marks_on_real_success(self):
56+
with mock.patch.object(w2w, "wigle_list_transactions", return_value=["T2"]), \
57+
mock.patch.object(w2w, "wigle_download_csv", return_value=b"x"), \
58+
mock.patch.object(w2w, "upload_csv_bytes", return_value=0):
59+
rc = w2w.pull_from_wigle_push_to_wdgwars(
60+
"tok", "key", "file", latest=1, dry_run=False,
61+
chunk_rows=10000, cooldown_sec=0)
62+
self.assertEqual(rc, 0)
63+
self.assertIn("T2", w2w._load_processed_transids())
64+
65+
def test_dry_run_does_not_mark(self):
66+
with mock.patch.object(w2w, "wigle_list_transactions", return_value=["T3"]), \
67+
mock.patch.object(w2w, "wigle_download_csv", return_value=b"x"), \
68+
mock.patch.object(w2w, "upload_csv_bytes", return_value=0):
69+
w2w.pull_from_wigle_push_to_wdgwars(
70+
"tok", "key", "file", latest=1, dry_run=True,
71+
chunk_rows=10000, cooldown_sec=0)
72+
self.assertNotIn("T3", w2w._load_processed_transids())
73+
74+
def test_failed_upload_not_marked(self):
75+
with mock.patch.object(w2w, "wigle_list_transactions", return_value=["T4"]), \
76+
mock.patch.object(w2w, "wigle_download_csv", return_value=b"x"), \
77+
mock.patch.object(w2w, "upload_csv_bytes", return_value=1):
78+
rc = w2w.pull_from_wigle_push_to_wdgwars(
79+
"tok", "key", "file", latest=1, dry_run=False,
80+
chunk_rows=10000, cooldown_sec=0)
81+
self.assertEqual(rc, 1)
82+
self.assertNotIn("T4", w2w._load_processed_transids())
83+
84+
def test_reprocess_ignores_state(self):
85+
w2w._mark_transid_processed("T5")
86+
with mock.patch.object(w2w, "wigle_list_transactions", return_value=["T5"]), \
87+
mock.patch.object(w2w, "wigle_download_csv", return_value=b"x") as dl, \
88+
mock.patch.object(w2w, "upload_csv_bytes", return_value=0):
89+
w2w.pull_from_wigle_push_to_wdgwars(
90+
"tok", "key", "file", latest=1, dry_run=False,
91+
chunk_rows=10000, cooldown_sec=0, reprocess=True)
92+
dl.assert_called_once()
93+
94+
95+
if __name__ == "__main__":
96+
unittest.main()

wigle_to_wdgwars.py

Lines changed: 55 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
"""
3535
from __future__ import annotations
3636

37-
__version__ = "1.5.1"
37+
__version__ = "1.6.0"
3838
GITHUB_REPO = "HiroAlleyCat/wigle-to-wdgwars"
3939
GITHUB_URL = f"https://github.com/{GITHUB_REPO}"
4040

@@ -102,6 +102,7 @@
102102
WIGLE_KEY_FILE = CONFIG_DIR / "wigle.key"
103103
COOLDOWN_FILE = CONFIG_DIR / "cooldown.json"
104104
HWM_FILE = CONFIG_DIR / "hwm.json"
105+
PROCESSED_FILE = CONFIG_DIR / "processed-transids.json"
105106

106107
# ───────────────────────────── Self-update / version check ──────────────────
107108
#
@@ -1264,29 +1265,73 @@ def wigle_download_csv(token: str, transid: str) -> bytes:
12641265
sys.exit(f"[wigle] CSV download for {transid} timed out after retries: {last_err}")
12651266

12661267

1268+
def _load_processed_transids() -> set:
1269+
"""Transids already pushed to WDGoWars (persisted in PROCESSED_FILE).
1270+
1271+
Lets the daily pull skip re-downloading uploads it has already sent.
1272+
WiGLE regenerates each CSV server-side (minutes for a big upload), so
1273+
re-pulling one that's already on WDGoWars is pure waste."""
1274+
try:
1275+
data = json.loads(PROCESSED_FILE.read_text())
1276+
return set(data.get("processed", []))
1277+
except Exception:
1278+
return set()
1279+
1280+
1281+
def _mark_transid_processed(transid: str) -> None:
1282+
"""Record a transid as successfully pushed. Best-effort; never raises."""
1283+
try:
1284+
seen = _load_processed_transids()
1285+
seen.add(transid)
1286+
PROCESSED_FILE.parent.mkdir(parents=True, exist_ok=True)
1287+
PROCESSED_FILE.write_text(json.dumps({"processed": sorted(seen)}, indent=2))
1288+
except Exception as e:
1289+
print(f"[wigle] warning: could not record {transid} as processed: {e}",
1290+
file=sys.stderr)
1291+
1292+
12671293
def pull_from_wigle_push_to_wdgwars(wigle_token: str, wdg_key: str, field: str,
12681294
latest: int, dry_run: bool, chunk_rows: int,
12691295
cooldown_sec: float,
1270-
since_seconds: int = 0) -> int:
1296+
since_seconds: int = 0,
1297+
reprocess: bool = False) -> int:
12711298
"""Pull your latest WiGLE upload(s) and push each to WDGoWars.
12721299
12731300
``since_seconds > 0`` drops rows older than the cutoff from each
1274-
downloaded WiGLE CSV before uploading."""
1301+
downloaded WiGLE CSV before uploading.
1302+
1303+
Uploads already recorded in PROCESSED_FILE are skipped (no re-download)
1304+
unless ``reprocess`` is set. A transid is recorded only after a real
1305+
(non-dry-run) successful push, so failed uploads and dry-runs are retried."""
12751306
transids = wigle_list_transactions(wigle_token, latest)
12761307
if not transids:
12771308
print("[wigle] no uploads found on your account", file=sys.stderr)
12781309
return 0
1279-
print(f"[wigle] pulling {len(transids)} most-recent upload(s): "
1280-
f"{', '.join(transids)}", file=sys.stderr)
1310+
1311+
processed = set() if reprocess else _load_processed_transids()
1312+
pending = [t for t in transids if t not in processed]
1313+
skipped = [t for t in transids if t in processed]
1314+
if skipped:
1315+
print(f"[wigle] skipping {len(skipped)} already-processed upload(s): "
1316+
f"{', '.join(skipped)} (use --reprocess to force)", file=sys.stderr)
1317+
if not pending:
1318+
print("[wigle] nothing new to pull — all recent uploads already processed",
1319+
file=sys.stderr)
1320+
return 0
1321+
1322+
print(f"[wigle] pulling {len(pending)} upload(s): "
1323+
f"{', '.join(pending)}", file=sys.stderr)
12811324
rc = 0
1282-
for tid in transids:
1325+
for tid in pending:
12831326
csv_bytes = wigle_download_csv(wigle_token, tid)
12841327
print(f"[wigle] {tid}: {len(csv_bytes) / 1024:.1f} KB -> WDGoWars",
12851328
file=sys.stderr)
12861329
r = upload_csv_bytes(csv_bytes, f"{tid}.csv", wdg_key, field,
12871330
dry_run, chunk_rows, cooldown_sec,
12881331
since_seconds=since_seconds)
12891332
rc = rc or r
1333+
if r == 0 and not dry_run:
1334+
_mark_transid_processed(tid)
12901335
return rc
12911336

12921337

@@ -1843,6 +1888,9 @@ def main() -> int:
18431888
ap.add_argument("--wigle-latest", type=int, default=1, metavar="N",
18441889
help="with --from-wigle, how many most-recent WiGLE uploads to pull "
18451890
"(default: 1)")
1891+
ap.add_argument("--reprocess", action="store_true",
1892+
help="with --from-wigle, re-pull uploads even if already recorded "
1893+
"as processed (ignores the processed-transids state file)")
18461894
ap.add_argument("--setup", action="store_true",
18471895
help="interactive first-time setup — prompts for your "
18481896
"WDGoWars and WiGLE keys, validates them, saves them "
@@ -1944,7 +1992,7 @@ def main() -> int:
19441992
return pull_from_wigle_push_to_wdgwars(
19451993
wigle_token, key, args.field, args.wigle_latest,
19461994
args.dry_run, args.chunk_size, args.chunk_cooldown,
1947-
since_seconds=since_seconds)
1995+
since_seconds=since_seconds, reprocess=args.reprocess)
19481996

19491997
if args.aircraft_json:
19501998
return upload_aircraft_json(Path(args.aircraft_json), key,

0 commit comments

Comments
 (0)