Source code for operators.cost_quest.cqa_get_location_id
from airflow.models import BaseOperator
from airflow.utils.decorators import apply_defaults
from config import Config
from airflow.models import BaseOperator
MAX_LOCATIONS = 1000 # Maximum number of locations to process in a single request
[docs]
class CQAGetLocationId(BaseOperator):
ui_color = "#A6E6A6"
@apply_defaults
def __init__(
self,
query: str,
longitude_col_name: str,
latitude_col_name: str,
verbose: bool = True,
*args,
**kwargs,
):
"""
An operator to get the location id from CostQuest API from longitude and latitude
:param query: the SQL query to get the longitude and latitude
:type query: str
:param longitude_col_name: the name of the column containing the longitude
:type longitude_col_name: str
:param latitude_col_name: the name of the column containing the latitude
:type latitude_col_name: str
:param verbose: whether to print debug information, defaults to True
:type verbose: bool, optional
"""
self.query = query
self.longitude_col_name = longitude_col_name
self.latitude_col_name = latitude_col_name
self.verbose = verbose
super().__init__(*args, **kwargs)
[docs]
def get_api_token(self) -> str:
from airflow.hooks.base import BaseHook
return BaseHook.get_connection(Config.CQA_API_KEY).password
[docs]
def p(self, message: str) -> None:
"""
Print a message if verbose is True
"""
if self.verbose:
print(message)
[docs]
def store_longitudes_latitudes(self) -> None:
import awswrangler as wr
self.p(f"Running SQL to get longitudes and latitudes:\n{self.query}")
df = wr.athena.read_sql_query(sql=self.query, database=Config.DATA_LAKE_DB)
if self.longitude_col_name not in df.columns or self.latitude_col_name not in df.columns:
raise ValueError(
f"Longitude and latitude columns must be in the query result. "
f"Found columns: {df.columns.tolist()}"
)
if df[self.longitude_col_name].isnull().any() or df[self.latitude_col_name].isnull().any():
raise ValueError(
"Longitude and latitude columns must not contain null or NaN values. "
"Please check your query and ensure it returns valid data."
)
self.longitudes = df[self.longitude_col_name].astype(float).tolist()
self.latitudes = df[self.latitude_col_name].astype(float).tolist()
if not self.longitudes or not self.latitudes:
raise ValueError(
"No longitudes or latitudes found in the query result. "
"Please check your query and ensure it returns valid data."
)
if len(self.longitudes) != len(self.latitudes):
raise ValueError(
"The number of longitudes and latitudes must match. "
f"Found {len(self.longitudes)} longitudes and {len(self.latitudes)} latitudes."
)
[docs]
def fetch_location_id(self, longitudes: list[float], latitudes: list[float], token: str, retries=3) -> int:
import requests
import time
url = 'https://api.costquest.com/fabricext/202406/locate'
headers = {
"apikey": token,
'Content-Type': "application/json",
'User-Agent': "insomnia/11.1.0"
}
payload = [
{"longitude": lon, "latitude": lat, "sourcekey": i}
for i, (lon, lat) in enumerate(zip(longitudes, latitudes))
]
for attempt in range(retries):
response = requests.post(url, headers=headers, json=payload)
if response.status_code == 200:
data = response.json()
return [item['location_id'] for item in data]
elif response.status_code == 429:
self.p(f"Rate limit exceeded for {url}. Waiting for retry...")
time.sleep(60)
else:
self.p(f"Error: {response.status_code} - {response.text}")
break
self.p(f"Max retries exceeded for fetching location ids from CQA.")
return []
[docs]
def execute(self, context) -> list[int]:
self.store_longitudes_latitudes()
token = self.get_api_token()
# Split longitudes and latitudes into chunks of MAX_LOCATIONS
chunks = [
(self.longitudes[i:i + MAX_LOCATIONS], self.latitudes[i:i + MAX_LOCATIONS])
for i in range(0, len(self.longitudes), MAX_LOCATIONS)
]
location_ids = []
for i, (longitudes_chunk, latitudes_chunk) in enumerate(chunks):
self.p(f"Fetching location ids for chunk {i + 1}/{len(chunks)} with {len(longitudes_chunk)} locations.")
location_ids.extend(self.fetch_location_id(longitudes_chunk, latitudes_chunk, token))
self.p(f"Fetched {len(location_ids)} location ids from CostQuest API.")
if len(location_ids) != len(self.longitudes):
raise ValueError(
f"Expected {len(self.longitudes)} location ids, but got {len(location_ids)}. "
"Please check the API response and ensure all locations were processed."
)
return location_ids