Skip to content

Commit aab8492

Browse files
Merge pull request #51 from osgeonepal/feat/hybrid-history
feat(hisotry): add osm hisotry for the stats
2 parents 3eed08e + 420c431 commit aab8492

47 files changed

Lines changed: 2617 additions & 85 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/windows-exe.yml

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
name: windows-exe
2+
3+
on:
4+
push:
5+
tags: ["v*", "*.*.*"]
6+
workflow_dispatch:
7+
8+
permissions:
9+
contents: write
10+
11+
jobs:
12+
build:
13+
runs-on: windows-latest
14+
steps:
15+
- uses: actions/checkout@v4
16+
17+
- name: Set up uv
18+
uses: astral-sh/setup-uv@v5
19+
with:
20+
python-version: "3.12"
21+
22+
- name: Install osmsg + PyInstaller
23+
run: |
24+
uv venv
25+
uv pip install . pyinstaller
26+
27+
- name: Build osmsg.exe
28+
run: uv run pyinstaller --clean --noconfirm packaging/pyinstaller/osmsg.spec
29+
30+
- name: Smoke-test the exe
31+
run: dist\osmsg.exe --version
32+
33+
- name: Upload build artifact
34+
uses: actions/upload-artifact@v4
35+
with:
36+
name: osmsg-windows-x64
37+
path: dist/osmsg.exe
38+
39+
- name: Attach to release
40+
if: startsWith(github.ref, 'refs/tags/')
41+
uses: softprops/action-gh-release@v2
42+
with:
43+
files: dist/osmsg.exe

.gitignore

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,19 @@ dist
99
node_modules/
1010
stats.*
1111
*.parquet
12+
13+
.claude/
14+
15+
# experiment artifacts (large)
16+
experiments/parquet-history/cache/
17+
experiments/parquet-history/data/
18+
experiments/parquet-history/out/
19+
experiments/parquet-history/out_demo/
20+
experiments/parquet-history/planet_work/
21+
22+
# QA / scratch (generated)
23+
experiments/parquet-history/qa_alltime/
24+
experiments/parquet-history/*_work/
25+
/qa/
26+
27+
CLAUDE.md

.pre-commit-config.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ repos:
1111
- id: check-merge-conflict
1212
- id: check-toml
1313
- id: check-yaml
14+
exclude: ^packaging/conda/meta\.yaml$ # conda recipe uses Jinja templating, not plain YAML
1415
- id: check-json
1516
- id: fix-byte-order-marker
1617

README.md

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,15 @@ of nodes, ways, and relations created, modified, or deleted, written to parquet,
1414

1515
A Project of [OSGeo Nepal](https://osgeonepal.org).
1616

17-
## Features
17+
## What does it do?
1818

1919
- Per-user create/modify/delete counts over any time window.
2020
- Tag and hashtag breakdowns (e.g. `building`, `#hotosm`).
2121
- Country and custom-boundary filters via Geofabrik.
2222
- Cron-friendly resume with `--update`.
23+
- One-command setup: `osmsg --insert` loads all history into your store, `osmsg --update` keeps it current.
2324
- Outputs you can query: parquet, csv, json, markdown, DuckDB, Postgres.
25+
- Cloud-native history: months covered by a published parquet dataset are read remotely.
2426

2527
## Install
2628

@@ -36,6 +38,16 @@ docker run --rm -v "$PWD:/work" -w /work ghcr.io/osgeonepal/osmsg:latest --last
3638
`uvx` can run osmsg in a throwaway environment , no install, no virtualenv to manage. Works
3739
with any flag combination, e.g. `uvx --from osmsg osmsg --last hour --tags building --summary -f parquet -f markdown`.
3840

41+
More ways to install:
42+
43+
```bash
44+
conda install -c conda-forge osmsg # conda / mamba
45+
brew install osgeonepal/tap/osmsg # macOS / Linux (Homebrew tap)
46+
```
47+
48+
On Windows, download `osmsg.exe` from the [latest release](https://github.com/osgeonepal/osmsg/releases)
49+
and run it directly, no Python required.
50+
3951
## Quick start
4052

4153
```bash
@@ -46,6 +58,38 @@ osmsg --hashtags hotosm --last day # only changesets tagged #hotosm
4658

4759
That's it. A `stats.duckdb` and a `stats.parquet` show up in your current folder.
4860

61+
## Set up a full history store
62+
63+
Two commands give you a complete, self-updating store. The first loads all of OSM history from the
64+
published dataset and records where to resume; the second catches up to now and runs on a schedule.
65+
66+
```bash
67+
osmsg --insert # load all history into stats.duckdb, then exit
68+
osmsg --update # catch up to now (repeat on cron)
69+
```
70+
71+
`osmsg` clears the multi-week backlog on day diffs, then refines to finer diffs as the store stays
72+
current. For near-real-time, run `osmsg --update --url minute`.
73+
74+
Pick your store with one flag. DuckDB is the default (`stats.duckdb`); add a DSN for Postgres:
75+
76+
```bash
77+
osmsg --insert --psql-dsn "postgresql://user:pass@localhost/osmsg"
78+
osmsg --update --psql-dsn "postgresql://user:pass@localhost/osmsg"
79+
```
80+
81+
Load only a slice with `--start/--end`; `--update` then continues from the end of that slice:
82+
83+
```bash
84+
osmsg --insert --start 2020-01-01 --end 2023-01-01
85+
```
86+
87+
Already have the planet files? Insert from them directly:
88+
89+
```bash
90+
osmsg --insert --osh-file history-latest.osh.pbf --changeset-file changesets-latest.osm.bz2
91+
```
92+
4993
## Tutorials
5094

5195
### 1. Stats for a country
@@ -153,13 +197,31 @@ docker-compose `environment:` block all reach the same setting. CLI flag wins ov
153197
| `--output-dir` | `OSMSG_OUTPUT_DIR` | `.` | Where `<name>.duckdb` and exports are written. |
154198
| `--format` / `-f` | `OSMSG_FORMAT` | `parquet` | Repeat for multiple. Comma-separated when set via env. |
155199
| `--psql-dsn` | `OSMSG_PSQL_DSN` | unset | libpq DSN for `-f psql`. |
200+
| `--psql-bulk` | `OSMSG_PSQL_BULK` | off | Faster first full load to Postgres. |
201+
| `--history` / `--no-history` | `OSMSG_HISTORY` | on | Read covered months from the published dataset. |
202+
| `--history-url` | `OSMSG_HISTORY_URL` | `osmsg-history` | Published dataset location. |
203+
| `--insert` | (none) | off | Load history into the store and seed resume, then exit. No window loads all of it. |
204+
| `--osh-file` / `--changeset-file` | (none) | unset | Insert from local planet history + changeset files instead of the dataset. |
156205
| `--changeset-pad-hours` | `OSMSG_CHANGESET_PAD_HOURS` | `1` | See below. |
157206
| (auto-bootstrap on `--update`) | `OSMSG_BOOTSTRAP` | `hour` | `hour`, `day`, or `week`. Used when `--update` runs against an empty DB. |
158207
| (auto-bootstrap on `--update`) | `OSMSG_BOOTSTRAP_DAYS` | unset | Integer N; overrides `OSMSG_BOOTSTRAP`. |
159208
| OSM credentials (Geofabrik) | `OSM_USERNAME`, `OSM_PASSWORD` | unset | Required only when a Geofabrik URL is in use. |
160209

161210
A `.env` file at the working directory is loaded automatically.
162211

212+
## Maintainers
213+
214+
Generating and publishing the history dataset is the `osmsg maintain` group:
215+
216+
```bash
217+
osmsg maintain month 2026-06 --repo osgeonepal/osmsg-history # append one finished month
218+
osmsg maintain month 2026-06 --no-upload # generate locally, review, upload later
219+
osmsg maintain convert history.osh.pbf changesets.osm.bz2 2005-01-01 2026-06-01 work --parts 24
220+
osmsg maintain publish work/out --repo osgeonepal/osmsg-history
221+
```
222+
223+
See [experiments/parquet-history](./experiments/parquet-history/README.md) for the full-history batch.
224+
163225
## Documentation
164226

165227
- [Installation](./docs/Installation.md)

api/routers/v1.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ async def get_user_stats(
4444
list[str] | None, Parameter(description="Filter to changesets carrying any of these hashtags. Repeatable.")
4545
] = None,
4646
tags: Annotated[bool, Parameter(description="Include per-user tag_stats breakdown in the response.")] = True,
47-
limit: Annotated[int, Parameter(ge=1, le=1000, description="Page size (11000).")] = 100,
47+
limit: Annotated[int, Parameter(ge=1, le=1000, description="Page size (1 to 1000).")] = 100,
4848
offset: Annotated[int, Parameter(ge=0, description="Page offset.")] = 0,
4949
) -> UserStatsResponse:
5050
start = start or (datetime.min.replace(tzinfo=UTC) if end else None)

docs/Manual.md

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ osmsg --country nepal --country india --country africa # Geofabrik regions, re
2525
```
2626

2727
> When `--url` is omitted, osmsg picks a planet replication granularity that fits the requested
28-
> span: minute for spans under 6h, hour for 6h7d, day for ≥7d. A warning prints when the
28+
> span: minute for spans under 6h, hour for 6h to 7d, day for 7d or more. A warning prints when the
2929
> auto-switch happens; pass `--url` explicitly to override (also suppressed by `--country`,
3030
> `--update`, or multiple `--url` values).
3131
@@ -41,7 +41,7 @@ osmsg --boundary '{"type":"Polygon",...}' # inline GeoJSON string
4141
```
4242

4343
> `--boundary` filters changesets whose bounding box intersects the given geometry.
44-
> A Geofabrik region name resolves from the same index as `--country` no separate file needed.
44+
> A Geofabrik region name resolves from the same index as `--country`, no separate file needed.
4545
> `--boundary` only filters; it does not change the replication source.
4646
> To scope the replication source to a country's diffs, use `--country` instead.
4747
>
@@ -107,6 +107,72 @@ Downloaded `.osc.gz` files cache to a per-user dir (`~/Library/Caches/osmsg` on
107107
`~/.cache/osmsg` on Linux). Re-running the same range reuses them, so no network is needed.
108108
`--cache-dir` to relocate, `--delete-temp` to clean up after a run.
109109

110+
## Setting up a store
111+
112+
`--insert` loads history into the store and seeds the resume position, then exits. Follow it with
113+
`--update` to catch up to now and keep current. DuckDB is the default store; pass `--psql-dsn` to use
114+
Postgres (no separate `-f psql` needed).
115+
116+
```bash
117+
osmsg --insert # load all published history into stats.duckdb
118+
osmsg --update # catch up to now, then run on cron
119+
120+
osmsg --insert --psql-dsn "host=localhost dbname=osm user=osm" # into Postgres (bulk first load)
121+
osmsg --insert --start 2020-01-01 --end 2023-01-01 # a slice; --update continues from its end
122+
osmsg --insert --osh-file history.osh.pbf --changeset-file changesets.osm.bz2 # from local files
123+
```
124+
125+
- No window loads the whole dataset; `--start/--end` loads a slice and resumes from the slice end.
126+
- `--osh-file` with `--changeset-file` converts local planet files into the store (offline, or a custom
127+
extract). Give both together.
128+
- The Postgres load uses the bulk path (drops indexes and keys, rebuilds after).
129+
130+
`--insert` and `--update` pick the replication granularity from how far behind the store is. A fresh
131+
store clears the multi-week backlog on day diffs (tens of files), then refines to hour and minute as it
132+
stays current. For near-real-time, run `osmsg --update --url minute`. A store tracks one granularity at
133+
a time; changing it hands off at the day boundary, so the windows stay disjoint. Pass `--url` to either
134+
command to set the granularity yourself.
135+
136+
## Cloud-native history
137+
138+
Months covered by a published parquet dataset (default `kshitijrajsharma/osmsg-history` on
139+
HuggingFace) are read remotely. The recent uncovered tail uses the live replication path. This is on
140+
by default.
141+
142+
```bash
143+
osmsg --start 2015-01-01 --end 2020-01-01 # read from the dataset
144+
osmsg --start 2024-01-01 # covered months remote, current month live
145+
osmsg --last week --no-history # live path only
146+
```
147+
148+
- `--no-history` (env `OSMSG_HISTORY=0`) uses the live path.
149+
- `--history-url` (env `OSMSG_HISTORY_URL`) sets the dataset location.
150+
- The live path is used when the dataset is unreachable, with `--update`, and with `--length`.
151+
152+
### Postgres as a source of truth
153+
154+
`osmsg --insert --psql-dsn ...` loads the dataset into osmsg's schema and seeds the resume position;
155+
`osmsg --update --psql-dsn ...` then keeps Postgres current. `--psql-bulk` (env `OSMSG_PSQL_BULK`)
156+
forces the bulk path on a plain run; `--insert` already uses it.
157+
158+
## Maintaining the dataset
159+
160+
`osmsg maintain` builds and publishes the history parquet.
161+
162+
```bash
163+
osmsg maintain month 2026-06 --repo osgeonepal/osmsg-history # build one finished month and upload
164+
osmsg maintain month 2026-06 --no-upload # build locally, review, upload later
165+
osmsg maintain publish out --repo osgeonepal/osmsg-history # write + upload manifest.json
166+
osmsg maintain convert history.osh.pbf changesets.osm.bz2 2005-01-01 2026-06-01 work --parts 24
167+
```
168+
169+
`month` builds from the live day diffs, exports the two partitions, uploads, and advances the
170+
manifest. It refuses to publish a month whose data stops short of the month boundary (pass
171+
`--allow-incomplete` to override), so published months are complete by construction. Re-running
172+
`osmsg maintain month <YYYY-MM>` rebuilds a month and overwrites its published partition, which repairs
173+
a month that was first generated from a mid-day planet snapshot. `convert` turns local planet files
174+
into the datasets out of core. Uploads use the `hf` CLI (`uvx`), so be logged in to HuggingFace.
175+
110176
## Credentials
111177

112178
`--country` and any `geofabrik` URL need OSM credentials. Resolution order:

docs/infra.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -43,18 +43,18 @@ $EDITOR infra/.env
4343

4444
| Variable | Default | Notes |
4545
| --- | --- | --- |
46-
| `OSMSG_DOMAIN` | `localhost` | Your domain enables automatic HTTPS via Caddy |
46+
| `OSMSG_DOMAIN` | `localhost` | Your domain, enables automatic HTTPS via Caddy |
4747
| `OSMSG_SCHEDULE` | `*/2 * * * *` | supercronic cron expression |
4848
| `OSMSG_BOOTSTRAP` | `hour` | First-run window: `hour`/`day`/`week`/`month`/`year` |
4949
| `OSMSG_BOOTSTRAP_DAYS` | _unset_ | Exact day count for first run (alternative to `OSMSG_BOOTSTRAP`) |
5050
| `OSM_USERNAME` | _unset_ | OSM account username (required for Geofabrik country replication) |
5151
| `OSM_PASSWORD` | _unset_ | OSM account password (required for Geofabrik country replication) |
52-
| `OSMSG_EXTRA_ARGS` | _see example_ | osmsg args applied on every tick country, format, tags, boundary, etc. |
52+
| `OSMSG_EXTRA_ARGS` | _see example_ | osmsg args applied on every tick (country, format, tags, boundary, etc.) |
5353

54-
`OSMSG_EXTRA_ARGS` runs on every tick. Do not put `--last`, `--days`, or `--update` here
54+
`OSMSG_EXTRA_ARGS` runs on every tick. Do not put `--last`, `--days`, or `--update` here;
5555
tick adds those automatically based on whether state exists.
5656

57-
Geofabrik sub-daily replication uses your OSM credentials directly no browser opt-in required.
57+
Geofabrik sub-daily replication uses your OSM credentials directly, no browser opt-in required.
5858

5959
### Start
6060

@@ -75,7 +75,7 @@ docker compose pull && docker compose up -d
7575

7676
## Run as a systemd service
7777

78-
Only the `infra/` directory needs to be on the server no source code or build tools required.
78+
Only the `infra/` directory needs to be on the server; no source code or build tools required.
7979

8080
**1. Place files:**
8181

@@ -85,7 +85,7 @@ cp infra/docker-compose.yml infra/Caddyfile infra/osmsg.service /opt/osmsg/infra
8585
cp infra/.env.example /opt/osmsg/infra/.env
8686
$EDITOR /opt/osmsg/infra/.env
8787

88-
# The pgdata Docker volume binds to /mnt create the directory first
88+
# The pgdata Docker volume binds to /mnt, create the directory first
8989
mkdir -p /mnt/osmsg/pgdata
9090
```
9191

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# Historical parquet datasets
2+
3+
osmsg precomputes OSM history into time-partitioned parquet published on HuggingFace, so past-window
4+
queries read remotely instead of re-downloading replication diffs. The read side is built into osmsg
5+
(`osmsg --start ... --end ...` serves covered months from the dataset, `osmsg --insert` loads it into
6+
a store). The build and publish side is the `osmsg maintain` subcommand group; this directory holds
7+
only the server batch script that wraps it.
8+
9+
## Datasets
10+
11+
Two datasets, both partitioned `year=*/month=*` and Morton(centroid)-sorted for time and bbox pruning:
12+
13+
- **changefiles**, per-changeset counts + poi + `tag_stats` JSON + `created_at` + bbox.
14+
- **changesets**, per-changeset metadata: uid, username, `created_at`, editor, hashtags, bbox.
15+
16+
## Build the full history (server batch)
17+
18+
```bash
19+
./planet_batch.sh <start YYYY-MM-DD> <end YYYY-MM-DD>
20+
```
21+
22+
Downloads `history-latest.osm.pbf` + `changesets-latest.osm.bz2`, time-filters with `osmium`, then
23+
runs `osmsg maintain convert` to stream and aggregate out of core. Needs the `osmium` CLI and ~150 GB
24+
free. The conversion alone is `osmsg maintain convert <osh> <changesets> <start> <end> <work_dir>
25+
--parts N`.
26+
27+
## Publish and maintain
28+
29+
```bash
30+
osmsg maintain publish <out_dir> --drop-last --repo <repo> # write + upload manifest.json
31+
osmsg maintain month <YYYY-MM> --repo <repo> # append one finished month
32+
osmsg maintain month <YYYY-MM> --no-upload # generate locally, review, upload later
33+
```
34+
35+
`osmsg maintain month` builds the month from the live day diffs, exports the two partitions, uploads
36+
them, and advances the manifest. It refuses to publish a month that stops short of its boundary
37+
(`--allow-incomplete` overrides), and re-running it rebuilds and overwrites a month, which repairs one
38+
first generated from a mid-day planet snapshot.
39+
40+
## Load into a store
41+
42+
`osmsg --insert` loads the published parquet into an osmsg DuckDB or Postgres store and seeds the
43+
resume position; see the [Manual](../../docs/Manual.md).
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
#!/usr/bin/env bash
2+
# Planet batch: download once, time-filter (C++), convert out-of-core to changefiles/changesets parquet.
3+
# Server job, not in-session: the history file is ~148 GB and the streaming pass is hours.
4+
#
5+
# Usage: ./planet_batch.sh <start YYYY-MM-DD> <end YYYY-MM-DD> [history_url]
6+
#
7+
# history_url defaults to the planet full-history directory's latest file. Confirm the current name at
8+
# https://planet.openstreetmap.org/pbf/full-history/ (the dated history-YYMMDD.osh.pbf). Needs the
9+
# osmium CLI (osmium-tool) for time-filter and ~150 GB free for the download plus the windowed extract.
10+
set -euo pipefail
11+
12+
start="${1:?start date YYYY-MM-DD}"
13+
end="${2:?end date YYYY-MM-DD}"
14+
history_url="${3:-https://planet.openstreetmap.org/pbf/full-history/history-latest.osm.pbf}"
15+
changeset_url="https://planet.openstreetmap.org/planet/changesets-latest.osm.bz2"
16+
17+
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
18+
work="$here/planet_work"
19+
mkdir -p "$work"
20+
21+
command -v osmium >/dev/null || { echo "osmium-tool required (time-filter). Install osmium-tool."; exit 1; }
22+
23+
echo ">>> [1/4] download history + changeset dump (resumable)"
24+
curl -fSL -C - -o "$work/history.osh.pbf" "$history_url"
25+
curl -fSL -C - -o "$work/changesets.osm.bz2" "$changeset_url"
26+
27+
echo ">>> [2/4] time-filter history to [$start, $end] (C++, avoids Python touching all versions)"
28+
osmium time-filter -O -o "$work/history-window.osh.pbf" \
29+
"$work/history.osh.pbf" "${start}T00:00:00Z" "${end}T00:00:00Z"
30+
31+
echo ">>> [3/4] stream + aggregate out-of-core to parquet"
32+
uv run --project "$here/../.." osmsg maintain convert \
33+
"$work/history-window.osh.pbf" "$work/changesets.osm.bz2" "$start" "$end" "$work" --parts 24
34+
35+
echo ">>> [4/4] done. datasets in $work/out/{changefiles,changesets}."
36+
echo " publish: uv run --project $here/../.. osmsg maintain publish $work/out --repo <repo>"

0 commit comments

Comments
 (0)