diff --git a/CHANGES.rst b/CHANGES.rst index 3d9011a..037800e 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,7 +5,9 @@ Changelog 1.2.53 (unreleased) ------------------- -- Nothing changed yet. +- WEB-4423 : Use ``RateLimiter`` in ``geocode_object`` to respect Nominatim's 1 req/s + policy and catch ``GeocoderRateLimited`` so bulk imports are not aborted on 429. + [remdub] 1.2.52 (2026-04-22) diff --git a/src/imio/smartweb/common/tests/test_utils.py b/src/imio/smartweb/common/tests/test_utils.py index ea0fe44..b08e00c 100644 --- a/src/imio/smartweb/common/tests/test_utils.py +++ b/src/imio/smartweb/common/tests/test_utils.py @@ -85,9 +85,6 @@ def test_translate_vocabulary_term(self): ) def test_geolocation(self): - attr = {"geocode.return_value": mock.Mock(latitude=1, longitude=2)} - geopy.geocoders.Nominatim = mock.Mock(return_value=mock.Mock(**attr)) - obj = GeolocatedObject() obj.geolocation = Geolocation(0, 0) @@ -97,7 +94,9 @@ def test_geolocation(self): self.assertEqual(obj.geolocation.longitude, 0) obj.street = "My beautiful street" - geocoded = geocode_object(obj) + with patch("imio.smartweb.common.utils._geocode") as mock_geocode: + mock_geocode.return_value = mock.Mock(latitude=1, longitude=2) + geocoded = geocode_object(obj) self.assertTrue(geocoded) self.assertEqual(obj.geolocation.latitude, 1) self.assertEqual(obj.geolocation.longitude, 2) @@ -110,11 +109,21 @@ def test_geocode_object_geocoder_unavailable(self): obj.zipcode = "12345" obj.city = "Testville" obj.country = "be" - with patch("geopy.geocoders.Nominatim") as mock_nominatim, patch( - "geopy.exc.GeocoderUnavailable", new=geopy.exc.GeocoderUnavailable - ): - instance = mock_nominatim.return_value - instance.geocode.side_effect = geopy.exc.GeocoderUnavailable + with patch("imio.smartweb.common.utils._geocode") as mock_geocode: + mock_geocode.side_effect = geopy.exc.GeocoderUnavailable + result = geocode_object(obj) + self.assertFalse(result) + + def test_geocode_object_geocoder_rate_limited(self): + obj = GeolocatedObject() + obj.street = "Test Street" + obj.number = "1" + obj.complement = "" + obj.zipcode = "12345" + obj.city = "Testville" + obj.country = "be" + with patch("imio.smartweb.common.utils._geocode") as mock_geocode: + mock_geocode.side_effect = geopy.exc.GeocoderRateLimited("rate limited") result = geocode_object(obj) self.assertFalse(result) diff --git a/src/imio/smartweb/common/utils.py b/src/imio/smartweb/common/utils.py index e1a6197..91b48a1 100644 --- a/src/imio/smartweb/common/utils.py +++ b/src/imio/smartweb/common/utils.py @@ -24,6 +24,13 @@ import geopy import re import unicodedata +from geopy.extra.rate_limiter import RateLimiter + + +_geolocator = geopy.geocoders.Nominatim(user_agent="contact@imio.be", timeout=3) +_geocode = RateLimiter( + _geolocator.geocode, min_delay_seconds=1, swallow_exceptions=False +) def get_vocabulary(voc_name, obj=None): @@ -80,14 +87,13 @@ def geocode_object(obj): address = " ".join(filter(None, [street, entity, country])) if not address: return - geolocator = geopy.geocoders.Nominatim(user_agent="contact@imio.be", timeout=3) location = None try: - location = geolocator.geocode(address) - except geopy.exc.GeocoderUnavailable: + location = _geocode(address) + except (geopy.exc.GeocoderUnavailable, geopy.exc.GeocoderRateLimited): api.portal.show_message( _( - "Error: Geolocation service is unavailable. Your content is not geocoded." + "Error: Geolocation service is unavailable or rate limited. Your content is not geocoded." ), request=getRequest(), type="warning",