working cache
Some checks failed
Build and deploy the backend to staging / Build and push image (pull_request) Successful in 2m19s
Run linting on the backend code / Build (pull_request) Successful in 25s
Run testing on the backend code / Build (pull_request) Failing after 7m37s
Build and deploy the backend to staging / Deploy to staging (pull_request) Successful in 24s
Some checks failed
Build and deploy the backend to staging / Build and push image (pull_request) Successful in 2m19s
Run linting on the backend code / Build (pull_request) Successful in 25s
Run testing on the backend code / Build (pull_request) Failing after 7m37s
Build and deploy the backend to staging / Deploy to staging (pull_request) Successful in 24s
This commit is contained in:
133
backend/src/overpass/caching_strategy.py
Normal file
133
backend/src/overpass/caching_strategy.py
Normal file
@@ -0,0 +1,133 @@
|
||||
import os
|
||||
import xml.etree.ElementTree as ET
|
||||
import hashlib
|
||||
import ujson
|
||||
|
||||
from ..constants import OSM_CACHE_DIR
|
||||
|
||||
|
||||
def get_cache_key(query: str) -> str:
|
||||
"""
|
||||
Generate a unique cache key for the query using a hash function.
|
||||
This ensures that queries with different parameters are cached separately.
|
||||
"""
|
||||
return hashlib.md5(query.encode('utf-8')).hexdigest()
|
||||
|
||||
|
||||
class CachingStrategyBase:
|
||||
def get(self, key):
|
||||
raise NotImplementedError('Subclass should implement get')
|
||||
|
||||
def set(self, key, data):
|
||||
raise NotImplementedError('Subclass should implement set')
|
||||
|
||||
def close(self):
|
||||
pass
|
||||
|
||||
|
||||
# For later use if xml does not suit well
|
||||
class JSONCache(CachingStrategyBase):
|
||||
def __init__(self, cache_dir=OSM_CACHE_DIR):
|
||||
# Add the class name as a suffix to the directory
|
||||
self._cache_dir = f'{cache_dir}_JSON'
|
||||
if not os.path.exists(self._cache_dir):
|
||||
os.makedirs(self._cache_dir)
|
||||
|
||||
def _filename(self, key):
|
||||
return os.path.join(self._cache_dir, f'{key}.json')
|
||||
|
||||
def get(self, key):
|
||||
filename = self._filename(key)
|
||||
if os.path.exists(filename):
|
||||
with open(filename, 'r') as file:
|
||||
return ujson.load(file)
|
||||
return None
|
||||
|
||||
def set(self, key, value):
|
||||
with open(self._filename(key), 'w') as file:
|
||||
ujson.dump(value, file)
|
||||
|
||||
|
||||
class XMLCache(CachingStrategyBase):
|
||||
def __init__(self, cache_dir=OSM_CACHE_DIR):
|
||||
# Add the class name as a suffix to the directory
|
||||
self._cache_dir = f'{cache_dir}_XML'
|
||||
if not os.path.exists(self._cache_dir):
|
||||
os.makedirs(self._cache_dir)
|
||||
|
||||
def _filename(self, key):
|
||||
return os.path.join(self._cache_dir, f'{key}.xml')
|
||||
|
||||
def get(self, key):
|
||||
"""Retrieve XML data from the cache and parse it as an ElementTree."""
|
||||
filename = self._filename(key)
|
||||
if os.path.exists(filename):
|
||||
try:
|
||||
# Parse and return the cached XML data
|
||||
tree = ET.parse(filename)
|
||||
return tree.getroot() # Return the root element of the parsed XML
|
||||
except ET.ParseError:
|
||||
print(f"Error parsing cached XML file: {filename}")
|
||||
return None
|
||||
return None
|
||||
|
||||
def set(self, key, value):
|
||||
"""Save the XML data as an ElementTree to the cache."""
|
||||
filename = self._filename(key)
|
||||
tree = ET.ElementTree(value) # value is expected to be an ElementTree root element
|
||||
try:
|
||||
# Write the XML data to a file
|
||||
with open(filename, 'wb') as file:
|
||||
tree.write(file, encoding='utf-8', xml_declaration=True)
|
||||
except IOError as e:
|
||||
print(f"Error writing to cache file: {filename} - {e}")
|
||||
|
||||
|
||||
class CachingStrategy:
|
||||
__strategy = XMLCache() # Default caching strategy
|
||||
|
||||
# Dictionary to map string identifiers to caching strategy classes
|
||||
__strategies = {
|
||||
'XML': XMLCache,
|
||||
'JSON': JSONCache,
|
||||
# Add more strategies here if needed
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def use(cls, strategy_name='XML', **kwargs):
|
||||
"""
|
||||
Set the caching strategy based on the strategy_name provided.
|
||||
|
||||
Args:
|
||||
strategy_name (str): The name of the caching strategy (e.g., 'XML').
|
||||
**kwargs: Additional keyword arguments to pass when initializing the strategy.
|
||||
"""
|
||||
# If a previous strategy exists, close it
|
||||
if cls.__strategy:
|
||||
cls.__strategy.close()
|
||||
|
||||
# Retrieve the strategy class based on the strategy name
|
||||
strategy_class = cls.__strategies.get(strategy_name)
|
||||
|
||||
if not strategy_class:
|
||||
raise ValueError(f"Unknown caching strategy: {strategy_name}")
|
||||
|
||||
# Instantiate the new strategy with the provided arguments
|
||||
cls.__strategy = strategy_class(**kwargs)
|
||||
return cls.__strategy
|
||||
|
||||
@classmethod
|
||||
def get(cls, key):
|
||||
"""Get data from the current strategy's cache."""
|
||||
if not cls.__strategy:
|
||||
raise RuntimeError("Caching strategy has not been set.")
|
||||
return cls.__strategy.get(key)
|
||||
|
||||
@classmethod
|
||||
def set(cls, key, value):
|
||||
"""Set data in the current strategy's cache."""
|
||||
if not cls.__strategy:
|
||||
raise RuntimeError("Caching strategy has not been set.")
|
||||
cls.__strategy.set(key, value)
|
||||
|
||||
|
114
backend/src/overpass/overpass.py
Normal file
114
backend/src/overpass/overpass.py
Normal file
@@ -0,0 +1,114 @@
|
||||
from typing import Literal, List
|
||||
import urllib
|
||||
import json
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
from .caching_strategy import get_cache_key, CachingStrategy
|
||||
|
||||
|
||||
ElementTypes = List[Literal['way', 'node', 'relation']]
|
||||
|
||||
|
||||
def build_query(area: tuple, element_types: ElementTypes, selector: str,
|
||||
conditions=[], out='center'):
|
||||
"""
|
||||
Constructs a query string for the Overpass API to retrieve OpenStreetMap (OSM) data.
|
||||
|
||||
Args:
|
||||
area (tuple): A tuple representing the geographical search area, typically in the format
|
||||
(radius, latitude, longitude). The first element is a string like "around:2000"
|
||||
specifying the search radius, and the second and third elements represent
|
||||
the latitude and longitude as floats or strings.
|
||||
element_types (list[str]): A list of OSM element types to search for. Must be one or more of
|
||||
'Way', 'Node', or 'Relation'.
|
||||
selector (str): The key or tag to filter the OSM elements (e.g., 'amenity', 'highway', etc.).
|
||||
conditions (list, optional): A list of conditions to apply as additional filters for the
|
||||
selected OSM elements. The conditions should be written in
|
||||
the Overpass QL format, and they are combined with '&&' if
|
||||
multiple are provided. Defaults to an empty list.
|
||||
out (str, optional): Specifies the output type, such as 'center', 'body', or 'tags'.
|
||||
Defaults to 'center'.
|
||||
|
||||
Returns:
|
||||
str: The constructed Overpass QL query string.
|
||||
|
||||
Notes:
|
||||
- If no conditions are provided, the query will just use the `selector` to filter the OSM
|
||||
elements without additional constraints.
|
||||
- The search area must always formatted as "(radius, lat, lon)".
|
||||
"""
|
||||
if not isinstance(conditions, list) :
|
||||
conditions = [conditions]
|
||||
|
||||
query = '('
|
||||
|
||||
# Round the radius to nearest 50 and coordinates to generate less queries
|
||||
search_radius = round(area[0] / 50) * 50
|
||||
loc = tuple((round(area[1], 2), round(area[2], 2)))
|
||||
search_area = f"(around:{search_radius}, {str(loc[0])}, {str(loc[1])})"
|
||||
|
||||
if conditions :
|
||||
conditions = '(if: ' + ' && '.join(conditions) + ')'
|
||||
else :
|
||||
conditions = ''
|
||||
|
||||
for elem in element_types :
|
||||
query += elem + '[' + selector + ']' + conditions + search_area + ';'
|
||||
|
||||
query += ');' + f'out {out};'
|
||||
|
||||
return query
|
||||
|
||||
|
||||
def send_overpass_query(query: str, use_cache: bool = True) -> dict:
|
||||
"""
|
||||
Sends the Overpass QL query to the Overpass API and returns the parsed JSON response.
|
||||
|
||||
Args:
|
||||
query (str): The Overpass QL query to be sent to the Overpass API.
|
||||
|
||||
Returns:
|
||||
dict: The parsed JSON response from the Overpass API, or None if the request fails.
|
||||
"""
|
||||
|
||||
# Generate a cache key for the current query
|
||||
cache_key = get_cache_key(query)
|
||||
|
||||
# Try to fetch the result from the cache
|
||||
cached_response = CachingStrategy.get(cache_key)
|
||||
if cached_response:
|
||||
print("Cache hit!")
|
||||
return cached_response
|
||||
|
||||
# Define the Overpass API endpoint
|
||||
overpass_url = "https://overpass-api.de/api/interpreter"
|
||||
|
||||
# Prepare the data to be sent as POST request, encoded as bytes
|
||||
data = urllib.parse.urlencode({'data': query}).encode('utf-8')
|
||||
|
||||
# Create a custom header with a User-Agent
|
||||
headers = {
|
||||
'User-Agent': 'Mozilla/5.0 (compatible; OverpassQuery/1.0; +http://example.com)',
|
||||
}
|
||||
|
||||
try:
|
||||
# Create a Request object with the specified URL, data, and headers
|
||||
request = urllib.request.Request(overpass_url, data=data, headers=headers)
|
||||
|
||||
# Send the request and read the response
|
||||
with urllib.request.urlopen(request) as response:
|
||||
# Read and decode the response
|
||||
response_data = response.read().decode('utf-8')
|
||||
root = ET.fromstring(response_data)
|
||||
|
||||
# Cache the response data as an ElementTree root
|
||||
CachingStrategy.set(cache_key, root)
|
||||
|
||||
return root
|
||||
|
||||
except urllib.error.URLError as e:
|
||||
print(f"Error connecting to Overpass API: {e}")
|
||||
return None
|
||||
except json.JSONDecodeError:
|
||||
print("Error decoding the JSON response from Overpass API.")
|
||||
return None
|
Reference in New Issue
Block a user