commit 580d27028bb33e6345d786712af79e4edfeb22e6 Author: alzyras Date: Fri Jan 30 13:17:42 2026 +0200 Initial commit diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..2810362 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,12 @@ +.git +.gitignore +.env +__pycache__ +*.pyc +*.pyo +*.pyd +.Python +env +venv +.venv +*.db \ No newline at end of file diff --git a/.env.tmpl b/.env.tmpl new file mode 100644 index 0000000..f3c1897 --- /dev/null +++ b/.env.tmpl @@ -0,0 +1 @@ +EXAMPLE_ENV_VARIABLE="..." \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2761c12 --- /dev/null +++ b/.gitignore @@ -0,0 +1,40 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*/__pycache__/ +*.py[cod] + +# Distribution / packaging +.Python +env/ +.venv +venv + +# Unit test / coverage reports +.coverage +.coverage.* +.cache +coverage.xml + +# Pycharm +.idea + +# VS Code +.vscode/ + +# Jupyter NB Checkpoints +.ipynb_checkpoints/ + +# Mac OS-specific storage files +.DS_Store + +# Caches +.mypy_cache/ +.pytest_cache/ +.ruff_cache/ +.neptune/ +/cache/* +!/cache/.gitkeep + +# Personal files +credentials.json +.env \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..027e7b4 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,38 @@ +# Repository Guidelines + +## Project Structure & Module Organization +- `uv_app/` holds application code (`settings.py` for env-loaded config, `logging_config.py` for basic logging, `__main__.py` entrypoint placeholder). Keep new modules small and focused; prefer `uv_app/feature/.py` for grouped logic. +- `tests/` contains pytest suites; mirror module paths (e.g., `uv_app/example_util.py` → `tests/example_util_test.py`). +- `notebooks/` is for exploratory work; avoid depending on it for runtime logic and keep outputs trimmed before committing. +- Root files: `pyproject.toml` (project + tooling), `uv.lock` (dependency lock), `Dockerfile` (runtime build), `README.md` (quickstart). + +## Build, Test, and Development Commands +- Install + sync env: `uv sync` (creates venv and installs deps). +- Run full format + lint: `uv run poe x` (runs `ruff format` then `ruff check --fix`). +- Tests: `uv run pytest` (add `-k ` for focused runs). + +## Dependency Management +- Use `uv add ` to add dependencies; this updates `pyproject.toml` and `uv.lock`. +- For dev dependencies, use `uv add --dev `. +- To remove packages, use `uv remove `. + +# Docker & Containerization +- The `Dockerfile.main` sets up a lightweight Python environment using the locked dependencies from `uv.lock`. +- The `Dockerfile.tests` sets up a pytest environment for running tests in a containerized setup. +- Use `docker compose build` to build images. +- Use `docker compose up main` to build and run the app container or `docker compose run main` if application is job-based. +- Use `docker compose run tests` to run tests in a containerized environment. + +## Coding Style & Naming Conventions +- Python 3.14; Use type hints on. Use double quotes (enforced by ruff `flake8-quotes`), Google-style docstrings when needed. +- Follow Ruff config (`select = ["ALL"]` with targeted ignores). Keep new ignores local and justified. +- Modules and files: lowercase with underscores; classes: `PascalCase`; functions/vars: `snake_case`. Tests mirror subject names and use descriptive test function names. + +## Testing Guidelines +- Framework: pytest. Parametrize where inputs vary. +- Place unit tests alongside corresponding modules under `tests/`; name files `_test.py`. +- Add regression tests with clear arrange/act/assert sections. Aim to cover new branches; prefer deterministic data over randomness. + +## Configuration & Secrets +- Settings load via `pydantic-settings` with `.env` discovery (`find_dotenv`). Keep secrets out of git; document required keys in `.env.example` if adding new settings. +- Seperate settings classes based on purpose, for example PostgresSettings, AppSettings, etc. \ No newline at end of file diff --git a/Dockerfile.main b/Dockerfile.main new file mode 100644 index 0000000..7e6f37e --- /dev/null +++ b/Dockerfile.main @@ -0,0 +1,24 @@ +FROM python:3.14-slim AS builder + +WORKDIR /app +ENV UV_COMPILE_BYTECODE=1 + +COPY pyproject.toml uv.lock ./ +RUN pip install --no-cache-dir uv \ + && uv sync --frozen --no-install-project --no-dev + +COPY uv_app/ ./uv_app/ + +FROM python:3.14-slim + +WORKDIR /app + +COPY --from=builder /app/.venv /app/.venv +COPY --from=builder /app/uv_app ./uv_app + +RUN adduser --disabled-password --gecos '' appuser && chown -R appuser /app +USER appuser + +EXPOSE 8080 +ENTRYPOINT ["/app/.venv/bin/python", "-m", "uv_app"] +CMD ["main"] diff --git a/Dockerfile.test b/Dockerfile.test new file mode 100644 index 0000000..ac1699a --- /dev/null +++ b/Dockerfile.test @@ -0,0 +1,24 @@ +FROM python:3.14-slim AS builder + +WORKDIR /app +ENV UV_COMPILE_BYTECODE=1 + +COPY pyproject.toml uv.lock ./ +RUN pip install --no-cache-dir uv \ + && uv sync --frozen --no-install-project --group dev + +COPY uv_app/ ./uv_app/ +COPY tests/ ./tests/ + +FROM python:3.14-slim + +WORKDIR /app + +COPY --from=builder /app/.venv /app/.venv +COPY --from=builder /app/uv_app ./uv_app +COPY --from=builder /app/tests ./tests + +RUN adduser --disabled-password --gecos '' appuser && chown -R appuser /app +USER appuser + +CMD ["/app/.venv/bin/pytest", "-q"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..2bdac83 --- /dev/null +++ b/README.md @@ -0,0 +1,15 @@ +## uv-project-template + + +## Installation + +To create and install virtual environment: + +```bash +uv sync +``` +During development, you can lint and format code using: + +```bash +uv run poe x +``` diff --git a/compose.yml b/compose.yml new file mode 100644 index 0000000..b17542d --- /dev/null +++ b/compose.yml @@ -0,0 +1,14 @@ +services: + main: + build: + context: . + dockerfile: Dockerfile.main + env_file: + - .env + + tests: + build: + context: . + dockerfile: Dockerfile.test + env_file: + - .env diff --git a/notebooks/.gitkeep b/notebooks/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..34a04d9 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,71 @@ +[project] +name = "uv_app" +version = "0.1.2" +description = "" +readme = "README.md" +requires-python = ">=3.14.0, <3.15" +dependencies = [ + "click>=8.3.1", + "pydantic-settings>=2.12.0", + "python-dotenv>=1.2.1", +] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + + + +[tool.poe.tasks] +lint = "uv run ruff check . --fix" +format = "uv run ruff format ." +x = ["format", "lint"] + +[dependency-groups] +dev = [ + "poethepoet>=0.32.2", + "pytest>=8.3.3", + "ruff>=0.14.6", +] + +[tool.ruff] +exclude = [".venv"] + +[tool.ruff.lint] +select = ["ALL"] +ignore = [ + "ANN002", # Missing type annotation for args + "ANN003", # Missing type annotation for kwarg + "ERA001", # Commented out code + "S104", # Possible binding to all interfaces + "BLE001", # Do not catch Exception + "FBT", # Bools in arguments + "DTZ", # Datetime timezone + "EM", # f-strings in exception messages + "FIX", # Left out TODO, FIXME, etc. + "INT", # f-string in function execeution before calls + "G", # Logging linting + "TD", # Rules for TODO + "E501", # Line too long + "E722", # Do not use bare except + "W505", # Doc line too long + "D100", # Missing docstring + "D101", # Missing docstring + "D102", # Missing docstring + "D103", # Missing docstring + "D104", # Missing docstring + "D105", # Missing docstring + "D106", # Missing docstring + "D107", # Missing docstring +] + +[tool.ruff.lint.per-file-ignores] +"tests/**/*.py" = ["S101"] + +[tool.ruff.lint.pydocstyle] +convention = "google" + +[tool.ruff.lint.flake8-quotes] +docstring-quotes = "double" +inline-quotes = "double" +multiline-quotes = "double" diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/example_test.py b/tests/example_test.py new file mode 100644 index 0000000..721ce7f --- /dev/null +++ b/tests/example_test.py @@ -0,0 +1,15 @@ +import pytest + +from uv_app.example_util import addition + + +@pytest.mark.parametrize( + ("a", "b", "expected"), + [ + (2, 3, 5), + (1.5, 2.5, 4.0), + (-1, 1, 0), + ], +) +def test_addition_returns_sum(a: float, b: float, expected: float) -> None: + assert addition(a, b) == expected diff --git a/uv_app/__init__.py b/uv_app/__init__.py new file mode 100644 index 0000000..2600a9f --- /dev/null +++ b/uv_app/__init__.py @@ -0,0 +1,3 @@ +from uv_app import logging_config, settings + +__all__ = ["logging_config", "settings"] diff --git a/uv_app/__main__.py b/uv_app/__main__.py new file mode 100644 index 0000000..8033aec --- /dev/null +++ b/uv_app/__main__.py @@ -0,0 +1,16 @@ +import click + + +@click.group() +def cli() -> None: + """Application Command Line Interface.""" + + +@cli.command() +def main() -> None: + """An example command that prints a message.""" + click.echo("This is an example command from uv_app.") + + +if __name__ == "__main__": + cli() diff --git a/uv_app/core/__init__.py b/uv_app/core/__init__.py new file mode 100644 index 0000000..be48c79 --- /dev/null +++ b/uv_app/core/__init__.py @@ -0,0 +1 @@ +"""Core utilities for the application.""" diff --git a/uv_app/core/db.py b/uv_app/core/db.py new file mode 100644 index 0000000..f48b84d --- /dev/null +++ b/uv_app/core/db.py @@ -0,0 +1,31 @@ +import os +from typing import Optional + +import psycopg2 +from dotenv import load_dotenv + +# Force reload environment variables from .env, ignoring system vars. +load_dotenv(override=True) + +DB_HOST = os.getenv("DB_HOST") +DB_PORT = os.getenv("DB_PORT") +DB_NAME = os.getenv("DB_NAME") +DB_USER = os.getenv("DB_USER") +DB_PASSWORD = os.getenv("DB_PASSWORD") + + +def connect_to_db() -> Optional[psycopg2.extensions.connection]: + """Establish a connection to the PostgreSQL database.""" + try: + return psycopg2.connect( + host=DB_HOST, + port=DB_PORT, + database=DB_NAME, + user=DB_USER, + password=DB_PASSWORD, + # Set the client encoding to UTF-8. + options="-c client_encoding=utf8", + ) + except psycopg2.Error as exc: + print(f"Error connecting to PostgreSQL database: {exc}") + return None diff --git a/uv_app/example_util.py b/uv_app/example_util.py new file mode 100644 index 0000000..3e92ede --- /dev/null +++ b/uv_app/example_util.py @@ -0,0 +1,3 @@ +def addition(a: float, b: float) -> float | int: + """Returns the sum of two numbers.""" + return a + b diff --git a/uv_app/logging_config.py b/uv_app/logging_config.py new file mode 100644 index 0000000..afaa2e7 --- /dev/null +++ b/uv_app/logging_config.py @@ -0,0 +1,9 @@ +import logging + +LOGGER = logging.getLogger(__name__) + +logging.basicConfig( + format="%(asctime)s [%(levelname)s] - <%(name)s> - %(message)s", + level=logging.INFO, + handlers=[logging.StreamHandler()], +) diff --git a/uv_app/settings.py b/uv_app/settings.py new file mode 100644 index 0000000..becb10c --- /dev/null +++ b/uv_app/settings.py @@ -0,0 +1,18 @@ +from dotenv import find_dotenv +from pydantic_settings import BaseSettings, SettingsConfigDict + +DEFAULT_CONFIG_SETTINGS = SettingsConfigDict( + env_file=find_dotenv(), + env_file_encoding="utf-8", + env_ignore_empty=True, + extra="ignore", + validate_default=True, +) + + +class AppSettings(BaseSettings): + model_config = DEFAULT_CONFIG_SETTINGS + EXAMPLE_ENV_VARIABLE: str + + +app_settings = AppSettings()