Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions docs/source/example.rst
Original file line number Diff line number Diff line change
Expand Up @@ -258,3 +258,28 @@ Line 20-21:

Line 22-25:
The resolved nodes have been added to the result set and are available to be used again later.

Serialization
----

Result objects can be converted to a dictionary, in the same format as the
Overpass API ``json`` output format.

.. code-block:: pycon
>>> import overpy, simplejson
>>> api = overpy.Overpass()
>>> result = api.query("[out:xml];node(50.745,7.17,50.75,7.18);out;")
>>> other_result = overpy.Result.from_json(result.to_json())
>>> assert other_result != result
>>> assert other_result.to_json() == result.to_json()
>>> assert len(result.nodes) == len(other_result.nodes)
>>> assert len(result.ways) == len(other_result.ways)

Serializing the dictionary to JSON requires rendering Decimal values as JSON
numbers, and then parsing with ``Overpass.parse_json()``. The third-party
package ``simplejson`` works for this application:

.. code-block:: pycon
>>> result_str = simplejson.dumps(result.to_json())
>>> new_result = api.parse_json(result_str)
>>> assert len(result.nodes) == len(new_result.nodes)
55 changes: 55 additions & 0 deletions overpy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,16 @@
"visible": lambda v: v.lower() == "true"
}

GLOBAL_ATTRIBUTE_SERIALIZERS: Dict[str, Callable] = {
"timestamp": lambda dt: datetime.strftime(dt, "%Y-%m-%dT%H:%M:%SZ"),
}


def _attributes_to_json(attributes: dict):
def attr_serializer(k):
return GLOBAL_ATTRIBUTE_SERIALIZERS.get(k, lambda v: v)
return {k: attr_serializer(k)(v) for k, v in attributes.items()}


def is_valid_type(
element: Union["Area", "Node", "Relation", "Way"],
Expand Down Expand Up @@ -366,6 +376,18 @@ def from_json(cls, data: dict, api: Optional[Overpass] = None) -> "Result":

return result

def to_json(self) -> dict:
def elements_to_json():
for elem_cls in [Node, Way, Relation, Area]:
for element in self.get_elements(elem_cls):
yield element.to_json()

return {
"version": 0.6,
"generator": "Overpy Serializer",
"elements": list(elements_to_json())
}

@classmethod
def from_xml(
cls,
Expand Down Expand Up @@ -664,6 +686,11 @@ def from_json(cls: Type[ElementTypeVar], data: dict, result: Optional[Result] =
"""
raise NotImplementedError

def to_json(self) -> dict:
d = {"type": self._type_value, "id": self.id, "tags": self.tags}
d.update(_attributes_to_json(self.attributes))
return d

@classmethod
def from_xml(
cls: Type[ElementTypeVar],
Expand Down Expand Up @@ -826,6 +853,12 @@ def from_json(cls, data: dict, result: Optional[Result] = None) -> "Node":

return cls(node_id=node_id, lat=lat, lon=lon, tags=tags, attributes=attributes, result=result)

def to_json(self) -> dict:
d = super().to_json()
d["lat"] = self.lat
d["lon"] = self.lon
return d

@classmethod
def from_xml(cls, child: xml.etree.ElementTree.Element, result: Optional[Result] = None) -> "Node":
"""
Expand Down Expand Up @@ -1012,6 +1045,13 @@ def from_json(cls, data: dict, result: Optional[Result] = None) -> "Way":
way_id=way_id
)

def to_json(self) -> dict:
d = super().to_json()
if self.center_lat is not None and self.center_lon is not None:
d["center"] = {"lat": self.center_lat, "lon": self.center_lon}
d["nodes"] = self._node_ids
return d

@classmethod
def from_xml(cls, child: xml.etree.ElementTree.Element, result: Optional[Result] = None) -> "Way":
"""
Expand Down Expand Up @@ -1151,6 +1191,14 @@ def from_json(cls, data: dict, result: Optional[Result] = None) -> "Relation":
result=result
)

def to_json(self) -> dict:
d = super().to_json()
if self.center_lat is not None and self.center_lon is not None:
d["center"] = {"lat": self.center_lat, "lon": self.center_lon}

d["members"] = [member.to_json() for member in self.members]
return d

@classmethod
def from_xml(cls, child: xml.etree.ElementTree.Element, result: Optional[Result] = None) -> "Relation":
"""
Expand Down Expand Up @@ -1291,6 +1339,13 @@ def from_json(cls, data: dict, result: Optional[Result] = None) -> "RelationMemb
result=result
)

def to_json(self):
d = {"type": self._type_value, "ref": self.ref, "role": self.role}
if self.geometry is not None:
d["geometry"] = [{"lat": v.lat, "lon": v.lon} for v in self.geometry]
d.update(_attributes_to_json(self.attributes))
return d

@classmethod
def from_xml(
cls,
Expand Down
16 changes: 16 additions & 0 deletions tests/test_json.py
Original file line number Diff line number Diff line change
@@ -1,62 +1,78 @@
import pytest
import simplejson

import overpy

from tests import read_file
from tests.base_class import BaseTestAreas, BaseTestNodes, BaseTestRelation, BaseTestWay


def reparse(api: overpy.Overpass, r: overpy.Result):
# we need `simplejson` because core `json` can't serialize Decimals in the way
# that we would like without enormous hacks
return api.parse_json(simplejson.dumps(r.to_json()))


class TestAreas(BaseTestAreas):
def test_area01(self):
api = overpy.Overpass()
result = api.parse_json(read_file("json/area-01.json"))
self._test_area01(result)
self._test_area01(reparse(api, result))


class TestNodes(BaseTestNodes):
def test_node01(self):
api = overpy.Overpass()
result = api.parse_json(read_file("json/node-01.json"))
self._test_node01(result)
self._test_node01(reparse(api, result))


class TestRelation(BaseTestRelation):
def test_relation01(self):
api = overpy.Overpass()
result = api.parse_json(read_file("json/relation-01.json"))
self._test_relation01(result)
self._test_relation01(reparse(api, result))

def test_relation02(self):
api = overpy.Overpass()
result = api.parse_json(read_file("json/relation-02.json"))
self._test_relation02(result)
self._test_relation02(reparse(api, result))

def test_relation03(self):
api = overpy.Overpass()
result = api.parse_json(read_file("json/relation-03.json"))
self._test_relation03(result)
self._test_relation03(reparse(api, result))

def test_relation04(self):
api = overpy.Overpass()
result = api.parse_json(read_file("json/relation-04.json"))
self._test_relation04(result)
self._test_relation04(reparse(api, result))


class TestWay(BaseTestWay):
def test_way01(self):
api = overpy.Overpass()
result = api.parse_json(read_file("json/way-01.json"))
self._test_way01(result)
self._test_way01(reparse(api, result))

def test_way02(self):
api = overpy.Overpass()
result = api.parse_json(read_file("json/way-02.json"))
self._test_way02(result)
self._test_way02(reparse(api, result))

def test_way03(self):
api = overpy.Overpass()
result = api.parse_json(read_file("json/way-03.json"))
self._test_way03(result)
self._test_way03(reparse(api, result))

def test_way04(self):
api = overpy.Overpass()
Expand Down
1 change: 1 addition & 0 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ envlist = py36,py37,py38,py39,pypy3
[testenv]
deps =
pytest
simplejson
pytest-cov
commands = pytest --cov overpy --cov-report=term-missing -v tests/

Expand Down