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.
Files changed (55) hide show
  1. {osmsg-1.3.0 → osmsg-1.3.2}/PKG-INFO +2 -1
  2. osmsg-1.3.2/osmsg/__version__.py +1 -0
  3. {osmsg-1.3.0 → osmsg-1.3.2}/osmsg/_http.py +2 -4
  4. osmsg-1.3.2/osmsg/_tick.py +172 -0
  5. osmsg-1.3.2/osmsg/catalog.py +502 -0
  6. {osmsg-1.3.0 → osmsg-1.3.2}/osmsg/cli.py +17 -5
  7. {osmsg-1.3.0 → osmsg-1.3.2}/osmsg/db/__init__.py +2 -12
  8. {osmsg-1.3.0 → osmsg-1.3.2}/osmsg/db/duckdb_schema.py +1 -1
  9. {osmsg-1.3.0 → osmsg-1.3.2}/osmsg/db/ingest.py +83 -54
  10. {osmsg-1.3.0 → osmsg-1.3.2}/osmsg/db/queries.py +3 -3
  11. {osmsg-1.3.0 → osmsg-1.3.2}/osmsg/db/schema.py +4 -6
  12. {osmsg-1.3.0 → osmsg-1.3.2}/osmsg/exceptions.py +2 -4
  13. {osmsg-1.3.0 → osmsg-1.3.2}/osmsg/export/psql.py +70 -19
  14. {osmsg-1.3.0 → osmsg-1.3.2}/osmsg/geofabrik.py +2 -5
  15. {osmsg-1.3.0 → osmsg-1.3.2}/osmsg/handlers.py +26 -18
  16. {osmsg-1.3.0 → osmsg-1.3.2}/osmsg/history.py +19 -12
  17. osmsg-1.3.2/osmsg/maintain/check.py +115 -0
  18. {osmsg-1.3.0 → osmsg-1.3.2}/osmsg/maintain/cli.py +30 -2
  19. {osmsg-1.3.0 → osmsg-1.3.2}/osmsg/maintain/convert.py +133 -37
  20. {osmsg-1.3.0 → osmsg-1.3.2}/osmsg/maintain/month.py +7 -2
  21. osmsg-1.3.2/osmsg/maintain/refresh.py +89 -0
  22. {osmsg-1.3.0 → osmsg-1.3.2}/osmsg/maintain/rollup.py +12 -40
  23. {osmsg-1.3.0 → osmsg-1.3.2}/osmsg/models.py +4 -10
  24. {osmsg-1.3.0 → osmsg-1.3.2}/osmsg/pg_schema.py +10 -1
  25. {osmsg-1.3.0 → osmsg-1.3.2}/osmsg/pipeline.py +33 -17
  26. osmsg-1.3.2/osmsg/prune.py +64 -0
  27. osmsg-1.3.2/osmsg/query.py +1065 -0
  28. {osmsg-1.3.0 → osmsg-1.3.2}/osmsg/replication.py +4 -12
  29. osmsg-1.3.2/osmsg/stats.py +72 -0
  30. {osmsg-1.3.0 → osmsg-1.3.2}/osmsg/tm.py +2 -6
  31. {osmsg-1.3.0 → osmsg-1.3.2}/osmsg/workers.py +2 -4
  32. {osmsg-1.3.0 → osmsg-1.3.2}/pyproject.toml +2 -1
  33. osmsg-1.3.0/osmsg/__version__.py +0 -1
  34. osmsg-1.3.0/osmsg/_tick.py +0 -96
  35. osmsg-1.3.0/osmsg/catalog.py +0 -212
  36. osmsg-1.3.0/osmsg/prune.py +0 -50
  37. osmsg-1.3.0/osmsg/query.py +0 -462
  38. {osmsg-1.3.0 → osmsg-1.3.2}/LICENSE +0 -0
  39. {osmsg-1.3.0 → osmsg-1.3.2}/README.md +0 -0
  40. {osmsg-1.3.0 → osmsg-1.3.2}/osmsg/__init__.py +0 -0
  41. {osmsg-1.3.0 → osmsg-1.3.2}/osmsg/auth.py +0 -0
  42. {osmsg-1.3.0 → osmsg-1.3.2}/osmsg/boundary.py +0 -0
  43. {osmsg-1.3.0 → osmsg-1.3.2}/osmsg/export/__init__.py +0 -0
  44. {osmsg-1.3.0 → osmsg-1.3.2}/osmsg/export/csv.py +0 -0
  45. {osmsg-1.3.0 → osmsg-1.3.2}/osmsg/export/json.py +0 -0
  46. {osmsg-1.3.0 → osmsg-1.3.2}/osmsg/export/markdown.py +0 -0
  47. {osmsg-1.3.0 → osmsg-1.3.2}/osmsg/export/parquet.py +0 -0
  48. {osmsg-1.3.0 → osmsg-1.3.2}/osmsg/fetch.py +0 -0
  49. {osmsg-1.3.0 → osmsg-1.3.2}/osmsg/gui.py +0 -0
  50. {osmsg-1.3.0 → osmsg-1.3.2}/osmsg/maintain/__init__.py +0 -0
  51. {osmsg-1.3.0 → osmsg-1.3.2}/osmsg/maintain/manifest.py +0 -0
  52. {osmsg-1.3.0 → osmsg-1.3.2}/osmsg/maintain/parquet.py +0 -0
  53. {osmsg-1.3.0 → osmsg-1.3.2}/osmsg/maintain/pbf_split.py +0 -0
  54. {osmsg-1.3.0 → osmsg-1.3.2}/osmsg/py.typed +0 -0
  55. {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.0
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 + connect/read timeouts.
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())