osmsg 1.2.2__tar.gz → 1.2.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. {osmsg-1.2.2 → osmsg-1.2.4}/PKG-INFO +6 -4
  2. {osmsg-1.2.2 → osmsg-1.2.4}/README.md +5 -3
  3. osmsg-1.2.4/osmsg/__version__.py +1 -0
  4. {osmsg-1.2.2 → osmsg-1.2.4}/osmsg/_http.py +7 -3
  5. {osmsg-1.2.2 → osmsg-1.2.4}/osmsg/gui.py +74 -12
  6. {osmsg-1.2.2 → osmsg-1.2.4}/osmsg/pipeline.py +26 -6
  7. {osmsg-1.2.2 → osmsg-1.2.4}/pyproject.toml +1 -1
  8. osmsg-1.2.2/osmsg/__version__.py +0 -1
  9. {osmsg-1.2.2 → osmsg-1.2.4}/LICENSE +0 -0
  10. {osmsg-1.2.2 → osmsg-1.2.4}/osmsg/__init__.py +0 -0
  11. {osmsg-1.2.2 → osmsg-1.2.4}/osmsg/_tick.py +0 -0
  12. {osmsg-1.2.2 → osmsg-1.2.4}/osmsg/auth.py +0 -0
  13. {osmsg-1.2.2 → osmsg-1.2.4}/osmsg/boundary.py +0 -0
  14. {osmsg-1.2.2 → osmsg-1.2.4}/osmsg/cli.py +0 -0
  15. {osmsg-1.2.2 → osmsg-1.2.4}/osmsg/db/__init__.py +0 -0
  16. {osmsg-1.2.2 → osmsg-1.2.4}/osmsg/db/duckdb_schema.py +0 -0
  17. {osmsg-1.2.2 → osmsg-1.2.4}/osmsg/db/ingest.py +0 -0
  18. {osmsg-1.2.2 → osmsg-1.2.4}/osmsg/db/queries.py +0 -0
  19. {osmsg-1.2.2 → osmsg-1.2.4}/osmsg/db/schema.py +0 -0
  20. {osmsg-1.2.2 → osmsg-1.2.4}/osmsg/exceptions.py +0 -0
  21. {osmsg-1.2.2 → osmsg-1.2.4}/osmsg/export/__init__.py +0 -0
  22. {osmsg-1.2.2 → osmsg-1.2.4}/osmsg/export/csv.py +0 -0
  23. {osmsg-1.2.2 → osmsg-1.2.4}/osmsg/export/json.py +0 -0
  24. {osmsg-1.2.2 → osmsg-1.2.4}/osmsg/export/markdown.py +0 -0
  25. {osmsg-1.2.2 → osmsg-1.2.4}/osmsg/export/parquet.py +0 -0
  26. {osmsg-1.2.2 → osmsg-1.2.4}/osmsg/export/psql.py +0 -0
  27. {osmsg-1.2.2 → osmsg-1.2.4}/osmsg/fetch.py +0 -0
  28. {osmsg-1.2.2 → osmsg-1.2.4}/osmsg/geofabrik.py +0 -0
  29. {osmsg-1.2.2 → osmsg-1.2.4}/osmsg/handlers.py +0 -0
  30. {osmsg-1.2.2 → osmsg-1.2.4}/osmsg/history.py +0 -0
  31. {osmsg-1.2.2 → osmsg-1.2.4}/osmsg/maintain/__init__.py +0 -0
  32. {osmsg-1.2.2 → osmsg-1.2.4}/osmsg/maintain/cli.py +0 -0
  33. {osmsg-1.2.2 → osmsg-1.2.4}/osmsg/maintain/convert.py +0 -0
  34. {osmsg-1.2.2 → osmsg-1.2.4}/osmsg/maintain/manifest.py +0 -0
  35. {osmsg-1.2.2 → osmsg-1.2.4}/osmsg/maintain/month.py +0 -0
  36. {osmsg-1.2.2 → osmsg-1.2.4}/osmsg/maintain/parquet.py +0 -0
  37. {osmsg-1.2.2 → osmsg-1.2.4}/osmsg/maintain/pbf_split.py +0 -0
  38. {osmsg-1.2.2 → osmsg-1.2.4}/osmsg/models.py +0 -0
  39. {osmsg-1.2.2 → osmsg-1.2.4}/osmsg/pg_schema.py +0 -0
  40. {osmsg-1.2.2 → osmsg-1.2.4}/osmsg/py.typed +0 -0
  41. {osmsg-1.2.2 → osmsg-1.2.4}/osmsg/replication.py +0 -0
  42. {osmsg-1.2.2 → osmsg-1.2.4}/osmsg/tm.py +0 -0
  43. {osmsg-1.2.2 → osmsg-1.2.4}/osmsg/ui.py +0 -0
  44. {osmsg-1.2.2 → osmsg-1.2.4}/osmsg/workers.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: osmsg
3
- Version: 1.2.2
3
+ Version: 1.2.4
4
4
  Summary: OpenStreetMap Stats Generator: Commandline
5
5
  Keywords: osm,stats,commandline,openstreetmap
6
6
  Author: Kshitij Raj Sharma
@@ -78,8 +78,9 @@ brew install osgeonepal/tap/osmsg # macOS / Linux (Homebrew tap)
78
78
  ```
79
79
 
80
80
  On Windows, download `osmsg.exe` from the [latest release](https://github.com/osgeonepal/osmsg/releases)
81
- and double-click it to open the desktop app. Fill in the dates and options, click Run, and open the
82
- output folder. The CLI below is for macOS, Linux, and pip/uv users.
81
+ and double-click it to open the desktop app. Pick a Quick range (last hour, day, week, month, year, or
82
+ all time) or type your own dates, set the options, click Compute, and open the output folder. The CLI
83
+ below is for macOS, Linux, and pip/uv users.
83
84
 
84
85
  ## Quick start
85
86
 
@@ -207,7 +208,8 @@ Same pipeline as the CLI.
207
208
  osmsg --config nepal.yaml
208
209
  ```
209
210
 
210
- Any flag works as a YAML key. See [docs/Manual.md](./docs/Manual.md) for the full list.
211
+ Each option is a YAML key written with its underscore name: `output_dir`, `history_url`, `all_stats`,
212
+ `formats`, `psql_dsn`, and so on (not the dashed flag). See [docs/Manual.md](./docs/Manual.md).
211
213
 
212
214
  ## Output formats
213
215
 
@@ -46,8 +46,9 @@ brew install osgeonepal/tap/osmsg # macOS / Linux (Homebrew tap)
46
46
  ```
47
47
 
48
48
  On Windows, download `osmsg.exe` from the [latest release](https://github.com/osgeonepal/osmsg/releases)
49
- and double-click it to open the desktop app. Fill in the dates and options, click Run, and open the
50
- output folder. The CLI below is for macOS, Linux, and pip/uv users.
49
+ and double-click it to open the desktop app. Pick a Quick range (last hour, day, week, month, year, or
50
+ all time) or type your own dates, set the options, click Compute, and open the output folder. The CLI
51
+ below is for macOS, Linux, and pip/uv users.
51
52
 
52
53
  ## Quick start
53
54
 
@@ -175,7 +176,8 @@ Same pipeline as the CLI.
175
176
  osmsg --config nepal.yaml
176
177
  ```
177
178
 
178
- Any flag works as a YAML key. See [docs/Manual.md](./docs/Manual.md) for the full list.
179
+ Each option is a YAML key written with its underscore name: `output_dir`, `history_url`, `all_stats`,
180
+ `formats`, `psql_dsn`, and so on (not the dashed flag). See [docs/Manual.md](./docs/Manual.md).
179
181
 
180
182
  ## Output formats
181
183
 
@@ -0,0 +1 @@
1
+ __version__ = "1.2.4"
@@ -11,7 +11,7 @@ from requests.adapters import HTTPAdapter
11
11
  from urllib3.util.retry import Retry
12
12
 
13
13
  USER_AGENT = "osmsg"
14
- DEFAULT_TIMEOUT = (10, 60) # (connect, read) seconds
14
+ DEFAULT_TIMEOUT = (30, 120) # (connect, read) seconds
15
15
 
16
16
 
17
17
  class _TimeoutSession(requests.Session):
@@ -26,10 +26,14 @@ def make_session() -> requests.Session:
26
26
  """Fresh session with the standard timeout + retry policy (use when a flow needs its own cookie jar)."""
27
27
  s = _TimeoutSession()
28
28
  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,
31
34
  status_forcelist=(429, 500, 502, 503, 504),
32
35
  allowed_methods=frozenset({"GET", "POST", "HEAD"}),
36
+ respect_retry_after_header=True,
33
37
  )
34
38
  adapter = HTTPAdapter(max_retries=retry, pool_maxsize=32)
35
39
  s.mount("https://", adapter)
@@ -7,14 +7,41 @@ import os
7
7
  import queue
8
8
  import sys
9
9
  import threading
10
+ import webbrowser
10
11
  from pathlib import Path
11
12
  from typing import Any
12
13
 
14
+ from .__version__ import __version__
13
15
  from .exceptions import NoDataFoundError, OsmsgError
14
16
  from .pipeline import RunConfig, run
15
17
 
16
18
  UTC = dt.UTC
17
19
  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
+ ]
25
+ PRESETS = ["Last hour", "Last day", "Last week", "Last month", "Last year", "All time"]
26
+ _PRESET_DELTAS = {
27
+ "Last hour": dt.timedelta(hours=1),
28
+ "Last day": dt.timedelta(days=1),
29
+ "Last week": dt.timedelta(days=7),
30
+ "Last month": dt.timedelta(days=30),
31
+ "Last year": dt.timedelta(days=365),
32
+ }
33
+
34
+
35
+ def preset_range(name: str, now: dt.datetime | None = None) -> tuple[dt.datetime, dt.datetime]:
36
+ """Resolve a quick-range label to a (start, end) window."""
37
+ now = now or dt.datetime.now(UTC)
38
+ if name == "All time":
39
+ return dt.datetime(2005, 1, 1, tzinfo=UTC), now
40
+ return now - _PRESET_DELTAS[name], now
41
+
42
+
43
+ def _fmt(when: dt.datetime) -> str:
44
+ return when.strftime("%Y-%m-%d %H:%M:%S")
18
45
 
19
46
 
20
47
  def _parse_date(value: str) -> dt.datetime | None:
@@ -89,6 +116,7 @@ class App:
89
116
  from tkinter import filedialog, scrolledtext, ttk
90
117
 
91
118
  self._tk = tk
119
+ self._ttk = ttk
92
120
  self._filedialog = filedialog
93
121
  self.events: queue.Queue = queue.Queue()
94
122
  self.out_dir = str(Path.home() / "osmsg")
@@ -112,31 +140,63 @@ class App:
112
140
  ttk.Entry(frame, textvariable=var, width=40).grid(row=i, column=1, columnspan=3, sticky="we", pady=2)
113
141
  self.vars[key] = var
114
142
 
143
+ preset_frame = ttk.LabelFrame(frame, text="Quick range", padding=6)
144
+ preset_frame.grid(row=5, column=0, columnspan=4, sticky="we", pady=6)
145
+ for i, name in enumerate(PRESETS):
146
+ ttk.Button(preset_frame, text=name, width=11, command=lambda n=name: self._apply_preset(n)).grid(
147
+ row=0, column=i, padx=2
148
+ )
149
+
115
150
  self.vars["all_tags"] = tk.BooleanVar()
116
151
  self.vars["summary"] = tk.BooleanVar()
117
- ttk.Checkbutton(frame, text="All tags", variable=self.vars["all_tags"]).grid(row=5, column=0, sticky="w")
118
- ttk.Checkbutton(frame, text="Daily summary", variable=self.vars["summary"]).grid(row=5, column=1, sticky="w")
152
+ ttk.Checkbutton(frame, text="All tags", variable=self.vars["all_tags"]).grid(row=6, column=0, sticky="w")
153
+ ttk.Checkbutton(frame, text="Daily summary", variable=self.vars["summary"]).grid(row=6, column=1, sticky="w")
119
154
 
120
155
  fmt_frame = ttk.LabelFrame(frame, text="Formats", padding=6)
121
- fmt_frame.grid(row=6, column=0, columnspan=4, sticky="we", pady=6)
156
+ fmt_frame.grid(row=7, column=0, columnspan=4, sticky="we", pady=6)
122
157
  for i, name in enumerate(FORMATS):
123
158
  var = tk.BooleanVar(value=name in ("parquet", "csv"))
124
159
  ttk.Checkbutton(fmt_frame, text=name, variable=var).grid(row=0, column=i, padx=4)
125
160
  self.vars[name] = var
126
161
 
127
162
  self.out_label = ttk.Label(frame, text=f"Output: {self.out_dir}")
128
- self.out_label.grid(row=7, column=0, columnspan=3, sticky="w")
129
- ttk.Button(frame, text="Choose folder", command=self._choose_folder).grid(row=7, column=3, sticky="e")
163
+ self.out_label.grid(row=8, column=0, columnspan=3, sticky="w")
164
+ ttk.Button(frame, text="Choose folder", command=self._choose_folder).grid(row=8, column=3, sticky="e")
130
165
 
131
- self.run_btn = ttk.Button(frame, text="Run", command=self._on_run)
132
- self.run_btn.grid(row=8, column=0, pady=8, sticky="w")
166
+ self.run_btn = ttk.Button(frame, text="Compute", command=self._on_run)
167
+ self.run_btn.grid(row=9, column=0, pady=8, sticky="w")
133
168
  self.open_btn = ttk.Button(frame, text="Open output folder", command=lambda: _open_folder(Path(self.out_dir)))
134
- self.open_btn.grid(row=8, column=1, pady=8, sticky="w")
169
+ self.open_btn.grid(row=9, column=1, pady=8, sticky="w")
170
+ self.spinner = ttk.Progressbar(frame, mode="indeterminate", length=160)
171
+ self.spinner.grid(row=9, column=2, columnspan=2, pady=8, sticky="we")
135
172
 
136
173
  self.log = scrolledtext.ScrolledText(frame, width=70, height=14, state="disabled")
137
- self.log.grid(row=9, column=0, columnspan=4, sticky="nsew")
174
+ 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")
138
178
  self.root.after(120, self._drain)
139
179
 
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
+
195
+ def _apply_preset(self, name: str) -> None:
196
+ start, end = preset_range(name)
197
+ self.vars["start"].set(_fmt(start))
198
+ self.vars["end"].set(_fmt(end))
199
+
140
200
  def _choose_folder(self) -> None:
141
201
  chosen = self._filedialog.askdirectory(initialdir=self.out_dir)
142
202
  if chosen:
@@ -155,8 +215,9 @@ class App:
155
215
  except OsmsgError as exc:
156
216
  self._append(f"\n{exc}\n")
157
217
  return
158
- self.run_btn.config(state="disabled")
159
- self._append(f"\nRunning into {self.out_dir} ...\n")
218
+ self.run_btn.config(state="disabled", text="Running...")
219
+ self.spinner.start(12)
220
+ self._append(f"\nComputing into {self.out_dir} ...\n")
160
221
  threading.Thread(target=self._worker, args=(cfg,), daemon=True).start()
161
222
 
162
223
  def _worker(self, cfg: RunConfig) -> None:
@@ -182,7 +243,8 @@ class App:
182
243
  self._append(payload)
183
244
  else:
184
245
  self._append(f"\n{payload}\n")
185
- self.run_btn.config(state="normal")
246
+ self.spinner.stop()
247
+ self.run_btn.config(state="normal", text="Compute")
186
248
  except queue.Empty:
187
249
  pass
188
250
  self.root.after(120, self._drain)
@@ -14,6 +14,7 @@ from pathlib import Path
14
14
  from typing import Any
15
15
 
16
16
  import duckdb
17
+ import requests
17
18
  from platformdirs import user_cache_dir
18
19
  from shapely.ops import unary_union
19
20
 
@@ -539,6 +540,11 @@ def _processing_config(cfg: RunConfig, *, parquet_dir: Path, geom_wkt: str | Non
539
540
  }
540
541
 
541
542
 
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
+
542
548
  def _download_all(
543
549
  urls: list[str],
544
550
  mode: str,
@@ -548,12 +554,19 @@ def _download_all(
548
554
  label: str,
549
555
  description: str = "downloading",
550
556
  ) -> 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
557
570
 
558
571
 
559
572
  def _process_all(
@@ -727,6 +740,13 @@ def run(cfg: RunConfig) -> dict[str, Any]:
727
740
  else f"first run with {cfg.changeset_pad_hours}h backward pad"
728
741
  )
729
742
  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
+ )
730
750
 
731
751
  cs_frontier_ts = cs_repl.sequence_to_timestamp(cs_end)
732
752
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "osmsg"
3
- version = "1.2.2"
3
+ version = "1.2.4"
4
4
  description = "OpenStreetMap Stats Generator: Commandline"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -1 +0,0 @@
1
- __version__ = "1.2.2"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes