Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/sample_python_app/app/lifecycle.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
def start_metrics_server(port: int) -> None:
"""Start the Prometheus metrics server on the specified port."""
if _port_in_use(port):
logger.error("Port %s already in use; metrics disabled", port)
logger.error(f"Port {port} already in use; metrics disabled")
return

logger.info(f"Starting Prometheus metrics on 0.0.0.0:{port}")
Expand Down
48 changes: 14 additions & 34 deletions src/sample_python_app/app/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,12 @@
Handles fetching, validation, and display of astronomical data.
"""

import json
import time
from datetime import date

import httpx
from pydantic import ValidationError

from sample_python_app.core import (
FETCH_COUNTER,
FETCH_DURATION,
FETCH_ERRORS,
setup_logger,
weather_settings,
)
Expand Down Expand Up @@ -41,30 +36,31 @@ def __init__(self, client: CustomHTTPClient) -> None:
self.client = client

def fetch(self, *, exit_on_error: bool = True) -> None:
"""Fetch astronomical data and display if not already displayed today."""
"""Fetch astronomical data and display if not already shown today."""
lat = weather_settings.LOCATION.latitude
lon = weather_settings.LOCATION.longitude
logger.info(f"Using latitude={lat} longitude={lon}")

logger.info("Using latitude=%s longitude=%s", lat, lon)

start = time.time()

try:
astro = fetch_astronomical_data_from_api(lat, lon, client=self.client)
forecast = fetch_hourly_forecast_from_api(lat, lon, client=self.client)
FETCH_COUNTER.inc()
except (
httpx.HTTPStatusError,
httpx.RequestError,
ValidationError,
json.JSONDecodeError,
AppError,
) as exc:
self._handle_fetch_error(exc, exit_on_error)
except AppError as exc:
logger.error("Weather fetch failed: %s", exc)
if exit_on_error:
raise SystemExit(1) from None
return
finally:
FETCH_DURATION.observe(time.time() - start)
today_str = date.today().isoformat()
if self._last_displayed_day != today_str:

today = date.today().isoformat()

if self._last_displayed_day != today:
display_astronomical_data(astro, forecast)
self._last_displayed_day = today_str
self._last_displayed_day = today

def reset_display(self):
"""Reset the last displayed day so display will occur again."""
Expand All @@ -82,22 +78,6 @@ def close(self) -> None:
except Exception:
logger.exception("Error closing HTTP client in AstroFetcher")

def _handle_fetch_error(self, exc: Exception, exit_on_error: bool) -> None:
FETCH_ERRORS.inc()
if isinstance(exc, httpx.HTTPStatusError):
logger.error("HTTP status error: %s", exc)
elif isinstance(exc, httpx.RequestError):
logger.error("Network error: %s", exc)
elif isinstance(exc, ValidationError):
logger.error("Validation error: %s", exc)
elif isinstance(exc, json.JSONDecodeError):
logger.error("JSON decode error: %s", exc)
else:
logger.exception("Unexpected error")
if exit_on_error:
raise SystemExit(1) from exc
raise AppError(str(exc)) from exc


runner_client = CustomHTTPClient(
headers=weather_settings.WEATHER_HEADERS,
Expand Down
6 changes: 6 additions & 0 deletions src/sample_python_app/core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
FETCH_COUNTER,
FETCH_DURATION,
FETCH_ERRORS,
HTTP_REQUEST_DURATION,
HTTP_REQUEST_EXCEPTIONS,
HTTP_REQUESTS,
)

__all__ = [
Expand All @@ -16,4 +19,7 @@
"FETCH_COUNTER",
"FETCH_DURATION",
"FETCH_ERRORS",
"HTTP_REQUESTS",
"HTTP_REQUEST_EXCEPTIONS",
"HTTP_REQUEST_DURATION",
]
19 changes: 18 additions & 1 deletion src/sample_python_app/core/metrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,27 @@
"fetch_duration_seconds",
"Duration of astronomical data fetches in seconds",
)

HTTP_REQUESTS = Counter(
"http_requests_total",
"Total HTTP requests made",
["method", "host", "path", "status_code"],
)
HTTP_REQUEST_EXCEPTIONS = Counter(
"http_request_exceptions_total",
"Total HTTP request exceptions",
["method", "host", "path", "exception_type"],
)
HTTP_REQUEST_DURATION = Histogram(
"http_request_duration_seconds",
"HTTP request latency in seconds",
["method", "host", "path"],
)
__all__ = [
"FETCH_COUNTER",
"FETCH_ERRORS",
"FETCH_DURATION",
"HTTP_REQUESTS",
"HTTP_REQUEST_EXCEPTIONS",
"HTTP_REQUEST_DURATION",
"start_http_server",
]
5 changes: 4 additions & 1 deletion src/sample_python_app/exceptions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

from sample_python_app.exceptions.custom import (
AppError,
HTTPTimeoutError,
NetworkError,
ServiceError,
)

__all__ = ["AppError"]
__all__ = ["AppError", "HTTPTimeoutError", "NetworkError", "ServiceError"]
32 changes: 32 additions & 0 deletions src/sample_python_app/exceptions/custom.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,38 @@
"""Custom exceptions for the sample_python_app project."""


class HTTPTimeoutError(Exception):
"""Raised when an HTTP request times out."""

def __init__(self, message: str) -> None:
"""Initialize the HTTPTimeoutError with a message."""
super().__init__(message)


class NetworkError(Exception):
"""Raised when a network-level error occurs.

(connection refused, DNS failure, etc.).
"""

def __init__(self, message: str) -> None:
"""Initialize the NetworkError with a message."""
super().__init__(message)


class ServiceError(Exception):
"""Raised when the HTTP response returns an error status (4xx or 5xx)."""

def __init__(self, status_code: int, body: str | None = None) -> None:
"""Initialize the ServiceError with status code and optional response body."""
self.status_code = status_code
self.body = body
msg = f"Service returned status {status_code}"
if body:
msg += f": {body}"
super().__init__(msg)


class AppError(Exception):
"""Base exception for the application."""

Expand Down
Loading
Loading