From 182060c195c2f8e9912accbed79823cd8d58fbb7 Mon Sep 17 00:00:00 2001 From: Martijn van Exel Date: Mon, 5 Jan 2026 14:11:11 -0700 Subject: [PATCH 1/5] Add live query runner script and docs --- README.md | 2 + docs/live-queries.md | 29 ++++++ tools/live_queries.py | 215 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 246 insertions(+) create mode 100644 docs/live-queries.md create mode 100644 tools/live_queries.py diff --git a/README.md b/README.md index cb3dd2f4bb..fcd9f5fac6 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,8 @@ uv run ty check uv run pytest tests/ --import-mode importlib ``` +For running live query examples against the public API, see `docs/live-queries.md`. + ## Usage ### `API()` constructor diff --git a/docs/live-queries.md b/docs/live-queries.md new file mode 100644 index 0000000000..fbc7da765f --- /dev/null +++ b/docs/live-queries.md @@ -0,0 +1,29 @@ +# Live Query Runner + +This script is a lightweight trust tool for running known Overpass QL examples against the live API. + +## Usage + +List examples: + +```bash +python tools/live_queries.py --list +``` + +Run by index or key: + +```bash +python tools/live_queries.py --run 1 +python tools/live_queries.py --run cafes_sf_bbox +``` + +Custom query: + +```bash +python tools/live_queries.py --query 'node["amenity"="cafe"](37.77,-122.45,37.79,-122.43)' --responseformat geojson +``` + +Notes: +- Uses the public Overpass API by default. +- `--model` enables Pydantic models. +- `--save` writes raw output to disk. diff --git a/tools/live_queries.py b/tools/live_queries.py new file mode 100644 index 0000000000..b07af5c417 --- /dev/null +++ b/tools/live_queries.py @@ -0,0 +1,215 @@ +#!/usr/bin/env python3 + +import argparse +import json +import sys +from dataclasses import dataclass +from typing import Optional + +import overpass + + +@dataclass(frozen=True) +class ExampleQuery: + key: str + title: str + query: str + responseformat: str = "geojson" + verbosity: str = "body" + build: bool = True + + +EXAMPLES: list[ExampleQuery] = [ + ExampleQuery( + key="cafes_sf_bbox", + title="Cafes in a small SF bbox", + query='node["amenity"="cafe"](37.770,-122.450,37.790,-122.430)', + ), + ExampleQuery( + key="admin_boundaries_uluru", + title="Administrative boundaries (Uluru bbox)", + query='rel["boundary"="administrative"](-25.38653,130.99883,-25.31478,131.08938)', + ), + ExampleQuery( + key="route_bus_sf", + title="Bus route relations in SF bbox (with recursion)", + query=( + 'relation["route"="bus"](37.770,-122.520,37.805,-122.380);' + "(._;>;);" + ), + verbosity="body geom", + ), + ExampleQuery( + key="is_in_paris_admin2", + title="Find admin areas containing a point (Paris, admin_level=2)", + query=( + "is_in(48.856089,2.29789);" + 'area._[admin_level="2"];' + ), + ), + ExampleQuery( + key="bus_stops_bonn", + title="Bus stops in Bonn (area by name)", + query=( + 'area[name="Bonn"];' + "node(area)[highway=bus_stop];" + ), + ), + ExampleQuery( + key="multipolygon_relations", + title="Multipolygon relations in a small bbox", + query='rel[type=multipolygon](37.770,-122.450,37.790,-122.430)', + verbosity="body geom", + ), +] + + +def list_examples() -> None: + for idx, ex in enumerate(EXAMPLES, start=1): + print(f"{idx:2d}. {ex.title} ({ex.key})") + + +def run_query( + query: str, + *, + responseformat: str, + verbosity: str, + build: bool, + model: bool, + endpoint: Optional[str], + timeout: Optional[float], + debug: bool, +) -> object: + api = overpass.API(endpoint=endpoint, timeout=timeout, debug=debug) if endpoint else overpass.API(timeout=timeout, debug=debug) + return api.get( + query, + responseformat=responseformat, + verbosity=verbosity, + build=build, + model=model, + ) + + +def summarize(result: object, responseformat: str, model: bool) -> str: + if model: + if hasattr(result, "features"): + return f"GeoJSON FeatureCollection: {len(result.features)} features" + if hasattr(result, "elements"): + return f"Overpass JSON: {len(result.elements)} elements" + if hasattr(result, "rows"): + return f"CSV: {len(result.rows)} rows" + if hasattr(result, "text"): + return f"XML: {len(result.text)} chars" + if responseformat == "geojson" and isinstance(result, dict): + return f"GeoJSON FeatureCollection: {len(result.get('features', []))} features" + if responseformat == "json" and isinstance(result, dict): + return f"Overpass JSON: {len(result.get('elements', []))} elements" + if responseformat.startswith("csv") and isinstance(result, list): + return f"CSV: {max(len(result) - 1, 0)} rows" + if responseformat == "xml" and isinstance(result, str): + return f"XML: {len(result)} chars" + return f"Result type: {type(result).__name__}" + + +def main() -> int: + parser = argparse.ArgumentParser(description="Run live Overpass QL queries.") + parser.add_argument("--list", action="store_true", help="List built-in example queries") + parser.add_argument("--run", type=str, help="Run by example key or numeric index") + parser.add_argument("--query", type=str, help="Run a custom query string") + parser.add_argument("--responseformat", type=str, default="geojson") + parser.add_argument("--verbosity", type=str, default="body") + parser.add_argument("--build", action="store_true", help="Build query wrapper (default)") + parser.add_argument("--no-build", action="store_true", help="Use raw query without wrapper") + parser.add_argument("--model", action="store_true", help="Return Pydantic models") + parser.add_argument("--endpoint", type=str, default=None) + parser.add_argument("--timeout", type=float, default=25.0) + parser.add_argument("--debug", action="store_true") + parser.add_argument("--pretty", action="store_true", help="Pretty-print JSON output") + parser.add_argument("--save", type=str, help="Write raw output to a file") + + args = parser.parse_args() + + if args.list: + list_examples() + return 0 + + if args.query: + query = args.query + responseformat = args.responseformat + verbosity = args.verbosity + build = not args.no_build + elif args.run: + example: Optional[ExampleQuery] = None + if args.run.isdigit(): + idx = int(args.run) - 1 + if 0 <= idx < len(EXAMPLES): + example = EXAMPLES[idx] + else: + for ex in EXAMPLES: + if ex.key == args.run: + example = ex + break + if example is None: + print("Unknown example. Use --list to see options.", file=sys.stderr) + return 2 + query = example.query + responseformat = example.responseformat + verbosity = example.verbosity + build = example.build + else: + list_examples() + choice = input("Choose an example number (or 'c' for custom): ").strip() + if choice.lower() == "c": + query = input("Enter Overpass QL: ").strip() + responseformat = input("responseformat [geojson|json|xml|csv(...)]: ").strip() or "geojson" + verbosity = input("verbosity [ids|skel|body|tags|meta ...]: ").strip() or "body" + build = input("build wrapper? [Y/n]: ").strip().lower() != "n" + else: + if not choice.isdigit(): + print("Invalid selection.", file=sys.stderr) + return 2 + idx = int(choice) - 1 + if idx < 0 or idx >= len(EXAMPLES): + print("Invalid selection.", file=sys.stderr) + return 2 + example = EXAMPLES[idx] + query = example.query + responseformat = example.responseformat + verbosity = example.verbosity + build = example.build + + result = run_query( + query, + responseformat=responseformat, + verbosity=verbosity, + build=build, + model=args.model, + endpoint=args.endpoint, + timeout=args.timeout, + debug=args.debug, + ) + + print(f"Query: {query}") + print(summarize(result, responseformat, args.model)) + + if args.save: + if hasattr(result, "to_geojson"): + raw = result.to_geojson() + elif hasattr(result, "text"): + raw = result.text + else: + raw = json.dumps(result, indent=2 if args.pretty else None) + with open(args.save, "w", encoding="utf-8") as fp: + fp.write(raw) + print(f"Wrote {args.save}") + return 0 + + if args.pretty and isinstance(result, dict): + print(json.dumps(result, indent=2)) + elif isinstance(result, str): + print(result) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) From 9ce2b083676a91bb883c18a77117b0013d81044e Mon Sep 17 00:00:00 2001 From: Martijn van Exel Date: Mon, 5 Jan 2026 14:11:45 -0700 Subject: [PATCH 2/5] Ignore local Overpass QL PDF --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 666caf2883..9bf49a3fd1 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,4 @@ venv/ .venv/ .tox .ruff_cache/ +docs/Overpass API_Overpass QL - OpenStreetMap Wiki.pdf From d9e3fb16754513dd34befe7f58d80797de83665e Mon Sep 17 00:00:00 2001 From: Martijn van Exel Date: Mon, 5 Jan 2026 14:14:37 -0700 Subject: [PATCH 3/5] Make live query tool compatible with non-model API --- tools/live_queries.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/tools/live_queries.py b/tools/live_queries.py index b07af5c417..1efcec264e 100644 --- a/tools/live_queries.py +++ b/tools/live_queries.py @@ -80,14 +80,19 @@ def run_query( timeout: Optional[float], debug: bool, ) -> object: - api = overpass.API(endpoint=endpoint, timeout=timeout, debug=debug) if endpoint else overpass.API(timeout=timeout, debug=debug) - return api.get( - query, - responseformat=responseformat, - verbosity=verbosity, - build=build, - model=model, + api = ( + overpass.API(endpoint=endpoint, timeout=timeout, debug=debug) + if endpoint + else overpass.API(timeout=timeout, debug=debug) ) + kwargs = { + "responseformat": responseformat, + "verbosity": verbosity, + "build": build, + } + if model and "model" in api.get.__code__.co_varnames: + kwargs["model"] = True + return api.get(query, **kwargs) def summarize(result: object, responseformat: str, model: bool) -> str: From c55620192b9c71cdff957db054149e1702856c6f Mon Sep 17 00:00:00 2001 From: Martijn van Exel Date: Mon, 5 Jan 2026 14:19:25 -0700 Subject: [PATCH 4/5] Add Salt Lake County highways example query --- tools/live_queries.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tools/live_queries.py b/tools/live_queries.py index 1efcec264e..eec37381da 100644 --- a/tools/live_queries.py +++ b/tools/live_queries.py @@ -61,6 +61,18 @@ class ExampleQuery: query='rel[type=multipolygon](37.770,-122.450,37.790,-122.430)', verbosity="body geom", ), + ExampleQuery( + key="slc_highways_since_2020", + title="Salt Lake County highways changed since 2020-08-01", + query=( + 'area[name="Salt Lake County"]->.a;' + 'way[highway~"primary|secondary|trunk|motorway|residential|tertiary"]' + '(if: version() == 1)' + '(newer:"2020-08-01T00:00:00Z")' + "(area.a);" + ), + verbosity="geom", + ), ] From 4b1b4b730abae8e438eb7cadb8760aae73758acf Mon Sep 17 00:00:00 2001 From: Martijn van Exel Date: Mon, 5 Jan 2026 14:19:51 -0700 Subject: [PATCH 5/5] Remove admin boundaries example --- tools/live_queries.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/tools/live_queries.py b/tools/live_queries.py index eec37381da..ee35434bf6 100644 --- a/tools/live_queries.py +++ b/tools/live_queries.py @@ -25,11 +25,6 @@ class ExampleQuery: title="Cafes in a small SF bbox", query='node["amenity"="cafe"](37.770,-122.450,37.790,-122.430)', ), - ExampleQuery( - key="admin_boundaries_uluru", - title="Administrative boundaries (Uluru bbox)", - query='rel["boundary"="administrative"](-25.38653,130.99883,-25.31478,131.08938)', - ), ExampleQuery( key="route_bus_sf", title="Bus route relations in SF bbox (with recursion)",