init commit

This commit is contained in:
2026-01-26 15:08:24 -06:00
commit 67225a725a
33 changed files with 3350 additions and 0 deletions

6
app/utils/__init__.py Normal file
View File

@@ -0,0 +1,6 @@
"""Utility modules for weather alerts."""
from app.utils.http_client import HttpClient
from app.utils.logging_config import configure_logging, get_logger
__all__ = ["HttpClient", "configure_logging", "get_logger"]

162
app/utils/http_client.py Normal file
View File

@@ -0,0 +1,162 @@
"""Centralized HTTP client wrapper with retries and consistent error handling."""
from dataclasses import dataclass
from typing import Any, Optional
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
from app.utils.logging_config import get_logger
@dataclass
class HttpResponse:
"""Wrapper for HTTP response data."""
status_code: int
json_data: Optional[dict[str, Any]]
text: str
success: bool
class HttpClient:
"""HTTP client with automatic retries, timeouts, and logging."""
def __init__(
self,
timeout: int = 30,
max_retries: int = 3,
backoff_factor: float = 0.5,
) -> None:
"""Initialize the HTTP client.
Args:
timeout: Request timeout in seconds.
max_retries: Maximum number of retry attempts.
backoff_factor: Multiplier for exponential backoff between retries.
"""
self.timeout = timeout
self.logger = get_logger(__name__)
# Configure retry strategy
retry_strategy = Retry(
total=max_retries,
backoff_factor=backoff_factor,
status_forcelist=[429, 500, 502, 503, 504],
allowed_methods=["GET", "POST"],
)
# Create session with retry adapter
self.session = requests.Session()
adapter = HTTPAdapter(max_retries=retry_strategy)
self.session.mount("http://", adapter)
self.session.mount("https://", adapter)
def get(
self,
url: str,
params: Optional[dict[str, Any]] = None,
headers: Optional[dict[str, str]] = None,
) -> HttpResponse:
"""Perform a GET request.
Args:
url: The URL to request.
params: Optional query parameters.
headers: Optional HTTP headers.
Returns:
HttpResponse with status, data, and success flag.
"""
self.logger.debug("http_get", url=url, params=params)
try:
response = self.session.get(
url,
params=params,
headers=headers,
timeout=self.timeout,
)
return self._build_response(response)
except requests.RequestException as e:
self.logger.error("http_get_failed", url=url, error=str(e))
return HttpResponse(
status_code=0,
json_data=None,
text=str(e),
success=False,
)
def post(
self,
url: str,
data: Optional[dict[str, Any]] = None,
json_data: Optional[dict[str, Any]] = None,
headers: Optional[dict[str, str]] = None,
) -> HttpResponse:
"""Perform a POST request.
Args:
url: The URL to request.
data: Optional form data.
json_data: Optional JSON data.
headers: Optional HTTP headers.
Returns:
HttpResponse with status, data, and success flag.
"""
self.logger.debug("http_post", url=url)
try:
response = self.session.post(
url,
data=data,
json=json_data,
headers=headers,
timeout=self.timeout,
)
return self._build_response(response)
except requests.RequestException as e:
self.logger.error("http_post_failed", url=url, error=str(e))
return HttpResponse(
status_code=0,
json_data=None,
text=str(e),
success=False,
)
def _build_response(self, response: requests.Response) -> HttpResponse:
"""Build an HttpResponse from a requests Response.
Args:
response: The requests library Response object.
Returns:
HttpResponse wrapper.
"""
json_data = None
if response.headers.get("content-type", "").startswith("application/json"):
try:
json_data = response.json()
except ValueError:
pass
success = 200 <= response.status_code < 300
self.logger.debug(
"http_response",
status_code=response.status_code,
success=success,
)
return HttpResponse(
status_code=response.status_code,
json_data=json_data,
text=response.text,
success=success,
)
def close(self) -> None:
"""Close the HTTP session."""
self.session.close()

View File

@@ -0,0 +1,54 @@
"""Structlog configuration for consistent logging throughout the application."""
import logging
import sys
from typing import Optional
import structlog
def configure_logging(log_level: str = "INFO") -> None:
"""Configure structlog with console output.
Args:
log_level: The logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL).
"""
level = getattr(logging, log_level.upper(), logging.INFO)
# Configure standard library logging
logging.basicConfig(
format="%(message)s",
stream=sys.stdout,
level=level,
)
# Configure structlog
structlog.configure(
processors=[
structlog.stdlib.filter_by_level,
structlog.stdlib.add_logger_name,
structlog.stdlib.add_log_level,
structlog.stdlib.PositionalArgumentsFormatter(),
structlog.processors.TimeStamper(fmt="iso"),
structlog.processors.StackInfoRenderer(),
structlog.processors.format_exc_info,
structlog.processors.UnicodeDecoder(),
structlog.dev.ConsoleRenderer(colors=sys.stdout.isatty()),
],
wrapper_class=structlog.stdlib.BoundLogger,
context_class=dict,
logger_factory=structlog.stdlib.LoggerFactory(),
cache_logger_on_first_use=True,
)
def get_logger(name: Optional[str] = None) -> structlog.stdlib.BoundLogger:
"""Get a configured logger instance.
Args:
name: Optional name for the logger. If not provided, uses the calling module.
Returns:
A configured structlog logger.
"""
return structlog.get_logger(name)