[FIX] partner_geolocalize_usability: avoid Nominatim HTTP 429 rejections
The stock ``base_geocoder._call_openstreetmap`` sends a generic Odoo ``User-Agent`` shared across thousands of instances, which Nominatim rate-limits and blocks (HTTP 429 "Too many requests"), so mass geolocation fails regardless of client-side throttling. Override ``_call_openstreetmap`` to: - send a deployment-specific ``User-Agent`` read from a new system parameter ``partner_geolocalize_usability.user_agent`` (shipped with an obvious placeholder so an unconfigured instance fails loudly instead of silently reusing the banned generic agent); - throttle requests to one per second (``time.sleep``) before each call, complying with the Nominatim usage policy even when callers loop over many partners; - return ``None`` instead of raising ``IndexError`` when Nominatim returns an empty result for an unknown address. Document the required system parameter in the README and add tests covering the User-Agent selection, throttling and empty/blank results.
This commit is contained in:
79
partner_geolocalize_usability/models/base_geocoder.py
Normal file
79
partner_geolocalize_usability/models/base_geocoder.py
Normal file
@@ -0,0 +1,79 @@
|
||||
import logging
|
||||
import time
|
||||
|
||||
import requests
|
||||
|
||||
from odoo import api, models
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
# System parameter holding the User-Agent sent to Nominatim.
|
||||
# Nominatim's usage policy requires a genuine, identifying User-Agent with a
|
||||
# valid contact. The stock Odoo User-Agent is shared by thousands of instances
|
||||
# and is rate-limited/blocked (HTTP 429), so we let each deployment set its own.
|
||||
USER_AGENT_PARAM = "partner_geolocalize_usability.user_agent"
|
||||
|
||||
# Placeholder shown until the deployment sets a real value. It intentionally
|
||||
# does NOT identify any real service so an unconfigured instance fails loudly
|
||||
# rather than silently reusing a banned generic agent.
|
||||
USER_AGENT_PLACEHOLDER = (
|
||||
"SET-ME partner_geolocalize_usability (contact: you@example.invalid)"
|
||||
)
|
||||
|
||||
# Nominatim's public API allows at most 1 request per second. We wait slightly
|
||||
# more than a second between calls to stay safely under the limit.
|
||||
NOMINATIM_MIN_INTERVAL = 1.1
|
||||
|
||||
|
||||
class GeoCoder(models.AbstractModel):
|
||||
_inherit = "base.geocoder"
|
||||
|
||||
def _get_nominatim_user_agent(self):
|
||||
"""Return the configured User-Agent for Nominatim requests.
|
||||
|
||||
Falls back to an obvious placeholder so a misconfigured instance is easy
|
||||
to spot in logs (and so we never silently reuse Odoo's banned agent).
|
||||
"""
|
||||
return (
|
||||
self.env["ir.config_parameter"]
|
||||
.sudo()
|
||||
.get_param(USER_AGENT_PARAM, USER_AGENT_PLACEHOLDER)
|
||||
)
|
||||
|
||||
@api.model
|
||||
def _call_openstreetmap(self, addr, **kw):
|
||||
"""Query Nominatim with a configurable User-Agent and rate limiting.
|
||||
|
||||
Overrides the stock implementation to fix two problems that cause
|
||||
HTTP 429 ("Too many requests"):
|
||||
|
||||
* the shared/generic Odoo User-Agent is blocked by Nominatim -> use a
|
||||
deployment-specific one from a system parameter;
|
||||
* bursts of requests exceed the 1 req/s policy -> throttle between calls.
|
||||
"""
|
||||
if not addr:
|
||||
_logger.info("invalid address given")
|
||||
return None
|
||||
url = "https://nominatim.openstreetmap.org/search"
|
||||
try:
|
||||
headers = {"User-Agent": self._get_nominatim_user_agent()}
|
||||
# Respect Nominatim's 1 req/s policy. Sleeping before the call keeps
|
||||
# the throttle effective even when callers loop over many records.
|
||||
time.sleep(NOMINATIM_MIN_INTERVAL)
|
||||
response = requests.get(
|
||||
url, headers=headers, params={"format": "json", "q": addr}
|
||||
)
|
||||
_logger.info("openstreetmap nominatim service called")
|
||||
if response.status_code != 200:
|
||||
_logger.warning(
|
||||
"Request to openstreetmap failed.\nCode: %s\nContent: %s",
|
||||
response.status_code,
|
||||
response.content,
|
||||
)
|
||||
result = response.json()
|
||||
except Exception as e:
|
||||
self._raise_query_error(e)
|
||||
if not result:
|
||||
return None
|
||||
geo = result[0]
|
||||
return float(geo["lat"]), float(geo["lon"])
|
||||
Reference in New Issue
Block a user