Repository: pantherale0/pyfuelprices
Pull request #46: Refactor fuel price sources to improve structure and maintainability
Author: login: pantherale0
Labels: name: bug
description: Something isn't working
Created: 2025-05-06T20:38:10Z | Merged: 2025-05-06T21:26:16Z

- Moved constant definitions for Austria, Brazil, and Germany into separate const.py files for better organization.
- Updated the SpritpreisrechnerATSource to utilize the new constants.
- Refactored GasPassSource and TankerKoenigSource to streamline area updates and improve error handling.
- Simplified update methods across various sources, focusing on area-specific updates.
- Enhanced logging for better debugging and tracking of API requests and responses.
- Removed redundant code and comments to clean up the source files.
- Added dotenv support for environment variable management in test.py.


Diff:
diff --git a/.gitignore b/.gitignore
index 1f31bb1..3d1e1c3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,3 +7,4 @@ __pycache__
 dist
 htmlcov
 venv
+.env_config
\ No newline at end of file
diff --git a/pyfuelprices/__init__.py b/pyfuelprices/__init__.py
index a635e31..a1574fa 100644
--- a/pyfuelprices/__init__.py
+++ b/pyfuelprices/__init__.py
@@ -9,12 +9,12 @@
 
 from pyfuelprices.sources import (
     Source,
-    geocode_reverse_lookup,
     UpdateFailedError
 )
 from pyfuelprices.sources.mapping import SOURCE_MAP, COUNTRY_MAP, FULL_COUNTRY_MAP
 from .const import PROP_FUEL_LOCATION_SOURCE
 from .enum import SupportsConfigType
+from .helpers import geocoder
 from .fuel_locations import FuelLocation
 from .schemas import BASE_CONFIG_SCHEMA
 
@@ -69,7 +69,11 @@ async def find_fuel_locations_from_point(self,
                 coordinates=coordinates,
                 radius=radius
             )
-        geocoded = (await geocode_reverse_lookup(coordinates)).raw['address']['country_code']
+        try:
+            geocoded = (await geocoder.geocode_reverse_lookup(coordinates)).raw['address']['country_code']
+        except Exception as e:
+            _LOGGER.error(f"Geocoding failed: {e}")
+            return [] # Or handle the error as appropriate
         if geocoded.upper() not in FULL_COUNTRY_MAP:
             raise ValueError("No data source exists for the given coordinates.", geocoded)
         locations = []
diff --git a/pyfuelprices/helpers.py b/pyfuelprices/helpers.py
index 0d51caa..429a0e7 100644
--- a/pyfuelprices/helpers.py
+++ b/pyfuelprices/helpers.py
@@ -1,21 +1,20 @@
 """Fuel Price helpers."""
 
+import asyncio
+import time
 import math
+import logging
 
 from geopy import (
     Nominatim,
     location,
-    adapters
+    adapters,
+    exc as geopyexc
 )
 
 from ._version import __version__ as VERSION
 
-async def geocode_reverse_lookup(coordinates: tuple) -> location.Location:
-    """Reverse GeoCode lookup."""
-    async with Nominatim(
-        user_agent=f"pyfuelprices-{VERSION}",
-        adapter_factory=adapters.AioHTTPAdapter) as geolocator:
-        return await geolocator.reverse(coordinates, exactly_one=True, timeout=15, addressdetails=True)
+_LOGGER = logging.getLogger(__name__)
 
 class BoundingBox(object):
     """Represent a bounding box."""
@@ -24,30 +23,77 @@ class BoundingBox(object):
     lat_max = None
     lon_max = None
 
-def get_bounding_box(latitude_in_degrees, longitude_in_degrees, half_side_in_miles):
-    """Return a bounding box from lat/long/radius"""
-    assert half_side_in_miles > 0
-    assert latitude_in_degrees >= -90.0 and latitude_in_degrees  <= 90.0
-    assert longitude_in_degrees >= -180.0 and longitude_in_degrees <= 180.0
-
-    half_side_in_km = half_side_in_miles * 1.609344
-    lat = math.radians(latitude_in_degrees)
-    lon = math.radians(longitude_in_degrees)
-
-    radius  = 6371
-    # Radius of the parallel at given latitude
-    parallel_radius = radius*math.cos(lat)
-
-    lat_min = lat - half_side_in_km/radius
-    lat_max = lat + half_side_in_km/radius
-    lon_min = lon - half_side_in_km/parallel_radius
-    lon_max = lon + half_side_in_km/parallel_radius
-    rad2deg = math.degrees
-
-    box = BoundingBox()
-    box.lat_min = rad2deg(lat_min)
-    box.lon_min = rad2deg(lon_min)
-    box.lat_max = rad2deg(lat_max)
-    box.lon_max = rad2deg(lon_max)
-
-    return box
+class LeakyBucket:
+    """Represent a leaky bucket algorithm rate limiter."""
+    def __init__(self, max_requests, rate):
+        """Create a Leaky Bucket."""
+        self.capacity = max_requests
+        self.rate = rate
+        self.tokens = 0
+        self.last_time = time.time()
+
+    def _update_tokens(self):
+        """Update tokens."""
+        now = time.time()
+        elapsed_time = now - self.last_time
+        self.tokens = min(self.capacity, self.tokens + elapsed_time * self.rate)
+        self.last_time = now
+
+    def allow_request(self) -> bool:
+        """Allow a given request."""
+        self._update_tokens()
+        if self.tokens >= 1:
+            self.tokens -= 1
+            return True
+        return False
+
+class GeoCodeHandler:
+    """Represent a geocode handler."""
+    _lookup_lock: asyncio.Lock = asyncio.Lock()
+    _rate_limiter: LeakyBucket = LeakyBucket(
+        max_requests=1,
+        rate=1
+    )
+
+    async def geocode_reverse_lookup(self, coordinates: tuple) -> location.Location:
+        """Reverse GeoCode lookup."""
+        async with self._lookup_lock:
+            while not self._rate_limiter.allow_request():
+                _LOGGER.debug("Geocoder rate limit reached (1req/s), sleeping for 1s")
+                await asyncio.sleep(1)
+            async with Nominatim(
+                user_agent=f"pyfuelprices-{VERSION}",
+                adapter_factory=adapters.AioHTTPAdapter) as geolocator:
+                return await geolocator.reverse(
+                    coordinates, exactly_one=True, timeout=15, addressdetails=True
+                )
+
+    def get_bounding_box(self, latitude_in_degrees, longitude_in_degrees, half_side_in_miles):
+        """Return a bounding box from lat/long/radius"""
+        assert half_side_in_miles > 0
+        assert latitude_in_degrees >= -90.0 and latitude_in_degrees  <= 90.0
+        assert longitude_in_degrees >= -180.0 and longitude_in_degrees <= 180.0
+
+        half_side_in_km = half_side_in_miles * 1.609344
+        lat = math.radians(latitude_in_degrees)
+        lon = math.radians(longitude_in_degrees)
+
+        radius  = 6371
+        # Radius of the parallel at given latitude
+        parallel_radius = radius*math.cos(lat)
+
+        lat_min = lat - half_side_in_km/radius
+        lat_max = lat + half_side_in_km/radius
+        lon_min = lon - half_side_in_km/parallel_radius
+        lon_max = lon + half_side_in_km/parallel_radius
+        rad2deg = math.degrees
+
+        box = BoundingBox()
+        box.lat_min = rad2deg(lat_min)
+        box.lon_min = rad2deg(lon_min)
+        box.lat_max = rad2deg(lat_max)
+        box.lon_max = rad2deg(lon_max)
+
+        return box
+
+geocoder = GeoCodeHandler()
diff --git a/pyfuelprices/sources/__init__.py b/pyfuelprices/sources/__init__.py
index 84607b5..f5b27e2 100644
--- a/pyfuelprices/sources/__init__.py
+++ b/pyfuelprices/sources/__init__.py
@@ -1,20 +1,13 @@
 """Data sources and translators for pyfuelprices."""
 
+import asyncio
 import logging
-import json
-import csv
-
 from datetime import timedelta, datetime
 from typing import final
 from geopy import (
-    distance,
-    Nominatim,
-    location,
-    adapters,
-    exc as geopyexc
+    distance   
 )
 import aiohttp
-import voluptuous as vol
 
 from pyfuelprices.const import (
     PROP_FUEL_LOCATION_PREVENT_CACHE_CLEANUP,
@@ -23,7 +16,6 @@
     PROP_AREA_RADIUS
 )
 from pyfuelprices.fuel_locations import FuelLocation, Fuel
-from pyfuelprices.helpers import geocode_reverse_lookup
 from pyfuelprices.enum import SupportsConfigType
 from pyfuelprices.schemas import SOURCE_BASE_CONFIG
 
@@ -40,7 +32,7 @@ class Source:
     _timeout: int = 30
     _configured_areas: list[dict] = []
     _client_session: aiohttp.ClientSession = None
-    update_interval: timedelta = timedelta(days=1)
+    update_interval: timedelta = None
     next_update: datetime = datetime.now()
     provider_name: str = ""
     location_cache: dict[str, FuelLocation] = None
@@ -49,11 +41,17 @@ class Source:
     attr_config = SOURCE_BASE_CONFIG
 
     def __init__(self,
+                 configured_areas: list[dict] = None,
                  update_interval: timedelta = timedelta(days=1),
                  client_session: aiohttp.ClientSession = None,
                  configuration: dict | None = None) -> None:
         """Start a new instance of a source."""
-        self.update_interval = update_interval
+        self._configured_areas = configured_areas or []
+        if self.update_interval is None:
+            if update_interval is not None:
+                self.update_interval = update_interval
+            else:
+                self.update_interval = timedelta(days=1)
         self._client_session: aiohttp.ClientSession = client_session
         if client_session is None:
             self._client_session = aiohttp.ClientSession(
@@ -101,46 +99,27 @@ async def search_sites(self, coordinates, radius: float) -> list[dict]:
                 )
         return locations
 
+    async def update_area(self, area: dict) -> bool:
+        """Update a given area."""
+        raise NotImplementedError("Not implemented.")
+
     async def update(self, areas=None, force=False) -> list[FuelLocation]:
         """Update hooks for the data source."""
         _LOGGER.debug("Starting update hook for %s to url %s", self.provider_name, self._url)
-        self._configured_areas=[] if areas is None else areas
+        areas = areas or self._configured_areas
         self._clear_cache()
-        if self.next_update <= datetime.now() or force:
-            response = await self._client_session.request(
-                method=self._method,
-                url=self._url,
-                json=self._request_body,
-                headers=self._headers
-            )
-            _LOGGER.debug("Update request completed for %s with status %s",
-                        self.provider_name, response.status)
-            if response.status == 200:
-                self.next_update = datetime.now() + self.update_interval
-                if "application/json" not in response.content_type:
-                    return await self.parse_response(
-                        response=json.loads(await response.text())
-                    )
-                if response.content_type == "text/csv":
-                    return await self.parse_response(
-                        response=list(csv.DictReader(await response.text()))
-                    )
-                return await self.parse_response(
-                    response=await response.json()
-                )
-            if response.status == 403:
-                raise ServiceBlocked(
-                    status=response.status,
-                    response=await response.text(),
-                    headers=response.headers,
-                    service=self.provider_name
-                )
-            raise UpdateFailedError(
-                status=response.status,
-                response=await response.text(),
-                headers=response.headers,
-                service=self.provider_name
-            )
+        if self.next_update > datetime.now() and not force:
+            _LOGGER.debug("Ignoring update request")
+            return
+        coros = [
+            self.update_area(a) for a in areas
+        ]
+        results = await asyncio.gather(*coros, return_exceptions=True)
+        for result in results:
+            if isinstance(result, Exception):
+                _LOGGER.error("Update area failed: %s", result)
+        self.next_update = datetime.now() + self.update_interval
+        return list(self.location_cache.values())
 
     async def parse_response(self, response) -> list[FuelLocation]:
         """Parses the response from the update hook."""
diff --git a/pyfuelprices/sources/argentina/gobenergy.py b/pyfuelprices/sources/argentina/gobenergy.py
index 51242f9..253033f 100644
--- a/pyfuelprices/sources/argentina/gobenergy.py
+++ b/pyfuelprices/sources/argentina/gobenergy.py
@@ -2,7 +2,7 @@
 
 import logging
 import json
-from datetime import datetime, timedelta
+from datetime import timedelta
 
 from pyfuelprices.const import (
     PROP_FUEL_LOCATION_SOURCE,
@@ -12,9 +12,10 @@
     PROP_AREA_LONG,
     PROP_AREA_RADIUS
 )
-from pyfuelprices.sources import Source, geocode_reverse_lookup, geopyexc
+from pyfuelprices.sources import Source
 from pyfuelprices.fuel import Fuel
 from pyfuelprices.fuel_locations import FuelLocation
+from pyfuelprices.helpers import geocoder, geopyexc
 
 from .const import AR_GOB_ENERGY_ID, AR_GOB_DATASOURCE, AR_GOB_TIMEOUT
 
@@ -56,51 +57,47 @@ async def search_sites(self, coordinates, radius: float) -> list[dict]:
         )
         return await super().search_sites(coordinates, radius)
 
-    async def update(self, areas=None, force=False) -> list[FuelLocation]:
-        """Update hooks for the data source."""
-        _LOGGER.debug("Starting update hook for %s to url %s", self.provider_name, self._url)
-        self._configured_areas=[] if areas is None else areas
-        self._clear_cache()
-        if self.next_update <= datetime.now() or force:
-            for area in self._configured_areas:
-                try:
-                    geocode = await geocode_reverse_lookup(
-                        (area[PROP_AREA_LAT], area[PROP_AREA_LONG])
-                    )
-                except geopyexc.GeocoderTimedOut:
-                    _LOGGER.warning("Timeout occured while geocoding area %s.",
-                                    area)
-                    continue
-                if geocode.raw["address"]["country_code"] != "ar":
-                    _LOGGER.debug("Skipping area %s as not in AR.",
-                                area)
-                    continue
-                response = json.loads(await self._send_request(
-                    url=self._url,
-                    body={
-                        "resource_id": "80ac25de-a44a-4445-9215-090cf55cfda5",
-                        "q": "",
-                        "filters": {
-                            "provincia": str(geocode.raw["address"]["state"]).upper(),
-                            "localidad": str(geocode.raw["address"]["town"]).upper()
-                        },
-                        "limit": 500,
-                        "offset": 0
-                    }
-                ))
-                if response["success"]:
-                    await self.parse_response(response["result"]["records"])
-                else:
-                    _LOGGER.error("Error sending request to %s: %s",
-                                self.provider_name,
-                                response)
-            self.next_update = datetime.now() + self.update_interval
-        return list(self.location_cache.values())
+    async def update_area(self, area: dict) -> bool:
+        """Update a given area."""
+        try:
+            geocode = await geocoder.geocode_reverse_lookup(
+                (area[PROP_AREA_LAT], area[PROP_AREA_LONG])
+            )
+        except geopyexc.GeocoderTimedOut:
+            _LOGGER.warning("Timeout occured while geocoding area %s.",
+                            area)
+            return False
+        if geocode.raw["address"]["country_code"] != "ar":
+            _LOGGER.debug("Skipping area %s as not in AR.",
+                        area)
+            return False
+        response = json.loads(await self._send_request(
+            url=self._url,
+            body={
+                "resource_id": "80ac25de-a44a-4445-9215-090cf55cfda5",
+                "q": "",
+                "filters": {
+                    "provincia": str(geocode.raw["address"]["state"]).upper(),
+                    "localidad": str(geocode.raw["address"]["town"]).upper()
+                },
+                "limit": 500,
+                "offset": 0
+            }
+        ))
+        if response["success"]:
+            await self.parse_response(response["result"]["records"])
+            return True
+        else:
+            _LOGGER.error("Error sending request to %s: %s",
+                        self.provider_name,
+                        response)
+            return False
 
     async def parse_response(self, response) -> list[FuelLocation]:
         """Converts CMA data into fuel price mapping."""
         for location_raw in response:
             site_id = f"{self.provider_name}_{location_raw['idempresa']}"
+            _LOGGER.debug("Parsing location %s", site_id)
             location = FuelLocation.create(
                     site_id=site_id,
                     name=f"{location_raw['empresabandera']} {location_raw['empresa']}",
diff --git a/pyfuelprices/sources/australia/fuelsnoop.py b/pyfuelprices/sources/australia/fuelsnoop.py
index 5cecc63..bda06f1 100644
--- a/pyfuelprices/sources/australia/fuelsnoop.py
+++ b/pyfuelprices/sources/australia/fuelsnoop.py
@@ -1,11 +1,8 @@
 """FuelSnoop data source for Australia (QLD)."""
 
-from datetime import datetime
 import logging
 import json
 
-from geopy import distance
-
 from pyfuelprices.const import (
     PROP_FUEL_LOCATION_PREVENT_CACHE_CLEANUP,
     PROP_FUEL_LOCATION_SOURCE,
@@ -15,7 +12,7 @@
     PROP_AREA_RADIUS
 )
 from pyfuelprices.fuel_locations import FuelLocation, Fuel
-from pyfuelprices.helpers import get_bounding_box
+from pyfuelprices.helpers import geocoder
 from pyfuelprices.sources import Source
 
 from .const import FUELSNOOP_API_HEADERS, FUELSNOOP_API_SITES
@@ -57,32 +54,29 @@ async def search_sites(self, coordinates, radius: float) -> list[dict]:
         )
         return await super().search_sites(coordinates, radius)
 
-    async def update(self, areas=None, force=None) -> list[FuelLocation]:
-        """Custom update handler to look all products."""
-        self._configured_areas=[] if areas is None else areas
-        if datetime.now() > self.next_update or force:
-            for area in self._configured_areas:
-                _LOGGER.debug("Searching FuelSnoop for FuelLocations at area %s",
-                              area)
-                bbox = get_bounding_box(
-                    area[PROP_AREA_LAT],
-                    area[PROP_AREA_LONG],
-                    area[PROP_AREA_RADIUS]
-                )
-                response_raw = await self._send_request(
-                    url=FUELSNOOP_API_SITES,
-                    body={
-                        "min_lng": bbox.lon_min,
-                        "min_lat": bbox.lat_min,
-                        "max_lng": bbox.lon_max,
-                        "max_lat": bbox.lat_max,
-                        "brand_names": []
-                    }
-                )
-                if response_raw is not None:
-                    await self.parse_response(json.loads(response_raw))
-            self.next_update += self.update_interval
-            return list(self.location_cache.values())
+    async def update_area(self, area)  -> bool:
+        """Update a given area."""
+        _LOGGER.debug("Searching FuelSnoop for FuelLocations at area %s",
+                        area)
+        bbox = geocoder.get_bounding_box(
+            area[PROP_AREA_LAT],
+            area[PROP_AREA_LONG],
+            area[PROP_AREA_RADIUS]
+        )
+        response_raw = await self._send_request(
+            url=FUELSNOOP_API_SITES,
+            body={
+                "min_lng": bbox.lon_min,
+                "min_lat": bbox.lat_min,
+                "max_lng": bbox.lon_max,
+                "max_lat": bbox.lat_max,
+                "brand_names": []
+            }
+        )
+        if response_raw is not None:
+            await self.parse_response(json.loads(response_raw))
+            return True
+        return False
 
     async def parse_response(self, response) -> list[FuelLocation]:
         for station in response:
diff --git a/pyfuelprices/sources/australia/fuelwatch.py b/pyfuelprices/sources/australia/fuelwatch.py
index 8ebe08b..09b9d59 100644
--- a/pyfuelprices/sources/australia/fuelwatch.py
+++ b/pyfuelprices/sources/australia/fuelwatch.py
@@ -92,11 +92,9 @@ def _update_fuel_station_prices(self, station, site_id):
                 )
             )
 
-    async def update(self, areas=None, force=None) -> list[FuelLocation]:
+    async def update(self, areas=None, force=False) -> list[FuelLocation]:
         """Custom update handler to look all products."""
-        # _df_columns = ["lat", "long"]
-        # _df_data = []
-        if datetime.now() > self.next_update:
+        if datetime.now() > self.next_update or force:
             if len(self._fuel_products) == 0:
                 await self.get_fuel_products()
             for product in self._fuel_products:
@@ -117,8 +115,14 @@ async def update(self, areas=None, force=None) -> list[FuelLocation]:
             self.next_update += self.update_interval
             return list(self.location_cache.values())
 
+    async def update_area(self, area) -> bool:
+        """Method not used."""
+        raise NotImplementedError
+
     async def parse_response(self, response) -> list[FuelLocation]:
         """Method not used."""
+        raise NotImplementedError
 
     def parse_fuels(self, fuels) -> list[Fuel]:
         """Method not used."""
+        raise NotImplementedError
diff --git a/pyfuelprices/sources/australia/petrolspy.py b/pyfuelprices/sources/australia/petrolspy.py
index 0ae22c8..1d810ec 100644
--- a/pyfuelprices/sources/australia/petrolspy.py
+++ b/pyfuelprices/sources/australia/petrolspy.py
@@ -1,11 +1,8 @@
 """FuelSnoop data source for Australia (QLD)."""
 
-from datetime import datetime
 import logging
 import json
 
-from geopy import distance
-
 from pyfuelprices.const import (
     PROP_FUEL_LOCATION_PREVENT_CACHE_CLEANUP,
     PROP_FUEL_LOCATION_SOURCE,
@@ -15,7 +12,7 @@
     PROP_AREA_RADIUS
 )
 from pyfuelprices.fuel_locations import FuelLocation, Fuel
-from pyfuelprices.helpers import get_bounding_box
+from pyfuelprices.helpers import geocoder
 from pyfuelprices.sources import Source
 
 from .const import PETROLSPY_API_HEADERS, PETROLSPY_API_SITES
@@ -59,30 +56,27 @@ async def search_sites(self, coordinates, radius: float) -> list[dict]:
         )
         return await super().search_sites(coordinates, radius)
 
-    async def update(self, areas=None, force=None) -> list[FuelLocation]:
-        """Custom update handler to look all products."""
-        self._configured_areas=[] if areas is None else areas
-        if datetime.now() > self.next_update or force:
-            for area in self._configured_areas:
-                _LOGGER.debug("Searching PetrolSpy for FuelLocations at area %s",
-                              area)
-                bbox = get_bounding_box(
-                    area[PROP_AREA_LAT],
-                    area[PROP_AREA_LONG],
-                    area[PROP_AREA_RADIUS]
-                )
-                response_raw = await self._send_request(
-                    url=PETROLSPY_API_SITES.format(
-                        LAT_MAX=bbox.lat_max,
-                        LNG_MAX=bbox.lon_max,
-                        LAT_MIN=bbox.lat_min,
-                        LNG_MIN=bbox.lon_min
-                    )
-                )
-                if response_raw is not None:
-                    await self.parse_response(json.loads(response_raw))
-            self.next_update += self.update_interval
-            return list(self.location_cache.values())
+    async def update_area(self, area) -> bool:
+        """Update a given area."""
+        _LOGGER.debug("Searching PetrolSpy for FuelLocations at area %s",
+                        area)
+        bbox = geocoder.get_bounding_box(
+            area[PROP_AREA_LAT],
+            area[PROP_AREA_LONG],
+            area[PROP_AREA_RADIUS]
+        )
+        response_raw = await self._send_request(
+            url=PETROLSPY_API_SITES.format(
+                LAT_MAX=bbox.lat_max,
+                LNG_MAX=bbox.lon_max,
+                LAT_MIN=bbox.lat_min,
+                LNG_MIN=bbox.lon_min
+            )
+        )
+        if response_raw is not None:
+            await self.parse_response(json.loads(response_raw))
+            return True
+        return False
 
     async def parse_response(self, response) -> list[FuelLocation]:
         for station in response["message"]["list"]:
diff --git a/pyfuelprices/sources/austria/const.py b/pyfuelprices/sources/austria/const.py
new file mode 100644
index 0000000..e560125
--- /dev/null
+++ b/pyfuelprices/sources/austria/const.py
@@ -0,0 +1,13 @@
+"""Austria consts."""
+
+CONST_API_BASE = "https://www.spritpreisrechner.at/api"
+CONST_API_SEARCH_REGIONS = (
+    f"{CONST_API_BASE}/search/gas-stations/by-region?"
+    "code={REG_CODE}&fuelType={FUEL_TYPE}&includeClosed=true&type={REG_TYPE}"
+)
+CONST_API_SEARCH_ADDRESS = (
+    f"{CONST_API_BASE}/search/gas-stations/by-address?"
+    "latitude={LAT}&longitude={LONG}&fuelType={FUEL_TYPE}&includeClosed=true"
+)
+CONST_API_GET_REGIONS = f"{CONST_API_BASE}/regions"
+CONST_FUELS = ["DIE", "SUP", "GAS"]
\ No newline at end of file
diff --git a/pyfuelprices/sources/austria/spritpreisrechner.py b/pyfuelprices/sources/austria/spritpreisrechner.py
index 9e81321..2877855 100644
--- a/pyfuelprices/sources/austria/spritpreisrechner.py
+++ b/pyfuelprices/sources/austria/spritpreisrechner.py
@@ -17,22 +17,16 @@
 )
 from pyfuelprices.fuel_locations import FuelLocation, Fuel
 from pyfuelprices.sources import Source
-from pyfuelprices.helpers import geocode_reverse_lookup
+from pyfuelprices.helpers import geocoder
 
-_LOGGER = logging.getLogger(__name__)
-
-CONST_API_BASE = "https://www.spritpreisrechner.at/api"
-CONST_API_SEARCH_REGIONS = (
-    f"{CONST_API_BASE}/search/gas-stations/by-region?"
-    "code={REG_CODE}&fuelType={FUEL_TYPE}&includeClosed=true&type={REG_TYPE}"
+from .const import (
+    CONST_API_GET_REGIONS,
+    CONST_API_SEARCH_ADDRESS,
+    CONST_API_SEARCH_REGIONS,
+    CONST_FUELS
 )
-CONST_API_SEARCH_ADDRESS = (
-    f"{CONST_API_BASE}/search/gas-stations/by-address?"
-    "latitude={LAT}&longitude={LONG}&fuelType={FUEL_TYPE}&includeClosed=true"
-)
-CONST_API_GET_REGIONS = f"{CONST_API_BASE}/regions"
-CONST_FUELS = ["DIE", "SUP", "GAS"]
 
+_LOGGER = logging.getLogger(__name__)
 
 class SpripreisrechnerATSource(Source):
     """Data source for spritpreisrechner."""
@@ -98,17 +92,17 @@ def _update_fuel_station_prices(self, station, site_id):
             self.location_cache[site_id].last_updated = datetime.now()
             self.location_cache[site_id].next_update = self.next_update + self.update_interval
 
-    async def update_area(self, area: dict):
+    async def update_area(self, area: dict) -> bool:
         """Update given areas."""
         _LOGGER.debug("Searching FuelGR for FuelLocations at area %s", area)
         parser_coords = (area[PROP_AREA_LAT], area[PROP_AREA_LONG])
-        geocoded = await geocode_reverse_lookup(parser_coords)
+        geocoded = await geocoder.geocode_reverse_lookup(parser_coords)
         if geocoded is None:
             _LOGGER.debug("Geocode failed, skipping area %s", area)
-            return
+            return False
         if geocoded.raw["address"]["country_code"] not in ["at"]:
             _LOGGER.debug("Geocode not within AT, skipping area %s", area)
-            return
+            return False
         for fuel in CONST_FUELS:
             await self.parse_response(
                 json.loads(
@@ -119,6 +113,7 @@ async def update_area(self, area: dict):
                     )
                 )
             )
+        return True
 
     async def update_region(self, region_code, region_type):
         """Update fuels for a given region."""
@@ -171,3 +166,4 @@ async def parse_response(self, response) -> list[FuelLocation]:
 
     def parse_fuels(self, fuels) -> list[Fuel]:
         """Method not used."""
+        raise NotImplementedError
diff --git a/pyfuelprices/sources/brazil/const.py b/pyfuelprices/sources/brazil/const.py
new file mode 100644
index 0000000..6cf8032
--- /dev/null
+++ b/pyfuelprices/sources/brazil/const.py
@@ -0,0 +1,15 @@
+"""Brazil consts."""
+
+from pyfuelprices.build_data import (
+    GASPASS_URL
+)
+
+CONST_GET_FUELS = f"{GASPASS_URL}/api/v1/gaspass/"
+CONST_FUELS = [
+    "ultimo_preco_alcool",
+    "ultimo_preco_diesel",
+    "ultimo_preco_diesel_s10",
+    "ultimo_preco_gasolina",
+    "ultimo_preco_gasolina_adt",
+    "ultimo_preco_gnv"
+]
diff --git a/pyfuelprices/sources/brazil/gaspass.py b/pyfuelprices/sources/brazil/gaspass.py
index 26289ec..7e5fb44 100644
--- a/pyfuelprices/sources/brazil/gaspass.py
+++ b/pyfuelprices/sources/brazil/gaspass.py
@@ -4,7 +4,6 @@
 import json
 
 from datetime import datetime, timedelta
-from geopy import distance
 
 from pyfuelprices.const import (
     PROP_AREA_LAT,
@@ -14,38 +13,28 @@
     PROP_FUEL_LOCATION_PREVENT_CACHE_CLEANUP,
     PROP_FUEL_LOCATION_SOURCE_ID,
 )
-from pyfuelprices.build_data import (
-    GASPASS_URL
-)
 from pyfuelprices.fuel_locations import Fuel, FuelLocation
+from pyfuelprices.helpers import geocoder, geopyexc
 from pyfuelprices.sources import (
     Source,
-    geocode_reverse_lookup,
-    geopyexc,
     UpdateFailedError
 )
+from .const import (
+    CONST_FUELS,
+    CONST_GET_FUELS
+)
 
 _LOGGER = logging.getLogger(__name__)
 
-CONST_GET_FUELS = f"{GASPASS_URL}/api/v1/gaspass/"
-CONST_FUELS = [
-    "ultimo_preco_alcool",
-    "ultimo_preco_diesel",
-    "ultimo_preco_diesel_s10",
-    "ultimo_preco_gasolina",
-    "ultimo_preco_gasolina_adt",
-    "ultimo_preco_gnv"
-]
-
 class GasPassSource(Source):
     """GasPass Source."""
     provider_name = "gaspass"
     location_cache: dict[str, FuelLocation] = {}
 
-    def __init__(self, update_interval = ..., client_session = None):
+    def __init__(self, configured_areas = None, update_interval = ..., client_session = None, configuration = None):
         if update_interval.seconds < 3600:
             update_interval = timedelta(hours=1)
-        super().__init__(update_interval, client_session)
+        super().__init__(configured_areas, update_interval, client_session, configuration)
 
     async def _send_request(self, lat, long, radius):
         """Send a request to the API for a given postcode and radius."""
@@ -90,36 +79,32 @@ async def search_sites(self, coordinates, radius: float) -> list[dict]:
         )
         return await super().search_sites(coordinates, radius)
 
-    async def update(self, areas=None, force=None) -> list[FuelLocation]:
-        """Custom update handler as this needs to query GasPass on areas."""
-        if datetime.now() > self.next_update or force:
-            self._configured_areas=[] if areas is None else areas
-            for area in self._configured_areas:
-                try:
-                    geocode = await geocode_reverse_lookup(
-                        (area[PROP_AREA_LAT], area[PROP_AREA_LONG])
-                    )
-                except geopyexc.GeocoderTimedOut:
-                    _LOGGER.warning("Timeout occured while geocoding area %s.",
-                                    area)
-                    continue
-                if geocode.raw["address"]["country_code"] != "br":
-                    _LOGGER.debug("Skipping area %s as not in BR.",
-                                area)
-                    continue
-                response_raw = json.loads(await self._send_request(
-                    lat=area[PROP_AREA_LAT],
-                    long=area[PROP_AREA_LONG],
-                    radius=area[PROP_AREA_RADIUS]
-                ))
-                if response_raw["message"] == "ok":
-                    await self.parse_response(response_raw)
-                else:
-                    _LOGGER.error("Error sending request to %s: %s",
-                                self.provider_name,
-                                response_raw)
-            self.next_update += self.update_interval
-        return list(self.location_cache.values())
+    async def update_area(self, area) -> bool:
+        """Update a given area."""
+        try:
+            geocode = await geocoder.geocode_reverse_lookup(
+                (area[PROP_AREA_LAT], area[PROP_AREA_LONG])
+            )
+        except geopyexc.GeocoderTimedOut:
+            _LOGGER.warning("Timeout occured while geocoding area %s.",
+                            area)
+            return False
+        if geocode.raw["address"]["country_code"] != "br":
+            _LOGGER.debug("Skipping area %s as not in BR.",
+                        area)
+            return False
+        response_raw = json.loads(await self._send_request(
+            lat=area[PROP_AREA_LAT],
+            long=area[PROP_AREA_LONG],
+            radius=area[PROP_AREA_RADIUS]
+        ))
+        if response_raw["message"] == "ok":
+            await self.parse_response(response_raw)
+            return True
+        _LOGGER.error("Error sending request to %s: %s",
+                    self.provider_name,
+                    response_raw)
+        return False
 
     def _parse_raw(self, station: dict) -> FuelLocation:
         """Parse a single raw instance of a fuel site."""
diff --git a/pyfuelprices/sources/germany/const.py b/pyfuelprices/sources/germany/const.py
new file mode 100644
index 0000000..e57e6cf
--- /dev/null
+++ b/pyfuelprices/sources/germany/const.py
@@ -0,0 +1,7 @@
+"""Germany consts."""
+
+CONST_TANKERKOENIG_API_BASE = "https://tankerkoenig.de/ajax_v3_public/"
+CONST_TANKERKOENIG_GET_STATIONS = (
+    f"{CONST_TANKERKOENIG_API_BASE}get_stations_near_postcode.php"
+    "?postcode={POSTCODE}&radius={RADIUS}"
+)
diff --git a/pyfuelprices/sources/germany/tankerkoenig.py b/pyfuelprices/sources/germany/tankerkoenig.py
index 4f0ca35..ba23fe0 100644
--- a/pyfuelprices/sources/germany/tankerkoenig.py
+++ b/pyfuelprices/sources/germany/tankerkoenig.py
@@ -15,19 +15,15 @@
     DESKTOP_USER_AGENT
 )
 from pyfuelprices.fuel_locations import Fuel, FuelLocation
+from pyfuelprices.helpers import geopyexc, geocoder
 from pyfuelprices.sources import (
-    Source,
-    geocode_reverse_lookup,
-    geopyexc
+    Source
+)
+from .const import (
+    CONST_TANKERKOENIG_GET_STATIONS
 )
-
 _LOGGER = logging.getLogger(__name__)
 
-CONST_TANKERKOENIG_API_BASE = "https://tankerkoenig.de/ajax_v3_public/"
-CONST_TANKERKOENIG_GET_STATIONS = (
-    f"{CONST_TANKERKOENIG_API_BASE}get_stations_near_postcode.php"
-    "?postcode={POSTCODE}&radius={RADIUS}"
-)
 
 class TankerKoenigSource(Source):
     """TankerKoenig Source."""
@@ -73,35 +69,31 @@ async def search_sites(self, coordinates, radius: float) -> list[dict]:
         )
         return await super().search_sites(coordinates, radius)
 
-    async def update(self, areas=None, force=None) -> list[FuelLocation]:
-        """Custom update handler as this needs to query TankerKoenig on areas."""
-        if datetime.now() > self.next_update or force:
-            self._configured_areas=[] if areas is None else areas
-            for area in self._configured_areas:
-                try:
-                    geocode = await geocode_reverse_lookup(
-                        (area[PROP_AREA_LAT], area[PROP_AREA_LONG])
-                    )
-                except geopyexc.GeocoderTimedOut:
-                    _LOGGER.warning("Timeout occured while geocoding area %s.",
-                                    area)
-                    continue
-                if geocode.raw["address"]["country_code"] != "de":
-                    _LOGGER.debug("Skipping area %s as not in DE.",
-                                area)
-                    continue
-                response_raw = json.loads(await self._send_request(
-                    postcode=geocode.raw["address"]["postcode"],
-                    radius=area[PROP_AREA_RADIUS]
-                ))
-                if response_raw["ok"]:
-                    await self.parse_response(response_raw["data"])
-                else:
-                    _LOGGER.error("Error sending request to %s: %s",
-                                self.provider_name,
-                                response_raw)
-            self.next_update += self.update_interval
-        return list(self.location_cache.values())
+    async def update_area(self, area) -> bool:
+        """Update a given area."""
+        try:
+            geocode = await geocoder.geocode_reverse_lookup(
+                (area[PROP_AREA_LAT], area[PROP_AREA_LONG])
+            )
+        except geopyexc.GeocoderTimedOut:
+            _LOGGER.warning("Timeout occured while geocoding area %s.",
+                            area)
+            return False
+        if geocode.raw["address"]["country_code"] != "de":
+            _LOGGER.debug("Skipping area %s as not in DE.",
+                        area)
+            return False
+        response_raw = json.loads(await self._send_request(
+            postcode=geocode.raw["address"]["postcode"],
+            radius=area[PROP_AREA_RADIUS]
+        ))
+        if response_raw["ok"]:
+            await self.parse_response(response_raw["data"])
+            return True
+        _LOGGER.error("Error sending request to %s: %s",
+                    self.provider_name,
+                    response_raw)
+        return False
 
     def _parse_raw(self, station: dict) -> FuelLocation:
         """Parse a single raw instance of a fuel site."""
diff --git a/pyfuelprices/sources/greece/fuelgr.py b/pyfuelprices/sources/greece/fuelgr.py
index b591bf7..82cb817 100644
--- a/pyfuelprices/sources/greece/fuelgr.py
+++ b/pyfuelprices/sources/greece/fuelgr.py
@@ -3,10 +3,9 @@
 import logging
 import asyncio
 
-from datetime import timedelta, datetime
+from datetime import timedelta
 
 import xmltodict
-from geopy import distance
 
 from pyfuelprices.const import (
     PROP_AREA_LAT,
@@ -15,8 +14,9 @@
     PROP_FUEL_LOCATION_PREVENT_CACHE_CLEANUP,
     PROP_FUEL_LOCATION_SOURCE_ID
 )
-from pyfuelprices.sources import Source, geocode_reverse_lookup, geopyexc
+from pyfuelprices.sources import Source
 from pyfuelprices.fuel_locations import Fuel, FuelLocation
+from pyfuelprices.helpers import geopyexc, geocoder
 
 from .const import (
     FUELGR_GET_DATA,
@@ -64,20 +64,20 @@ async def search_sites(self, coordinates, radius: float) -> list[dict]:
         )
         return await super().search_sites(coordinates, radius)
 
-    async def update_area(self, area: dict):
+    async def update_area(self, area: dict) -> bool:
         """Used by asyncio to update areas."""
         try:
-            geocode = await geocode_reverse_lookup(
+            geocode = await geocoder.geocode_reverse_lookup(
                 (area[PROP_AREA_LAT], area[PROP_AREA_LONG])
             )
         except geopyexc.GeocoderTimedOut:
             _LOGGER.warning("Timeout occured while geocoding area %s.",
                             area)
-            return None
+            return False
         if geocode.raw["address"]["country_code"] != "gr":
             _LOGGER.debug("Skipping area %s as not in GR.",
                         area)
-            return None
+            return False
         self._parser_coords = (area[PROP_AREA_LAT], area[PROP_AREA_LONG])
         _LOGGER.debug("Searching FuelGR for FuelLocations at area %s",
                     area)
@@ -96,23 +96,10 @@ async def update_area(self, area: dict):
         response_raw = response_raw["gss"]["gs"]
         _LOGGER.debug("Found %s fuel stations",
                     len(response_raw))
-        
         await self.parse_response(
             response=response_raw
         )
-
-    async def update(self, areas=None, force=None) -> list[FuelLocation]:
-        """Update the cached data."""
-        if self.next_update <= datetime.now() or force:
-            self._configured_areas=[] if areas is None else areas
-            try:
-                self.next_update += self.update_interval
-                coros = [self.update_area(a) for a in self._configured_areas]
-                asyncio.gather(*coros)
-            except Exception as exc:
-                _LOGGER.error(exc)
-
-        return list(self.location_cache.values())
+        return True
 
     async def get_fuel_station_fuels(self, station_id) -> list[Fuel]:
         """Return a list of fuels from a fuel station."""
diff --git a/pyfuelprices/sources/netherlands/directlease.py b/pyfuelprices/sources/netherlands/directlease.py
index 3534090..fd0923d 100644
--- a/pyfuelprices/sources/netherlands/directlease.py
+++ b/pyfuelprices/sources/netherlands/directlease.py
@@ -3,6 +3,8 @@
 import random
 import re
 import logging
+import json
+import csv
 import uuid
 import hashlib
 from datetime import datetime, timedelta
@@ -177,9 +179,46 @@ async def get_site(self, site_id) -> FuelLocation:
         await self.location_cache[site_id].dynamic_build_fuels()
         return self.location_cache[site_id]
 
+    async def _update(self):
+        """Internal update method."""
+        response = await self._client_session.request(
+            method=self._method,
+            url=self._url,
+            json=self._request_body,
+            headers=self._headers
+        )
+        _LOGGER.debug("Update request completed for %s with status %s",
+                    self.provider_name, response.status)
+        if response.status == 200:
+            self.next_update = datetime.now() + self.update_interval
+            if "application/json" not in response.content_type:
+                return await self.parse_response(
+                    response=json.loads(await response.text())
+                )
+            if response.content_type == "text/csv":
+                return await self.parse_response(
+                    response=list(csv.DictReader(await response.text()))
+                )
+            return await self.parse_response(
+                response=await response.json()
+            )
+        if response.status == 403:
+            raise ServiceBlocked(
+                status=response.status,
+                response=await response.text(),
+                headers=response.headers,
+                service=self.provider_name
+            )
+        raise UpdateFailedError(
+            status=response.status,
+            response=await response.text(),
+            headers=response.headers,
+            service=self.provider_name
+        )
+
     async def update(self, areas=None, force=None) -> list[FuelLocation]:
         try:
-            await super().update(areas)
+            await self._update()
         except UpdateFailedError as err:
             if err.status == 403:
                 # 403 for directlease is an IP block
@@ -187,7 +226,12 @@ async def update(self, areas=None, force=None) -> list[FuelLocation]:
                                 "1) Contact tankservice-block@app-it-up.com for further support. "
                                 "2) Change your WAN IP address or use a VPN. "
                                 "3) Use a proxy server to connect. "))
-                raise ServiceBlocked(err.status, err.response, err.headers, self.provider_name) from err
+                raise ServiceBlocked(
+                    err.status,
+                    err.response,
+                    err.headers,
+                    self.provider_name
+                ) from err
 
         self.next_update = datetime.now() + timedelta(
             days=1,
diff --git a/pyfuelprices/sources/new_zealand/finelly.py b/pyfuelprices/sources/new_zealand/finelly.py
index 4690353..aa06b09 100644
--- a/pyfuelprices/sources/new_zealand/finelly.py
+++ b/pyfuelprices/sources/new_zealand/finelly.py
@@ -51,7 +51,7 @@ def _build_request_url(self, lat: float, long: float, radius: float):
             USER=self.configuration["USER_ID"]
         )
 
-    async def update_area(self, area: dict):
+    async def update_area(self, area: dict) -> bool:
         """Update a given area."""
         response = await self._client_session.get(
             url=self._build_request_url(
@@ -61,23 +61,12 @@ async def update_area(self, area: dict):
             )
         )
         if response is None:
-            return
+            return False
         if not response.ok:
             _LOGGER.error("Error communicating with Finelly API.")
-            return
+            return False
         await self.parse_response(await response.json())
-
-    async def update(self, areas=None, force=False):
-        """Update Finelly areas."""
-        self._configured_areas = areas or []
-        self._clear_cache()
-        if self.next_update > datetime.now() and not force:
-            return
-        coros = [
-            self.update_area(a) for a in self._configured_areas
-        ]
-        await asyncio.gather(*coros)
-        return list(self.location_cache.values())
+        return True
 
     async def search_sites(self, coordinates, radius: float) -> list[dict]:
         """Return all available sites within a given radius."""
diff --git a/pyfuelprices/sources/romania/pecoonline.py b/pyfuelprices/sources/romania/pecoonline.py
index 40a1959..18a993f 100644
--- a/pyfuelprices/sources/romania/pecoonline.py
+++ b/pyfuelprices/sources/romania/pecoonline.py
@@ -3,7 +3,7 @@
 import logging
 import uuid
 import json
-from datetime import timedelta
+from datetime import timedelta, datetime
 
 from pyfuelprices.const import (
     PROP_FUEL_LOCATION_SOURCE,
@@ -52,6 +52,9 @@ async def get_site(self, site_id) -> FuelLocation:
 
     async def update(self, areas=None, force=None) -> list[FuelLocation]:
         """Update the cached data."""
+        if self.next_update > datetime.now() and not force:
+            _LOGGER.debug("Ignoring update request")
+            return
         try:
             response_raw = await self._send_request(
                 url=self._url,
@@ -65,9 +68,9 @@ async def update(self, areas=None, force=None) -> list[FuelLocation]:
             await self.parse_response(
                 response=json.loads(response_raw)
             )
+            self.next_update = datetime.now() + self.update_interval
         except Exception as exc:
             _LOGGER.error(exc)
-
         return list(self.location_cache.values())
 
     async def parse_raw_fuel_station(self, station: dict) -> FuelLocation:
diff --git a/pyfuelprices/sources/slovenia/goriva.py b/pyfuelprices/sources/slovenia/goriva.py
index d4bf27c..43e9002 100644
--- a/pyfuelprices/sources/slovenia/goriva.py
+++ b/pyfuelprices/sources/slovenia/goriva.py
@@ -2,7 +2,6 @@
 
 import logging
 import json
-from datetime import datetime
 
 from pyfuelprices.const import (
     PROP_FUEL_LOCATION_SOURCE,
@@ -12,9 +11,10 @@
     PROP_AREA_LONG,
     PROP_AREA_RADIUS
 )
-from pyfuelprices.sources import Source, geocode_reverse_lookup, geopyexc
+from pyfuelprices.sources import Source
 from pyfuelprices.fuel import Fuel
 from pyfuelprices.fuel_locations import FuelLocation
+from pyfuelprices.helpers import geocoder, geopyexc
 
 _LOGGER = logging.getLogger(__name__)
 
@@ -58,39 +58,34 @@ async def search_sites(self, coordinates, radius: float) -> list[dict]:
         data = await super().search_sites(coordinates, radius)
         return data
 
-    async def update(self, areas=None, force=False) -> list[FuelLocation]:
-        """Update hooks for the data source."""
-        _LOGGER.debug("Starting update hook for %s to url %s", self.provider_name, self._url)
-        self._configured_areas=[] if areas is None else areas
-        self._clear_cache()
-        if self.next_update <= datetime.now() or force:
-            for area in self._configured_areas:
-                try:
-                    geocode = await geocode_reverse_lookup(
-                        (area[PROP_AREA_LAT], area[PROP_AREA_LONG])
-                    )
-                except geopyexc.GeocoderTimedOut:
-                    _LOGGER.warning("Timeout occured while geocoding area %s.",
-                                    area)
-                    continue
-                if geocode.raw["address"]["country_code"] != "si":
-                    _LOGGER.debug("Skipping area %s as not in SI.",
-                                area)
-                    continue
-                response = json.loads(await self._send_request(
-                    url=self._url,
-                    radius=area[PROP_AREA_RADIUS],
-                    lat=area[PROP_AREA_LAT],
-                    long=area[PROP_AREA_LONG]
-                ))
-                await self.parse_response(response["results"])
-            self.next_update = datetime.now() + self.update_interval
-        return list(self.location_cache.values())
+    async def update_area(self, area) -> bool:
+        """Update a given area."""
+        try:
+            geocode = await geocoder.geocode_reverse_lookup(
+                (area[PROP_AREA_LAT], area[PROP_AREA_LONG])
+            )
+        except geopyexc.GeocoderTimedOut:
+            _LOGGER.warning("Timeout occured while geocoding area %s.",
+                            area)
+            return False
+        if geocode.raw["address"]["country_code"] != "si":
+            _LOGGER.debug("Skipping area %s as not in SI.",
+                        area)
+            return False
+        response = json.loads(await self._send_request(
+            url=self._url,
+            radius=area[PROP_AREA_RADIUS],
+            lat=area[PROP_AREA_LAT],
+            long=area[PROP_AREA_LONG]
+        ))
+        await self.parse_response(response["results"])
+        return True
 
     async def parse_response(self, response) -> list[FuelLocation]:
-        """Converts CMA data into fuel price mapping."""
+        """Converts data into fuel price mapping."""
         for location_raw in response:
             site_id = f"{self.provider_name}_{location_raw['pk']}"
+            _LOGGER.debug("Parsing location %s", site_id)
             location = FuelLocation.create(
                     site_id=site_id,
                     name=location_raw['name'],
diff --git a/pyfuelprices/sources/switzerland/comparis.py b/pyfuelprices/sources/switzerland/comparis.py
index a69dc01..c32195a 100644
--- a/pyfuelprices/sources/switzerland/comparis.py
+++ b/pyfuelprices/sources/switzerland/comparis.py
@@ -15,11 +15,10 @@
     PROP_FUEL_LOCATION_SOURCE_ID,
     DESKTOP_USER_AGENT
 )
+from pyfuelprices.helpers import geocoder, geopyexc
 from pyfuelprices.fuel_locations import Fuel, FuelLocation
 from pyfuelprices.sources import (
-    Source,
-    geocode_reverse_lookup,
-    geopyexc
+    Source
 )
 
 _LOGGER = logging.getLogger(__name__)
@@ -57,43 +56,40 @@ async def _send_request(self, postcode, radius):
                     _LOGGER.debug("Content within script tag with id='__NEXT_DATA__':")
                     return extracted_content
                 else:
-                    _LOGGER.error("Script tag with id='__NEXT_DATA__' not found in the HTML content.")
+                    _LOGGER.error(
+                        "Script tag with id='__NEXT_DATA__' not found in the HTML content."
+                    )
             _LOGGER.error("Error sending request to %s: %s",
                             url,
                             response)
             return "{}"
 
-    async def update(self, areas=None, force=None) -> list[FuelLocation]:
-        """Custom update handler as this needs to query Comparis on areas."""
-        if datetime.now() > self.next_update:
-            self._configured_areas=[] if areas is None else areas
-            for area in self._configured_areas:
-                try:
-                    geocode = await geocode_reverse_lookup(
-                        (area[PROP_AREA_LAT], area[PROP_AREA_LONG])
-                    )
-                except geopyexc.GeocoderTimedOut:
-                    _LOGGER.warning("Timeout occured while geocoding area %s.",
-                                    area)
-                    continue
-                if geocode.raw["address"]["country_code"] != "ch":
-                    _LOGGER.debug("Skipping area %s as not in CH.",
-                                area)
-                    continue
-                response_raw = json.loads(await self._send_request(
-                    postcode=geocode.raw["address"]["postcode"],
-                    radius=area[PROP_AREA_RADIUS]
-                ))
-                
-                if len(response_raw) != 0:
-                    response_raw = response_raw["props"]["pageProps"]
-                    await self.parse_response(response_raw["data"])
-                else:
-                    _LOGGER.error("Error sending request to %s: %s",
-                                self.provider_name,
-                                response_raw)
-            self.next_update += self.update_interval
-        return list(self.location_cache.values())
+    async def update_area(self, area) -> bool:
+        """Update a given area."""
+        try:
+            geocode = await geocoder.geocode_reverse_lookup(
+                (area[PROP_AREA_LAT], area[PROP_AREA_LONG])
+            )
+        except geopyexc.GeocoderTimedOut:
+            _LOGGER.warning("Timeout occured while geocoding area %s.",
+                            area)
+            return False
+        if geocode.raw["address"]["country_code"] != "ch":
+            _LOGGER.debug("Skipping area %s as not in CH.",
+                        area)
+            return False
+        response_raw = json.loads(await self._send_request(
+            postcode=geocode.raw["address"]["postcode"],
+            radius=area[PROP_AREA_RADIUS]
+        ))
+        if len(response_raw) != 0:
+            response_raw = response_raw["props"]["pageProps"]
+            await self.parse_response(response_raw["data"])
+            return True
+        _LOGGER.error("Error sending request to %s: %s",
+                    self.provider_name,
+                    response_raw)
+        return False
 
     def _parse_raw(self, station: dict) -> FuelLocation:
         """Parse a single raw instance of a fuel site."""
@@ -147,7 +143,7 @@ def parse_fuels(self, fuels: dict[str, object]) -> list[Fuel]:
         """Parse fuel data from Comparis."""
         fuels_parsed = []
         for k in fuels:
-            if fuels[k] != None:
+            if fuels[k] is not None:
                 fuels_parsed.append(Fuel(
                     fuel_type=str(k).upper(),
                     cost=fuels[k]["displayPrice"],
diff --git a/pyfuelprices/sources/uk/__init__.py b/pyfuelprices/sources/uk/__init__.py
index c3bbdd3..01783ea 100644
--- a/pyfuelprices/sources/uk/__init__.py
+++ b/pyfuelprices/sources/uk/__init__.py
@@ -1,19 +1,70 @@
 """UK Fuel Sources and parsers."""
 
+import logging
+import json
+import csv
+from datetime import datetime
+
 from pyfuelprices.const import (
     PROP_FUEL_LOCATION_SOURCE,
     PROP_FUEL_LOCATION_PREVENT_CACHE_CLEANUP,
     PROP_FUEL_LOCATION_SOURCE_ID
 )
-from pyfuelprices.sources import Source
+from pyfuelprices.sources import Source, UpdateFailedError, ServiceBlocked
 from pyfuelprices.fuel import Fuel
 from pyfuelprices.fuel_locations import FuelLocation
 
+_LOGGER = logging.getLogger(__name__)
+
 class CMAParser(Source):
     """This parser is specific for the scheme by the CMA."""
 
     location_cache: dict[str, FuelLocation] = {}
 
+    async def update_area(self, area):
+        """Method not used."""
+        raise NotImplementedError
+
+    async def update(self, areas=None, force=False):
+        """Update data."""
+        if self.next_update > datetime.now() and not force:
+            _LOGGER.debug("Ignoring update request")
+            return
+        response = await self._client_session.request(
+            method=self._method,
+            url=self._url,
+            json=self._request_body,
+            headers=self._headers
+        )
+        _LOGGER.debug("Update request completed for %s with status %s",
+                    self.provider_name, response.status)
+        if response.status == 200:
+            self.next_update = datetime.now() + self.update_interval
+            if "application/json" not in response.content_type:
+                return await self.parse_response(
+                    response=json.loads(await response.text())
+                )
+            if response.content_type == "text/csv":
+                return await self.parse_response(
+                    response=list(csv.DictReader(await response.text()))
+                )
+            return await self.parse_response(
+                response=await response.json()
+            )
+        if response.status == 403:
+            raise ServiceBlocked(
+                status=response.status,
+                response=await response.text(),
+                headers=response.headers,
+                service=self.provider_name
+            )
+        raise UpdateFailedError(
+            status=response.status,
+            response=await response.text(),
+            headers=response.headers,
+            service=self.provider_name
+        )
+
     async def parse_response(self, response) -> list[FuelLocation]:
         """Converts CMA data into fuel price mapping."""
         for location_raw in response["stations"]:
diff --git a/pyfuelprices/sources/uk/costco.py b/pyfuelprices/sources/uk/costco.py
index 64044cf..d74199e 100644
--- a/pyfuelprices/sources/uk/costco.py
+++ b/pyfuelprices/sources/uk/costco.py
@@ -7,7 +7,7 @@
     DESKTOP_USER_AGENT
 )
 from pyfuelprices.fuel_locations import Fuel, FuelLocation
-from pyfuelprices.sources import Source
+from pyfuelprices.sources.uk import CMAParser
 
 COSTCO_FUEL_MAPPING = {
     "Premium Unleaded Petrol": "E5",
@@ -15,7 +15,7 @@
     "Unleaded Petrol": "E10"
 }
 
-class CostcoUKSource(Source):
+class CostcoUKSource(CMAParser):
     """A Costco data source."""
     _url = "https://www.costco.co.uk/store-finder/search?q=United%20Kingdom&page=0"
     _headers = {
diff --git a/pyfuelprices/sources/uk/petrolprices.py b/pyfuelprices/sources/uk/petrolprices.py
index 27e3488..afe8c6d 100644
--- a/pyfuelprices/sources/uk/petrolprices.py
+++ b/pyfuelprices/sources/uk/petrolprices.py
@@ -101,7 +101,7 @@ async def _refresh_access_token(self):
                 self._refresh_token = response["refreshToken"]
                 self._at_expires = datetime.now() + timedelta(hours=23)
 
-    async def update_area(self, area: dict, fuel_type: str, fuel_code: str):
+    async def update_area(self, area: dict, fuel_type: str, fuel_code: str) -> bool:
         """Update a given area."""
         response = await self._send_request(
             CONST_PETROLPRICES_FUELSTATIONS.format(
@@ -114,24 +114,27 @@ async def update_area(self, area: dict, fuel_type: str, fuel_code: str):
             method="GET"
         )
         if response is None:
-            return
+            return False
         response["fuel_code"] = fuel_code
         await self.parse_response(response)
+        return True
 
     async def update(self, areas=None, force=False):
         """Update PetrolPrices data."""
-        self._configured_areas = areas or []
+        areas = areas or self._configured_areas
         self._clear_cache()
         if self.next_update > datetime.now() and not force:
+            _LOGGER.debug("Ignoring update request")
             return
         coros = [
-            self.update_area(a, ft, code) for a in self._configured_areas
+            self.update_area(a, ft, code) for a in areas
             for ft, code in CONST_PETROLPRICES_FUEL_MAP.items()
         ]
         results = await asyncio.gather(*coros)
         for result in results:
             if isinstance(result, Exception):
                 _LOGGER.exception("Error updating area: %s", result)
+        self.next_update = datetime.now() + self.update_interval
         return list(self.location_cache.values())
 
     async def parse_or_update_station(self, station: dict, fuel_type: str):
diff --git a/pyfuelprices/sources/uk/podpoint.py b/pyfuelprices/sources/uk/podpoint.py
index d318f85..50422f0 100644
--- a/pyfuelprices/sources/uk/podpoint.py
+++ b/pyfuelprices/sources/uk/podpoint.py
@@ -12,8 +12,9 @@
     PROP_FUEL_LOCATION_PREVENT_CACHE_CLEANUP,
     PROP_FUEL_LOCATION_SOURCE_ID
 )
-from pyfuelprices.sources import Source, geocode_reverse_lookup, geopyexc
+from pyfuelprices.sources import Source
 from pyfuelprices.fuel_locations import Fuel, FuelLocation
+from pyfuelprices.helpers import geocoder, geopyexc
 
 from .const import CONST_PODPOINT_BASE, CONST_PODPOINT_LOCATION, CONST_PODPOINT_SEARCH
 
@@ -52,20 +53,20 @@ async def search_sites(self, coordinates, radius: float) -> list[dict]:
         )
         return await super().search_sites(coordinates, radius)
 
-    async def update_area(self, area: dict):
+    async def update_area(self, area: dict) -> bool:
         """Update a single area."""
         try:
-            geocode = await geocode_reverse_lookup(
+            geocode = await geocoder.geocode_reverse_lookup(
                 (area[PROP_AREA_LAT], area[PROP_AREA_LONG])
             )
         except geopyexc.GeocoderTimedOut:
             _LOGGER.warning("Timeout occured while geocoding area %s.",
                             area)
-            return None
+            return False
         if geocode.raw["address"]["country_code"] != "gb":
             _LOGGER.debug("Skipping area %s as not in GB.",
                         area)
-            return None
+            return False
         _LOGGER.debug("Searching Pod Point for FuelLocations at area %s", area)
         response_raw = await self._send_request(
             url=CONST_PODPOINT_SEARCH.format(
@@ -75,6 +76,7 @@ async def update_area(self, area: dict):
             )
         )
         await self.parse_response(response_raw)
+        return True
 
     async def get_and_parse_pod(self, response: dict):
         """Retrieves pod details and parses the response."""
@@ -148,16 +150,3 @@ async def parse_response(self, response):
         """Parse fuel station response."""
         coros = [self.get_and_parse_pod(x) for x in response["addresses"]]
         await asyncio.gather(*coros)
-
-    async def update(self, areas=None, force=False):
-        """Update data source."""
-        if self.next_update <= datetime.now() or force:
-            self._configured_areas=[] if areas is None else areas
-            try:
-                self.next_update += self.update_interval
-                coros = [self.update_area(a) for a in self._configured_areas]
-                await asyncio.gather(*coros)
-            except Exception as exc:
-                _LOGGER.exception(exc, exc_info=exc)
-
-        return list(self.location_cache.values())
diff --git a/pyfuelprices/sources/usa/gasbuddy.py b/pyfuelprices/sources/usa/gasbuddy.py
index 8138b0c..f1e94c3 100644
--- a/pyfuelprices/sources/usa/gasbuddy.py
+++ b/pyfuelprices/sources/usa/gasbuddy.py
@@ -15,7 +15,7 @@
     PROP_FUEL_LOCATION_PREVENT_CACHE_CLEANUP,
     PROP_FUEL_LOCATION_SOURCE_ID
 )
-from pyfuelprices.helpers import geocode_reverse_lookup, get_bounding_box
+from pyfuelprices.helpers import geocoder
 from pyfuelprices.sources import Source
 
 _LOGGER = logging.getLogger(__name__)
@@ -86,40 +86,38 @@ async def search_sites(self, coordinates, radius: float) -> list[dict]:
         )
         return await super().search_sites(coordinates, radius)
 
-    async def update(self, areas=None, force=None) -> list[FuelLocation]:
-        """Custom update handler as this needs to query GasBuddy on areas."""
-        self._configured_areas=[] if areas is None else areas
-        for area in self._configured_areas:
-            self._parser_coords = (area[PROP_AREA_LAT], area[PROP_AREA_LONG])
-            self._parser_radius = area[PROP_AREA_RADIUS]
-            geocoded = await geocode_reverse_lookup(self._parser_coords)
-            if geocoded is None:
-                _LOGGER.debug("Geocode failed, skipping area %s", area)
-                continue
-            if geocoded.raw["address"]["country_code"] not in ["us", "ca"]:
-                _LOGGER.debug("Geocode not within USA, skipping area %s", area)
-                continue
-            _LOGGER.debug("Searching GasBuddy for FuelLocations at area %s",
+    async def update_area(self, area) -> bool:
+        """Update a given area."""
+        coords = (area[PROP_AREA_LAT], area[PROP_AREA_LONG])
+        radius = area[PROP_AREA_RADIUS]
+        geocoded = await geocoder.geocode_reverse_lookup(self._parser_coords)
+        if geocoded is None:
+            _LOGGER.debug("Geocode failed, skipping area %s", area)
+            return False
+        if geocoded.raw["address"]["country_code"] not in ["us", "ca"]:
+            _LOGGER.debug("Geocode not within USA, skipping area %s", area)
+            return False
+        _LOGGER.debug("Searching GasBuddy for FuelLocations at area %s",
                             area)
-            bbox = get_bounding_box(area[PROP_AREA_LAT],
-                                    area[PROP_AREA_LONG],
-                                    area[PROP_AREA_RADIUS])
-            response_raw = await self._send_request(
-                url=CONST_GASBUDDY_STATIONS_FMT.format(
-                    AUTHID=str(uuid.uuid4()),
-                    COUNTRY="US",
-                    DISTANCEFMT="auto",
-                    LIMIT=1000,
-                    LAT=area[PROP_AREA_LAT],
-                    LONG=area[PROP_AREA_LONG],
-                    MIN_LAT=bbox.lat_min,
-                    MIN_LON=bbox.lon_min,
-                    MAX_LAT=bbox.lat_max,
-                    MAX_LON=bbox.lon_max
-                )
+        bbox = geocoder.get_bounding_box(area[PROP_AREA_LAT],
+                                area[PROP_AREA_LONG],
+                                area[PROP_AREA_RADIUS])
+        response_raw = await self._send_request(
+            url=CONST_GASBUDDY_STATIONS_FMT.format(
+                AUTHID=str(uuid.uuid4()),
+                COUNTRY="US",
+                DISTANCEFMT="auto",
+                LIMIT=1000,
+                LAT=area[PROP_AREA_LAT],
+                LONG=area[PROP_AREA_LONG],
+                MIN_LAT=bbox.lat_min,
+                MIN_LON=bbox.lon_min,
+                MAX_LAT=bbox.lat_max,
+                MAX_LON=bbox.lon_max
             )
-            await self.parse_response(json.loads(response_raw))
-        return list(self.location_cache.values())
+        )
+        await self._parse_response(json.loads(response_raw), coords, radius)
+        return True
 
     async def parse_raw_fuel_station(self, station) -> FuelLocation:
         """Converts a raw instance of a fuel station into a fuel location."""
@@ -150,15 +148,15 @@ async def parse_raw_fuel_station(self, station) -> FuelLocation:
             await self.location_cache[site_id].update(loc)
         return self.location_cache[site_id]
 
-    async def parse_response(self, response) -> list[FuelLocation]:
+    async def _parse_response(self, response, coords, radius) -> list[FuelLocation]:
         if response.get("stations", None) is not None:
             response = response["stations"]
         for station in response:
             info = station["info"]
             if distance.distance(
-                self._parser_coords,
+                coords,
                 (info["latitude"], info["longitude"])
-            ).miles <= self._parser_radius:
+            ).miles <= radius:
                 await self.parse_raw_fuel_station(station=station)
 
         return list(self.location_cache.values())
@@ -175,3 +173,7 @@ def parse_fuels(self, fuels) -> list[Fuel]:
                 props=fuel
             ))
         return fuel_parsed
+
+    async def parse_response(self, response):
+        """Method not used."""
+        raise NotImplementedError
diff --git a/requirements.txt b/requirements.txt
index f7c310e..72267c1 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,4 +1,5 @@
 aiohttp>=2.8.0
 geopy==2.4.1
 xmltodict
-voluptuous
\ No newline at end of file
+voluptuous
+dotenv
\ No newline at end of file
diff --git a/test.py b/test.py
index e2d4bdf..044a6d9 100644
--- a/test.py
+++ b/test.py
@@ -1,12 +1,17 @@
 
 import logging
 import asyncio
+import os
+
+from dotenv import load_dotenv
 
 import voluptuous as vol
 
 from pyfuelprices import FuelPrices, SOURCE_MAP
 from pyfuelprices.const import PROP_AREA_LAT, PROP_AREA_LONG, PROP_AREA_RADIUS
 
+load_dotenv(".env_config")
+
 _LOGGER = logging.getLogger(__name__)
 
 async def main():
@@ -20,86 +25,91 @@ async def main():
                 PROP_AREA_LAT: 52.041627,
                 PROP_AREA_LONG: -0.759651 # UK
             },
-            # {
-            #     PROP_AREA_RADIUS: 5.0,
-            #     PROP_AREA_LAT: 52.23817,
-            #     PROP_AREA_LONG: 6.58763 # Netherlands
-            # },
-            # {
-            #     PROP_AREA_LAT: 49.134068,
-            #     PROP_AREA_LONG: -122.889980,
-            #     PROP_AREA_RADIUS: 5.0
-            # }, # CA
-            # {
-            #     PROP_AREA_LAT: 38.906316,
-            #     PROP_AREA_LONG: -77.054750,
-            #     PROP_AREA_RADIUS: 5.0 # US (No locality)
-            # },
-            # {
-            #     PROP_AREA_LAT: 40.157349,
-            #     PROP_AREA_LONG: -75.217079,
-            #     PROP_AREA_RADIUS: 5.0 # US (With locality)
-            # },
-            # {
-            #     PROP_AREA_LAT: 53.068464,
-            #     PROP_AREA_LONG: 12.532709,
-            #     PROP_AREA_RADIUS: 5.0
-            # },
-            # {
-            #     PROP_AREA_LAT: -31.99432700,
-            #     PROP_AREA_LONG: 115.93068100,
-            #     PROP_AREA_RADIUS: 5.0
-            # },
-            # {
-            #     PROP_AREA_LAT: 48.212120650046984,
-            #     PROP_AREA_LONG: 14.287071446311938,
-            #     PROP_AREA_RADIUS: 25.0 # austria
-            # },
-            # {
-            #     PROP_AREA_LAT: 39.2062720,
-            #     PROP_AREA_LONG: 22.2513570,
-            #     PROP_AREA_RADIUS: 5.0 # Greece
-            # },
-            # {
-            #     PROP_AREA_LAT: -27.470750,
-            #     PROP_AREA_LONG: 153.036804,
-            #     PROP_AREA_RADIUS: 15.0 # AUS (FuelSnoop)
-            # },
+            {
+                PROP_AREA_RADIUS: 5.0,
+                PROP_AREA_LAT: 52.23817,
+                PROP_AREA_LONG: 6.58763 # Netherlands
+            },
+            {
+                PROP_AREA_LAT: 49.134068,
+                PROP_AREA_LONG: -122.889980,
+                PROP_AREA_RADIUS: 5.0
+            }, # CA
+            {
+                PROP_AREA_LAT: 38.906316,
+                PROP_AREA_LONG: -77.054750,
+                PROP_AREA_RADIUS: 5.0 # US (No locality)
+            },
+            {
+                PROP_AREA_LAT: 40.157349,
+                PROP_AREA_LONG: -75.217079,
+                PROP_AREA_RADIUS: 5.0 # US (With locality)
+            },
+            {
+                PROP_AREA_LAT: 53.068464,
+                PROP_AREA_LONG: 12.532709,
+                PROP_AREA_RADIUS: 5.0
+            },
+            {
+                PROP_AREA_LAT: -31.99432700,
+                PROP_AREA_LONG: 115.93068100,
+                PROP_AREA_RADIUS: 5.0
+            },
+            {
+                PROP_AREA_LAT: 48.212120650046984,
+                PROP_AREA_LONG: 14.287071446311938,
+                PROP_AREA_RADIUS: 25.0 # austria
+            },
+            {
+                PROP_AREA_LAT: 39.2062720,
+                PROP_AREA_LONG: 22.2513570,
+                PROP_AREA_RADIUS: 5.0 # Greece
+            },
+            {
+                PROP_AREA_LAT: -27.470750,
+                PROP_AREA_LONG: 153.036804,
+                PROP_AREA_RADIUS: 15.0 # AUS (FuelSnoop)
+            },
             {
                 PROP_AREA_LAT: -36.789642,
                 PROP_AREA_LONG: 174.728652,
                 PROP_AREA_RADIUS: 15.0 # NZ (PetrolSpy)
             },
-            # {
-            #     PROP_AREA_LAT: 46.945200,
-            #     PROP_AREA_LONG: 7.464844,
-            #     PROP_AREA_RADIUS: 15.0 # CH
-            # },
-            # {
-            #     PROP_AREA_LAT: -23.5557714,
-            #     PROP_AREA_LONG: -46.6395571,
-            #     PROP_AREA_RADIUS: 25.0 # Brazil
-            # },
-            # {
-            #     PROP_AREA_LAT: -34.658476,
-            #     PROP_AREA_LONG: -58.529443,
-            #     PROP_AREA_RADIUS: 25.0 # Argentina
-            # },
-            # {
-            #     PROP_AREA_LAT: 46.053478,
-            #     PROP_AREA_LONG: 14.510424,
-            #     PROP_AREA_RADIUS: 25.0 # Slovenia
-            # },
+            {
+                PROP_AREA_LAT: 46.945200,
+                PROP_AREA_LONG: 7.464844,
+                PROP_AREA_RADIUS: 15.0 # CH
+            },
+            {
+                PROP_AREA_LAT: -23.5557714,
+                PROP_AREA_LONG: -46.6395571,
+                PROP_AREA_RADIUS: 25.0 # Brazil
+            },
+            {
+                PROP_AREA_LAT: -34.658476,
+                PROP_AREA_LONG: -58.529443,
+                PROP_AREA_RADIUS: 25.0 # Argentina
+            },
+            {
+                PROP_AREA_LAT: 46.053478,
+                PROP_AREA_LONG: 14.510424,
+                PROP_AREA_RADIUS: 25.0 # Slovenia
+            },
         ],
         "country_code": ""
     }
     for src in enabled_sources:
         if src in SOURCE_MAP:
+            configs["providers"].setdefault(src, {})
             if FuelPrices.source_requires_config(src):
                 schema: vol.Schema = SOURCE_MAP[src][0].attr_config
                 for attr in schema.schema:
-                    configs["providers"].setdefault(src, {})
-                    configs["providers"][src][str(attr)] = input("Enter a value for " + str(attr) + ": ")
+                    env_name = f"{src.upper()}_{str(attr).upper()}"
+                    env = os.environ.get(env_name)
+                    configs["providers"][src][str(attr)] = env or input("Enter a value for " + str(attr) + ": ")
+            else:
+                configs["providers"][src]
+
     data = FuelPrices.create(configuration=configs)
     while True:
         try:
@@ -112,69 +122,69 @@ async def main():
         ):
             _LOGGER.info("Found location: %s", loc)
 
-        # _LOGGER.info("Comparis CH test...")
-        # for loc in await data.find_fuel_locations_from_point(
-        #     coordinates=(47.042277, 9.068475),
-        #     radius=5.0
-        # ):
-        #     _LOGGER.info("Found location: %s", loc)
-
-        # _LOGGER.info("FuelWatch AU test...")
-        # for loc in await data.find_fuel_locations_from_point(
-        #     coordinates=(-31.99432700, 115.93068100),
-        #     radius=5.0
-        # ):
-        #     _LOGGER.info("Found location: %s", loc)
-
-        # _LOGGER.info("DirectLease NL test...")
-        # for loc in await data.find_fuel_locations_from_point(
-        #     coordinates=(52.23817, 6.58763),
-        #     radius=5.0
-        # ):
-        #     _LOGGER.info("Found location: %s", loc)
-
-        # _LOGGER.info("Fuels test (DirectLease): %s", await data.find_fuel_from_point(
-        #     coordinates=(52.23817, 6.58763),
-        #     radius=5.0,
-        #     fuel_type="B7"
-        # ))
-
-
-        # _LOGGER.info("Austria test...")
-        # for loc in await data.find_fuel_locations_from_point(
-        #     coordinates=(48.212120650046984, 14.287071446311938),
-        #     radius=25.0,
-        # ):
-        #     _LOGGER.info("Found location: %s", loc)
-
-        # _LOGGER.info("GasBuddy USA Test...")
-        # for loc in await data.find_fuel_locations_from_point(
-        #     coordinates=(38.906316, -77.054750),
-        #     radius=5.0
-        # ):
-        #     _LOGGER.info("Found location: %s", loc)
-
-        # if "gasbuddy" in data.configured_sources:
-        #     _LOGGER.info("GasBuddy retrieve individual station test: %s",
-        #                 await data.configured_sources["gasbuddy"].get_site(916))
-
-        # _LOGGER.info("Fuels test: %s", await data.find_fuel_from_point(
-        #     coordinates=(52.570419, 1.115850),
-        #     radius=25.0,
-        #     fuel_type="B7"
-        # )) #UK
-
-        # _LOGGER.info("Fuels test: %s", await data.find_fuel_from_point(
-        #     coordinates=(39.2062720, 22.2513570),
-        #     radius=30.0,
-        #     fuel_type="95"
-        # )) #GR
-
-        # _LOGGER.info("Fuels test: %s", await data.find_fuel_from_point(
-        #     coordinates=(-27.470750, 153.036804),
-        #     radius=20.0,
-        #     fuel_type="DSL"
-        # )) #AUS (FuelSnoop)
+        _LOGGER.info("Comparis CH test...")
+        for loc in await data.find_fuel_locations_from_point(
+            coordinates=(47.042277, 9.068475),
+            radius=5.0
+        ):
+            _LOGGER.info("Found location: %s", loc)
+
+        _LOGGER.info("FuelWatch AU test...")
+        for loc in await data.find_fuel_locations_from_point(
+            coordinates=(-31.99432700, 115.93068100),
+            radius=5.0
+        ):
+            _LOGGER.info("Found location: %s", loc)
+
+        _LOGGER.info("DirectLease NL test...")
+        for loc in await data.find_fuel_locations_from_point(
+            coordinates=(52.23817, 6.58763),
+            radius=5.0
+        ):
+            _LOGGER.info("Found location: %s", loc)
+
+        _LOGGER.info("Fuels test (DirectLease): %s", await data.find_fuel_from_point(
+            coordinates=(52.23817, 6.58763),
+            radius=5.0,
+            fuel_type="B7"
+        ))
+
+
+        _LOGGER.info("Austria test...")
+        for loc in await data.find_fuel_locations_from_point(
+            coordinates=(48.212120650046984, 14.287071446311938),
+            radius=25.0,
+        ):
+            _LOGGER.info("Found location: %s", loc)
+
+        _LOGGER.info("GasBuddy USA Test...")
+        for loc in await data.find_fuel_locations_from_point(
+            coordinates=(38.906316, -77.054750),
+            radius=5.0
+        ):
+            _LOGGER.info("Found location: %s", loc)
+
+        if "gasbuddy" in data.configured_sources:
+            _LOGGER.info("GasBuddy retrieve individual station test: %s",
+                        await data.configured_sources["gasbuddy"].get_site(916))
+
+        _LOGGER.info("Fuels test: %s", await data.find_fuel_from_point(
+            coordinates=(52.570419, 1.115850),
+            radius=25.0,
+            fuel_type="B7"
+        )) #UK
+
+        _LOGGER.info("Fuels test: %s", await data.find_fuel_from_point(
+            coordinates=(39.2062720, 22.2513570),
+            radius=30.0,
+            fuel_type="95"
+        )) #GR
+
+        _LOGGER.info("Fuels test: %s", await data.find_fuel_from_point(
+            coordinates=(-27.470750, 153.036804),
+            radius=20.0,
+            fuel_type="DSL"
+        )) #AUS (FuelSnoop)
 
         await asyncio.sleep(15)
 


Reviews:
user: login: gemini-code-assist[bot]
body: ## Code Review
The pull request refactors the fuel price sources to improve structure and maintainability, which is a great goal. The changes include moving constants to separate files, streamlining area updates, enhancing logging, and removing redundant code. Overall, the changes seem well-organized and contribute to a cleaner codebase. However, there are a few areas that could be improved, particularly in error handling and consistency.

### Summary of Findings
* **Inconsistent Error Handling**: Some `update_area` methods lack explicit error returns, which can make debugging and monitoring more difficult. Adding error returns would provide more clarity on the success or failure of each area update.
* **Potential Race Condition in `geocode_reverse_lookup`**: The `geocode_reverse_lookup` function uses global variables and a lock to prevent excessive API calls. While this is a good start, it might be better to encapsulate this logic within a class to avoid polluting the global namespace and improve testability.
* **Missing `update_area` Implementation in `FuelWatchSource`**: The `FuelWatchSource` overrides the `update` method but does not implement `update_area`, which is now the standard update method. This could lead to unexpected behavior or missed updates for specific areas.

### Merge Readiness
The pull request is a good step towards improving the fuel price sources. However, the inconsistent error handling and potential race condition in `geocode_reverse_lookup` should be addressed before merging. I am unable to directly approve this pull request, and recommend that others review and approve this code before merging. At a minimum, the high severity comments should be addressed before merging.
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T20:42:43Z
user: login: gemini-code-assist[bot]
body: ## Code Review
This pull request refactors the fuel price sources to improve structure and maintainability, which is a great goal. The changes include moving constants to separate files, streamlining area updates, enhancing logging, and removing redundant code. Overall, the changes seem well-organized and contribute to a cleaner codebase. However, there are a few areas that could be improved.

### Summary of Findings
* **Missing Error Handling in `update` Methods**: The `update` methods in several source files now rely on `update_area` to handle errors, but the main `update` method doesn't explicitly handle or log failures from `update_area`. This could lead to silent failures and make debugging difficult.
* **Inconsistent Return Types for `update_area`**: The `update_area` methods across different source files have inconsistent return types (bool or None). Standardizing this to a boolean return type would improve predictability and maintainability.
* **Potential Geocoding Rate Limiting Issues**: The `GeoCodeHandler` introduces a delay to avoid rate limiting, but it might not be sufficient under heavy load. Consider implementing a more robust rate limiting strategy.

### Merge Readiness
The pull request is a good step towards improving the fuel price sources. However, the missing error handling in `update` methods and the inconsistent return types for `update_area` should be addressed before merging. The potential geocoding rate limiting issues should also be considered. I am unable to approve this pull request, and recommend that others review and approve this code before merging. At a minimum, the HIGH severity issues should be addressed before merging.
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T21:03:45Z
user: login: gemini-code-assist[bot]
body: ## Code Review
This pull request refactors the fuel price sources to improve structure and maintainability, which is a great goal. The changes include moving constants to separate files, streamlining area updates, enhancing logging, and removing redundant code. Overall, the changes seem well-organized and contribute to a cleaner codebase. However, there are a few areas that could be improved for better error handling and code clarity.

### Summary of Findings
* **Exception Handling in Geocoding**: The try-except block in `__init__.py` and other files catches all exceptions during geocoding. It's better to catch specific exceptions to handle them appropriately and allow other exceptions to propagate.
* **Return Values for `update_area`**: The `update_area` methods across various sources now return boolean values indicating success or failure. Ensure that these return values are consistently used and handled in the calling functions to provide proper feedback on update status.
* **Update Method Logic**: The `update` method in `pyfuelprices/sources/__init__.py` has been refactored to use `asyncio.gather`. Ensure that errors during area updates are properly logged and handled to prevent silent failures.

### Merge Readiness
The pull request is a good step towards improving the fuel price sources. However, the exception handling in geocoding should be refined, and the return values of `update_area` should be consistently used. I recommend addressing these issues before merging. I am unable to approve this pull request, and users should have others review and approve this code before merging.
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T21:23:45Z

Review comments:
diff_hunk: @@ -117,6 +115,9 @@ async def update(self, areas=None, force=None) -> list[FuelLocation]:
             self.next_update += self.update_interval
             return list(self.location_cache.values())
 
+    async def update_area(self, area):
+        """Method not used."""
path: pyfuelprices/sources/australia/fuelwatch.py
user: login: gemini-code-assist[bot]
body: ![high](https://img.shields.io/badge/priority-high-orange)

This method is not used, but it is required by the base class. Consider raising a `NotImplementedError` or removing this method entirely and adjusting the base class if appropriate. It's unusual to have a method that does nothing.

```python
    async def update_area(self, area):
        """Method not used.""
        raise NotImplementedError
```
created_at: 2025-05-06T20:42:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -10,12 +13,22 @@
 
 from ._version import __version__ as VERSION
 
+_geocode_last_lookup = None
+_lookup_lock = asyncio.Lock()
+
 async def geocode_reverse_lookup(coordinates: tuple) -> location.Location:
     """Reverse GeoCode lookup."""
-    async with Nominatim(
-        user_agent=f"pyfuelprices-{VERSION}",
-        adapter_factory=adapters.AioHTTPAdapter) as geolocator:
-        return await geolocator.reverse(coordinates, exactly_one=True, timeout=15, addressdetails=True)
+    global _geocode_last_lookup
+    global _lookup_lock
+    async with _lookup_lock:
+        if _geocode_last_lookup is None:
+            _geocode_last_lookup = datetime.now()
+        if datetime.now().timestamp() - _geocode_last_lookup.timestamp() < 1:
+            await asyncio.sleep(1)
path: pyfuelprices/helpers.py
user: login: gemini-code-assist[bot]
body: ![medium](https://img.shields.io/badge/priority-medium-yellow)

Consider encapsulating the `_geocode_last_lookup` and `_lookup_lock` variables within a class to avoid polluting the global namespace and improve testability. This could also help manage the state and concurrency more effectively.

For example, you could create a `GeoCodeHandler` class that manages the rate limiting and caching of geocode lookups.
created_at: 2025-05-06T20:42:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -101,46 +108,58 @@ async def search_sites(self, coordinates, radius: float) -> list[dict]:
                 )
         return locations
 
+    async def update_area(self, area: dict):
+        """Update a given area."""
+        raise NotImplementedError("Not implemented.")
path: pyfuelprices/sources/__init__.py
user: login: gemini-code-assist[bot]
body: ![medium](https://img.shields.io/badge/priority-medium-yellow)

Consider adding a return value to `update_area` to indicate success or failure. This would allow for better error handling and monitoring of individual area updates. For example, you could return a boolean or an error object.

```suggestion
    async def update_area(self, area: dict) -> bool:
        """Update a given area."""
        raise NotImplementedError("Not implemented.")
```
created_at: 2025-05-06T20:42:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -56,51 +56,46 @@ async def search_sites(self, coordinates, radius: float) -> list[dict]:
         )
         return await super().search_sites(coordinates, radius)
 
-    async def update(self, areas=None, force=False) -> list[FuelLocation]:
-        """Update hooks for the data source."""
-        _LOGGER.debug("Starting update hook for %s to url %s", self.provider_name, self._url)
-        self._configured_areas=[] if areas is None else areas
-        self._clear_cache()
-        if self.next_update <= datetime.now() or force:
-            for area in self._configured_areas:
-                try:
-                    geocode = await geocode_reverse_lookup(
-                        (area[PROP_AREA_LAT], area[PROP_AREA_LONG])
-                    )
-                except geopyexc.GeocoderTimedOut:
-                    _LOGGER.warning("Timeout occured while geocoding area %s.",
-                                    area)
-                    continue
-                if geocode.raw["address"]["country_code"] != "ar":
-                    _LOGGER.debug("Skipping area %s as not in AR.",
-                                area)
-                    continue
-                response = json.loads(await self._send_request(
-                    url=self._url,
-                    body={
-                        "resource_id": "80ac25de-a44a-4445-9215-090cf55cfda5",
-                        "q": "",
-                        "filters": {
-                            "provincia": str(geocode.raw["address"]["state"]).upper(),
-                            "localidad": str(geocode.raw["address"]["town"]).upper()
-                        },
-                        "limit": 500,
-                        "offset": 0
-                    }
-                ))
-                if response["success"]:
-                    await self.parse_response(response["result"]["records"])
-                else:
-                    _LOGGER.error("Error sending request to %s: %s",
-                                self.provider_name,
-                                response)
-            self.next_update = datetime.now() + self.update_interval
-        return list(self.location_cache.values())
+    async def update_area(self, area: dict):
+        """Update a given area."""
path: pyfuelprices/sources/argentina/gobenergy.py
user: login: gemini-code-assist[bot]
body: ![medium](https://img.shields.io/badge/priority-medium-yellow)

Consider adding a return type to indicate success or failure of the update, and to handle exceptions more explicitly. This will help with debugging and monitoring.

```suggestion
    async def update_area(self, area: dict) -> bool:
        """Update a given area."""
```
created_at: 2025-05-06T20:42:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -57,32 +57,27 @@ async def search_sites(self, coordinates, radius: float) -> list[dict]:
         )
         return await super().search_sites(coordinates, radius)
 
-    async def update(self, areas=None, force=None) -> list[FuelLocation]:
-        """Custom update handler to look all products."""
-        self._configured_areas=[] if areas is None else areas
-        if datetime.now() > self.next_update or force:
-            for area in self._configured_areas:
-                _LOGGER.debug("Searching FuelSnoop for FuelLocations at area %s",
-                              area)
-                bbox = get_bounding_box(
-                    area[PROP_AREA_LAT],
-                    area[PROP_AREA_LONG],
-                    area[PROP_AREA_RADIUS]
-                )
-                response_raw = await self._send_request(
-                    url=FUELSNOOP_API_SITES,
-                    body={
-                        "min_lng": bbox.lon_min,
-                        "min_lat": bbox.lat_min,
-                        "max_lng": bbox.lon_max,
-                        "max_lat": bbox.lat_max,
-                        "brand_names": []
-                    }
-                )
-                if response_raw is not None:
-                    await self.parse_response(json.loads(response_raw))
-            self.next_update += self.update_interval
-            return list(self.location_cache.values())
+    async def update_area(self, area):
+        """Update a given area."""
path: pyfuelprices/sources/australia/fuelsnoop.py
user: login: gemini-code-assist[bot]
body: ![medium](https://img.shields.io/badge/priority-medium-yellow)

Consider adding a return type to indicate success or failure of the update, and to handle exceptions more explicitly. This will help with debugging and monitoring.

```suggestion
    async def update_area(self, area) -> bool:
        """Update a given area."""
```
created_at: 2025-05-06T20:42:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -90,36 +80,31 @@ async def search_sites(self, coordinates, radius: float) -> list[dict]:
         )
         return await super().search_sites(coordinates, radius)
 
-    async def update(self, areas=None, force=None) -> list[FuelLocation]:
-        """Custom update handler as this needs to query GasPass on areas."""
-        if datetime.now() > self.next_update or force:
-            self._configured_areas=[] if areas is None else areas
-            for area in self._configured_areas:
-                try:
-                    geocode = await geocode_reverse_lookup(
-                        (area[PROP_AREA_LAT], area[PROP_AREA_LONG])
-                    )
-                except geopyexc.GeocoderTimedOut:
-                    _LOGGER.warning("Timeout occured while geocoding area %s.",
-                                    area)
-                    continue
-                if geocode.raw["address"]["country_code"] != "br":
-                    _LOGGER.debug("Skipping area %s as not in BR.",
-                                area)
-                    continue
-                response_raw = json.loads(await self._send_request(
-                    lat=area[PROP_AREA_LAT],
-                    long=area[PROP_AREA_LONG],
-                    radius=area[PROP_AREA_RADIUS]
-                ))
-                if response_raw["message"] == "ok":
-                    await self.parse_response(response_raw)
-                else:
-                    _LOGGER.error("Error sending request to %s: %s",
-                                self.provider_name,
-                                response_raw)
-            self.next_update += self.update_interval
-        return list(self.location_cache.values())
+    async def update_area(self, area):
+        """Update a given area."""
path: pyfuelprices/sources/brazil/gaspass.py
user: login: gemini-code-assist[bot]
body: ![medium](https://img.shields.io/badge/priority-medium-yellow)

Consider adding a return type to indicate success or failure of the update, and to handle exceptions more explicitly. This will help with debugging and monitoring.

```suggestion
    async def update_area(self, area) -> bool:
        """Update a given area."""
```
created_at: 2025-05-06T20:42:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -73,35 +70,30 @@ async def search_sites(self, coordinates, radius: float) -> list[dict]:
         )
         return await super().search_sites(coordinates, radius)
 
-    async def update(self, areas=None, force=None) -> list[FuelLocation]:
-        """Custom update handler as this needs to query TankerKoenig on areas."""
-        if datetime.now() > self.next_update or force:
-            self._configured_areas=[] if areas is None else areas
-            for area in self._configured_areas:
-                try:
-                    geocode = await geocode_reverse_lookup(
-                        (area[PROP_AREA_LAT], area[PROP_AREA_LONG])
-                    )
-                except geopyexc.GeocoderTimedOut:
-                    _LOGGER.warning("Timeout occured while geocoding area %s.",
-                                    area)
-                    continue
-                if geocode.raw["address"]["country_code"] != "de":
-                    _LOGGER.debug("Skipping area %s as not in DE.",
-                                area)
-                    continue
-                response_raw = json.loads(await self._send_request(
-                    postcode=geocode.raw["address"]["postcode"],
-                    radius=area[PROP_AREA_RADIUS]
-                ))
-                if response_raw["ok"]:
-                    await self.parse_response(response_raw["data"])
-                else:
-                    _LOGGER.error("Error sending request to %s: %s",
-                                self.provider_name,
-                                response_raw)
-            self.next_update += self.update_interval
-        return list(self.location_cache.values())
+    async def update_area(self, area):
+        """Update a given area."""
path: pyfuelprices/sources/germany/tankerkoenig.py
user: login: gemini-code-assist[bot]
body: ![medium](https://img.shields.io/badge/priority-medium-yellow)

Consider adding a return type to indicate success or failure of the update, and to handle exceptions more explicitly. This will help with debugging and monitoring.

```suggestion
    async def update_area(self, area) -> bool:
        """Update a given area."""
```
created_at: 2025-05-06T20:42:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -57,43 +57,39 @@ async def _send_request(self, postcode, radius):
                     _LOGGER.debug("Content within script tag with id='__NEXT_DATA__':")
                     return extracted_content
                 else:
-                    _LOGGER.error("Script tag with id='__NEXT_DATA__' not found in the HTML content.")
+                    _LOGGER.error(
+                        "Script tag with id='__NEXT_DATA__' not found in the HTML content."
+                    )
             _LOGGER.error("Error sending request to %s: %s",
                             url,
                             response)
             return "{}"
 
-    async def update(self, areas=None, force=None) -> list[FuelLocation]:
-        """Custom update handler as this needs to query Comparis on areas."""
-        if datetime.now() > self.next_update:
-            self._configured_areas=[] if areas is None else areas
-            for area in self._configured_areas:
-                try:
-                    geocode = await geocode_reverse_lookup(
-                        (area[PROP_AREA_LAT], area[PROP_AREA_LONG])
-                    )
-                except geopyexc.GeocoderTimedOut:
-                    _LOGGER.warning("Timeout occured while geocoding area %s.",
-                                    area)
-                    continue
-                if geocode.raw["address"]["country_code"] != "ch":
-                    _LOGGER.debug("Skipping area %s as not in CH.",
-                                area)
-                    continue
-                response_raw = json.loads(await self._send_request(
-                    postcode=geocode.raw["address"]["postcode"],
-                    radius=area[PROP_AREA_RADIUS]
-                ))
-                
-                if len(response_raw) != 0:
-                    response_raw = response_raw["props"]["pageProps"]
-                    await self.parse_response(response_raw["data"])
-                else:
-                    _LOGGER.error("Error sending request to %s: %s",
-                                self.provider_name,
-                                response_raw)
-            self.next_update += self.update_interval
-        return list(self.location_cache.values())
+    async def update_area(self, area):
+        """Update a given area."""
path: pyfuelprices/sources/switzerland/comparis.py
user: login: gemini-code-assist[bot]
body: ![medium](https://img.shields.io/badge/priority-medium-yellow)

Consider adding a return type to indicate success or failure of the update, and to handle exceptions more explicitly. This will help with debugging and monitoring.

```suggestion
    async def update_area(self, area) -> bool:
        """Update a given area."""
```
created_at: 2025-05-06T20:42:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -86,40 +86,37 @@ async def search_sites(self, coordinates, radius: float) -> list[dict]:
         )
         return await super().search_sites(coordinates, radius)
 
-    async def update(self, areas=None, force=None) -> list[FuelLocation]:
-        """Custom update handler as this needs to query GasBuddy on areas."""
-        self._configured_areas=[] if areas is None else areas
-        for area in self._configured_areas:
-            self._parser_coords = (area[PROP_AREA_LAT], area[PROP_AREA_LONG])
-            self._parser_radius = area[PROP_AREA_RADIUS]
-            geocoded = await geocode_reverse_lookup(self._parser_coords)
-            if geocoded is None:
-                _LOGGER.debug("Geocode failed, skipping area %s", area)
-                continue
-            if geocoded.raw["address"]["country_code"] not in ["us", "ca"]:
-                _LOGGER.debug("Geocode not within USA, skipping area %s", area)
-                continue
-            _LOGGER.debug("Searching GasBuddy for FuelLocations at area %s",
+    async def update_area(self, area):
+        """Update a given area."""
path: pyfuelprices/sources/usa/gasbuddy.py
user: login: gemini-code-assist[bot]
body: ![medium](https://img.shields.io/badge/priority-medium-yellow)

Consider adding a return type to indicate success or failure of the update, and to handle exceptions more explicitly. This will help with debugging and monitoring.

```suggestion
    async def update_area(self, area) -> bool:
        """Update a given area."""
```
created_at: 2025-05-06T20:42:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -101,46 +99,58 @@ async def search_sites(self, coordinates, radius: float) -> list[dict]:
                 )
         return locations
 
+    async def update_area(self, area: dict) -> bool:
+        """Update a given area."""
+        raise NotImplementedError("Not implemented.")
+
     async def update(self, areas=None, force=False) -> list[FuelLocation]:
         """Update hooks for the data source."""
         _LOGGER.debug("Starting update hook for %s to url %s", self.provider_name, self._url)
-        self._configured_areas=[] if areas is None else areas
+        areas = areas or self._configured_areas
         self._clear_cache()
-        if self.next_update <= datetime.now() or force:
-            response = await self._client_session.request(
-                method=self._method,
-                url=self._url,
-                json=self._request_body,
-                headers=self._headers
-            )
-            _LOGGER.debug("Update request completed for %s with status %s",
-                        self.provider_name, response.status)
-            if response.status == 200:
-                self.next_update = datetime.now() + self.update_interval
-                if "application/json" not in response.content_type:
-                    return await self.parse_response(
-                        response=json.loads(await response.text())
-                    )
-                if response.content_type == "text/csv":
-                    return await self.parse_response(
-                        response=list(csv.DictReader(await response.text()))
-                    )
-                return await self.parse_response(
-                    response=await response.json()
-                )
-            if response.status == 403:
-                raise ServiceBlocked(
-                    status=response.status,
-                    response=await response.text(),
-                    headers=response.headers,
-                    service=self.provider_name
-                )
-            raise UpdateFailedError(
-                status=response.status,
-                response=await response.text(),
-                headers=response.headers,
-                service=self.provider_name
-            )
+        if self.next_update > datetime.now() and not force:
+            _LOGGER.debug("Ignoring update request")
+            return
+        coros = [
+            self.update_area(area) for area in areas
+        ]
+        await asyncio.gather(*coros)
path: pyfuelprices/sources/__init__.py
user: login: gemini-code-assist[bot]
body: ![high](https://img.shields.io/badge/priority-high-orange)

The `update` method gathers the results of `update_area` but doesn't check for exceptions. If `update_area` fails, the exception will be swallowed, leading to silent failures. Consider adding error handling to log or re-raise exceptions from `update_area`.

```suggestion
        results = await asyncio.gather(*coros, return_exceptions=True)
        for result in results:
            if isinstance(result, Exception):
                _LOGGER.error(f"Update area failed: {result}")
```
created_at: 2025-05-06T21:03:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -56,51 +57,47 @@ async def search_sites(self, coordinates, radius: float) -> list[dict]:
         )
         return await super().search_sites(coordinates, radius)
 
-    async def update(self, areas=None, force=False) -> list[FuelLocation]:
-        """Update hooks for the data source."""
-        _LOGGER.debug("Starting update hook for %s to url %s", self.provider_name, self._url)
-        self._configured_areas=[] if areas is None else areas
-        self._clear_cache()
-        if self.next_update <= datetime.now() or force:
-            for area in self._configured_areas:
-                try:
-                    geocode = await geocode_reverse_lookup(
-                        (area[PROP_AREA_LAT], area[PROP_AREA_LONG])
-                    )
-                except geopyexc.GeocoderTimedOut:
-                    _LOGGER.warning("Timeout occured while geocoding area %s.",
-                                    area)
-                    continue
-                if geocode.raw["address"]["country_code"] != "ar":
-                    _LOGGER.debug("Skipping area %s as not in AR.",
-                                area)
-                    continue
-                response = json.loads(await self._send_request(
-                    url=self._url,
-                    body={
-                        "resource_id": "80ac25de-a44a-4445-9215-090cf55cfda5",
-                        "q": "",
-                        "filters": {
-                            "provincia": str(geocode.raw["address"]["state"]).upper(),
-                            "localidad": str(geocode.raw["address"]["town"]).upper()
-                        },
-                        "limit": 500,
-                        "offset": 0
-                    }
-                ))
-                if response["success"]:
-                    await self.parse_response(response["result"]["records"])
-                else:
-                    _LOGGER.error("Error sending request to %s: %s",
-                                self.provider_name,
-                                response)
-            self.next_update = datetime.now() + self.update_interval
-        return list(self.location_cache.values())
+    async def update_area(self, area: dict) -> bool:
+        """Update a given area."""
+        try:
+            geocode = await geocoder.geocode_reverse_lookup(
+                (area[PROP_AREA_LAT], area[PROP_AREA_LONG])
+            )
+        except geopyexc.GeocoderTimedOut:
+            _LOGGER.warning("Timeout occured while geocoding area %s.",
+                            area)
+            return False
+        if geocode.raw["address"]["country_code"] != "ar":
+            _LOGGER.debug("Skipping area %s as not in AR.",
+                        area)
+            return False
+        response = json.loads(await self._send_request(
+            url=self._url,
+            body={
+                "resource_id": "80ac25de-a44a-4445-9215-090cf55cfda5",
+                "q": "",
+                "filters": {
+                    "provincia": str(geocode.raw["address"]["state"]).upper(),
+                    "localidad": str(geocode.raw["address"]["town"]).upper()
+                },
+                "limit": 500,
+                "offset": 0
+            }
+        ))
+        if response["success"]:
+            await self.parse_response(response["result"]["records"])
+            return True
+        else:
+            _LOGGER.error("Error sending request to %s: %s",
+                        self.provider_name,
+                        response)
+            return True
path: pyfuelprices/sources/argentina/gobenergy.py
user: login: gemini-code-assist[bot]
body: ![high](https://img.shields.io/badge/priority-high-orange)

This `return True` should likely be `return False` since an error occurred. Returning `True` indicates success, which is misleading in this case.

```suggestion
            return False
```
created_at: 2025-05-06T21:03:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -69,7 +69,7 @@ async def find_fuel_locations_from_point(self,
                 coordinates=coordinates,
                 radius=radius
             )
-        geocoded = (await geocode_reverse_lookup(coordinates)).raw['address']['country_code']
+        geocoded = (await geocoder.geocode_reverse_lookup(coordinates)).raw['address']['country_code']
path: pyfuelprices/__init__.py
user: login: gemini-code-assist[bot]
body: ![medium](https://img.shields.io/badge/priority-medium-yellow)

Consider adding a try-except block around this line to handle potential exceptions during geocoding. This would prevent the entire process from crashing if the geocoder fails.

```suggestion
        try:
            geocoded = (await geocoder.geocode_reverse_lookup(coordinates)).raw['address']['country_code']
        except Exception as e:
            _LOGGER.error(f"Geocoding failed: {e}")
            return [] # Or handle the error as appropriate
```
created_at: 2025-05-06T21:03:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,53 +1,71 @@
 """Fuel Price helpers."""
 
+import asyncio
 import math
 
+from datetime import datetime
+
 from geopy import (
     Nominatim,
     location,
-    adapters
+    adapters,
+    exc as geopyexc
 )
 
 from ._version import __version__ as VERSION
 
-async def geocode_reverse_lookup(coordinates: tuple) -> location.Location:
-    """Reverse GeoCode lookup."""
-    async with Nominatim(
-        user_agent=f"pyfuelprices-{VERSION}",
-        adapter_factory=adapters.AioHTTPAdapter) as geolocator:
-        return await geolocator.reverse(coordinates, exactly_one=True, timeout=15, addressdetails=True)
-
 class BoundingBox(object):
     """Represent a bounding box."""
     lat_min = None
     lon_min = None
     lat_max = None
     lon_max = None
 
-def get_bounding_box(latitude_in_degrees, longitude_in_degrees, half_side_in_miles):
-    """Return a bounding box from lat/long/radius"""
-    assert half_side_in_miles > 0
-    assert latitude_in_degrees >= -90.0 and latitude_in_degrees  <= 90.0
-    assert longitude_in_degrees >= -180.0 and longitude_in_degrees <= 180.0
-
-    half_side_in_km = half_side_in_miles * 1.609344
-    lat = math.radians(latitude_in_degrees)
-    lon = math.radians(longitude_in_degrees)
-
-    radius  = 6371
-    # Radius of the parallel at given latitude
-    parallel_radius = radius*math.cos(lat)
-
-    lat_min = lat - half_side_in_km/radius
-    lat_max = lat + half_side_in_km/radius
-    lon_min = lon - half_side_in_km/parallel_radius
-    lon_max = lon + half_side_in_km/parallel_radius
-    rad2deg = math.degrees
-
-    box = BoundingBox()
-    box.lat_min = rad2deg(lat_min)
-    box.lon_min = rad2deg(lon_min)
-    box.lat_max = rad2deg(lat_max)
-    box.lon_max = rad2deg(lon_max)
-
-    return box
+class GeoCodeHandler:
+    """Represent a geocode handler."""
+    _geocode_last_lookup: datetime | None = None
+    _lookup_lock: asyncio.Lock = asyncio.Lock()
+
+    async def geocode_reverse_lookup(self, coordinates: tuple) -> location.Location:
+        """Reverse GeoCode lookup."""
+        async with self._lookup_lock:
+            if self._geocode_last_lookup is None:
+                self._geocode_last_lookup = datetime.now()
+            if datetime.now().timestamp() - self._geocode_last_lookup.timestamp() < 1:
+                await asyncio.sleep(1)
path: pyfuelprices/helpers.py
user: login: gemini-code-assist[bot]
body: ![medium](https://img.shields.io/badge/priority-medium-yellow)

While this `asyncio.sleep` helps with rate limiting, consider using a more robust rate limiting mechanism, such as a token bucket or a leaky bucket algorithm, to handle bursts of requests more effectively.

```suggestion
            # Consider a more robust rate limiting mechanism here
            await asyncio.sleep(1)
```
created_at: 2025-05-06T21:03:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -175,3 +173,7 @@ def parse_fuels(self, fuels) -> list[Fuel]:
                 props=fuel
             ))
         return fuel_parsed
+
+    async def parse_response(self, response):
+        """Method not used."""
+        raise NotImplementedError
path: pyfuelprices/sources/usa/gasbuddy.py
user: login: gemini-code-assist[bot]
body: ![medium](https://img.shields.io/badge/priority-medium-yellow)

This method is not used and raises a `NotImplementedError`. It should be removed to avoid confusion and simplify the codebase.

```suggestion
    #async def parse_response(self, response):
    #    """Method not used."""
    #    raise NotImplementedError
```
created_at: 2025-05-06T21:03:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -69,7 +69,11 @@ async def find_fuel_locations_from_point(self,
                 coordinates=coordinates,
                 radius=radius
             )
-        geocoded = (await geocode_reverse_lookup(coordinates)).raw['address']['country_code']
+        try:
+            geocoded = (await geocoder.geocode_reverse_lookup(coordinates)).raw['address']['country_code']
+        except Exception as e:
+            _LOGGER.error(f"Geocoding failed: {e}")
+            return [] # Or handle the error as appropriate
path: pyfuelprices/__init__.py
user: login: gemini-code-assist[bot]
body: ![medium](https://img.shields.io/badge/priority-medium-yellow)

Catching `Exception` is generally discouraged as it can mask unexpected errors. It's better to catch specific exceptions like `geopyexc.GeocoderTimedOut` or `geopyexc.GeocoderServiceError` and handle them appropriately. This allows other unexpected exceptions to propagate and be caught elsewhere, aiding in debugging.

Consider re-raising the exception if you can't handle it, or logging the exception and continuing with a default value.

```python
        except geopyexc.GeocoderTimedOut as e:
            _LOGGER.error(f"Geocoding timed out: {e}")
            return [] # Or handle the error as appropriate
        except geopyexc.GeocoderServiceError as e:
            _LOGGER.error(f"Geocoding service error: {e}")
            return []
```
created_at: 2025-05-06T21:23:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -101,46 +99,27 @@ async def search_sites(self, coordinates, radius: float) -> list[dict]:
                 )
         return locations
 
+    async def update_area(self, area: dict) -> bool:
+        """Update a given area."""
+        raise NotImplementedError("Not implemented.")
+
     async def update(self, areas=None, force=False) -> list[FuelLocation]:
         """Update hooks for the data source."""
         _LOGGER.debug("Starting update hook for %s to url %s", self.provider_name, self._url)
-        self._configured_areas=[] if areas is None else areas
+        areas = areas or self._configured_areas
         self._clear_cache()
-        if self.next_update <= datetime.now() or force:
-            response = await self._client_session.request(
-                method=self._method,
-                url=self._url,
-                json=self._request_body,
-                headers=self._headers
-            )
-            _LOGGER.debug("Update request completed for %s with status %s",
-                        self.provider_name, response.status)
-            if response.status == 200:
-                self.next_update = datetime.now() + self.update_interval
-                if "application/json" not in response.content_type:
-                    return await self.parse_response(
-                        response=json.loads(await response.text())
-                    )
-                if response.content_type == "text/csv":
-                    return await self.parse_response(
-                        response=list(csv.DictReader(await response.text()))
-                    )
-                return await self.parse_response(
-                    response=await response.json()
-                )
-            if response.status == 403:
-                raise ServiceBlocked(
-                    status=response.status,
-                    response=await response.text(),
-                    headers=response.headers,
-                    service=self.provider_name
-                )
-            raise UpdateFailedError(
-                status=response.status,
-                response=await response.text(),
-                headers=response.headers,
-                service=self.provider_name
-            )
+        if self.next_update > datetime.now() and not force:
+            _LOGGER.debug("Ignoring update request")
+            return
+        coros = [
+            self.update_area(a) for a in areas
+        ]
+        results = await asyncio.gather(*coros, return_exceptions=True)
+        for result in results:
+            if isinstance(result, Exception):
+                _LOGGER.error("Update area failed: %s", result)
path: pyfuelprices/sources/__init__.py
user: login: gemini-code-assist[bot]
body: ![medium](https://img.shields.io/badge/priority-medium-yellow)

It's good that you're catching exceptions in the `asyncio.gather` call, but consider logging the specific area that failed to update. This will help in debugging and identifying problematic areas.

```python
            if isinstance(result, Exception):
                _LOGGER.error(f"Update area {areas[i]} failed: %s", result)
```
created_at: 2025-05-06T21:23:45Z
author_association: CONTRIBUTOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-05-06T20:38:26Z
body: <h1>Dependency Review</h1>
✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.<h2>OpenSSF Scorecard</h2>
<table><tr><th>Package</th><th>Version</th><th>Score</th><th>Details</th></tr>
<tr><td><a href="https://github.com/pedroburon/dotenv"> pip/dotenv </a></td><td></td>
      <td>:green_circle: 3.3</td><td><details><summary>Details</summary><table><tr><th>Check</th><th>Score</th><th>Reason</th></tr><tr><td>Packaging</td><td>:warning: -1</td><td>packaging workflow not detected</td></tr><tr><td>Token-Permissions</td><td>:warning: -1</td><td>No tokens found</td></tr><tr><td>Code-Review</td><td>:warning: 2</td><td>Found 6/25 approved changesets -- score normalized to 2</td></tr><tr><td>Pinned-Dependencies</td><td>:warning: -1</td><td>no dependencies found</td></tr><tr><td>Binary-Artifacts</td><td>:green_circle: 10</td><td>no binaries found in the repo</td></tr><tr><td>Dangerous-Workflow</td><td>:warning: -1</td><td>no workflows found</td></tr><tr><td>Maintained</td><td>:warning: 0</td><td>1 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 0</td></tr><tr><td>CII-Best-Practices</td><td>:warning: 0</td><td>no effort to earn an OpenSSF best practices badge detected</td></tr><tr><td>Security-Policy</td><td>:warning: 0</td><td>security policy file not detected</td></tr><tr><td>Fuzzing</td><td>:warning: 0</td><td>project is not fuzzed</td></tr><tr><td>Vulnerabilities</td><td>:green_circle: 10</td><td>0 existing vulnerabilities detected</td></tr><tr><td>License</td><td>:green_circle: 10</td><td>license file detected</td></tr><tr><td>Signed-Releases</td><td>:warning: -1</td><td>no releases found</td></tr><tr><td>Branch-Protection</td><td>:warning: 0</td><td>branch protection not enabled on development/release branches</td></tr><tr><td>SAST</td><td>:warning: 0</td><td>SAST tool is not run on all commits -- score normalized to 0</td></tr></table></details></td></tr>
</table><h2>Scanned Files</h2>
<ul><li>requirements.txt</li></ul>


<!-- dependency-review-pr-comment-marker -->
author_association: CONTRIBUTOR
user: login: pantherale0
created_at: 2025-05-06T20:42:18Z
body: /gemini review
author_association: OWNER
user: login: pantherale0
created_at: 2025-05-06T21:03:24Z
body: /gemini review
author_association: OWNER
user: login: pantherale0
created_at: 2025-05-06T21:23:20Z
body: /gemini review
author_association: OWNER