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
Binary file modified .coverage
Binary file not shown.
22 changes: 22 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
__pycache__
*.pyc
*.pyo
*.pyd
.Python
env/
venv/
.venv/
pip-log.txt
pip-delete-this-directory.txt
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.log
.git
.mypy_cache
.pytest_cache
.hypothesis
2 changes: 1 addition & 1 deletion .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ updates:
- package-ecosystem: "pip"
directory: "/"
schedule:
interval: "daily"
interval: "weekly"
time: "09:00"
timezone: "America/New_York"
labels:
Expand Down
11 changes: 11 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,14 @@ repos:
- id: bandit
args: ["-r", "src"]
exclude: ^tests/

- repo: https://github.com/pypa/pip-audit
rev: v2.7.3
hooks:
- id: pip-audit
args: ["--progress-spinner", "off"]

- repo: https://github.com/Yelp/detect-secrets
rev: v1.5.0
hooks:
- id: detect-secrets
3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,6 @@
],
"mypy-type-checker.args": [
"--config-file=pyproject.toml"
]
],
"makefile.configureOnOpen": false
}
7 changes: 5 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Use a Python image with uv pre-installed
FROM ghcr.io/astral-sh/uv:python3.13-bookworm-slim
# Use standard Python image to avoid ghcr.io auth issues
FROM python:3.13-slim

# Install uv
RUN pip install uv

# Set the working directory to /app
WORKDIR /app
Expand Down
55 changes: 0 additions & 55 deletions SECURITY.md

This file was deleted.

18 changes: 17 additions & 1 deletion Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,10 @@ tasks:
- uv run python -m mypy src tests

security:
desc: Run bandit security checks
desc: Run all security checks (bandit, pip-audit)
cmds:
- uv run bandit -r src
- task: security-audit

check:
desc: Run all checks (lint, format, type-check, security, test)
Expand Down Expand Up @@ -103,6 +104,21 @@ tasks:
cmds:
- docker run --rm defaultpython

docker-up:
desc: Start services with docker compose
cmds:
- docker compose up --build

docker-down:
desc: Stop services
cmds:
- docker compose down

security-audit:
desc: Check for known security vulnerabilities in dependencies
cmds:
- uv run pip-audit

pre-commit:
desc: Run pre-commit hooks on all files
cmds:
Expand Down
13 changes: 13 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
services:
app:
build: .
image: defaultpython-app
environment:
- APP_ENV=production
volumes:
- .:/app
- /app/.venv
# If/when you switch to a web server, uncomment ports:
# ports:
# - "8000:8000"
# command: uvicorn defaultpython.main:app --host 0.0.0.0 --port 8000 --reload
4 changes: 4 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ requires-python = ">=3.13"
dependencies = [
"fastapi>=0.128.0",
"pydantic-settings>=2.12.0",
"python-dotenv>=1.2.1",
"uvicorn>=0.40.0",
]

Expand Down Expand Up @@ -58,6 +59,9 @@ disallow_untyped_defs = true
[dependency-groups]
dev = [
"bandit>=1.9.2",
"detect-secrets>=1.5.0",
"pip-audit>=2.7.3",
"vulture>=2.11",
"commitizen>=4.11.3",
"httpx>=0.28.1",
"mypy>=1.19.1",
Expand Down
24 changes: 18 additions & 6 deletions src/defaultpython/api.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import logging
from collections.abc import AsyncGenerator
from contextlib import asynccontextmanager

Expand All @@ -7,6 +8,8 @@
from .config import get_settings
from .main import main as run_main_logic

logger = logging.getLogger(__name__)


@asynccontextmanager
async def lifespan(_app: FastAPI) -> AsyncGenerator[None]:
Expand All @@ -16,12 +19,14 @@ async def lifespan(_app: FastAPI) -> AsyncGenerator[None]:
"""
# Startup logic
settings = get_settings()
print(f"Starting {settings.PROJECT_NAME} in {settings.ENVIRONMENT} mode...")
logger.info(
"Starting %s in %s mode...", settings.PROJECT_NAME, settings.ENVIRONMENT
)

yield

# Shutdown logic
print(f"Shutting down {settings.PROJECT_NAME}...")
logger.info("Shutting down %s...", settings.PROJECT_NAME)


def create_app() -> FastAPI:
Expand All @@ -32,18 +37,25 @@ def create_app() -> FastAPI:
title=settings.PROJECT_NAME,
version=settings.VERSION,
lifespan=lifespan,
docs_url=None,
redoc_url=None,
docs_url="/docs" if settings.DEBUG else None,
redoc_url="/redoc" if settings.DEBUG else None,
)

@app.exception_handler(Exception)
async def global_exception_handler(
_request: Request, exc: Exception
request: Request, exc: Exception
) -> JSONResponse:
"""Global exception handler to return consistent JSON errors."""
logger.exception(
"Unhandled exception: %s", exc, extra={"path": request.url.path}
)

# Don't expose internal error details in production
error_detail = str(exc) if settings.DEBUG else "Internal Server Error"

return JSONResponse(
status_code=500,
content={"detail": "Internal Server Error", "error": str(exc)},
content={"detail": "Internal Server Error", "error": error_detail},
)

@app.get("/health")
Expand Down
4 changes: 2 additions & 2 deletions src/defaultpython/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@ def main() -> None:
"""
load_dotenv()

app_env: str | None = os.getenv("APP_ENV", "development")
app_env: str = os.getenv("APP_ENV", "development")

logger.info("Starting defaultpython in %s mode", app_env)
print(f"Hello from defaultpython! Environment: {app_env}")
logger.info("Hello from defaultpython! Environment: %s", app_env)


if __name__ == "__main__":
Expand Down
6 changes: 0 additions & 6 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,6 @@
from fastapi.testclient import TestClient

from defaultpython.api import app
from defaultpython.config import Settings, get_settings


@pytest.fixture
def settings() -> Settings:
return get_settings()


@pytest.fixture
Expand Down
19 changes: 14 additions & 5 deletions tests/test_main.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
from unittest.mock import patch

import pytest

from defaultpython.main import main


def test_main_runs() -> None:
"""Basic test to ensure main function exists and runs without error."""
# Since main() currently just prints, we'll just call it.
# In a real app, you might mock stdout or check return values.
main()
def test_main_runs(monkeypatch: pytest.MonkeyPatch) -> None:
"""Test main function loads environment and logs correctly."""
monkeypatch.setenv("APP_ENV", "testing")

with patch("defaultpython.main.logger") as mock_logger:
main()
# Verify logger was called with the environment
assert mock_logger.info.call_count == 2
calls = [str(call) for call in mock_logger.info.call_args_list]
assert any("testing" in str(call) for call in calls)
Loading
Loading