Skip to content

Commit ad3bd4c

Browse files
fix(download): fix the concurrency on osmsg
add the about section too
1 parent 6b5a363 commit ad3bd4c

5 files changed

Lines changed: 95 additions & 10 deletions

File tree

osmsg/_http.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from urllib3.util.retry import Retry
1212

1313
USER_AGENT = "osmsg"
14-
DEFAULT_TIMEOUT = (10, 60) # (connect, read) seconds
14+
DEFAULT_TIMEOUT = (30, 120) # (connect, read) seconds
1515

1616

1717
class _TimeoutSession(requests.Session):
@@ -26,10 +26,14 @@ def make_session() -> requests.Session:
2626
"""Fresh session with the standard timeout + retry policy (use when a flow needs its own cookie jar)."""
2727
s = _TimeoutSession()
2828
retry = Retry(
29-
total=5,
30-
backoff_factor=0.5,
29+
total=10,
30+
connect=10,
31+
read=10,
32+
backoff_factor=1.0,
33+
backoff_max=120,
3134
status_forcelist=(429, 500, 502, 503, 504),
3235
allowed_methods=frozenset({"GET", "POST", "HEAD"}),
36+
respect_retry_after_header=True,
3337
)
3438
adapter = HTTPAdapter(max_retries=retry, pool_maxsize=32)
3539
s.mount("https://", adapter)

osmsg/gui.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,21 @@
77
import queue
88
import sys
99
import threading
10+
import webbrowser
1011
from pathlib import Path
1112
from typing import Any
1213

14+
from .__version__ import __version__
1315
from .exceptions import NoDataFoundError, OsmsgError
1416
from .pipeline import RunConfig, run
1517

1618
UTC = dt.UTC
1719
FORMATS = ["parquet", "csv", "json", "markdown"]
20+
ABOUT_LINKS = [
21+
("Star osmsg on GitHub", "https://github.com/osgeonepal/osmsg"),
22+
("Report a bug or request a feature", "https://github.com/osgeonepal/osmsg/issues"),
23+
("Sponsor the developer", "https://github.com/sponsors/kshitijrajsharma"),
24+
]
1825
PRESETS = ["Last hour", "Last day", "Last week", "Last month", "Last year", "All time"]
1926
_PRESET_DELTAS = {
2027
"Last hour": dt.timedelta(hours=1),
@@ -109,6 +116,7 @@ def __init__(self) -> None:
109116
from tkinter import filedialog, scrolledtext, ttk
110117

111118
self._tk = tk
119+
self._ttk = ttk
112120
self._filedialog = filedialog
113121
self.events: queue.Queue = queue.Queue()
114122
self.out_dir = str(Path.home() / "osmsg")
@@ -164,8 +172,26 @@ def __init__(self) -> None:
164172

165173
self.log = scrolledtext.ScrolledText(frame, width=70, height=14, state="disabled")
166174
self.log.grid(row=10, column=0, columnspan=4, sticky="nsew")
175+
176+
ttk.Button(frame, text="About", command=self._show_about).grid(row=11, column=0, pady=(6, 0), sticky="w")
177+
ttk.Label(frame, text="A project of OSGeo Nepal").grid(row=11, column=1, columnspan=3, pady=(6, 0), sticky="e")
167178
self.root.after(120, self._drain)
168179

180+
def _show_about(self) -> None:
181+
tk, ttk = self._tk, self._ttk
182+
win = tk.Toplevel(self.root)
183+
win.title("About osmsg")
184+
box = ttk.Frame(win, padding=16)
185+
box.grid(sticky="nsew")
186+
ttk.Label(box, text=f"osmsg {__version__}", font=("", 12, "bold")).grid(sticky="w")
187+
ttk.Label(box, text="OpenStreetMap Stats Generator").grid(sticky="w")
188+
ttk.Label(box, text="A project of OSGeo Nepal").grid(sticky="w", pady=(0, 10))
189+
for text, url in ABOUT_LINKS:
190+
link = ttk.Label(box, text=text, foreground="#1a73e8", cursor="hand2")
191+
link.grid(sticky="w", pady=2)
192+
link.bind("<Button-1>", lambda _event, target=url: webbrowser.open(target))
193+
ttk.Button(box, text="Close", command=win.destroy).grid(sticky="e", pady=(12, 0))
194+
169195
def _apply_preset(self, name: str) -> None:
170196
start, end = preset_range(name)
171197
self.vars["start"].set(_fmt(start))

osmsg/pipeline.py

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from typing import Any
1515

1616
import duckdb
17+
import requests
1718
from platformdirs import user_cache_dir
1819
from shapely.ops import unary_union
1920

@@ -539,6 +540,11 @@ def _processing_config(cfg: RunConfig, *, parquet_dir: Path, geom_wkt: str | Non
539540
}
540541

541542

543+
# Replication servers throttle many concurrent connections, so downloads stay polite regardless of
544+
# the worker count used for local parsing. Already-downloaded files are cached, so a rerun resumes.
545+
_DOWNLOAD_WORKERS = 4
546+
547+
542548
def _download_all(
543549
urls: list[str],
544550
mode: str,
@@ -548,12 +554,19 @@ def _download_all(
548554
label: str,
549555
description: str = "downloading",
550556
) -> None:
551-
with (
552-
progress_bar(len(urls), unit=label, description=description) as advance,
553-
concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as pool,
554-
):
555-
for _ in pool.map(lambda u: download_osm_file(u, mode=mode, cookie=cookie, cache_dir=cache_dir), urls):
556-
advance()
557+
workers = min(max_workers, _DOWNLOAD_WORKERS)
558+
try:
559+
with (
560+
progress_bar(len(urls), unit=label, description=description) as advance,
561+
concurrent.futures.ThreadPoolExecutor(max_workers=workers) as pool,
562+
):
563+
for _ in pool.map(lambda u: download_osm_file(u, mode=mode, cookie=cookie, cache_dir=cache_dir), urls):
564+
advance()
565+
except requests.exceptions.RequestException as exc:
566+
raise OsmsgError(
567+
f"Network error downloading {label} after retries ({type(exc).__name__}). "
568+
"Re-run to resume: finished downloads are cached, so it continues from where it stopped."
569+
) from exc
557570

558571

559572
def _process_all(
@@ -727,6 +740,13 @@ def run(cfg: RunConfig) -> dict[str, Any]:
727740
else f"first run with {cfg.changeset_pad_hours}h backward pad"
728741
)
729742
info(f"Changesets: {len(urls)} files (seq {cs_start}-{cs_end}), {pad_note}.")
743+
if len(urls) > 5000:
744+
warn(
745+
f"Hashtag/changeset filtering downloads the per-minute changeset stream for the live "
746+
f"tail ({len(urls):,} files here). This is slow over a busy network and resumes from "
747+
f"cache if interrupted; a shorter range or waiting for the dataset to cover more months "
748+
f"reduces it."
749+
)
730750

731751
cs_frontier_ts = cs_repl.sequence_to_timestamp(cs_end)
732752

tests/test_download_resilience.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
"""Downloads stay polite and a network failure becomes a clear, resumable error."""
2+
3+
4+
import pytest
5+
import requests
6+
7+
from osmsg import pipeline
8+
from osmsg.exceptions import OsmsgError
9+
10+
11+
def test_download_concurrency_is_capped():
12+
assert pipeline._DOWNLOAD_WORKERS <= 4
13+
14+
15+
def test_download_all_wraps_network_error(monkeypatch, tmp_path):
16+
def boom(url, **kwargs):
17+
raise requests.exceptions.ConnectTimeout("planet timed out")
18+
19+
monkeypatch.setattr(pipeline, "download_osm_file", boom)
20+
with pytest.raises(OsmsgError, match="Re-run to resume"):
21+
pipeline._download_all(
22+
["https://planet.openstreetmap.org/replication/changesets/007/035/882.osm.gz"],
23+
"changeset",
24+
8,
25+
None,
26+
tmp_path,
27+
"changesets",
28+
)

tests/test_gui.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import pytest
66

77
from osmsg.exceptions import OsmsgError
8-
from osmsg.gui import PRESETS, build_config, preset_range
8+
from osmsg.gui import ABOUT_LINKS, PRESETS, build_config, preset_range
99

1010
UTC = dt.UTC
1111
NOW = dt.datetime(2026, 6, 24, 12, 0, tzinfo=UTC)
@@ -68,3 +68,10 @@ def test_every_preset_resolves():
6868
for name in PRESETS:
6969
start, end = preset_range(name, NOW)
7070
assert start < end
71+
72+
73+
def test_about_links():
74+
urls = {url for _label, url in ABOUT_LINKS}
75+
assert "https://github.com/osgeonepal/osmsg" in urls
76+
assert "https://github.com/osgeonepal/osmsg/issues" in urls
77+
assert "https://github.com/sponsors/kshitijrajsharma" in urls

0 commit comments

Comments
 (0)