Skip to content

Commit 0500f10

Browse files
authored
feat: initial release
feat: initial release
2 parents 2933c72 + 609f27f commit 0500f10

13 files changed

Lines changed: 1038 additions & 1 deletion

File tree

.github/workflows/ci-cd.yaml

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
name: CI/CD Pipeline
2+
3+
on:
4+
push:
5+
branches:
6+
- '**'
7+
tags:
8+
- 'v*.*.*'
9+
pull_request:
10+
branches:
11+
- '**'
12+
workflow_dispatch:
13+
14+
jobs:
15+
cache:
16+
uses: milsman2/python-app-template/.github/workflows/cache-uv-build.yaml@main
17+
18+
lint:
19+
uses: milsman2/python-app-template/.github/workflows/ruff.yaml@main
20+
needs: cache
21+
22+
test:
23+
needs: [lint, cache]
24+
uses: milsman2/python-app-template/.github/workflows/pytest.yaml@main
25+
26+
docker-build-and-image-scan:
27+
if: github.event_name == 'push'
28+
needs: test
29+
uses: milsman2/python-app-template/.github/workflows/docker-build-and-scan.yaml@main
30+
with:
31+
DOCKER_PATH_CONTEXT: .
32+
DOCKER_BUILD_DOCKERFILE: ./Dockerfile
33+
DOCKER_TAGS: ${{ github.repository }}:${{ github.sha }}
34+
DOCKER_LOAD_BOOL: false
35+
DOCKER_PUSH_BOOL: ${{ github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') }}
36+
secrets: inherit
37+
38+
release:
39+
if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')
40+
needs: [test, docker-build-and-image-scan]
41+
uses: milsman2/python-app-template/.github/workflows/release.yaml@main
42+
permissions:
43+
contents: write
44+
secrets: inherit

.python-version

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
3.13

Dockerfile

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
FROM ghcr.io/astral-sh/uv:python3.13-bookworm-slim AS builder
2+
ENV UV_COMPILE_BYTECODE=1 UV_LINK_MODE=copy
3+
ENV UV_NO_DEV=1
4+
ENV UV_PYTHON_DOWNLOADS=0
5+
WORKDIR /app
6+
RUN --mount=type=cache,target=/root/.cache/uv \
7+
--mount=type=bind,source=uv.lock,target=uv.lock \
8+
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
9+
uv sync --locked --no-install-project
10+
COPY . /app
11+
RUN --mount=type=cache,target=/root/.cache/uv \
12+
uv sync --locked
13+
14+
FROM python:3.13-slim-bookworm AS runtime
15+
RUN apt-get update \
16+
&& apt-get upgrade -y \
17+
&& apt-get clean \
18+
&& rm -rf /var/lib/apt/lists/*
19+
RUN groupadd --system --gid 999 nonroot \
20+
&& useradd --system --gid 999 --uid 999 --create-home nonroot
21+
COPY --from=builder --chown=nonroot:nonroot /app/ /app
22+
ENV PATH="/app/.venv/bin:$PATH"
23+
USER nonroot
24+
WORKDIR /app/src
25+
CMD ["python", "-m", "sample_python_app.main"]

README.md

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,74 @@
11
# python-response-time
2-
Python-based response time module
2+
3+
A Pythonic, DevOps-friendly HTTP benchmarking tool leveraging Pydantic v2 for robust configuration, Loguru for structured logging, and a modern, testable workflow.
4+
5+
## Features
6+
7+
- **Configurable HTTP benchmarking** via environment variables or `.env` file (Pydantic v2 + pydantic-settings)
8+
- **Structured logging** with Loguru (console and file, colorized, with rotation)
9+
- **Pre-flight DevOps checks**: auto-formatting, linting, and test coverage in one command
10+
- **Modern Python packaging**: `pyproject.toml`-based, ready for CI/CD
11+
- **Type-safe, validated settings**: all config is validated at startup
12+
- **Tested and type-checked**: includes pytest-based tests for all config
13+
- **CLI entrypoints** for both benchmarking and checks
14+
15+
## Quickstart
16+
17+
```bash
18+
# Install with all dev dependencies
19+
uv pip install -e '.[build]'
20+
21+
# Run the HTTP benchmark
22+
uv run python-response-time
23+
24+
# Run all pre-flight DevOps checks (format, lint, test)
25+
uv run checks
26+
```
27+
28+
## Configuration
29+
30+
All settings are managed with Pydantic v2 and can be set via environment variables or a `.env` file:
31+
32+
| Variable | Type | Default | Description |
33+
|---------------|--------|--------------------------|------------------------------------|
34+
| TARGET_URL | str | https://httpbin.org/get | Target endpoint for benchmarking |
35+
| NUM_REQUESTS | int | 10 | Total number of requests |
36+
| CONCURRENCY | int | 2 | Number of concurrent requests |
37+
| TIMEOUT | float | 10.0 | Request timeout (seconds) |
38+
| LOG_LEVEL | str | INFO | Log level (DEBUG, INFO, etc.) |
39+
40+
Example `.env`:
41+
42+
```
43+
TARGET_URL=https://example.com/api
44+
NUM_REQUESTS=100
45+
CONCURRENCY=5
46+
TIMEOUT=5.0
47+
LOG_LEVEL=DEBUG
48+
```
49+
50+
## DevOps & Pythonic Practices
51+
52+
- **Pre-flight checks**: One command (`uv run checks`) runs ruff, isort, black, and pytest with coverage.
53+
- **Strict config validation**: Pydantic v2 ensures all settings are valid before running.
54+
- **Logging**: Loguru provides both human-friendly console logs and persistent file logs with rotation and compression.
55+
- **Modern packaging**: Uses `pyproject.toml` for dependencies, scripts, and tool config.
56+
- **CI/CD ready**: Semantic release and coverage tools are pre-configured.
57+
- **Type annotations**: All code is type-annotated for clarity and safety.
58+
59+
## Project Structure
60+
61+
```
62+
src/python_response_time/
63+
main.py # Benchmark runner
64+
pre_flight.py # DevOps checks
65+
core/
66+
config.py # Pydantic v2 settings
67+
logging.py # Loguru setup
68+
tests/
69+
test_basic.py # Pytest-based config tests
70+
```
71+
72+
## License
73+
74+
MIT

pyproject.toml

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
[project]
2+
name = "python-response-time"
3+
version = "0.1.0"
4+
description = "Add your description here"
5+
readme = "README.md"
6+
requires-python = ">=3.13"
7+
dependencies = [
8+
"black>=26.3.1",
9+
"coverage>=7.13.5",
10+
"isort>=8.0.1",
11+
"loguru>=0.7.3",
12+
"pydantic>=2.12.5",
13+
"pydantic-settings>=2.13.1",
14+
"pytest>=9.0.2",
15+
"requests>=2.32.5",
16+
"rich>=14.3.3",
17+
"ruff>=0.15.7",
18+
"typer>=0.24.1",
19+
]
20+
21+
[project.optional-dependencies]
22+
build = ["uv == 0.10.12"]
23+
24+
[build-system]
25+
requires = ["uv_build == 0.10.12"]
26+
build-backend = "uv_build"
27+
28+
[project.scripts]
29+
python-response-time = "python_response_time.main:run_app"
30+
checks = "python_response_time.pre_flight:run_checks"
31+
32+
[tool.semantic_release]
33+
allow_version_zero = true
34+
build_command = """
35+
python -m pip install -e '.[build]'
36+
uv lock --upgrade-package "$PACKAGE_NAME"
37+
git add uv.lock
38+
uv build
39+
"""
40+
version_toml = ["pyproject.toml:project.version"]
41+
42+
[tool.coverage.run]
43+
source = ["src"]
44+
45+
[tool.coverage.report]
46+
omit = ["tests/*"]
47+
48+
[tool.black]
49+
target-version = ["py313"]
50+
51+
[tool.ruff]
52+
line-length = 88
53+
exclude = ["__pycache__", "build", "dist", ".venv"]
54+
55+
[tool.ruff.lint]
56+
select = ["E", "F", "W", "C", "B", "I", "N", "D", "UP", "T", "A"]
57+
ignore = []
58+
59+
[tool.ruff.lint.per-file-ignores]
60+
"tests/*" = ["F401"]
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""python_response_time package."""
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
"""Core module for Python Response Time package."""
2+
3+
from python_response_time.core.config import app_settings
4+
from python_response_time.core.logging import setup_logger
5+
6+
__all__ = ["app_settings", "setup_logger"]
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
"""Configuration settings for the HTTP benchmark."""
2+
3+
from typing import Annotated
4+
5+
from pydantic import Field
6+
from pydantic_settings import BaseSettings, SettingsConfigDict
7+
8+
9+
class Settings(BaseSettings):
10+
"""Configuration settings for the HTTP benchmark."""
11+
12+
TARGET_URL: Annotated[
13+
str, Field(description="Target endpoint for benchmarking")
14+
] = "https://httpbin.org/get"
15+
NUM_REQUESTS: Annotated[
16+
int, Field(gt=0, le=1_000_000, description="Total number of requests")
17+
] = 10
18+
CONCURRENCY: Annotated[
19+
int, Field(gt=0, le=10_000, description="Concurrent requests")
20+
] = 2
21+
TIMEOUT: Annotated[
22+
float, Field(gt=0, le=120, description="Request timeout in seconds")
23+
] = 10.0
24+
LOG_LEVEL: Annotated[
25+
str, Field(pattern="^(DEBUG|INFO|WARNING|ERROR|CRITICAL)$")
26+
] = "INFO"
27+
28+
model_config = SettingsConfigDict(
29+
env_file=".env",
30+
env_file_encoding="utf-8",
31+
extra="forbid",
32+
validate_default=True,
33+
)
34+
35+
36+
app_settings = Settings()
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
"""Loguru logger configuration for python-app-template."""
2+
3+
import sys
4+
5+
from loguru import logger
6+
7+
log_format = (
8+
"<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> | "
9+
"<level>{level: <8}</level> | "
10+
"<cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> - "
11+
"<level>{message}</level>"
12+
)
13+
14+
15+
def setup_logger(level: str = "INFO"):
16+
"""Configure and return a Loguru logger instance.
17+
18+
Args:
19+
level (str, optional): Logging level for file outputs (e.g., "INFO", "DEBUG",
20+
"ERROR"). Defaults to "INFO".
21+
22+
Returns:
23+
logger: Configured Loguru logger instance.
24+
25+
"""
26+
logger.remove()
27+
if level.upper() == "SILENT":
28+
logger.add(
29+
"app.log",
30+
format=log_format,
31+
level="ERROR",
32+
rotation="1 MB",
33+
retention="10 days",
34+
compression="zip",
35+
)
36+
else:
37+
logger.add(
38+
sys.stdout,
39+
format=log_format,
40+
level=level,
41+
backtrace=True,
42+
diagnose=True,
43+
enqueue=True,
44+
)
45+
logger.add(
46+
"app.log",
47+
format=log_format,
48+
level=level,
49+
rotation="1 MB",
50+
retention="10 days",
51+
compression="zip",
52+
)
53+
return logger

src/python_response_time/main.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
"""Main entry point for the Python Response Time application."""
2+
3+
import time
4+
5+
import requests
6+
from loguru import logger
7+
8+
from python_response_time.core import app_settings, setup_logger
9+
10+
setup_logger(app_settings.LOG_LEVEL)
11+
12+
13+
def run_app():
14+
"""Run a simple HTTP benchmark against the configured target URL."""
15+
logger.info("Starting HTTP benchmark...")
16+
for i in range(app_settings.NUM_REQUESTS):
17+
start_time = time.time()
18+
try:
19+
response = requests.get(
20+
str(app_settings.TARGET_URL), timeout=app_settings.TIMEOUT
21+
)
22+
response.raise_for_status()
23+
elapsed_time_ms = (time.time() - start_time) * 1000
24+
logger.info(
25+
f"Request {i + 1}: {response.status_code} - {elapsed_time_ms:.2f} ms"
26+
)
27+
except requests.RequestException as e:
28+
logger.error(f"Request {i + 1} failed: {e}")
29+
30+
31+
if __name__ == "__main__":
32+
run_app()

0 commit comments

Comments
 (0)