Metadata-Version: 2.5
Name: osmpid
Version: 0.2.0
Summary: Parse, generate, and validate persistent identifiers for OpenStreetMap elements.
Project-URL: Homepage, https://gitlab.com/geometalab/osmpid
Project-URL: Documentation, https://gitlab.com/geometalab/osmpid/-/blob/main/README.md
Project-URL: Repository, https://gitlab.com/geometalab/osmpid
Project-URL: Issues, https://gitlab.com/geometalab/osmpid/-/issues
Project-URL: Changelog, https://gitlab.com/geometalab/osmpid/-/blob/main/CHANGELOG.md
Author: GeoMetaLab
License-Expression: MIT
License-File: LICENSE
Keywords: gis,openstreetmap,osm,persistent-identifier
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Science/Research
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Scientific/Engineering :: GIS
Classifier: Typing :: Typed
Requires-Python: >=3.11
Requires-Dist: httpx<1,>=0.27
Requires-Dist: pydantic<3,>=2.7
Provides-Extra: geoparquet
Requires-Dist: pyarrow<23,>=17; extra == 'geoparquet'
Description-Content-Type: text/markdown

# osmpid

`osmpid` is a typed Python library for parsing, serializing, generating, and
validating persistent identifiers for OpenStreetMap (OSM) elements.

It provides:

- A strict parser and serializer for OSMPID strings.
- Immutable Pydantic models for identifiers and validation results.
- Change detection for element versions, descendant timestamps, and tags.
- Synchronous clients for Overpass API and OSM API 0.6.
- An optional GeoParquet backend for local OSM datasets.
- A protocol and fake client for integrating and testing custom backends.

The project supports Python 3.11 and newer.

## The identifier

An OSMPID is a short ASCII string built from data OSM already publishes:

```
way/99887766@12;2026-03-10T08:00:00Z?amenity=cafe
```

- **`way/99887766`** — the element type and id, exactly as OSM writes them.
- **`@12`** — the element version. It moves whenever the element itself is
  edited.
- **`;2026-03-10T08:00:00Z`** — the newest timestamp anywhere in the
  element's descendant tree. This is what makes a moved member node
  detectable when the way's own version has not changed. A node has no
  children and omits the segment entirely.
- **`?amenity=cafe`** — optional tags that pin the meaning you referenced.
  An element tagged both `building=yes` and `amenity=restaurant` stands for
  two things at once; naming the tag says which of them your reference is
  about. A key on its own (`?amenity`) asks only that the key still be
  present.

So `node/2641352539@7?amenity=bar&building` is a complete identifier too.

Nothing else is needed to resolve an OSMPID: no registry, no side database,
no history access. See [docs/SPECIFICATION.md](https://gitlab.com/geometalab/osmpid/-/blob/main/docs/SPECIFICATION.md)
for the grammar and the exact change-detection rules, and
[docs/BENEFITS.md](https://gitlab.com/geometalab/osmpid/-/blob/main/docs/BENEFITS.md)
for the design goals behind them.

## Installation

Install the base package from PyPI:

```bash
python -m pip install osmpid
```

Install GeoParquet support:

```bash
python -m pip install "osmpid[geoparquet]"
```

Each release is also mirrored to the project's GitLab Package Registry, for
installs that have to resolve from GitLab:

```bash
python -m pip install osmpid \
  --extra-index-url https://gitlab.com/api/v4/projects/83207967/packages/pypi/simple
```

## Quick Start

Parse and serialize an identifier:

```python
from osmpid import parse, serialize

pid = parse("node/2641352539@7?amenity=bar&building")

assert pid.element_id == 2641352539
assert serialize(pid) == "node/2641352539@7?amenity=bar&building"
```

Generate an identifier from live OSM state:

```python
from osmpid import ElementType, OverpassClient, generate

with OverpassClient() as client:
    pid = generate(ElementType.NODE, 2641352539, client)
```

Validate a stored identifier:

```python
from osmpid import OverpassClient, parse, validate

pid = parse("node/2641352539@7?amenity=bar")

with OverpassClient() as client:
    result = validate(pid, client)

print(result.status)          # unchanged / changed / deleted / unknown
print(result.changed_reason)  # which of version, children, tags moved
print(result.current_pid)     # the identifier as it stands now
```

Runnable versions of these, offline and live, are in
[examples/](https://gitlab.com/geometalab/osmpid/-/tree/main/examples/).

## Backends

- **`OverpassClient`** — fetches an element and its descendants from an
  Overpass API endpoint.
- **`OsmApiClient`** — uses the official OSM API 0.6 endpoints. Note: it
  resolves children only one level deep — a relation's immediate members and
  the nodes of member ways, but not the members of nested sub-relations. Use
  `OverpassClient` when you need the full recursive descendant tree.
- **`GeoParquetClient`** — reads a local `osm-pbf-parquet` dataset into
  memory. It requires the `geoparquet` optional dependency.
- **`FakeOsmClient`** — provides deterministic in-memory data for tests.

Custom clients can implement the runtime-checkable `OsmClient` protocol:

```python
from osmpid import ElementType, OsmElement


class MyClient:
    def get_element_and_children(
        self,
        element_type: ElementType,
        element_id: int,
    ) -> tuple[OsmElement | None, list[OsmElement]]:
        ...
```

Neither network backend can report `deleted`: Overpass holds only current
data, and the OSM API answers 410 for a deleted element without saying what
it was. Both report `unknown` instead. Distinguishing the two needs a
history-capable backend.

### Talking to public OSM infrastructure

Overpass and the OSM API are donated infrastructure, and both usage policies
ask clients to identify themselves and to keep request rates modest. The
network clients therefore:

- send a `User-Agent` naming the library and version, which you should
  replace with one naming your application;
- issue at most one request per second per client, adjustable with
  `min_interval` (`0` disables it);
- retry 429, 502, 503, 504, and 509 up to three times, honouring
  `Retry-After` when the server sends one;
- retry stalled and dropped connections on the same budget, because that is
  how a busy Overpass instance usually declines a query;
- time out after 90 seconds on Overpass, which queues queries, and after 30
  on the OSM API, which does not.

```python
import httpx
from osmpid import OverpassClient
from osmpid.osm_client.overpass import DEFAULT_TIMEOUT

# An injected client is used as given: its User-Agent is the one that goes
# out on the wire, and its timeout is the one that applies. Set that
# yourself - httpx defaults to 5 seconds, which Overpass will regularly
# exceed just queuing the query.
transport = httpx.Client(
    headers={"User-Agent": "MyApp/2.0 (ops@example.org)"},
    timeout=DEFAULT_TIMEOUT,
)
client = OverpassClient(client=transport, min_interval=2.0)
```

Applications should handle `httpx` transport and HTTP status exceptions at
their own integration boundary.

## Development

The repository uses [uv](https://docs.astral.sh/uv/) for dependency and
environment management. See
[CONTRIBUTING.md](https://gitlab.com/geometalab/osmpid/-/blob/main/CONTRIBUTING.md)
for the full workflow.

```bash
uv sync --all-extras
uv run ruff check .
uv run ruff format --check .
uv run mypy
uv run pytest
```

Report security vulnerabilities as described in
[SECURITY.md](https://gitlab.com/geometalab/osmpid/-/blob/main/SECURITY.md),
not in a public issue.

See [CHANGELOG.md](https://gitlab.com/geometalab/osmpid/-/blob/main/CHANGELOG.md)
for release history.

## Versioning

The project follows [Semantic Versioning](https://semver.org/). Releases are
created from tags named `vX.Y.Z`; the tag must match the version in
`pyproject.toml`.

## License

`osmpid` is distributed under the MIT License. See
[LICENSE](https://gitlab.com/geometalab/osmpid/-/blob/main/LICENSE).
