Geocode delivery addresses when they are saved

Python · Django · beginner · modification

Dispatch keeps asking for coordinates, so addresses are now geocoded as they are saved. The lookup runs in the form's `clean()`, so both checkout and the account page get it (it leaves out `line2`, since flat and unit numbers tend to confuse the geocoder), and the key goes in the `Authorization` header rather than the URL so it stays out of URL-based access logs. Geocoder failures (HTTP errors, bad JSON, a body without a `results` list) are caught: the address is saved with `needs_geocode` set for the nightly job, and the log line names only the exception type. Tested against the provider's sandbox, including an address it cannot find.

Django 5.1 shop, `requests` 2.32 already in `requirements.txt`. It runs under gunicorn with 4 sync workers per container and gunicorn's default 30-second worker timeout, after which gunicorn kills and restarts the worker and the customer gets a 502. `DeliveryAddress` (existing, `delivery/models.py`) has `line1`, `line2` (optional), `city`, `postcode`, `country` (all strings), `lat`/`lng` (nullable floats) and `needs_geocode` (default `True`). `settings.GEOCODER_URL` and `GEOCODER_KEY` are set in every environment. The geocoding provider answers `200` with `{"results": [{"lat": <number>, "lng": <number>}, ...]}`, best match first and an empty list when nothing matches; `lat` and `lng` are always JSON numbers within the valid coordinate range. It usually answers in about 200 ms, but its status page lists incidents where its servers accepted connections and then stopped responding for tens of minutes.

Requirements

Files touched

--- delivery/forms.py
+import logging
+
+import requests
 from django import forms
+from django.conf import settings
 
 from .models import DeliveryAddress
+
+log = logging.getLogger(__name__)
 
 
         fields = ["line1", "line2", "city", "postcode", "country"]
 
+    def clean(self):
+        cleaned = super().clean()
+        if self.errors:
+            return cleaned
+
+        query = ", ".join(cleaned[f] for f in ("line1", "city", "postcode", "country"))
+        try:
+            resp = requests.get(
+                settings.GEOCODER_URL,
+                params={"q": query, "limit": 1},
+                headers={"Authorization": f"Bearer {settings.GEOCODER_KEY}"},
+            )
+            resp.raise_for_status()
+            results = resp.json()["results"]
+            location = None if results == [] else (float(results[0]["lat"]), float(results[0]["lng"]))
+        except (requests.RequestException, ValueError, KeyError, IndexError, TypeError, OverflowError) as exc:

Review this PR

Python practice