osmsg 1.3.0__tar.gz → 1.3.2__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.3.0 → osmsg-1.3.2}/PKG-INFO +2 -1
- osmsg-1.3.2/osmsg/__version__.py +1 -0
- {osmsg-1.3.0 → osmsg-1.3.2}/osmsg/_http.py +2 -4
- osmsg-1.3.2/osmsg/_tick.py +172 -0
- osmsg-1.3.2/osmsg/catalog.py +502 -0
- {osmsg-1.3.0 → osmsg-1.3.2}/osmsg/cli.py +17 -5
- {osmsg-1.3.0 → osmsg-1.3.2}/osmsg/db/__init__.py +2 -12
- {osmsg-1.3.0 → osmsg-1.3.2}/osmsg/db/duckdb_schema.py +1 -1
- {osmsg-1.3.0 → osmsg-1.3.2}/osmsg/db/ingest.py +83 -54
- {osmsg-1.3.0 → osmsg-1.3.2}/osmsg/db/queries.py +3 -3
- {osmsg-1.3.0 → osmsg-1.3.2}/osmsg/db/schema.py +4 -6
- {osmsg-1.3.0 → osmsg-1.3.2}/osmsg/exceptions.py +2 -4
- {osmsg-1.3.0 → osmsg-1.3.2}/osmsg/export/psql.py +70 -19
- {osmsg-1.3.0 → osmsg-1.3.2}/osmsg/geofabrik.py +2 -5
- {osmsg-1.3.0 → osmsg-1.3.2}/osmsg/handlers.py +26 -18
- {osmsg-1.3.0 → osmsg-1.3.2}/osmsg/history.py +19 -12
- osmsg-1.3.2/osmsg/maintain/check.py +115 -0
- {osmsg-1.3.0 → osmsg-1.3.2}/osmsg/maintain/cli.py +30 -2
- {osmsg-1.3.0 → osmsg-1.3.2}/osmsg/maintain/convert.py +133 -37
- {osmsg-1.3.0 → osmsg-1.3.2}/osmsg/maintain/month.py +7 -2
- osmsg-1.3.2/osmsg/maintain/refresh.py +89 -0
- {osmsg-1.3.0 → osmsg-1.3.2}/osmsg/maintain/rollup.py +12 -40
- {osmsg-1.3.0 → osmsg-1.3.2}/osmsg/models.py +4 -10
- {osmsg-1.3.0 → osmsg-1.3.2}/osmsg/pg_schema.py +10 -1
- {osmsg-1.3.0 → osmsg-1.3.2}/osmsg/pipeline.py +33 -17
- osmsg-1.3.2/osmsg/prune.py +64 -0
- osmsg-1.3.2/osmsg/query.py +1065 -0
- {osmsg-1.3.0 → osmsg-1.3.2}/osmsg/replication.py +4 -12
- osmsg-1.3.2/osmsg/stats.py +72 -0
- {osmsg-1.3.0 → osmsg-1.3.2}/osmsg/tm.py +2 -6
- {osmsg-1.3.0 → osmsg-1.3.2}/osmsg/workers.py +2 -4
- {osmsg-1.3.0 → osmsg-1.3.2}/pyproject.toml +2 -1
- osmsg-1.3.0/osmsg/__version__.py +0 -1
- osmsg-1.3.0/osmsg/_tick.py +0 -96
- osmsg-1.3.0/osmsg/catalog.py +0 -212
- osmsg-1.3.0/osmsg/prune.py +0 -50
- osmsg-1.3.0/osmsg/query.py +0 -462
- {osmsg-1.3.0 → osmsg-1.3.2}/LICENSE +0 -0
- {osmsg-1.3.0 → osmsg-1.3.2}/README.md +0 -0
- {osmsg-1.3.0 → osmsg-1.3.2}/osmsg/__init__.py +0 -0
- {osmsg-1.3.0 → osmsg-1.3.2}/osmsg/auth.py +0 -0
- {osmsg-1.3.0 → osmsg-1.3.2}/osmsg/boundary.py +0 -0
- {osmsg-1.3.0 → osmsg-1.3.2}/osmsg/export/__init__.py +0 -0
- {osmsg-1.3.0 → osmsg-1.3.2}/osmsg/export/csv.py +0 -0
- {osmsg-1.3.0 → osmsg-1.3.2}/osmsg/export/json.py +0 -0
- {osmsg-1.3.0 → osmsg-1.3.2}/osmsg/export/markdown.py +0 -0
- {osmsg-1.3.0 → osmsg-1.3.2}/osmsg/export/parquet.py +0 -0
- {osmsg-1.3.0 → osmsg-1.3.2}/osmsg/fetch.py +0 -0
- {osmsg-1.3.0 → osmsg-1.3.2}/osmsg/gui.py +0 -0
- {osmsg-1.3.0 → osmsg-1.3.2}/osmsg/maintain/__init__.py +0 -0
- {osmsg-1.3.0 → osmsg-1.3.2}/osmsg/maintain/manifest.py +0 -0
- {osmsg-1.3.0 → osmsg-1.3.2}/osmsg/maintain/parquet.py +0 -0
- {osmsg-1.3.0 → osmsg-1.3.2}/osmsg/maintain/pbf_split.py +0 -0
- {osmsg-1.3.0 → osmsg-1.3.2}/osmsg/py.typed +0 -0
- {osmsg-1.3.0 → osmsg-1.3.2}/osmsg/ui.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: osmsg
|
|
3
|
-
Version: 1.3.
|
|
3
|
+
Version: 1.3.2
|
|
4
4
|
Summary: OpenStreetMap Stats Generator: Commandline
|
|
5
5
|
Keywords: osm,stats,commandline,openstreetmap
|
|
6
6
|
Author: Kshitij Raj Sharma
|
|
@@ -13,6 +13,7 @@ Classifier: Programming Language :: Python :: 3
|
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.11
|
|
14
14
|
Classifier: Programming Language :: Python :: 3.12
|
|
15
15
|
Requires-Dist: duckdb>=1.5.2
|
|
16
|
+
Requires-Dist: huggingface-hub>=1.0
|
|
16
17
|
Requires-Dist: osmium>=4.3.1
|
|
17
18
|
Requires-Dist: platformdirs>=4.5.1
|
|
18
19
|
Requires-Dist: pyarrow>=24.0.0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.3.2"
|
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
"""Shared `requests.Session` with retry policy
|
|
2
|
-
|
|
3
|
-
Every HTTP call in osmsg goes through this session so retry behaviour and
|
|
4
|
-
timeout defaults are consistent. Per-request `timeout=` still wins.
|
|
1
|
+
"""Shared `requests.Session` with a retry policy and connect/read timeouts, so every osmsg HTTP call is
|
|
2
|
+
consistent. A per-request `timeout=` still wins.
|
|
5
3
|
"""
|
|
6
4
|
|
|
7
5
|
from __future__ import annotations
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""Worker tick: bootstrap on first run, --update thereafter."""
|
|
2
|
+
|
|
3
|
+
import fcntl
|
|
4
|
+
import os
|
|
5
|
+
import shlex
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
import duckdb
|
|
11
|
+
|
|
12
|
+
from .db import connect, create_tables, get_state, upsert_state
|
|
13
|
+
from .geofabrik import country_update_url
|
|
14
|
+
from .replication import resolve_url
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _has_state(db_path: Path, source_url: str) -> bool:
|
|
18
|
+
if not db_path.exists():
|
|
19
|
+
return False
|
|
20
|
+
conn = connect(str(db_path))
|
|
21
|
+
create_tables(conn)
|
|
22
|
+
result = get_state(conn, source_url) is not None
|
|
23
|
+
conn.close()
|
|
24
|
+
return result
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _has_any_state(db_path: Path) -> bool:
|
|
28
|
+
"""True if the store has a resume position for ANY source, so `--update` continues (and auto-refines)
|
|
29
|
+
it instead of re-bootstrapping."""
|
|
30
|
+
if not db_path.exists():
|
|
31
|
+
return False
|
|
32
|
+
conn = connect(str(db_path))
|
|
33
|
+
create_tables(conn)
|
|
34
|
+
row = conn.execute("SELECT count(*) FROM state").fetchone()
|
|
35
|
+
conn.close()
|
|
36
|
+
return bool(row and row[0])
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _parse_arg(args: list[str], flag: str) -> str | None:
|
|
40
|
+
for i, arg in enumerate(args):
|
|
41
|
+
if arg == flag and i + 1 < len(args):
|
|
42
|
+
return args[i + 1]
|
|
43
|
+
return None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _reset_store_buffer(db_path: Path) -> None:
|
|
47
|
+
"""Drop the store's data tables and recreate them empty, keeping `state`, so the store holds only
|
|
48
|
+
the next tick's delta rather than every row pushed since the last reset."""
|
|
49
|
+
if not db_path.exists():
|
|
50
|
+
return
|
|
51
|
+
conn = connect(str(db_path))
|
|
52
|
+
for table in ("changeset_stats", "changesets", "users"):
|
|
53
|
+
conn.execute(f"DROP TABLE IF EXISTS {table}")
|
|
54
|
+
create_tables(conn)
|
|
55
|
+
conn.close()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _read_pg_state(dsn: str) -> list[tuple]:
|
|
59
|
+
"""Read the resume state rows from the Postgres permanent copy (read-only), used to re-seed a rebuilt
|
|
60
|
+
store. The DSN is interpolated into ATTACH, so it must be trusted (same contract as export.psql)."""
|
|
61
|
+
conn = duckdb.connect()
|
|
62
|
+
try:
|
|
63
|
+
conn.execute("INSTALL postgres")
|
|
64
|
+
conn.execute("LOAD postgres")
|
|
65
|
+
safe_dsn = dsn.replace("'", "''")
|
|
66
|
+
conn.execute(f"ATTACH '{safe_dsn}' AS pg (TYPE postgres, READ_ONLY)")
|
|
67
|
+
rows = conn.execute("SELECT source_url, last_seq, last_ts, updated_at FROM pg.state").fetchall()
|
|
68
|
+
conn.execute("DETACH pg")
|
|
69
|
+
finally:
|
|
70
|
+
conn.close()
|
|
71
|
+
return rows
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _store_is_dirty(db_path: Path) -> bool:
|
|
75
|
+
"""A psql tick must start from an empty delta buffer (the last successful push reset it). Leftover data
|
|
76
|
+
means the previous push was interrupted, and the abrupt stop can also leave the store's index corrupt.
|
|
77
|
+
An unreadable store is treated as dirty so it gets rebuilt rather than crashing the run."""
|
|
78
|
+
if not db_path.exists():
|
|
79
|
+
return False
|
|
80
|
+
try:
|
|
81
|
+
conn = connect(str(db_path))
|
|
82
|
+
except duckdb.Error:
|
|
83
|
+
return True
|
|
84
|
+
try:
|
|
85
|
+
create_tables(conn)
|
|
86
|
+
row = conn.execute("SELECT count(*) FROM changeset_stats").fetchone()
|
|
87
|
+
return bool(row) and row[0] > 0
|
|
88
|
+
except duckdb.Error:
|
|
89
|
+
return True
|
|
90
|
+
finally:
|
|
91
|
+
conn.close()
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _rebuild_store_from_pg(db_path: Path, dsn: str) -> None:
|
|
95
|
+
"""Discard a dirty or corrupt delta buffer and rebuild it fresh, re-seeding the resume state from the
|
|
96
|
+
Postgres permanent copy so `--update` continues from the last durably pushed position: no gap, no
|
|
97
|
+
double-count (the push is ON CONFLICT DO NOTHING), and a clean index. This is the automatic recovery
|
|
98
|
+
that replaces manual store surgery after an interrupted push."""
|
|
99
|
+
pg_state = _read_pg_state(dsn)
|
|
100
|
+
for path in (db_path, db_path.with_name(db_path.name + ".wal")):
|
|
101
|
+
if path.exists():
|
|
102
|
+
path.unlink()
|
|
103
|
+
conn = connect(str(db_path))
|
|
104
|
+
try:
|
|
105
|
+
create_tables(conn)
|
|
106
|
+
for source_url, last_seq, last_ts, updated_at in pg_state:
|
|
107
|
+
upsert_state(conn, source_url=source_url, last_seq=last_seq, last_ts=last_ts, updated_at=updated_at)
|
|
108
|
+
finally:
|
|
109
|
+
conn.close()
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def main() -> int:
|
|
113
|
+
extra_args = shlex.split(os.environ.get("OSMSG_EXTRA_ARGS", ""))
|
|
114
|
+
bootstrap_days = os.environ.get("OSMSG_BOOTSTRAP_DAYS", "1")
|
|
115
|
+
name = _parse_arg(extra_args, "--name") or "stats"
|
|
116
|
+
out = Path(_parse_arg(extra_args, "--output-dir") or "/var/lib/osmsg")
|
|
117
|
+
country = _parse_arg(extra_args, "--country")
|
|
118
|
+
explicit_url = _parse_arg(extra_args, "--url")
|
|
119
|
+
url = explicit_url or "day"
|
|
120
|
+
|
|
121
|
+
out.mkdir(parents=True, exist_ok=True)
|
|
122
|
+
|
|
123
|
+
lock_path = out / f"{name}.lock"
|
|
124
|
+
lock_fd = os.open(str(lock_path), os.O_CREAT | os.O_RDWR, 0o644)
|
|
125
|
+
try:
|
|
126
|
+
fcntl.flock(lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
127
|
+
except BlockingIOError:
|
|
128
|
+
os.close(lock_fd)
|
|
129
|
+
print("[osmsg-tick] previous tick still running, skipping", flush=True)
|
|
130
|
+
return 0
|
|
131
|
+
|
|
132
|
+
try:
|
|
133
|
+
# Mirror pipeline._normalize_urls: explicit --url wins over --country's geofabrik default,
|
|
134
|
+
# otherwise --update can't find the state row and the DuckDB gets wiped every tick.
|
|
135
|
+
source_url = country_update_url(country) if country and explicit_url is None else resolve_url(url)
|
|
136
|
+
db_path = out / f"{name}.duckdb"
|
|
137
|
+
psql_dsn = _parse_arg(extra_args, "--psql-dsn")
|
|
138
|
+
|
|
139
|
+
# Self-heal
|
|
140
|
+
if psql_dsn and _store_is_dirty(db_path):
|
|
141
|
+
print("[osmsg-tick] store dirty from an interrupted push; rebuilding from Postgres state", flush=True)
|
|
142
|
+
_rebuild_store_from_pg(db_path, psql_dsn)
|
|
143
|
+
|
|
144
|
+
extra_set = set(extra_args)
|
|
145
|
+
cmd = ["osmsg"] + extra_args
|
|
146
|
+
if not (extra_set & {"--all", "--keys"}):
|
|
147
|
+
cmd.append("--all")
|
|
148
|
+
|
|
149
|
+
# Country tracks one geofabrik source; the planet run continues whatever was seeded, so accept any.
|
|
150
|
+
has_state = _has_state(db_path, source_url) if country else _has_any_state(db_path)
|
|
151
|
+
if has_state:
|
|
152
|
+
cmd.append("--update")
|
|
153
|
+
else:
|
|
154
|
+
# Cold start at day granularity; --update then refines to hour/minute as the backlog shrinks.
|
|
155
|
+
if explicit_url is None and not country:
|
|
156
|
+
cmd.extend(["--url", "day"])
|
|
157
|
+
cmd.extend(["--days", bootstrap_days])
|
|
158
|
+
|
|
159
|
+
print(f"[osmsg-tick] {' '.join(cmd)}", flush=True)
|
|
160
|
+
rc = subprocess.call(cmd)
|
|
161
|
+
# With a psql push, Postgres is the permanent copy and the DuckDB store is only a per-tick
|
|
162
|
+
# delta buffer. Clear its data (keeping the resume `state`) after a successful push so the store
|
|
163
|
+
# stays small and the next push stays fast; otherwise it re-pushes the whole growing store each tick.
|
|
164
|
+
if rc == 0 and psql_dsn:
|
|
165
|
+
_reset_store_buffer(db_path)
|
|
166
|
+
return rc
|
|
167
|
+
finally:
|
|
168
|
+
os.close(lock_fd)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
if __name__ == "__main__":
|
|
172
|
+
sys.exit(main())
|