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.
- {osmsg-1.2.2 → osmsg-1.2.4}/PKG-INFO +6 -4
- {osmsg-1.2.2 → osmsg-1.2.4}/README.md +5 -3
- osmsg-1.2.4/osmsg/__version__.py +1 -0
- {osmsg-1.2.2 → osmsg-1.2.4}/osmsg/_http.py +7 -3
- {osmsg-1.2.2 → osmsg-1.2.4}/osmsg/gui.py +74 -12
- {osmsg-1.2.2 → osmsg-1.2.4}/osmsg/pipeline.py +26 -6
- {osmsg-1.2.2 → osmsg-1.2.4}/pyproject.toml +1 -1
- osmsg-1.2.2/osmsg/__version__.py +0 -1
- {osmsg-1.2.2 → osmsg-1.2.4}/LICENSE +0 -0
- {osmsg-1.2.2 → osmsg-1.2.4}/osmsg/__init__.py +0 -0
- {osmsg-1.2.2 → osmsg-1.2.4}/osmsg/_tick.py +0 -0
- {osmsg-1.2.2 → osmsg-1.2.4}/osmsg/auth.py +0 -0
- {osmsg-1.2.2 → osmsg-1.2.4}/osmsg/boundary.py +0 -0
- {osmsg-1.2.2 → osmsg-1.2.4}/osmsg/cli.py +0 -0
- {osmsg-1.2.2 → osmsg-1.2.4}/osmsg/db/__init__.py +0 -0
- {osmsg-1.2.2 → osmsg-1.2.4}/osmsg/db/duckdb_schema.py +0 -0
- {osmsg-1.2.2 → osmsg-1.2.4}/osmsg/db/ingest.py +0 -0
- {osmsg-1.2.2 → osmsg-1.2.4}/osmsg/db/queries.py +0 -0
- {osmsg-1.2.2 → osmsg-1.2.4}/osmsg/db/schema.py +0 -0
- {osmsg-1.2.2 → osmsg-1.2.4}/osmsg/exceptions.py +0 -0
- {osmsg-1.2.2 → osmsg-1.2.4}/osmsg/export/__init__.py +0 -0
- {osmsg-1.2.2 → osmsg-1.2.4}/osmsg/export/csv.py +0 -0
- {osmsg-1.2.2 → osmsg-1.2.4}/osmsg/export/json.py +0 -0
- {osmsg-1.2.2 → osmsg-1.2.4}/osmsg/export/markdown.py +0 -0
- {osmsg-1.2.2 → osmsg-1.2.4}/osmsg/export/parquet.py +0 -0
- {osmsg-1.2.2 → osmsg-1.2.4}/osmsg/export/psql.py +0 -0
- {osmsg-1.2.2 → osmsg-1.2.4}/osmsg/fetch.py +0 -0
- {osmsg-1.2.2 → osmsg-1.2.4}/osmsg/geofabrik.py +0 -0
- {osmsg-1.2.2 → osmsg-1.2.4}/osmsg/handlers.py +0 -0
- {osmsg-1.2.2 → osmsg-1.2.4}/osmsg/history.py +0 -0
- {osmsg-1.2.2 → osmsg-1.2.4}/osmsg/maintain/__init__.py +0 -0
- {osmsg-1.2.2 → osmsg-1.2.4}/osmsg/maintain/cli.py +0 -0
- {osmsg-1.2.2 → osmsg-1.2.4}/osmsg/maintain/convert.py +0 -0
- {osmsg-1.2.2 → osmsg-1.2.4}/osmsg/maintain/manifest.py +0 -0
- {osmsg-1.2.2 → osmsg-1.2.4}/osmsg/maintain/month.py +0 -0
- {osmsg-1.2.2 → osmsg-1.2.4}/osmsg/maintain/parquet.py +0 -0
- {osmsg-1.2.2 → osmsg-1.2.4}/osmsg/maintain/pbf_split.py +0 -0
- {osmsg-1.2.2 → osmsg-1.2.4}/osmsg/models.py +0 -0
- {osmsg-1.2.2 → osmsg-1.2.4}/osmsg/pg_schema.py +0 -0
- {osmsg-1.2.2 → osmsg-1.2.4}/osmsg/py.typed +0 -0
- {osmsg-1.2.2 → osmsg-1.2.4}/osmsg/replication.py +0 -0
- {osmsg-1.2.2 → osmsg-1.2.4}/osmsg/tm.py +0 -0
- {osmsg-1.2.2 → osmsg-1.2.4}/osmsg/ui.py +0 -0
- {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.
|
|
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.
|
|
82
|
-
|
|
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
|
-
|
|
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.
|
|
50
|
-
|
|
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
|
-
|
|
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 = (
|
|
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=
|
|
30
|
-
|
|
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=
|
|
118
|
-
ttk.Checkbutton(frame, text="Daily summary", variable=self.vars["summary"]).grid(row=
|
|
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=
|
|
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=
|
|
129
|
-
ttk.Button(frame, text="Choose folder", command=self._choose_folder).grid(row=
|
|
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="
|
|
132
|
-
self.run_btn.grid(row=
|
|
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=
|
|
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=
|
|
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.
|
|
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.
|
|
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
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
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
|
|
osmsg-1.2.2/osmsg/__version__.py
DELETED
|
@@ -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
|