import os from datetime import datetime, timezone from os import path from urllib.parse import urlencode import jwt import requests from app.dd_trace_helpers import get_dd_trace_headers from app.logger import log from app.secrets_manager import SecretsManagerClient from app.config import ENV, SERVICES, SPOTIFY, QA_ENV def is_jwt_alive(token): """ Decode a JWT token and check whether it's expired. """ try: payload = jwt.decode(token, options={"verify_signature": False}) exp = payload.get("exp") if exp is None: return False, "Token has no 'exp' claim." exp_time = datetime.fromtimestamp(exp, tz=timezone.utc) now = datetime.now(timezone.utc) log.info(f"Now time: {now}") if now >= exp_time: log.warning(f"Token is expired at {exp_time.isoformat()}") return False log.info(f"Token is valid till {exp_time.isoformat()}") return True except jwt.DecodeError as e: return False, f"Failed to decode token: {e}" class ApiClient: """Client to connect to API.""" def __init__(self, application, timeout=60): log.info(f"Initializing APIClient for {application}") self.application = application self.base_url = SERVICES[application]["base_url"] self.timeout = int(timeout) self.session = requests.Session() secrets_manager_client = SecretsManagerClient() self.secrets = secrets_manager_client.get_secret_json()[self.application] self.token = self._get_api_token() self.session.headers["authorization"] = "Bearer {}".format(self.token) def _get_api_token(self): """ Retrieves a cached API token if valid, otherwise fetches a new one. """ today = str(datetime.today().date()) token_file = f"token_{self.application}_{today}.txt" log.info(f"Retrieving token from {token_file}") if path.exists(token_file): with open(token_file, 'r') as file: access_token = file.readline().strip() if is_jwt_alive(access_token) or self.application == SPOTIFY: return access_token os.remove(token_file) new_token = self.fetch_fresh_token() with open(token_file, 'w') as file: file.write(new_token) return new_token def fetch_fresh_token(self): log.info("Fetching fresh token") token_url = self.secrets["token_url"] headers = {"content-type": "application/x-www-form-urlencoded"} data = { "grant_type": self.secrets["grant_type"], "client_id": self.secrets["client_id"], "client_secret": self.secrets["client_secret"], "audience": self.secrets["audience"] } if ENV == QA_ENV: data.update({ "username": self.secrets.get("username"), "password": self.secrets.get("password"), "scope": self.secrets.get("auth_scope") }) response = self.session.post( url=token_url, data=urlencode(data), headers=headers ) try: access_token = response.json().get("access_token") except ValueError: log.error("Failed to parse token response") return None return access_token def update_token(self, new_token): if not new_token: log.error("Update_token called with None token — skipping update") return log.info("Updating API token and headers") self.token = new_token self.session.headers["authorization"] = f"Bearer {new_token}" def get(self, endpoint): url = self.base_url + endpoint log.info(f"GET request to {url}") response = self.session.get(url, timeout=self.timeout, headers=get_dd_trace_headers()) log.info(f"response status': {response.status_code}") response.raise_for_status() return response.json()