osmapi.OsmApi

The OsmApi module is a wrapper for the OpenStreetMap API. As such it provides an easy access to the functionality of the API.

You can find this module on PyPI or on GitHub.

Find all information about changes of the different versions of this module in the CHANGELOG.

Notes:

  • dictionary keys are _unicode_
  • changeset is _integer_
  • version is _integer_
  • tag is a _dictionary_
  • timestamp is _unicode_
  • user is _unicode_
  • uid is _integer_
  • node lat and lon are _floats_
  • way nd is list of _integers_
  • relation member is a _list of dictionaries_ like {"role": "", "ref":123, "type": "node"}
  • All method names are in snake_case. The deprecated CamelCase versions (e.g. NodeGet) were removed in version 6.0.
  1"""
  2The OsmApi module is a wrapper for the OpenStreetMap API.
  3As such it provides an easy access to the functionality of the API.
  4
  5You can find this module [on PyPI](https://pypi.python.org/pypi/osmapi)
  6or [on GitHub](https://github.com/metaodi/osmapi).
  7
  8Find all information about changes of the different versions of this module
  9[in the CHANGELOG](https://github.com/metaodi/osmapi/blob/master/CHANGELOG.md).
 10
 11
 12## Notes:
 13
 14* **dictionary keys** are _unicode_
 15* **changeset** is _integer_
 16* **version** is _integer_
 17* **tag** is a _dictionary_
 18* **timestamp** is _unicode_
 19* **user** is _unicode_
 20* **uid** is _integer_
 21* node **lat** and **lon** are _floats_
 22* way **nd** is list of _integers_
 23* relation **member** is a _list of dictionaries_ like
 24`{"role": "", "ref":123, "type": "node"}`
 25* All method names are in snake_case. The deprecated CamelCase versions
 26(e.g. `NodeGet`) were removed in version 6.0.
 27"""
 28
 29import re
 30import logging
 31from typing import Any, NoReturn
 32from xml.dom.minidom import Element
 33import requests
 34
 35from osmapi import __version__
 36from . import errors
 37from . import http
 38from . import xmlbuilder
 39from .node import NodeMixin
 40from .way import WayMixin
 41from .relation import RelationMixin
 42from .changeset import ChangesetMixin
 43from .note import NoteMixin
 44from .capabilities import CapabilitiesMixin
 45
 46logger = logging.getLogger(__name__)
 47
 48
 49class OsmApi(
 50    NodeMixin,
 51    WayMixin,
 52    RelationMixin,
 53    ChangesetMixin,
 54    NoteMixin,
 55    CapabilitiesMixin,
 56):
 57    """
 58    Main class of osmapi, instanciate this class to use osmapi
 59    """
 60
 61    def __init__(
 62        self,
 63        appid: str = "",
 64        created_by: str = f"osmapi/{__version__}",
 65        api: str = "https://www.openstreetmap.org",
 66        session: requests.Session | None = None,
 67        timeout: int = 30,
 68    ) -> None:
 69        """
 70        Initialized the OsmApi object.
 71
 72        To make authenticated requests (i.e. anything that writes to OSM),
 73        pass an authenticated `requests.Session` as the `session` parameter,
 74        see below. Username/password authentication was shut down by
 75        OpenStreetMap in July 2024 and the corresponding parameters
 76        (`username`, `password` and `passwordfile`) were removed in
 77        version 6.0 of osmapi, use OAuth 2.0 instead.
 78
 79        To credit the application that supplies changes to OSM, an `appid`
 80        can be provided.  This is a string identifying the application.
 81        If this is omitted "osmapi" is used.
 82
 83        It is possible to configure the URL to connect to using the `api`
 84        parameter.  By default this is the SSL version of the production API
 85        of OpenStreetMap, for testing purposes, one might prefer the official
 86        test instance at "api06.dev.openstreetmap.org" or any other valid
 87        OSM-API. To use an encrypted connection (HTTPS) simply add 'https://'
 88        in front of the hostname of the `api` parameter (e.g.
 89        https://api.openstreetmap.com).
 90
 91        The `session` parameter can be used to provide a custom requests
 92        http session object (requests.Session). This is how authentication
 93        is provided: any session that authenticates its requests works, be it
 94        by `session.auth`, by an `Authorization: Bearer` header, by a custom
 95        adapter, or by a `Session` subclass that adds the token per request.
 96        Sessions are also useful for custom adapters, hooks etc.
 97
 98        Without a session, requests that require authentication raise
 99        `OsmApi.AuthenticationMissingError` before anything is sent. With a
100        session, they are sent as they are, and a missing or invalid
101        authorization is reported by the API as
102        `OsmApi.UnauthorizedApiError`.
103
104        Finally the `timeout` parameter is used by the http session to
105        throw an expcetion if the the timeout (in seconds) has passed without
106        an answer from the server.
107        """
108        # Get API
109        self._api: str = api.strip("/")
110
111        # Get created_by
112        if not appid:
113            self._created_by: str = created_by
114        else:
115            self._created_by = f"{appid} ({created_by})"
116
117        # Initialisation
118        self._current_changeset_id: int = 0
119
120        # Http connection
121        self.http_session: requests.Session | None = session
122        self._timeout: int = timeout
123        self._session: http.OsmApiSession = http.OsmApiSession(
124            self._api,
125            self._created_by,
126            session=self.http_session,
127            timeout=self._timeout,
128        )
129
130    def __enter__(self) -> "OsmApi":
131        self._session = http.OsmApiSession(
132            self._api,
133            self._created_by,
134            session=self.http_session,
135            timeout=self._timeout,
136        )
137        return self
138
139    def __exit__(self, *args: Any) -> None:
140        self.close()
141
142    def close(self) -> None:
143        if self._session:
144            self._session.close()
145
146    ##################################################
147    # Internal method                                #
148    ##################################################
149
150    def _raise_write_error(self, e: errors.ApiError) -> NoReturn:
151        """
152        Translate an `ApiError` raised by an element write into a typed error.
153
154        A 409 means either that the changeset has since been closed or that the
155        element version is out of date; a 412 means a precondition (usually a
156        referenced element) was not met. Anything else is re-raised unchanged.
157        """
158        if e.status == 409:
159            if re.search(r"The changeset .* was closed at .*", e.payload_str):
160                raise errors.ChangesetClosedApiError(
161                    e.status, e.reason, e.payload
162                ) from e
163            raise errors.VersionMismatchApiError(e.status, e.reason, e.payload) from e
164        elif e.status == 412:
165            raise errors.PreconditionFailedApiError(
166                e.status, e.reason, e.payload
167            ) from e
168        raise e
169
170    def _do(  # type: ignore[return-value]
171        self, action: str, osm_type: str, osm_data: dict[str, Any]
172    ) -> dict[str, Any]:
173        if not self._current_changeset_id:
174            raise errors.NoChangesetOpenError(
175                "You need to open a changeset before uploading data"
176            )
177        if "timestamp" in osm_data:
178            osm_data.pop("timestamp")
179        osm_data["changeset"] = self._current_changeset_id
180        if action == "create":
181            return self._do_create(osm_type, osm_data)
182        elif action == "modify":
183            return self._do_modify(osm_type, osm_data)
184        elif action == "delete":
185            return self._do_delete(osm_type, osm_data)
186
187    def _do_create(self, osm_type: str, osm_data: dict[str, Any]) -> dict[str, Any]:
188        if osm_data.get("id", -1) > 0:
189            raise errors.OsmTypeAlreadyExistsError(f"This {osm_type} already exists")
190        try:
191            result = self._session._put(
192                f"/api/0.6/{osm_type}/create",
193                xmlbuilder._xml_build(osm_type, osm_data, data=self),
194            )
195        except errors.ApiError as e:
196            self._raise_write_error(e)
197        osm_data["id"] = int(result.strip())
198        osm_data["version"] = 1
199        return osm_data
200
201    def _do_modify(self, osm_type: str, osm_data: dict[str, Any]) -> dict[str, Any]:
202        try:
203            result = self._session._put(
204                f"/api/0.6/{osm_type}/{osm_data['id']}",
205                xmlbuilder._xml_build(osm_type, osm_data, data=self),
206            )
207        except errors.ApiError as e:
208            logger.error(e.reason)
209            self._raise_write_error(e)
210        osm_data["version"] = int(result.strip())
211        return osm_data
212
213    def _do_delete(self, osm_type: str, osm_data: dict[str, Any]) -> dict[str, Any]:
214        try:
215            result = self._session._delete(
216                f"/api/0.6/{osm_type}/{osm_data['id']}",
217                xmlbuilder._xml_build(osm_type, osm_data, data=self),
218            )
219        except errors.ApiError as e:
220            self._raise_write_error(e)
221        osm_data["version"] = int(result.strip())
222        osm_data["visible"] = False
223        return osm_data
224
225    def _add_changeset_data(self, change_data: list[dict[str, Any]], type: str) -> str:
226        data = ""
227        for changed_element in change_data:
228            changed_element["changeset"] = self._current_changeset_id
229            xml_data = xmlbuilder._xml_build(type, changed_element, False, data=self)
230            data += xml_data.decode("utf-8")
231        return data
232
233    def _assign_id_and_version(
234        self, response_data: list[Element], request_data: list[dict[str, Any]]
235    ) -> None:
236        for response, element in zip(response_data, request_data):
237            element["id"] = int(response.getAttribute("new_id"))
238            element["version"] = int(response.getAttribute("new_version"))
logger = <Logger osmapi.OsmApi (WARNING)>
 50class OsmApi(
 51    NodeMixin,
 52    WayMixin,
 53    RelationMixin,
 54    ChangesetMixin,
 55    NoteMixin,
 56    CapabilitiesMixin,
 57):
 58    """
 59    Main class of osmapi, instanciate this class to use osmapi
 60    """
 61
 62    def __init__(
 63        self,
 64        appid: str = "",
 65        created_by: str = f"osmapi/{__version__}",
 66        api: str = "https://www.openstreetmap.org",
 67        session: requests.Session | None = None,
 68        timeout: int = 30,
 69    ) -> None:
 70        """
 71        Initialized the OsmApi object.
 72
 73        To make authenticated requests (i.e. anything that writes to OSM),
 74        pass an authenticated `requests.Session` as the `session` parameter,
 75        see below. Username/password authentication was shut down by
 76        OpenStreetMap in July 2024 and the corresponding parameters
 77        (`username`, `password` and `passwordfile`) were removed in
 78        version 6.0 of osmapi, use OAuth 2.0 instead.
 79
 80        To credit the application that supplies changes to OSM, an `appid`
 81        can be provided.  This is a string identifying the application.
 82        If this is omitted "osmapi" is used.
 83
 84        It is possible to configure the URL to connect to using the `api`
 85        parameter.  By default this is the SSL version of the production API
 86        of OpenStreetMap, for testing purposes, one might prefer the official
 87        test instance at "api06.dev.openstreetmap.org" or any other valid
 88        OSM-API. To use an encrypted connection (HTTPS) simply add 'https://'
 89        in front of the hostname of the `api` parameter (e.g.
 90        https://api.openstreetmap.com).
 91
 92        The `session` parameter can be used to provide a custom requests
 93        http session object (requests.Session). This is how authentication
 94        is provided: any session that authenticates its requests works, be it
 95        by `session.auth`, by an `Authorization: Bearer` header, by a custom
 96        adapter, or by a `Session` subclass that adds the token per request.
 97        Sessions are also useful for custom adapters, hooks etc.
 98
 99        Without a session, requests that require authentication raise
100        `OsmApi.AuthenticationMissingError` before anything is sent. With a
101        session, they are sent as they are, and a missing or invalid
102        authorization is reported by the API as
103        `OsmApi.UnauthorizedApiError`.
104
105        Finally the `timeout` parameter is used by the http session to
106        throw an expcetion if the the timeout (in seconds) has passed without
107        an answer from the server.
108        """
109        # Get API
110        self._api: str = api.strip("/")
111
112        # Get created_by
113        if not appid:
114            self._created_by: str = created_by
115        else:
116            self._created_by = f"{appid} ({created_by})"
117
118        # Initialisation
119        self._current_changeset_id: int = 0
120
121        # Http connection
122        self.http_session: requests.Session | None = session
123        self._timeout: int = timeout
124        self._session: http.OsmApiSession = http.OsmApiSession(
125            self._api,
126            self._created_by,
127            session=self.http_session,
128            timeout=self._timeout,
129        )
130
131    def __enter__(self) -> "OsmApi":
132        self._session = http.OsmApiSession(
133            self._api,
134            self._created_by,
135            session=self.http_session,
136            timeout=self._timeout,
137        )
138        return self
139
140    def __exit__(self, *args: Any) -> None:
141        self.close()
142
143    def close(self) -> None:
144        if self._session:
145            self._session.close()
146
147    ##################################################
148    # Internal method                                #
149    ##################################################
150
151    def _raise_write_error(self, e: errors.ApiError) -> NoReturn:
152        """
153        Translate an `ApiError` raised by an element write into a typed error.
154
155        A 409 means either that the changeset has since been closed or that the
156        element version is out of date; a 412 means a precondition (usually a
157        referenced element) was not met. Anything else is re-raised unchanged.
158        """
159        if e.status == 409:
160            if re.search(r"The changeset .* was closed at .*", e.payload_str):
161                raise errors.ChangesetClosedApiError(
162                    e.status, e.reason, e.payload
163                ) from e
164            raise errors.VersionMismatchApiError(e.status, e.reason, e.payload) from e
165        elif e.status == 412:
166            raise errors.PreconditionFailedApiError(
167                e.status, e.reason, e.payload
168            ) from e
169        raise e
170
171    def _do(  # type: ignore[return-value]
172        self, action: str, osm_type: str, osm_data: dict[str, Any]
173    ) -> dict[str, Any]:
174        if not self._current_changeset_id:
175            raise errors.NoChangesetOpenError(
176                "You need to open a changeset before uploading data"
177            )
178        if "timestamp" in osm_data:
179            osm_data.pop("timestamp")
180        osm_data["changeset"] = self._current_changeset_id
181        if action == "create":
182            return self._do_create(osm_type, osm_data)
183        elif action == "modify":
184            return self._do_modify(osm_type, osm_data)
185        elif action == "delete":
186            return self._do_delete(osm_type, osm_data)
187
188    def _do_create(self, osm_type: str, osm_data: dict[str, Any]) -> dict[str, Any]:
189        if osm_data.get("id", -1) > 0:
190            raise errors.OsmTypeAlreadyExistsError(f"This {osm_type} already exists")
191        try:
192            result = self._session._put(
193                f"/api/0.6/{osm_type}/create",
194                xmlbuilder._xml_build(osm_type, osm_data, data=self),
195            )
196        except errors.ApiError as e:
197            self._raise_write_error(e)
198        osm_data["id"] = int(result.strip())
199        osm_data["version"] = 1
200        return osm_data
201
202    def _do_modify(self, osm_type: str, osm_data: dict[str, Any]) -> dict[str, Any]:
203        try:
204            result = self._session._put(
205                f"/api/0.6/{osm_type}/{osm_data['id']}",
206                xmlbuilder._xml_build(osm_type, osm_data, data=self),
207            )
208        except errors.ApiError as e:
209            logger.error(e.reason)
210            self._raise_write_error(e)
211        osm_data["version"] = int(result.strip())
212        return osm_data
213
214    def _do_delete(self, osm_type: str, osm_data: dict[str, Any]) -> dict[str, Any]:
215        try:
216            result = self._session._delete(
217                f"/api/0.6/{osm_type}/{osm_data['id']}",
218                xmlbuilder._xml_build(osm_type, osm_data, data=self),
219            )
220        except errors.ApiError as e:
221            self._raise_write_error(e)
222        osm_data["version"] = int(result.strip())
223        osm_data["visible"] = False
224        return osm_data
225
226    def _add_changeset_data(self, change_data: list[dict[str, Any]], type: str) -> str:
227        data = ""
228        for changed_element in change_data:
229            changed_element["changeset"] = self._current_changeset_id
230            xml_data = xmlbuilder._xml_build(type, changed_element, False, data=self)
231            data += xml_data.decode("utf-8")
232        return data
233
234    def _assign_id_and_version(
235        self, response_data: list[Element], request_data: list[dict[str, Any]]
236    ) -> None:
237        for response, element in zip(response_data, request_data):
238            element["id"] = int(response.getAttribute("new_id"))
239            element["version"] = int(response.getAttribute("new_version"))

Main class of osmapi, instanciate this class to use osmapi

OsmApi( appid: str = '', created_by: str = 'osmapi/6.0.0', api: str = 'https://www.openstreetmap.org', session: requests.sessions.Session | None = None, timeout: int = 30)
 62    def __init__(
 63        self,
 64        appid: str = "",
 65        created_by: str = f"osmapi/{__version__}",
 66        api: str = "https://www.openstreetmap.org",
 67        session: requests.Session | None = None,
 68        timeout: int = 30,
 69    ) -> None:
 70        """
 71        Initialized the OsmApi object.
 72
 73        To make authenticated requests (i.e. anything that writes to OSM),
 74        pass an authenticated `requests.Session` as the `session` parameter,
 75        see below. Username/password authentication was shut down by
 76        OpenStreetMap in July 2024 and the corresponding parameters
 77        (`username`, `password` and `passwordfile`) were removed in
 78        version 6.0 of osmapi, use OAuth 2.0 instead.
 79
 80        To credit the application that supplies changes to OSM, an `appid`
 81        can be provided.  This is a string identifying the application.
 82        If this is omitted "osmapi" is used.
 83
 84        It is possible to configure the URL to connect to using the `api`
 85        parameter.  By default this is the SSL version of the production API
 86        of OpenStreetMap, for testing purposes, one might prefer the official
 87        test instance at "api06.dev.openstreetmap.org" or any other valid
 88        OSM-API. To use an encrypted connection (HTTPS) simply add 'https://'
 89        in front of the hostname of the `api` parameter (e.g.
 90        https://api.openstreetmap.com).
 91
 92        The `session` parameter can be used to provide a custom requests
 93        http session object (requests.Session). This is how authentication
 94        is provided: any session that authenticates its requests works, be it
 95        by `session.auth`, by an `Authorization: Bearer` header, by a custom
 96        adapter, or by a `Session` subclass that adds the token per request.
 97        Sessions are also useful for custom adapters, hooks etc.
 98
 99        Without a session, requests that require authentication raise
100        `OsmApi.AuthenticationMissingError` before anything is sent. With a
101        session, they are sent as they are, and a missing or invalid
102        authorization is reported by the API as
103        `OsmApi.UnauthorizedApiError`.
104
105        Finally the `timeout` parameter is used by the http session to
106        throw an expcetion if the the timeout (in seconds) has passed without
107        an answer from the server.
108        """
109        # Get API
110        self._api: str = api.strip("/")
111
112        # Get created_by
113        if not appid:
114            self._created_by: str = created_by
115        else:
116            self._created_by = f"{appid} ({created_by})"
117
118        # Initialisation
119        self._current_changeset_id: int = 0
120
121        # Http connection
122        self.http_session: requests.Session | None = session
123        self._timeout: int = timeout
124        self._session: http.OsmApiSession = http.OsmApiSession(
125            self._api,
126            self._created_by,
127            session=self.http_session,
128            timeout=self._timeout,
129        )

Initialized the OsmApi object.

To make authenticated requests (i.e. anything that writes to OSM), pass an authenticated requests.Session as the session parameter, see below. Username/password authentication was shut down by OpenStreetMap in July 2024 and the corresponding parameters (username, password and passwordfile) were removed in version 6.0 of osmapi, use OAuth 2.0 instead.

To credit the application that supplies changes to OSM, an appid can be provided. This is a string identifying the application. If this is omitted "osmapi" is used.

It is possible to configure the URL to connect to using the api parameter. By default this is the SSL version of the production API of OpenStreetMap, for testing purposes, one might prefer the official test instance at "api06.dev.openstreetmap.org" or any other valid OSM-API. To use an encrypted connection (HTTPS) simply add 'https://' in front of the hostname of the api parameter (e.g. https://api.openstreetmap.com).

The session parameter can be used to provide a custom requests http session object (requests.Session). This is how authentication is provided: any session that authenticates its requests works, be it by session.auth, by an Authorization: Bearer header, by a custom adapter, or by a Session subclass that adds the token per request. Sessions are also useful for custom adapters, hooks etc.

Without a session, requests that require authentication raise OsmApi.AuthenticationMissingError before anything is sent. With a session, they are sent as they are, and a missing or invalid authorization is reported by the API as OsmApi.UnauthorizedApiError.

Finally the timeout parameter is used by the http session to throw an expcetion if the the timeout (in seconds) has passed without an answer from the server.

http_session: requests.sessions.Session | None
def close(self) -> None:
143    def close(self) -> None:
144        if self._session:
145            self._session.close()