[Git][debian-gis-team/pyosmium][master] 5 commits: New upstream version 3.5.0

Bas Couwenberg (@sebastic) gitlab at salsa.debian.org
Wed Nov 9 16:37:20 GMT 2022



Bas Couwenberg pushed to branch master at Debian GIS Project / pyosmium


Commits:
52a0bf72 by Bas Couwenberg at 2022-11-09T17:13:27+01:00
New upstream version 3.5.0
- - - - -
159e21cb by Bas Couwenberg at 2022-11-09T17:13:31+01:00
Update upstream source from tag 'upstream/3.5.0'

Update to upstream version '3.5.0'
with Debian dir b2c873d5b9c52c38e09632344ed3e371b7bbf130
- - - - -
ad5ff9e9 by Bas Couwenberg at 2022-11-09T17:14:10+01:00
New upstream release.

- - - - -
927fb086 by Bas Couwenberg at 2022-11-09T17:24:44+01:00
Update lintian overrides.

- - - - -
d67ab4c2 by Bas Couwenberg at 2022-11-09T17:24:44+01:00
Set distribution to unstable.

- - - - -


27 changed files:

- CHANGELOG.md
- debian/changelog
- debian/python3-pyosmium.lintian-overrides
- lib/osm.cc
- lib/osmium.cc
- lib/simple_writer.cc
- setup.py
- + src/osmium/_osmium.pyi
- + src/osmium/geom.pyi
- src/osmium/helper.py
- + src/osmium/index.pyi
- + src/osmium/io.pyi
- src/osmium/osm/__init__.py
- + src/osmium/osm/_osm.pyi
- src/osmium/osm/mutable.py
- + src/osmium/py.typed
- + src/osmium/replication/_replication.pyi
- src/osmium/replication/server.py
- src/osmium/replication/utils.py
- src/osmium/version.py
- test/helpers.py
- test/test_osm.py
- test/test_pyosmium_get_changes.py
- test/test_taglist.py
- test/test_writer.py
- tools/pyosmium-get-changes
- tools/pyosmium-up-to-date


Changes:

=====================================
CHANGELOG.md
=====================================
@@ -4,6 +4,32 @@
 All notable changes to this project will be documented in this file.
 This project adheres to [Semantic Versioning](http://semver.org/).
 
+## [3.5.0] - 2022-11-09
+
+### Added
+
+- type annotations for the public interface
+- new `ReplicationServer.set_request_parameter()` function to specify additional
+  parameters to give to `requests.get()`
+
+### Fixed
+
+- writer now rolls back data buffer after exceptions (fixes #212)
+- off-by-one error in computation of change ID from a start date
+- socket timeout in pyosmium-get-changes/pyosmium-up-to-date was ignored
+  falling back to waiting forever on requests
+
+### Changed
+
+- use format strings instead of `format()` where possible
+- pyosmium-get-changes now prints an error message when there is a HTTP error
+  during download
+- overwriting `ReplicationServer.open_url()` is no longer recommended,
+  use new `ReplicationServer.set_request_parameter()` function instead
+- cookies for pyosmium-get-changes/pyosmium-up-to-date are now set via
+  request parameters, removing the last use-case where urllib was used
+- update bundled pybind11 to 2.10.1
+
 ## [3.4.1] - 2022-07-28
 
 ### Fixed


=====================================
debian/changelog
=====================================
@@ -1,3 +1,10 @@
+pyosmium (3.5.0-1) unstable; urgency=medium
+
+  * New upstream release.
+  * Update lintian overrides.
+
+ -- Bas Couwenberg <sebastic at debian.org>  Wed, 09 Nov 2022 17:15:15 +0100
+
 pyosmium (3.4.1-1) unstable; urgency=medium
 
   * New upstream release.


=====================================
debian/python3-pyosmium.lintian-overrides
=====================================
@@ -2,3 +2,6 @@
 #  Fortify Source functions: no, only unprotected functions found!
 hardening-no-fortify-functions *
 
+# False positive, lat/lon
+spelling-error-in-binary lon long *
+


=====================================
lib/osm.cc
=====================================
@@ -102,7 +102,8 @@ PYBIND11_MODULE(_osm, m) {
              py::return_value_policy::reference_internal,
              "Extend the box to include the given location. If the location "
              "is invalid the box remains unchanged. If the box is invalid, it "
-             "will contain only the location after the operation.")
+             "will contain only the location after the operation. "
+             "Returns a reference to itself.")
         .def("extend",
              (osmium::Box& (osmium::Box::*)(osmium::Box const &))
                  &osmium::Box::extend,
@@ -110,7 +111,8 @@ PYBIND11_MODULE(_osm, m) {
              py::return_value_policy::reference_internal,
              "Extend the box to include the given box. If the box to be added "
              "is invalid the input box remains unchanged. If the input box is invalid, it "
-             "will become equal to the box that was added.")
+             "will become equal to the box that was added. "
+             "Returns a reference to itself.")
         .def("valid", &osmium::Box::valid,
              "Check if the box coordinates are defined and with the usual bounds.")
         .def("size", &osmium::Box::size,


=====================================
lib/osmium.cc
=====================================
@@ -37,7 +37,7 @@ PYBIND11_MODULE(_osmium, m) {
           "Apply a chain of handlers.");
     m.def("apply", [](osmium::io::Reader &rd, NodeLocationHandler &h)
                    { py::gil_scoped_release release; osmium::apply(rd, h); },
-          py::arg("reader"), py::arg("handler"),
+          py::arg("reader"), py::arg("node_handler"),
           "Apply a chain of handlers.");
     m.def("apply", [](osmium::io::Reader &rd, NodeLocationHandler &l,
                       BaseHandler &h)


=====================================
lib/simple_writer.cc
=====================================
@@ -30,6 +30,12 @@ public:
 
     void add_osmium_object(const osmium::OSMObject& o)
     {
+        if (!buffer) {
+            throw std::runtime_error{"Writer already closed."};
+        }
+
+        buffer.rollback();
+
         buffer.add_item(o);
         flush_buffer();
     }
@@ -40,6 +46,8 @@ public:
             throw std::runtime_error{"Writer already closed."};
         }
 
+        buffer.rollback();
+
         if (py::isinstance<osmium::Node>(o)) {
             buffer.add_item(o.cast<osmium::Node &>());
         } else {
@@ -65,6 +73,8 @@ public:
             throw std::runtime_error{"Writer already closed."};
         }
 
+        buffer.rollback();
+
         if (py::isinstance<osmium::Way>(o)) {
             buffer.add_item(o.cast<osmium::Way &>());
         } else {
@@ -88,6 +98,8 @@ public:
             throw std::runtime_error{"Writer already closed."};
         }
 
+        buffer.rollback();
+
         if (py::isinstance<osmium::Relation>(o)) {
             buffer.add_item(o.cast<osmium::Relation &>());
         } else {


=====================================
setup.py
=====================================
@@ -166,6 +166,9 @@ setup(
     ext_modules=[CMakeExtension('cmake_example')],
     packages = ['osmium', 'osmium/osm', 'osmium/replication'],
     package_dir = {'' : 'src'},
+    package_data = { 'osmium': ['py.typed', '*.pyi',
+                                'replication/_replication.pyi',
+                                'osm/_osm.pyi']},
     python_requires = ">=3.6",
     install_requires = ['requests'],
     cmdclass=dict(build_ext=CMakeBuild, sdist=Pyosmium_sdist),


=====================================
src/osmium/_osmium.pyi
=====================================
@@ -0,0 +1,55 @@
+from typing import overload, ByteString, Union
+import os
+
+import osmium.index
+import osmium.io
+
+StrPath = Union[str, 'os.PathLike[str]']
+
+class InvalidLocationError(Exception): ...
+
+class NodeLocationsForWays:
+    def __init__(self, locations: osmium.index.LocationTable) -> None: ...
+    def ignore_errors(self) -> None: ...
+
+class BaseHandler: ...
+
+class SimpleHandler(BaseHandler):
+    def __init__(self) -> None: ...
+    def apply_buffer(self, buffer: Union[ByteString, str], format: str, locations: bool = ..., idx: str = ...) -> None: ...
+    def apply_file(self, filename: StrPath, locations: bool = ..., idx: str = ...) -> None: ...
+
+class MergeInputReader:
+    def __init__(self) -> None: ...
+    def add_buffer(self, buffer: Union[ByteString, str], format: str) -> int: ...
+    def add_file(self, file: str) -> int: ...
+    def apply(self, handler: BaseHandler, idx: str = ..., simplify: bool = ...) -> None: ...
+    def apply_to_reader(self, reader: osmium.io.Reader, writer: osmium.io.Writer, with_history: bool = ...) -> None: ...
+
+class WriteHandler(BaseHandler):
+    @overload
+    def __init__(self, filename: str, bufsz: int, filetype: str) -> None: ...
+    @overload
+    def __init__(self, filename: str, bufsz: int) -> None: ...
+    @overload
+    def __init__(self, filename: str) -> None: ...
+    def close(self) -> None: ...
+
+class SimpleWriter:
+    @overload
+    def __init__(self, filename: str, bufsz: int, header: osmium.io.Header) -> None: ...
+    @overload
+    def __init__(self, filename: str, bufsz: int) -> None: ...
+    @overload
+    def __init__(self, filename: str) -> None: ...
+    def add_node(self, node: object) -> None: ...
+    def add_relation(self, relation: object) -> None: ...
+    def add_way(self, way: object) -> None: ...
+    def close(self) -> None: ...
+
+ at overload
+def apply(reader: osmium.io.Reader, handler: BaseHandler) -> None: ...
+ at overload
+def apply(reader: osmium.io.Reader, node_handler: NodeLocationsForWays) -> None: ...
+ at overload
+def apply(reader: osmium.io.Reader, node_handler: NodeLocationsForWays, handler: BaseHandler) -> None: ...


=====================================
src/osmium/geom.pyi
=====================================
@@ -0,0 +1,99 @@
+from typing import ClassVar
+
+from typing import overload
+import osmium.osm
+ALL: use_nodes
+BACKWARD: direction
+FORWARD: direction
+UNIQUE: use_nodes
+
+
+class use_nodes:
+    ALL: ClassVar[use_nodes] = ...
+    UNIQUE: ClassVar[use_nodes] = ...
+    def __init__(self, value: int) -> None: ...
+    @property
+    def name(self) -> str: ...
+    @property
+    def value(self) -> int: ...
+
+class direction:
+    BACKWARD: ClassVar[direction] = ...
+    FORWARD: ClassVar[direction] = ...
+    def __init__(self, value: int) -> None: ...
+    @property
+    def name(self) -> str: ...
+    @property
+    def value(self) -> int: ...
+
+class Coordinates:
+    @overload
+    def __init__(self) -> None: ...
+    @overload
+    def __init__(self, cx: float, cy: float) -> None: ...
+    @overload
+    def __init__(self, location: osmium.osm.Location) -> None: ...
+    def valid(self) -> bool: ...
+    @property
+    def x(self) -> float: ...
+    @property
+    def y(self) -> float: ...
+
+
+class GeoJSONFactory:
+    def __init__(self) -> None: ...
+    @overload
+    def create_linestring(self, list: osmium.osm.WayNodeList, use_nodes: use_nodes = ..., direction: direction = ...) -> str: ...
+    @overload
+    def create_linestring(self, way: osmium.osm.Way, use_nodes: use_nodes = ..., direction: direction = ...) -> str: ...
+    def create_multipolygon(self, area: osmium.osm.Area) -> str: ...
+    @overload
+    def create_point(self, location: osmium.osm.Location) -> str: ...
+    @overload
+    def create_point(self, node: osmium.osm.Node) -> str: ...
+    @overload
+    def create_point(self, ref: osmium.osm.NodeRef) -> str: ...
+    @property
+    def epsg(self) -> int: ...
+    @property
+    def proj_string(self) -> str: ...
+
+class WKBFactory:
+    def __init__(self) -> None: ...
+    @overload
+    def create_linestring(self, list: osmium.osm.WayNodeList, use_nodes: use_nodes = ..., direction: direction = ...) -> str: ...
+    @overload
+    def create_linestring(self, way: osmium.osm.Way, use_nodes: use_nodes = ..., direction: direction = ...) -> str: ...
+    def create_multipolygon(self, area: osmium.osm.Area) -> str: ...
+    @overload
+    def create_point(self, location: osmium.osm.Location) -> str: ...
+    @overload
+    def create_point(self, node: osmium.osm.Node) -> str: ...
+    @overload
+    def create_point(self, ref: osmium.osm.NodeRef) -> str: ...
+    @property
+    def epsg(self) -> int: ...
+    @property
+    def proj_string(self) -> str: ...
+
+class WKTFactory:
+    def __init__(self) -> None: ...
+    @overload
+    def create_linestring(self, list: osmium.osm._osm.WayNodeList, use_nodes: use_nodes = ..., direction: direction = ...) -> str: ...
+    @overload
+    def create_linestring(self, way: osmium.osm._osm.Way, use_nodes: use_nodes = ..., direction: direction = ...) -> str: ...
+    def create_multipolygon(self, area: osmium.osm._osm.Area) -> str: ....
+    @overload
+    def create_point(self, location: osmium.osm._osm.Location) -> str: ...
+    @overload
+    def create_point(self, node: osmium.osm._osm.Node) -> str: ...
+    @overload
+    def create_point(self, ref: osmium.osm._osm.NodeRef) -> str: ...
+    @property
+    def epsg(self) -> int: ...
+    @property
+    def proj_string(self) -> str: ...
+
+def haversine_distance(list: osmium.osm.WayNodeList) -> float: ...
+def lonlat_to_mercator(coordinate: Coordinates) -> Coordinates: ...
+def mercator_to_lonlat(coordinate: Coordinates) -> Coordinates: ...


=====================================
src/osmium/helper.py
=====================================
@@ -1,6 +1,17 @@
+from typing import Optional, Callable, TypeVar
+
 from osmium._osmium import SimpleHandler
+from osmium.osm import Node, Way, Relation, Area, Changeset
+
+T = TypeVar('T')
+HandlerFunc = Optional[Callable[[T], None]]
+
 
-def make_simple_handler(node=None, way=None, relation=None, area=None, changeset=None):
+def make_simple_handler(node: HandlerFunc[Node] = None,
+                        way: HandlerFunc[Way] = None,
+                        relation: HandlerFunc[Relation] = None,
+                        area: HandlerFunc[Area] = None,
+                        changeset: HandlerFunc[Changeset] = None) -> SimpleHandler:
     """ Convenience function that creates a `SimpleHandler` from a set of
         callback functions. Each of the parameters takes an optional callable
         that must expect a single positional parameter with the object being
@@ -10,14 +21,14 @@ def make_simple_handler(node=None, way=None, relation=None, area=None, changeset
         pass
 
     if node is not None:
-        __HandlerWithCallbacks.node = staticmethod(node)
+        setattr(__HandlerWithCallbacks, "node", staticmethod(node))
     if way is not None:
-        __HandlerWithCallbacks.way = staticmethod(way)
+        setattr(__HandlerWithCallbacks, "way", staticmethod(way))
     if relation is not None:
-        __HandlerWithCallbacks.relation = staticmethod(relation)
+        setattr(__HandlerWithCallbacks, "relation", staticmethod(relation))
     if area is not None:
-        __HandlerWithCallbacks.area = staticmethod(area)
+        setattr(__HandlerWithCallbacks, "area", staticmethod(area))
     if changeset is not None:
-        __HandlerWithCallbacks.changeset = staticmethod(changeset)
+        setattr(__HandlerWithCallbacks, "changeset", staticmethod(changeset))
 
     return __HandlerWithCallbacks()


=====================================
src/osmium/index.pyi
=====================================
@@ -0,0 +1,12 @@
+from typing import List
+
+import osmium.osm
+
+class LocationTable:
+    def clear(self) -> None: ...
+    def get(self, id: int) -> osmium.osm.Location: ...
+    def set(self, id: int, loc: osmium.osm.Location) -> None: ...
+    def used_memory(self) -> int: ...
+
+def create_map(map_type: str) -> LocationTable: ...
+def map_types() -> List[str]: ...


=====================================
src/osmium/io.pyi
=====================================
@@ -0,0 +1,41 @@
+from typing import Any
+
+from typing import overload
+
+import osmium.osm
+
+class File:
+    has_multiple_object_versions: bool
+    @overload
+    def __init__(self, filename: str) -> None: ...
+    @overload
+    def __init__(self, filename: str, format: str) -> None: ...
+    def parse_format(self, arg0: str) -> None: ...
+
+class Header:
+    has_multiple_object_versions: bool
+    def __init__(self) -> None: ...
+    def add_box(self, box: osmium.osm.Box) -> Header: ...
+    def box(self) -> osmium.osm.Box: ...
+    def get(self, key: str, default: str = ...) -> str: ...
+    def set(self, key: str, value: str) -> None: ...
+
+class Reader:
+    @overload
+    def __init__(self, filename: str) -> None: ...
+    @overload
+    def __init__(self, filename: str, types: osmium.osm.osm_entity_bits) -> None: ...
+    def close(self) -> None: ...
+    def eof(self) -> bool: ...
+    def header(self) -> Header: ...
+
+class Writer:
+    @overload
+    def __init__(self, filename: str) -> None: ...
+    @overload
+    def __init__(self, ffile: File) -> None: ...
+    @overload
+    def __init__(self, filename: str, header: Header) -> None: ...
+    @overload
+    def __init__(self, ffile: File, header: Header) -> None: ...
+    def close(self) -> int: ...


=====================================
src/osmium/osm/__init__.py
=====================================
@@ -1,97 +1,79 @@
-import osmium.osm.mutable
+from typing import Any, Callable, Sequence
+
+from osmium.osm.mutable import create_mutable_node, create_mutable_way, create_mutable_relation
 from ._osm import *
 
-def create_mutable_node(node, **args):
-    """ Create a mutable node replacing the properties given in the
-        named parameters. Note that this function only creates a shallow
-        copy which is still bound to the scope of the original object.
-    """
-    return osmium.osm.mutable.Node(base=node, **args)
-
-def create_mutable_way(way, **args):
-    """ Create a mutable way replacing the properties given in the
-        named parameters. Note that this function only creates a shallow
-        copy which is still bound to the scope of the original object.
-    """
-    return osmium.osm.mutable.Way(base=way, **args)
-
-def create_mutable_relation(rel, **args):
-    """ Create a mutable relation replacing the properties given in the
-        named parameters. Note that this function only creates a shallow
-        copy which is still bound to the scope of the original object.
-    """
-    return osmium.osm.mutable.Relation(base=rel, **args)
-
-Node.replace = create_mutable_node
-Way.replace = create_mutable_way
-Relation.replace = create_mutable_relation
-
-def _make_repr(attr_list):
+setattr(Node, 'replace', create_mutable_node)
+setattr(Way, 'replace', create_mutable_way)
+setattr(Relation, 'replace', create_mutable_relation)
+
+def _make_repr(*attrs: str) -> Callable[[object], str]:
     fmt_string = 'osmium.osm.{0}('\
-                 + ', '.join(['{0}={{1.{0}!r}}'.format(x) for x in attr_list])\
+                 + ', '.join([f'{x}={{1.{x}!r}}' for x in attrs])\
                  + ')'
 
     return lambda o: fmt_string.format(o.__class__.__name__, o)
 
-def _list_repr(obj):
+def _list_repr(obj: Sequence[Any]) -> str:
     return 'osmium.osm.{}([{}])'.format(obj.__class__.__name__,
                                         ', '.join(map(repr, obj)))
 
-def _list_elipse(obj):
+def _list_elipse(obj: Sequence[Any]) -> str:
     objects = ','.join(map(str, obj))
     if len(objects) > 50:
         objects = objects[:47] + '...'
     return objects
 
-Location.__repr__ = lambda l: 'osmium.osm.Location(x={0.x!r}, y={0.y!r})'.format(l) \
-                               if l.valid() else 'osmium.osm.Location()'
-Location.__str__ = lambda l: '{:f}/{:f}'.format(l.lon_without_check(),
-                                                l.lat_without_check()) \
-                             if l.valid() else 'invalid'
+setattr(Location, '__repr__',
+        lambda l: f'osmium.osm.Location(x={l.x!r}, y={l.y!r})'
+                      if l.valid() else 'osmium.osm.Location()')
+setattr(Location, '__str__',
+        lambda l: f'{l.lon_without_check():.7f}/{l.lat_without_check():.7f}'
+                      if l.valid() else 'invalid')
 
-Box.__repr__ = _make_repr(['bottom_left', 'top_right'])
-Box.__str__ = lambda b: '({0.bottom_left!s} {0.top_right!s})'.format(b)
+setattr(Box, '__repr__', _make_repr('bottom_left', 'top_right'))
+setattr(Box, '__str__', lambda b: f'({b.bottom_left!s} {b.top_right!s})')
 
-Tag.__repr__ = _make_repr(['k', 'v'])
-Tag.__str__ = lambda t: '{0.k}={0.v}'.format(t)
+setattr(Tag, '__repr__', _make_repr('k', 'v'))
+setattr(Tag, '__str__', lambda t: f'{t.k}={t.v}')
 
-TagList.__repr__ = lambda t: "osmium.osm.TagList({%s})" \
-                              % ', '.join(["%r: %r" % (i.k, i.v) for i in t])
-TagList.__str__ = lambda t: '{' + _list_elipse(t) + '}'
+setattr(TagList, '__repr__', lambda t: "osmium.osm.TagList({%s})"
+                                       % ', '.join([f"{i.k!r}: {i.v!r}" for i in t]))
+setattr(TagList, '__str__', lambda t: f'{{{_list_elipse(t)}}}')
 
-NodeRef.__repr__ = _make_repr(['ref', 'location'])
-NodeRef.__str__ = lambda n: '{0.ref:d}@{0.location!s}'.format(n) \
-                            if n.location.valid() else str(n.ref)
+setattr(NodeRef, '__repr__', _make_repr('ref', 'location'))
+setattr(NodeRef, '__str__', lambda n: f'{n.ref:d}@{n.location!s}'
+                                     if n.location.valid() else str(n.ref))
 
-NodeRefList.__repr__ = _list_repr
-NodeRefList.__str__ = lambda o: '[' + _list_elipse(o) + ']'
+setattr(NodeRefList, '__repr__', _list_repr)
+setattr(NodeRefList, '__str__', lambda o: f'[{_list_elipse(o)}]')
 
-RelationMember.__repr__ = _make_repr(['ref', 'type', 'role'])
-RelationMember.__str__ = lambda r: ('{0.type}{0.ref:d}@{0.role}' \
-                                   if r.role else '{0.type}{0.ref:d}').format(r)
+setattr(RelationMember, '__repr__', _make_repr('ref', 'type', 'role'))
+setattr(RelationMember, '__str__', lambda r: f'{r.type}{r.ref:d}@{r.role}' \
+                                             if r.role else f'{r.type}{r.ref:d}')
 
-RelationMemberList.__repr__ = _list_repr
-RelationMemberList.__str__ = lambda o: '[' + _list_elipse(o) + ']'
+setattr(RelationMemberList, '__repr__', _list_repr)
+setattr(RelationMemberList, '__str__', lambda o: f'[{_list_elipse(o)}]')
 
-OSMObject.__repr__ = _make_repr(['id', 'deleted', 'visible', 'version', 'changeset',
-                                 'uid', 'timestamp', 'user', 'tags'])
+setattr(OSMObject, '__repr__', _make_repr('id', 'deleted', 'visible', 'version',
+                                          'changeset', 'uid', 'timestamp', 'user',
+                                          'tags'))
 
-Node.__repr__ = _make_repr(['id', 'deleted', 'visible', 'version', 'changeset',
-                            'uid', 'timestamp', 'user', 'tags', 'location'])
-Node.__str__ = lambda n: 'n{0.id:d}: location={0.location!s} tags={0.tags!s}'\
-                         .format(n)
+setattr(Node, '__repr__', _make_repr('id', 'deleted', 'visible', 'version',
+                                     'changeset', 'uid', 'timestamp', 'user',
+                                     'tags', 'location'))
+setattr(Node, '__str__', lambda n: f'n{n.id:d}: location={n.location!s} tags={n.tags!s}')
 
-Way.__repr__ = _make_repr(['id', 'deleted', 'visible', 'version', 'changeset',
-                           'uid', 'timestamp', 'user', 'tags', 'nodes'])
-Way.__str__ = lambda o: 'w{0.id:d}: nodes={0.nodes!s} tags={0.tags!s}' \
-                         .format(o)
+setattr(Way, '__repr__', _make_repr('id', 'deleted', 'visible', 'version', 'changeset',
+                                   'uid', 'timestamp', 'user', 'tags', 'nodes'))
+setattr(Way, '__str__', lambda o: f'w{o.id:d}: nodes={o.nodes!s} tags={o.tags!s}')
 
-Relation.__repr__ = _make_repr(['id', 'deleted', 'visible', 'version', 'changeset',
-                                'uid', 'timestamp', 'user', 'tags', 'members'])
-Relation.__str__ = lambda o: 'r{0.id:d}: members={0.members!s}, tags={0.tags!s}' \
-                             .format(o)
+setattr(Relation, '__repr__', _make_repr('id', 'deleted', 'visible', 'version',
+                                         'changeset', 'uid', 'timestamp', 'user',
+                                         'tags', 'members'))
+setattr(Relation, '__str__', lambda o: f'r{o.id:d}: members={o.members!s}, tags={o.tags!s}')
 
-Changeset.__repr__ = _make_repr(['id', 'uid', 'created_at', 'closed_at', 'open',
-                                 'num_changes', 'bounds', 'user', 'tags'])
-Changeset.__str__ = lambda o: 'c{0.id:d}: closed_at={0.closed_at!s}, bounds={0.bounds!s}, tags={0.tags!s}' \
-                              .format(o)
+setattr(Changeset, '__repr__', _make_repr('id', 'uid', 'created_at', 'closed_at',
+                                          'open', 'num_changes', 'bounds', 'user',
+                                          'tags'))
+setattr(Changeset, '__str__', lambda o: f'c{o.id:d}: closed_at={o.closed_at!s}, bounds={o.bounds!s}, tags={o.tags!s}')


=====================================
src/osmium/osm/_osm.pyi
=====================================
@@ -0,0 +1,202 @@
+from typing import ClassVar, Iterator, Tuple, Optional, Any
+
+from typing import overload
+import datetime
+
+import osmium.osm.mutable
+
+ALL: osm_entity_bits
+AREA: osm_entity_bits
+CHANGESET: osm_entity_bits
+NODE: osm_entity_bits
+NOTHING: osm_entity_bits
+OBJECT: osm_entity_bits
+RELATION: osm_entity_bits
+WAY: osm_entity_bits
+
+
+class Location:
+    @overload
+    def __init__(self) -> None: ...
+    @overload
+    def __init__(self, lon: float, lat: float) -> None: ...
+    def lat_without_check(self) -> float: ...
+    def lon_without_check(self) -> float: ...
+    def valid(self) -> bool: ...
+    @property
+    def lat(self) -> float: ...
+    @property
+    def lon(self) -> float: ...
+    @property
+    def x(self) -> int: ...
+    @property
+    def y(self) -> int: ...
+
+class Box:
+    @overload
+    def __init__(self, minx: float, miny: float, maxx: float, maxy: float) -> None: ...
+    @overload
+    def __init__(self, bottom_left: Location, top_right: Location) -> None: ...
+    def contains(self, location: Location) -> bool: ...
+    @overload
+    def extend(self, location: Location) -> Box: ...
+    @overload
+    def extend(self, box: Box) -> Box: ...
+    def size(self) -> float: ...
+    def valid(self) -> bool: ...
+    @property
+    def bottom_left(self) -> Location: ...
+    @property
+    def top_right(self) -> Location: ...
+
+class TagIterator:
+    def __iter__(self) -> TagIterator: ...
+    def __len__(self) -> int: ...
+    def __next__(self) -> str: ...
+
+class Tag:
+    def __iter__(self) -> TagIterator: ...
+    @property
+    def k(self) -> str: ...
+    @property
+    def v(self) -> str: ...
+
+class TagList:
+    @overload
+    def get(self, key: str, default: str) -> str: ...
+    @overload
+    def get(self, key: str) -> Optional[str]: ...
+    def __contains__(self, key: str) -> bool: ...
+    def __getitem__(self, key: str) -> str: ...
+    def __iter__(self) -> Iterator[Tag]: ...
+    def __len__(self) -> int: ...
+
+class NodeRef:
+    @property
+    def lat(self) -> float: ...
+    @property
+    def location(self) -> Location: ...
+    @property
+    def lon(self) -> float: ...
+    @property
+    def ref(self) -> int: ...
+    @property
+    def x(self) -> int: ...
+    @property
+    def y(self) -> int: ...
+
+class RelationMember:
+    @property
+    def ref(self) -> int: ...
+    @property
+    def role(self) -> str: ...
+    @property
+    def type(self) -> str: ...
+
+class RelationMemberList:
+    def __iter__(self) -> Iterator[RelationMember]: ...
+    def __len__(self) -> int: ...
+
+class NodeRefList:
+    def ends_have_same_id(self) -> bool: ...
+    def ends_have_same_location(self) -> bool: ...
+    def is_closed(self) -> bool: ...
+    def __getitem__(self, idx: int) -> NodeRef: ...
+    def __iter__(self) -> Iterator[NodeRef]: ...
+    def __len__(self) -> int: ...
+
+class WayNodeList(NodeRefList):
+    pass
+
+class OuterRing(NodeRefList):
+    pass
+
+class InnerRing(NodeRefList):
+    pass
+
+class InnerRingIterator:
+    def __iter__(self) -> Iterator[InnerRing]: ...
+
+class OSMObject:
+    def positive_id(self) -> int: ...
+    def user_is_anonymous(self) -> bool: ...
+    @property
+    def changeset(self) -> int: ...
+    @property
+    def deleted(self) -> bool: ...
+    @property
+    def id(self) -> int: ...
+    @property
+    def tags(self) -> TagList: ...
+    @property
+    def timestamp(self) -> datetime.datetime: ...
+    @property
+    def uid(self) -> int: ...
+    @property
+    def user(self) -> str: ...
+    @property
+    def version(self) -> int: ...
+    @property
+    def visible(self) -> bool: ...
+
+class Node(OSMObject):
+    @property
+    def location(self) -> Location: ...
+    def replace(**args: Any) -> osmium.osm.mutable.Node: ...
+
+class Way(OSMObject):
+    def ends_have_same_id(self) -> bool: ...
+    def ends_have_same_location(self) -> bool: ...
+    def is_closed(self) -> bool: ...
+    @property
+    def nodes(self) -> WayNodeList: ...
+    def replace(**args: Any) -> osmium.osm.mutable.Way: ...
+
+class Relation(OSMObject):
+    @property
+    def members(self) -> RelationMemberList: ...
+    def replace(**args: Any) -> osmium.osm.mutable.Relation: ...
+
+class Area(OSMObject):
+    def from_way(self) -> bool: ...
+    def inner_rings(self, outer_ring: OuterRing) -> InnerRingIterator: ...
+    def is_multipolygon(self) -> bool: ...
+    def num_rings(self) -> Tuple[int,int]: ...
+    def orig_id(self) -> int: ...
+    def outer_rings(self) -> Iterator[OuterRing]: ...
+
+class Changeset:
+    def user_is_anonymous(self) -> bool: ...
+    @property
+    def bounds(self) -> Box: ...
+    @property
+    def closed_at(self) -> datetime.datetime: ...
+    @property
+    def created_at(self) -> datetime.datetime: ...
+    @property
+    def id(self) -> int: ...
+    @property
+    def num_changes(self) -> int: ...
+    @property
+    def open(self) -> bool: ...
+    @property
+    def tags(self) -> TagList: ...
+    @property
+    def uid(self) -> int: ...
+    @property
+    def user(self) -> str: ...
+
+class osm_entity_bits:
+    ALL: ClassVar[osm_entity_bits] = ...
+    AREA: ClassVar[osm_entity_bits] = ...
+    CHANGESET: ClassVar[osm_entity_bits] = ...
+    NODE: ClassVar[osm_entity_bits] = ...
+    NOTHING: ClassVar[osm_entity_bits] = ...
+    OBJECT: ClassVar[osm_entity_bits] = ...
+    RELATION: ClassVar[osm_entity_bits] = ...
+    WAY: ClassVar[osm_entity_bits] = ...
+    def __init__(self, value: int) -> None: ...
+    @property
+    def name(self) -> str: ...
+    @property
+    def value(self) -> int: ...


=====================================
src/osmium/osm/mutable.py
=====================================
@@ -1,4 +1,21 @@
-class OSMObject(object):
+from typing import Optional, Union, Any, Mapping, Sequence, Tuple, TYPE_CHECKING
+from datetime import datetime
+
+if TYPE_CHECKING:
+    import osmium.osm
+
+    OSMObjectLike = Union['OSMObject', osmium.osm.OSMObject]
+    NodeLike = Union[Node, osmium.osm.Node]
+    WayLike = Union[Way, osmium.osm.Way]
+    RelationLike = Union[Relation, osmium.osm.Relation]
+
+    TagSequence = Union[osmium.osm.TagList, Mapping[str, str], Sequence[Tuple[str, str]]]
+    LocationLike = Union[osmium.osm.Location, Tuple[float, float]]
+    NodeSequence = Union[osmium.osm.NodeRefList, Sequence[Union[osmium.osm.NodeRef, int]]]
+    MemberSequence = Union[osmium.osm.RelationMemberList,
+                       Sequence[Union[osmium.osm.RelationMember, Tuple[str, int, str]]]]
+
+class OSMObject:
     """Mutable version of ``osmium.osm.OSMObject``. It exposes the following
        attributes ``id``, ``version``, ``visible``, ``changeset``, ``timestamp``,
        ``uid`` and ``tags``. Timestamps may be strings or datetime objects.
@@ -9,8 +26,11 @@ class OSMObject(object):
        will be initialised first from the attributes of this base object..
     """
 
-    def __init__(self, base=None, id=None, version=None, visible=None, changeset=None,
-                 timestamp=None, uid=None, tags=None, user=None):
+    def __init__(self, base: Optional['OSMObjectLike'] = None,
+                 id: Optional[int] = None, version: Optional[int] = None,
+                 visible: Optional[bool] = None, changeset: Optional[int] = None,
+                 timestamp: Optional[datetime] = None, uid: Optional[int] = None,
+                 tags: Optional['TagSequence'] = None, user: Optional[str] = None) -> None:
         if base is None:
             self.id = id
             self.version = version
@@ -37,7 +57,9 @@ class Node(OSMObject):
        may either be an `osmium.osm.Location` or a tuple of lon/lat coordinates.
     """
 
-    def __init__(self, base=None, location=None, **attrs):
+    def __init__(self, base: Optional['NodeLike'] = None,
+                 location: Optional['LocationLike'] = None,
+                 **attrs: Any) -> None:
         OSMObject.__init__(self, base=base, **attrs)
         if base is None:
             self.location = location
@@ -52,7 +74,8 @@ class Way(OSMObject):
        ``osmium.osm.NodeRef`` or simple node ids.
     """
 
-    def __init__(self, base=None, nodes=None, **attrs):
+    def __init__(self, base: Optional['WayLike'] = None,
+                 nodes: Optional['NodeSequence'] = None, **attrs: Any) -> None:
         OSMObject.__init__(self, base=base, **attrs)
         if base is None:
             self.nodes = nodes
@@ -67,9 +90,33 @@ class Relation(OSMObject):
        member type should be a single character 'n', 'w' or 'r'.
     """
 
-    def __init__(self, base=None, members=None, **attrs):
+    def __init__(self, base: Optional['RelationLike'] = None,
+                 members: Optional['MemberSequence'] = None, **attrs: Any) -> None:
         OSMObject.__init__(self, base=base, **attrs)
         if base is None:
             self.members = members
         else:
             self.members = members if members is not None else base.members
+
+
+def create_mutable_node(node: 'NodeLike', **args: Any) -> Node:
+    """ Create a mutable node replacing the properties given in the
+        named parameters. Note that this function only creates a shallow
+        copy which is still bound to the scope of the original object.
+    """
+    return Node(base=node, **args)
+
+def create_mutable_way(way: 'WayLike', **args: Any) -> Way:
+    """ Create a mutable way replacing the properties given in the
+        named parameters. Note that this function only creates a shallow
+        copy which is still bound to the scope of the original object.
+    """
+    return Way(base=way, **args)
+
+def create_mutable_relation(rel: 'RelationLike', **args: Any) -> Relation:
+    """ Create a mutable relation replacing the properties given in the
+        named parameters. Note that this function only creates a shallow
+        copy which is still bound to the scope of the original object.
+    """
+    return Relation(base=rel, **args)
+


=====================================
src/osmium/py.typed
=====================================


=====================================
src/osmium/replication/_replication.pyi
=====================================
@@ -0,0 +1,3 @@
+import datetime
+
+def newest_change_from_file(filename: str) -> datetime.datetime: ...


=====================================
src/osmium/replication/server.py
=====================================
@@ -1,6 +1,6 @@
 """ Helper functions to communicate with replication servers.
 """
-
+from typing import NamedTuple, Optional, Any, Iterator, cast, Mapping, Tuple
 import requests
 import urllib.request as urlrequest
 from urllib.error import URLError
@@ -9,7 +9,7 @@ from collections import namedtuple
 from contextlib import contextmanager
 from math import ceil
 
-from osmium import MergeInputReader
+from osmium import MergeInputReader, BaseHandler
 from osmium import io as oio
 from osmium import version
 
@@ -18,75 +18,85 @@ import logging
 LOG = logging.getLogger('pyosmium')
 LOG.addHandler(logging.NullHandler())
 
-OsmosisState = namedtuple('OsmosisState', ['sequence', 'timestamp'])
-DownloadResult = namedtuple('DownloadResult', ['id', 'reader', 'newest'])
+class OsmosisState(NamedTuple):
+    sequence: int
+    timestamp: dt.datetime
+
+class DownloadResult(NamedTuple):
+    id: int
+    reader: MergeInputReader
+    newest: int
 
 class ReplicationServer:
     """ Represents a connection to a  server that publishes replication data.
         Replication change files allow to keep local OSM data up-to-date without
         downloading the full dataset again.
 
+        `url` contains the base URL of the replication service. This is the
+        directory that contains the state file with the current state. If the
+        replication service serves something other than osc.gz files, set
+        the `diff_type` to the given file suffix.
+
         ReplicationServer may be used as a context manager. In this case, it
         internally keeps a connection to the server making downloads faster.
     """
 
-    def __init__(self, url, diff_type='osc.gz'):
+    def __init__(self, url: str, diff_type: str = 'osc.gz') -> None:
         self.baseurl = url
         self.diff_type = diff_type
-        self.session = None
+        self.extra_request_params: Mapping[str, Any] = dict(timeout=60, stream=True)
+        self.session: Optional[requests.Session] = None
 
-    def close(self):
+    def close(self) -> None:
         """ Close any open connection to the replication server.
         """
         if self.session is not None:
             self.session.close()
             self.session = None
 
-    def __enter__(self):
+    def __enter__(self) -> 'ReplicationServer':
         self.session = requests.Session()
         return self
 
-    def __exit__(self, exc_type, exc_value, traceback):
+    def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> None:
         self.close()
 
-    def make_request(self, url):
-        headers = {"User-Agent" : "pyosmium/{}".format(version.pyosmium_release)}
+    def set_request_parameter(self, key: str, value: Any) -> None:
+        """ Set a parameter which will be handed to the requests library
+            when calling `requests.get()`. This
+            may be used to set custom headers, timeouts and similar parameters.
+            See the `requests documentation <https://requests.readthedocs.io/en/latest/api/?highlight=get#requests.request>`_
+            for possible parameters. Per default, a timeout of 60 sec is set
+            and streaming download enabled.
+        """
+        self.extra_request_params[key] = value
+
+    def make_request(self, url: str) -> urlrequest.Request:
+        headers = {"User-Agent" : f"pyosmium/{version.pyosmium_release}"}
         return urlrequest.Request(url, headers=headers)
 
-    def open_url(self, url):
+    def open_url(self, url: urlrequest.Request) -> Any:
         """ Download a resource from the given URL and return a byte sequence
             of the content.
-
-            This method has no support for cookies or any special authentication
-            methods. If you need these, you have to provide your own custom URL
-            opener. Overwrite open_url() with a method that receives an
-            urllib.Request object and returns a ByteIO-like object or a
-            requests.Response.
-
-            Example::
-
-                opener = urlrequest.build_opener()
-                opener.addheaders = [('X-Fancy-Header', 'important_content')]
-
-                svr = ReplicationServer()
-                svr.open_url = opener.open
         """
-        headers = dict()
-        for h in url.header_items():
-            headers[h[0]] = h[1]
+        if 'headers' in self.extra_request_params:
+            get_params = self.extra_request_params
+        else:
+            get_params = dict(self.extra_request_params)
+            get_params['headers'] = {k: v for k,v in url.header_items()}
 
         if self.session is not None:
-            return self.session.get(url.get_full_url(), headers=headers, stream=True)
+            return self.session.get(url.get_full_url(), **get_params)
 
         @contextmanager
-        def _get_url_with_session():
+        def _get_url_with_session() -> Iterator[requests.Response]:
             with requests.Session() as session:
-                request = session.get(url.get_full_url(), headers=headers, stream=True)
+                request = session.get(url.get_full_url(), **get_params)
                 yield request
 
         return _get_url_with_session()
 
-    def collect_diffs(self, start_id, max_size=1024):
+    def collect_diffs(self, start_id: int, max_size: int = 1024) -> Optional[DownloadResult]:
         """ Create a MergeInputReader and download diffs starting with sequence
             id `start_id` into it. `max_size`
             restricts the number of diffs that are downloaded. The download
@@ -131,7 +141,9 @@ class ReplicationServer:
 
         return DownloadResult(current_id - 1, rd, newest.sequence)
 
-    def apply_diffs(self, handler, start_id, max_size=1024, idx="", simplify=True):
+    def apply_diffs(self, handler: BaseHandler, start_id: int,
+                    max_size: int = 1024, idx: str = "",
+                    simplify: bool = True) -> Optional[int]:
         """ Download diffs starting with sequence id `start_id`, merge them
             together and then apply them to handler `handler`. `max_size`
             restricts the number of diffs that are downloaded. The download
@@ -165,9 +177,11 @@ class ReplicationServer:
 
         return diffs.id
 
-    def apply_diffs_to_file(self, infile, outfile, start_id, max_size=1024,
-                            set_replication_header=True, extra_headers=None,
-                            outformat=None):
+    def apply_diffs_to_file(self, infile: str, outfile: str,
+                            start_id: int, max_size: int = 1024,
+                            set_replication_header: bool = True,
+                            extra_headers: Optional[Mapping[str, str]] = None,
+                            outformat: Optional[str] = None) -> Optional[Tuple[int, int]]:
         """ Download diffs starting with sequence id `start_id`, merge them
             with the data from the OSM file named `infile` and write the result
             into a file with the name `outfile`. The output file must not yet
@@ -230,7 +244,8 @@ class ReplicationServer:
         return (diffs.id, diffs.newest)
 
 
-    def timestamp_to_sequence(self, timestamp, balanced_search=False):
+    def timestamp_to_sequence(self, timestamp: dt.datetime,
+                              balanced_search: bool = False) -> Optional[int]:
         """ Get the sequence number of the replication file that contains the
             given timestamp. The search algorithm is optimised for replication
             servers that publish updates in regular intervals. For servers
@@ -312,7 +327,7 @@ class ReplicationServer:
                 return lower.sequence
 
 
-    def get_state_info(self, seq=None, retries=2):
+    def get_state_info(self, seq: Optional[int] = None, retries: int = 2) -> Optional[OsmosisState]:
         """ Downloads and returns the state information for the given
             sequence. If the download is successful, a namedtuple with
             `sequence` and `timestamp` is returned, otherwise the function
@@ -359,7 +374,7 @@ class ReplicationServer:
 
         return None
 
-    def get_diff_block(self, seq):
+    def get_diff_block(self, seq: int) -> str:
         """ Downloads the diff with the given sequence number and returns
             it as a byte sequence. Throws a :code:`urllib.error.HTTPError`
             if the file cannot be downloaded.
@@ -367,13 +382,13 @@ class ReplicationServer:
         with self.open_url(self.make_request(self.get_diff_url(seq))) as resp:
             if hasattr(resp, 'content'):
                 # generated by requests
-                return resp.content
+                return cast(str, resp.content)
 
             # generated by urllib.request
-            return resp.read()
+            return cast(str, resp.read())
 
 
-    def get_state_url(self, seq):
+    def get_state_url(self, seq: Optional[int]) -> str:
         """ Returns the URL of the state.txt files for a given sequence id.
 
             If seq is `None` the URL for the latest state info is returned,
@@ -387,7 +402,7 @@ class ReplicationServer:
                (self.baseurl, seq / 1000000, (seq % 1000000) / 1000, seq % 1000)
 
 
-    def get_diff_url(self, seq):
+    def get_diff_url(self, seq: int) -> str:
         """ Returns the URL to the diff file for the given sequence id.
         """
         return '%s/%03i/%03i/%03i.%s' % \


=====================================
src/osmium/replication/utils.py
=====================================
@@ -1,5 +1,5 @@
 """ Helper functions for change file handling. """
-
+from typing import NamedTuple, Optional
 import logging
 import datetime as dt
 from collections import namedtuple
@@ -8,10 +8,13 @@ from osmium.osm import NOTHING
 
 LOG = logging.getLogger('pyosmium')
 
-ReplicationHeader = namedtuple('ReplicationHeader',
-                               ['url', 'sequence', 'timestamp'])
+class ReplicationHeader(NamedTuple):
+    url: Optional[str]
+    sequence: Optional[int]
+    timestamp: Optional[dt.datetime]
+
 
-def get_replication_header(fname):
+def get_replication_header(fname: str) -> ReplicationHeader:
     """ Scans the given file for an Osmosis replication header. It returns
         a namedtuple with `url`, `sequence` and `timestamp`. Each or all fields
         may be None, if the piece of information is not avilable. If any of
@@ -24,20 +27,21 @@ def get_replication_header(fname):
     r = oreader(fname, NOTHING)
     h = r.header()
 
-    ts = h.get("osmosis_replication_timestamp")
-    url = h.get("osmosis_replication_base_url")
+    tsstr = h.get("osmosis_replication_timestamp")
+    url: Optional[str] = h.get("osmosis_replication_base_url")
 
-    if url or ts:
+    if url or tsstr:
         LOG.debug("Replication information found in OSM file header.")
 
     if url:
         LOG.debug("Replication URL: %s", url)
         # the sequence ID is only considered valid, if an URL is given
-        seq = h.get("osmosis_replication_sequence_number")
-        if seq:
-            LOG.debug("Replication sequence: %s", seq)
+        seqstr = h.get("osmosis_replication_sequence_number")
+        seq: Optional[int]
+        if seqstr:
+            LOG.debug("Replication sequence: %s", seqstr)
             try:
-                seq = int(seq)
+                seq = int(seqstr)
                 if seq < 0:
                     LOG.warning("Sequence id '%d' in OSM file header is negative. Ignored.", seq)
                     seq = None
@@ -50,10 +54,10 @@ def get_replication_header(fname):
         url = None
         seq = None
 
-    if ts:
-        LOG.debug("Replication timestamp: %s", ts)
+    if tsstr:
+        LOG.debug("Replication timestamp: %s", tsstr)
         try:
-            ts = dt.datetime.strptime(ts, "%Y-%m-%dT%H:%M:%SZ")
+            ts = dt.datetime.strptime(tsstr, "%Y-%m-%dT%H:%M:%SZ")
             ts = ts.replace(tzinfo=dt.timezone.utc)
 
         except ValueError:


=====================================
src/osmium/version.py
=====================================
@@ -3,13 +3,13 @@ Version information.
 """
 
 # the major version
-pyosmium_major = '3.4'
+pyosmium_major = '3.5'
 # current release (Pip version)
-pyosmium_release = '3.4.1'
+pyosmium_release = '3.5.0'
 
 # libosmium version shipped with the Pip release
 libosmium_version = '2.18.0'
 # protozero version shipped with the Pip release
 protozero_version = '1.7.1'
 # pybind11 version shipped with the Pip release
-pybind11_version = '2.10.0'
+pybind11_version = '2.10.1'


=====================================
test/helpers.py
=====================================
@@ -7,9 +7,6 @@ import osmium
 def mkdate(*args):
     return datetime(*args, tzinfo=timezone.utc)
 
-def check_repr(o):
-    return not str(o).startswith('<') and not repr(o).startswith('<')
-
 class CountingHandler(osmium.SimpleHandler):
 
     def __init__(self):


=====================================
test/test_osm.py
=====================================
@@ -3,10 +3,11 @@
 # This file is part of Pyosmium.
 #
 # Copyright (C) 2022 Sarah Hoffmann.
+import re
 from itertools import count
 import pytest
 
-from helpers import check_repr, mkdate
+from helpers import mkdate
 
 import osmium as o
 
@@ -62,7 +63,9 @@ def area_importer(request, tmp_path, to_opl):
 def test_invalid_location():
     loc = o.osm.Location()
     assert not loc.valid()
-    assert check_repr(loc)
+    assert str(loc) == 'invalid'
+    assert repr(loc) == 'osmium.osm.Location()'
+
     with pytest.raises(o.InvalidLocationError):
         lat = loc.lat
     with pytest.raises(o.InvalidLocationError):
@@ -73,12 +76,13 @@ def test_invalid_location():
 
 
 def test_valid_location():
-    loc = o.osm.Location(1,10)
-    assert loc.lon == pytest.approx(1)
+    loc = o.osm.Location(-1, 10)
+    assert loc.lon == pytest.approx(-1)
     assert loc.lat == pytest.approx(10)
-    assert loc.x == 10000000
+    assert loc.x == -10000000
     assert loc.y == 100000000
-    assert check_repr(loc)
+    assert re.fullmatch('-1.0*/10.0*', str(loc))
+    assert repr(loc) == 'osmium.osm.Location(x=-10000000, y=100000000)'
 
 
 def test_node_attributes(test_importer):
@@ -92,7 +96,8 @@ def test_node_attributes(test_importer):
         assert n.timestamp == mkdate(2014, 1, 31, 6, 23, 35)
         assert n.user == u'änonymous'
         assert n.positive_id() == 1
-        assert check_repr(n)
+        assert str(n) == 'n1: location=invalid tags={}'
+        assert repr(n) == "osmium.osm.Node(id=1, deleted=False, visible=True, version=5, changeset=58674, uid=42, timestamp=datetime.datetime(2014, 1, 31, 6, 23, 35, tzinfo=datetime.timezone.utc), user='änonymous', tags=osmium.osm.TagList({}), location=osmium.osm.Location())"
 
     assert 1 == test_importer('n1 v5 c58674 t2014-01-31T06:23:35Z i42 uänonymous',
                               node=node)
@@ -125,8 +130,12 @@ def test_way_attributes(test_importer):
         assert not o.is_closed()
         assert not o.ends_have_same_id()
         assert not o.ends_have_same_location()
-        assert check_repr(o)
-        assert check_repr(o.nodes)
+
+        assert str(o) == 'w1: nodes=[1 at 0.0000000/0.0000000,2,3 at 1.0000000/1.0000000] tags={}'
+        assert repr(o) == "osmium.osm.Way(id=1, deleted=False, visible=True, version=5, changeset=58674, uid=42, timestamp=datetime.datetime(2014, 1, 31, 6, 23, 35, tzinfo=datetime.timezone.utc), user='anonymous', tags=osmium.osm.TagList({}), nodes=osmium.osm.WayNodeList([osmium.osm.NodeRef(ref=1, location=osmium.osm.Location(x=0, y=0)), osmium.osm.NodeRef(ref=2, location=osmium.osm.Location()), osmium.osm.NodeRef(ref=3, location=osmium.osm.Location(x=10000000, y=10000000))]))"
+
+        assert str(o.nodes) == '[1 at 0.0000000/0.0000000,2,3 at 1.0000000/1.0000000]'
+        assert repr(o.nodes) == "osmium.osm.WayNodeList([osmium.osm.NodeRef(ref=1, location=osmium.osm.Location(x=0, y=0)), osmium.osm.NodeRef(ref=2, location=osmium.osm.Location()), osmium.osm.NodeRef(ref=3, location=osmium.osm.Location(x=10000000, y=10000000))])"
 
     assert 1 == test_importer(['n1 x0 y0', 'n3 x1 y1',
                                'w1 v5 c58674 t2014-01-31T06:23:35Z i42 uanonymous Nn1,n2,n3'],
@@ -145,8 +154,12 @@ def test_relation_attributes(test_importer):
         assert o.timestamp == mkdate(2014, 1, 31, 6, 23, 35)
         assert o.user == ' anonymous'
         assert o.positive_id() == 1
-        assert check_repr(o)
-        assert check_repr(o.members)
+
+        assert str(o) == 'r1: members=[w1], tags={}'
+        assert repr(o) == "osmium.osm.Relation(id=1, deleted=False, visible=True, version=5, changeset=58674, uid=42, timestamp=datetime.datetime(2014, 1, 31, 6, 23, 35, tzinfo=datetime.timezone.utc), user=' anonymous', tags=osmium.osm.TagList({}), members=osmium.osm.RelationMemberList([osmium.osm.RelationMember(ref=1, type='w', role='')]))"
+
+        assert str(o.members) == '[w1]'
+        assert repr(o.members) == "osmium.osm.RelationMemberList([osmium.osm.RelationMember(ref=1, type='w', role='')])"
 
     assert 1 == test_importer('r1 v5 c58674 t2014-01-31T06:23:35Z i42 u%20%anonymous Mw1@',
                               relation=relation)
@@ -230,7 +243,8 @@ def test_changest_attributes(area_importer):
         assert 515288620 == c.bounds.top_right.y
         assert -1465242 == c.bounds.bottom_left.x
         assert 515288506 == c.bounds.bottom_left.y
-        assert check_repr(c)
+        assert str(c) == 'c34: closed_at=2005-04-09 20:54:39+00:00, bounds=(-0.1465242/51.5288506 -0.1464925/51.5288620), tags={}'
+        assert repr(c) == "osmium.osm.Changeset(id=34, uid=1, created_at=datetime.datetime(2005, 4, 9, 19, 54, 13, tzinfo=datetime.timezone.utc), closed_at=datetime.datetime(2005, 4, 9, 20, 54, 39, tzinfo=datetime.timezone.utc), open=False, num_changes=2, bounds=osmium.osm.Box(bottom_left=osmium.osm.Location(x=-1465242, y=515288506), top_right=osmium.osm.Location(x=-1464925, y=515288620)), user='Steve', tags=osmium.osm.TagList({}))"
 
     assert 1 == area_importer('c34 k2 s2005-04-09T19:54:13Z e2005-04-09T20:54:39Z '
                                'd34 i1 uSteve x-0.1465242 y51.5288506 X-0.1464925 Y51.5288620',


=====================================
test/test_pyosmium_get_changes.py
=====================================
@@ -48,13 +48,6 @@ class TestPyosmiumGetChanges:
         monkeypatch.setattr(osmium.replication.server.requests.Session, "get", mock_get)
 
 
-    @pytest.fixture
-    def mock_urllib(self, monkeypatch):
-        def mock_get(_, url, **kwargs):
-            return BytesIO(self.urls[url.get_full_url()])
-        monkeypatch.setattr(self.script['urlrequest'].OpenerDirector, "open", mock_get)
-
-
     def url(self, url, result):
         self.urls[url] = dedent(result).encode()
 
@@ -67,7 +60,7 @@ class TestPyosmiumGetChanges:
 
         output = capsys.readouterr().out.strip()
 
-        assert output == '454'
+        assert output == '453'
 
 
     def test_init_date(self, capsys, mock_requests):
@@ -85,14 +78,14 @@ class TestPyosmiumGetChanges:
 
         output = capsys.readouterr().out.strip()
 
-        assert output == '1'
+        assert output == '-1'
 
 
     def test_init_to_file(self, tmp_path):
         fname = tmp_path / 'db.seq'
 
         assert 0 == self.main('-I', '453', '-f', str(fname))
-        assert fname.read_text() == '454'
+        assert fname.read_text() == '453'
 
 
     def test_init_from_seq_file(self, tmp_path):
@@ -100,10 +93,10 @@ class TestPyosmiumGetChanges:
         fname.write_text('453')
 
         assert 0 == self.main('-f', str(fname))
-        assert fname.read_text() == '454'
+        assert fname.read_text() == '453'
 
 
-    def test_init_date_with_cookie(self, capsys, tmp_path, mock_urllib):
+    def test_init_date_with_cookie(self, capsys, tmp_path, mock_requests):
         self.url('https://planet.osm.org/replication/minute//state.txt',
                  """\
                     sequenceNumber=100
@@ -123,4 +116,4 @@ class TestPyosmiumGetChanges:
 
         output = capsys.readouterr().out.strip()
 
-        assert output == '1'
+        assert output == '-1'


=====================================
test/test_taglist.py
=====================================
@@ -5,8 +5,6 @@
 # Copyright (C) 2022 Sarah Hoffmann.
 import pytest
 
-from helpers import check_repr
-
 import osmium as o
 
 @pytest.fixture
@@ -19,7 +17,6 @@ def tag_handler(simple_handler):
                 del tags[None]
             tags.update(n.tags)
             tests(n)
-            assert check_repr(n.tags)
 
         simple_handler(data, node=node)
 
@@ -32,6 +29,8 @@ def test_empty_taglist_length(tag_handler):
     def tests(n):
         assert 0 == len(n.tags)
         assert not n.tags
+        assert str(n.tags) == '{}'
+        assert repr(n.tags) == 'osmium.osm.TagList({})'
 
     tags = tag_handler("n234 x1 y2", tests)
     assert tags == {}
@@ -91,6 +90,8 @@ def test_taglist_contains(tag_handler):
         assert "x" not in n.tags
         assert None not in n.tags
         assert "" not in n.tags
+        assert str(n.tags) == '{abba=x,2=vvv,xx=abba}'
+        assert repr(n.tags) == "osmium.osm.TagList({'abba': 'x', '2': 'vvv', 'xx': 'abba'})"
 
     tags = tag_handler("n234 Tabba=x,2=vvv,xx=abba", tests)
 


=====================================
test/test_writer.py
=====================================
@@ -246,3 +246,75 @@ def test_add_relation_after_close(tmp_path, simple_handler):
 
     with pytest.raises(RuntimeError, match='closed'):
         simple_handler(node_opl, relation=lambda o: writer.add_relation(o))
+
+
+ at pytest.mark.parametrize("final_item", (True, False))
+def test_catch_errors_in_add_node(tmp_path, final_item):
+    test_file = tmp_path / 'test.opl'
+
+    writer = o.SimpleWriter(str(test_file), 4000)
+
+    try:
+        writer.add_node(o.osm.mutable.Node(id=123))
+        with pytest.raises(TypeError):
+            writer.add_node(o.osm.mutable.Node(id=124, tags=34))
+        if not final_item:
+            writer.add_node(o.osm.mutable.Node(id=125))
+    finally:
+        writer.close()
+
+    output = test_file.read_text()
+
+    expected = 'n123 v0 dV c0 t i0 u T x y\n'
+    if not final_item:
+        expected += 'n125 v0 dV c0 t i0 u T x y\n'
+
+    assert output == expected
+
+
+ at pytest.mark.parametrize("final_item", (True, False))
+def test_catch_errors_in_add_way(tmp_path, final_item):
+    test_file = tmp_path / 'test.opl'
+
+    writer = o.SimpleWriter(str(test_file), 4000)
+
+    try:
+        writer.add_way(o.osm.mutable.Way(id=123, nodes=[1, 2, 3]))
+        with pytest.raises(TypeError):
+            writer.add_way(o.osm.mutable.Way(id=124, nodes=34))
+        if not final_item:
+            writer.add_way(o.osm.mutable.Way(id=125, nodes=[11, 12]))
+    finally:
+        writer.close()
+
+    output = test_file.read_text()
+
+    expected = 'w123 v0 dV c0 t i0 u T Nn1,n2,n3\n'
+    if not final_item:
+        expected += 'w125 v0 dV c0 t i0 u T Nn11,n12\n'
+
+    assert output == expected
+
+
+ at pytest.mark.parametrize("final_item", (True, False))
+def test_catch_errors_in_add_relation(tmp_path, final_item):
+    test_file = tmp_path / 'test.opl'
+
+    writer = o.SimpleWriter(str(test_file), 4000)
+
+    try:
+        writer.add_relation(o.osm.mutable.Relation(id=123))
+        with pytest.raises(TypeError):
+            writer.add_relation(o.osm.mutable.Relation(id=124, members=34))
+        if not final_item:
+            writer.add_relation(o.osm.mutable.Relation(id=125))
+    finally:
+        writer.close()
+
+    output = test_file.read_text()
+
+    expected = 'r123 v0 dV c0 t i0 u T M\n'
+    if not final_item:
+        expected += 'r125 v0 dV c0 t i0 u T M\n'
+
+    assert output == expected


=====================================
tools/pyosmium-get-changes
=====================================
@@ -27,18 +27,14 @@ cookies to the server and will save received cookies to the jar file.
 
 from argparse import ArgumentParser, RawDescriptionHelpFormatter, ArgumentTypeError
 import datetime as dt
-import socket
+import http.cookiejar
+
 from osmium.replication import server as rserv
 from osmium.replication import newest_change_from_file
 from osmium.replication.utils import get_replication_header
 from osmium.version import pyosmium_release
 from osmium import SimpleHandler, WriteHandler
 
-try:
-    import http.cookiejar as cookiejarlib
-except ImportError:
-    import cookielib as cookiejarlib
-import urllib.request as urlrequest
 
 import re
 import sys
@@ -62,18 +58,16 @@ class ReplicationStart(object):
             return self.seq_id + 1
 
         log.debug("Looking up sequence ID for timestamp %s" % self.date)
-        seq = svr.timestamp_to_sequence(self.date)
-
-        return seq + 1 if seq is not None else None
+        return svr.timestamp_to_sequence(self.date)
 
     @staticmethod
     def from_id(idstr):
         try:
-            seq_id=int(idstr)
+            seq_id = int(idstr)
         except ValueError:
             raise ArgumentTypeError("Sequence id '%s' is not a number" % idstr)
 
-        if seq_id < 0:
+        if seq_id < -1:
             raise ArgumentTypeError("Sequence id '%s' is negative" % idstr)
 
         return ReplicationStart(seq_id=seq_id)
@@ -213,15 +207,13 @@ def main(args):
             or 'https://planet.osm.org/replication/minute/'
     logging.info("Using replication server at %s" % url)
 
-    socket.setdefaulttimeout(options.socket_timeout)
-
     with rserv.ReplicationServer(url) as svr:
+        svr.set_request_parameter('timeout', options.socket_timeout or None)
+
         if options.cookie is not None:
-            # According to the documentation, the cookie jar loads the file only if FileCookieJar.load is called.
-            cookie_jar = cookiejarlib.MozillaCookieJar(options.cookie)
+            cookie_jar = http.cookiejar.MozillaCookieJar(options.cookie)
             cookie_jar.load(options.cookie)
-            opener = urlrequest.build_opener(urlrequest.HTTPCookieProcessor(cookie_jar))
-            svr.open_url = opener.open
+            svr.set_request_parameter('cookies', cookie_jar)
 
         startseq = options.start.get_sequence(svr)
         if startseq is None:
@@ -229,7 +221,7 @@ def main(args):
             return 1
 
         if options.outfile is None:
-            write_end_sequence(options.seq_file, startseq)
+            write_end_sequence(options.seq_file, startseq - 1)
             return 0
 
         log.debug("Starting download at ID %d (max %d MB)" % (startseq, options.outsize))
@@ -247,6 +239,7 @@ def main(args):
         cookie_jar.save(options.cookie)
 
     if endseq is None:
+        log.error("Error while downloading diffs.")
         return 3
 
     if options.outfile != '-' or options.seq_file is not None:


=====================================
tools/pyosmium-up-to-date
=====================================
@@ -35,7 +35,7 @@ import re
 import sys
 import traceback
 import logging
-import socket
+import http.cookiejar
 
 from argparse import ArgumentParser, RawDescriptionHelpFormatter
 import datetime as dt
@@ -47,15 +47,6 @@ from textwrap import dedent as msgfmt
 from tempfile import mktemp
 import os.path
 
-try:
-    import http.cookiejar as cookiejarlib
-except ImportError:
-    import cookielib as cookiejarlib
-try:
-    import urllib.request as urlrequest
-except ImportError:
-    import urllib2 as urlrequest
-
 log = logging.getLogger()
 
 def update_from_osm_server(ts, options):
@@ -71,14 +62,14 @@ def update_from_osm_server(ts, options):
 def update_from_custom_server(url, seq, ts, options):
     """Update from a custom URL, simply using the diff sequence as is."""
     with rserv.ReplicationServer(url, "osc.gz") as svr:
+        log.info("Using replication service at %s", url)
+
+        svr.set_request_parameter('timeout', options.socket_timeout or None)
+
         if options.cookie is not None:
-            # According to the documentation, the cookie jar loads the file only if FileCookieJar.load is called.
-            cookie_jar = cookiejarlib.MozillaCookieJar(options.cookie)
+            cookie_jar = http.cookiejar.MozillaCookieJar(options.cookie)
             cookie_jar.load(options.cookie)
-            opener = urlrequest.build_opener(urlrequest.HTTPCookieProcessor(cookie_jar))
-            svr.open_url = opener.open
-
-        log.info("Using replication service at %s", url)
+            svr.set_request_parameter('cookies', cookie_jar)
 
         current = svr.get_state_info()
         if current is None:
@@ -244,8 +235,6 @@ if __name__ == '__main__':
     options = get_arg_parser(from_main=True).parse_args()
     log.setLevel(max(3 - options.loglevel, 0) * 10)
 
-    socket.setdefaulttimeout(options.socket_timeout)
-
     try:
         url, seq, ts = compute_start_point(options)
     except RuntimeError as e:



View it on GitLab: https://salsa.debian.org/debian-gis-team/pyosmium/-/compare/d5f5d2f238cf61fba361b79a710922ca0df8f319...d67ab4c2e21c2cb43d98f74462b100ec0853aa35

-- 
View it on GitLab: https://salsa.debian.org/debian-gis-team/pyosmium/-/compare/d5f5d2f238cf61fba361b79a710922ca0df8f319...d67ab4c2e21c2cb43d98f74462b100ec0853aa35
You're receiving this email because of your account on salsa.debian.org.


-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://alioth-lists.debian.net/pipermail/pkg-grass-devel/attachments/20221109/b25e51d8/attachment-0001.htm>


More information about the Pkg-grass-devel mailing list