Ergonomic Python SDK for the Lodapi REST API (LoD2 buildings, terrain, 3D tiles).
Project description
lodapi — Python SDK
Ergonomic Python client for the Lodapi REST API: federated LoD2 buildings across all 16 German Bundesländer, DGM terrain elevation, and 3D Tiles tileset discovery.
v0 / unstable. This is
0.0.1. The public surface may change before0.1. Pin an exact version if you depend on it.
Install
pip install lodapi
pip install lodapi[geo] # adds geopandas/shapely → .to_geodataframe()
Requires Python 3.10+.
Quickstart
from lodapi import Client
# api_key is optional — without one you use the anonymous free tier.
c = Client(api_key="lod_...")
# One page of buildings for a WGS84 bbox (minLon, minLat, maxLon, maxLat):
page = c.buildings(bbox=(7.0, 50.9, 7.1, 51.0), limit=500)
print(page.count, page.next_cursor)
# Transparent cursor pagination — iterate every feature across all pages:
for feature in c.buildings.iter(bbox=(7.0, 50.9, 7.1, 51.0)):
...
# Or page-by-page:
for p in c.buildings.pages(bbox=(7.0, 50.9, 7.1, 51.0)):
...
# Single building + its roof surfaces:
b = c.buildings.get("DEBBAL0100000001")
roof = c.buildings.roof("DEBBAL0100000001")
# Stream a binary GLB of the bbox to disk (or get bytes back with out=None):
c.buildings.glb(bbox=(13.40, 52.51, 13.41, 52.52), out="city.glb")
# Terrain:
h = c.terrain.elevation(lat=50.94, lon=7.05) # ElevationResponse
prof = c.terrain.profile(coords=[(7.0, 50.9), (7.1, 51.0)], samples=100)
c.terrain.datasets() # DGM raster datasets
c.terrain.mesh_datasets() # quantized-mesh tilesets
# Datasets + 3D Tiles:
c.datasets() # LoD2 datasets per BL
c.tilesets(bbox=(6.93, 50.92, 7.02, 50.96)) # tilesets in a bbox
c.tilesets.get("nw-koeln") # one tileset
c.tilesets.tileset_json("nw-koeln") # raw 3D-Tiles root (dict)
c.close() # or use `with Client(...) as c: ...`
There is no bl parameter — federation across the 16 Bundesländer is
server-side and automatic. A bbox tuple (minLon, minLat, maxLon, maxLat) is
serialized to the API's "minLon,minLat,maxLon,maxLat" string for you; you may
also pass an already-formatted string.
Auth
Pass api_key="lod_..." to the constructor; it is sent as the X-API-Key
header. The free tier is anonymous — omit the key entirely and calls still
work. A malformed/unknown/revoked key raises AuthError on the first call.
Keys have the shape lod_ + 32 lowercase chars (see ADR-0014).
Soft quota
Per the Concierge-Billing MVP (ADR-0014), quota is soft — exceeding it is
not an error. The server may return X-Lodapi-Quota-Used / -Limit headers;
the SDK surfaces them after each call:
c.buildings(bbox=(...))
print(c.last_quota.used, c.last_quota.limit, c.last_quota.remaining)
Both fields are None for anonymous calls or keys without a configured quota.
GLB defaults
c.buildings.glb(...) applies a neutral, glTF-consumption-friendly default set
for frame/origin/rotation. These differ from the raw REST-API defaults: the API
itself does not rotate, while the SDK rotates Z-up → Y-up so the model
stands upright out-of-the-box in Blender / three.js / model-viewer / Cesium
(glTF is Y-up per spec).
| param | raw API default | SDK default | why the SDK differs |
|---|---|---|---|
target_frame |
utm |
utm |
metric, scale-true (not MapLibre-bound) |
origin |
center |
center |
model centred on the origin |
rotate_x |
0 |
-90 |
Z-up → Y-up for glTF-spec conformance |
rotate_z |
0 |
0 |
no extra spin around the vertical axis |
All other GLB params (z_base, compression, colorize_roofs,
merge_buildings, include_ground, weld_tolerance_m) pass through to the
API's own defaults when omitted. Every default is overridable per call:
# Reproduce the scenerii-App view (MapLibre-aligned):
c.buildings.glb(
bbox=(13.40, 52.51, 13.41, 52.52),
target_frame="mercator", origin="corner", rotate_x=-90, rotate_z=-90,
)
# Get raw geospatial Z-up (no rotation, matches the raw API):
c.buildings.glb(bbox=(13.40, 52.51, 13.41, 52.52), rotate_x=0)
Errors
All API errors follow RFC 7807 application/problem+json (base
https://lodapi.de/errors). The SDK parses the document onto a typed exception:
from lodapi import LodapiError, AuthError, NotFoundError, RateLimitError, ApiError
try:
c.buildings.get("NOPE")
except NotFoundError as exc:
print(exc.status, exc.title, exc.detail, exc.instance)
AuthError— 401NotFoundError— 404RateLimitError— 429 (reserved; quota is soft today)ApiError— any other non-2xxLodapiError— base class + transport/network errors
Transient 5xx and network errors are retried (default 2 retries, exponential backoff). 4xx are never retried.
GeoDataFrame support (geo extra)
With pip install lodapi[geo], building/roof FeatureCollections convert to a
geopandas.GeoDataFrame (CRS EPSG:4326):
page = c.buildings(bbox=(7.0, 50.9, 7.1, 51.0))
gdf = page.to_geodataframe() # this page's features
roof = c.buildings.roof("DEBBAL0100000001")
roof.to_geodataframe()
Without the extra, to_geodataframe() raises ImportError with an install
hint; the rest of the SDK works fine.
Configuration
Client(
api_key=None, # optional lod_* key
base_url="https://api.lodapi.de", # override for staging/local
timeout=30.0, # per-request seconds
max_retries=2, # transient 5xx/network only
)
Regenerating the response models
The response models in src/lodapi/models.py are hand-written, derived from
the OpenAPI snapshot at 04_engineering/api/openapi/openapi.json (the live API
serves it at GET /openapi.json). For ~11 endpoints this is leaner than full
codegen.
When the API surface grows or churns:
-
Refresh the snapshot (
GET /openapi.json→ the repo copy). -
Either edit
models.pyby hand against the newcomponents.schemas, or adopt codegen:uvx openapi-python-client generate \ --path 04_engineering/api/openapi/openapi.json
and re-point
resources/*at the generated models. The resource facade is intentionally decoupled from the model layer, so swapping the model source is mechanical.
Development
uv run --extra dev pytest # from code/sdk-python/
Tests mock all HTTP with respx — no live calls against prod.
License
Apache-2.0. © scenerii GmbH.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file lodapi-0.0.1.tar.gz.
File metadata
- Download URL: lodapi-0.0.1.tar.gz
- Upload date:
- Size: 17.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.21 {"installer":{"name":"uv","version":"0.11.21","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Debian GNU/Linux","version":"13","id":"trixie","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b6d05cdeda5c5aa7ca5473ef5c29db540b3d844af1b5d7446c6922bea7ea7b44
|
|
| MD5 |
723bf3e8a16aeb73ac570f461f957ffa
|
|
| BLAKE2b-256 |
f4b11a57d9d28568003f784de5e2789f6fd5cfbdc663c64fa8343c5a9e38770c
|
File details
Details for the file lodapi-0.0.1-py3-none-any.whl.
File metadata
- Download URL: lodapi-0.0.1-py3-none-any.whl
- Upload date:
- Size: 18.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.21 {"installer":{"name":"uv","version":"0.11.21","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Debian GNU/Linux","version":"13","id":"trixie","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e4b55aa898e3ec0801ed7506053b08fbb0bc45322c1e2ce25f6cb081e34f5145
|
|
| MD5 |
c0554b074e6cfb33f023a828b3cb226a
|
|
| BLAKE2b-256 |
735c117bd6097458e9e29960c704d38b466c998780fa9e0ac9ba06c27ef24613
|