Skip to content

Commit 9e9493d

Browse files
authored
feat: application refactor with prometheus
feat: application refactor with prometheus
2 parents cde64f5 + 50e2749 commit 9e9493d

17 files changed

Lines changed: 279 additions & 51 deletions

.github/workflows/ci-cd.yaml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,20 +13,20 @@ on:
1313

1414
jobs:
1515
cache:
16-
uses: ./.github/workflows/cache-uv-build.yaml
16+
uses: milsman2/python-app-template/.github/workflows/cache-uv-build.yaml@main
1717

1818
lint:
19-
uses: ./.github/workflows/ruff.yaml
19+
uses: milsman2/python-app-template/.github/workflows/ruff.yaml@main
2020
needs: cache
2121

2222
test:
2323
needs: [lint, cache]
24-
uses: ./.github/workflows/pytest.yaml
24+
uses: milsman2/python-app-template/.github/workflows/pytest.yaml@main
2525

2626
docker:
2727
if: github.event_name == 'push'
2828
needs: test
29-
uses: ./.github/workflows/docker-build-and-scan.yaml
29+
uses: milsman2/python-app-template/.github/workflows/docker-build-and-scan.yaml@main
3030
with:
3131
DOCKER_PATH_CONTEXT: .
3232
DOCKER_BUILD_DOCKERFILE: ./Dockerfile

.github/workflows/docker-build-and-scan.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ jobs:
4646
tags: ${{ inputs.DOCKER_TAGS }}
4747
- name: Run Trivy vulnerability scanner (remote)
4848
if: ${{ github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') }}
49-
uses: aquasecurity/trivy-action@0.33.1
49+
uses: aquasecurity/trivy-action@0.34.0
5050
with:
5151
image-ref: docker.io/${{ inputs.DOCKER_TAGS }}
5252
format: 'table'

.github/workflows/pytest.yaml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ jobs:
1414
steps:
1515
- name: Checkout repository
1616
uses: actions/checkout@v6
17-
1817
- name: Restore uv caches
1918
id: cache-restore
2019
uses: actions/cache/restore@v5

.github/workflows/ruff.yaml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ jobs:
1515
steps:
1616
- name: Checkout repository
1717
uses: actions/checkout@v6
18-
1918
- name: Restore global uv cache
2019
id: cache-restore
2120
uses: actions/cache/restore@v5

pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@ description = "Add your description here"
55
readme = "README.md"
66
requires-python = ">=3.13"
77
dependencies = [
8+
"apscheduler>=3.11.2",
89
"black>=26.1.0",
910
"coverage>=7.13.2",
1011
"httpx>=0.28.1",
1112
"isort>=7.0.0",
1213
"loguru>=0.7.3",
1314
"pathlib>=1.0.1",
15+
"prometheus-client>=0.24.1",
1416
"pydantic>=2.12.5",
1517
"pydantic-extra-types>=2.11.0",
1618
"pydantic-settings>=2.12.0",

src/sample_python_app/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,5 @@
11
"""Sample Python app package initialization."""
2+
3+
from sample_python_app.main import run_app
4+
5+
__all__ = ["run_app"]
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
"""App module for the sample Python application.
2+
3+
This module contains the main components for running the application, including
4+
the metrics server, scheduler, and the main application runner.
5+
"""
6+
7+
from sample_python_app.app.lifecycle import start_metrics_server
8+
from sample_python_app.app.runner import fetch_astro_data
9+
from sample_python_app.app.scheduler import start_scheduler
10+
11+
__all__ = ["start_metrics_server", "start_scheduler", "fetch_astro_data"]
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
"""Lifecycle management for metrics server.
2+
3+
Handles Prometheus metrics server startup and port checks.
4+
"""
5+
6+
import socket
7+
8+
from prometheus_client import start_http_server
9+
10+
from sample_python_app.core.logging import setup_logger
11+
12+
logger = setup_logger("normal")
13+
14+
15+
def start_metrics_server(port: int) -> None:
16+
"""Start the Prometheus metrics server on the specified port."""
17+
if _port_in_use(port):
18+
logger.error("Port %s already in use; metrics disabled", port)
19+
return
20+
21+
logger.info(f"Starting Prometheus metrics on 0.0.0.0:{port}")
22+
start_http_server(port, addr="0.0.0.0")
23+
24+
25+
def _port_in_use(port: int) -> bool:
26+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
27+
try:
28+
sock.bind(("localhost", port))
29+
except OSError:
30+
return True
31+
return False
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
"""Runner module for the sample Python app.
2+
3+
Handles fetching, validation, and display of astronomical data.
4+
"""
5+
6+
# app/runner.py
7+
import json
8+
import time
9+
10+
import httpx
11+
from pydantic import ValidationError
12+
13+
from sample_python_app.core import (
14+
FETCH_COUNTER,
15+
FETCH_DURATION,
16+
FETCH_ERRORS,
17+
display_astronomical_data,
18+
setup_logger,
19+
weather_settings,
20+
)
21+
from sample_python_app.exceptions import AppError
22+
from sample_python_app.services import fetch_astronomical_data_from_api
23+
24+
logger = setup_logger("normal")
25+
26+
27+
def fetch_astro_data(*, exit_on_error: bool = True) -> None:
28+
"""Fetch and display astronomical data once, with error handling."""
29+
lat = weather_settings.LOCATION.latitude
30+
lon = weather_settings.LOCATION.longitude
31+
logger.info(f"Using latitude={lat} longitude={lon}")
32+
33+
start = time.time()
34+
35+
try:
36+
astro = fetch_astronomical_data_from_api(lat, lon)
37+
FETCH_COUNTER.inc()
38+
except httpx.HTTPStatusError as exc:
39+
_handle_fetch_error(exc, exit_on_error)
40+
return
41+
except httpx.RequestError as exc:
42+
_handle_fetch_error(exc, exit_on_error)
43+
return
44+
except ValidationError as exc:
45+
_handle_fetch_error(exc, exit_on_error)
46+
return
47+
except json.JSONDecodeError as exc:
48+
_handle_fetch_error(exc, exit_on_error)
49+
return
50+
except AppError as exc:
51+
_handle_fetch_error(exc, exit_on_error)
52+
return
53+
finally:
54+
FETCH_DURATION.observe(time.time() - start)
55+
56+
display_astronomical_data(astro)
57+
58+
59+
def _handle_fetch_error(exc: Exception, exit_on_error: bool) -> None:
60+
"""Handle errors during the fetch operation.
61+
62+
Log appropriately and update metrics.
63+
"""
64+
FETCH_ERRORS.inc()
65+
if isinstance(exc, httpx.HTTPStatusError):
66+
logger.error("HTTP status error: %s", exc)
67+
elif isinstance(exc, httpx.RequestError):
68+
logger.error("Network error: %s", exc)
69+
elif isinstance(exc, ValidationError):
70+
logger.error("Validation error: %s", exc)
71+
elif isinstance(exc, json.JSONDecodeError):
72+
logger.error("JSON decode error: %s", exc)
73+
else:
74+
logger.exception("Unexpected error")
75+
76+
if exit_on_error:
77+
raise SystemExit(1) from exc
78+
79+
raise AppError(str(exc)) from exc
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
"""Scheduler module for periodic astronomical data fetch.
2+
3+
Initializes and runs the APScheduler job every 24 hours.
4+
"""
5+
6+
from datetime import datetime
7+
8+
from apscheduler.schedulers.blocking import BlockingScheduler
9+
10+
from sample_python_app.app.runner import fetch_astro_data
11+
from sample_python_app.core.logging import setup_logger
12+
13+
logger = setup_logger("normal")
14+
15+
16+
def start_scheduler(test_mode: bool = False) -> None:
17+
"""Start the scheduler to run the astronomical data fetch every 24 hours.
18+
19+
In test_mode, run the scheduled job once and do not block.
20+
"""
21+
scheduler = BlockingScheduler()
22+
scheduler.add_job(
23+
fetch_astro_data,
24+
trigger="interval",
25+
hours=24,
26+
next_run_time=datetime.now(),
27+
)
28+
logger.info("Scheduled astronomical fetch every 24 hours")
29+
if test_mode:
30+
# Run the job once for testing, do not block
31+
job = scheduler.get_jobs()[0]
32+
job.func()
33+
logger.info("Ran scheduled job once in test mode")
34+
return
35+
try:
36+
scheduler.start()
37+
except (KeyboardInterrupt, SystemExit):
38+
logger.info("Scheduler stopped")
39+
scheduler.shutdown()

0 commit comments

Comments
 (0)