From 580d27028bb33e6345d786712af79e4edfeb22e6 Mon Sep 17 00:00:00 2001 From: alzyras Date: Fri, 30 Jan 2026 13:17:42 +0200 Subject: [PATCH] Initial commit --- .dockerignore | 12 +++++++ .env.tmpl | 1 + .gitignore | 40 ++++++++++++++++++++++ AGENTS.md | 38 +++++++++++++++++++++ Dockerfile.main | 24 ++++++++++++++ Dockerfile.test | 24 ++++++++++++++ README.md | 15 +++++++++ compose.yml | 14 ++++++++ notebooks/.gitkeep | 0 pyproject.toml | 71 ++++++++++++++++++++++++++++++++++++++++ tests/__init__.py | 0 tests/example_test.py | 15 +++++++++ uv_app/__init__.py | 3 ++ uv_app/__main__.py | 16 +++++++++ uv_app/core/__init__.py | 1 + uv_app/core/db.py | 31 ++++++++++++++++++ uv_app/example_util.py | 3 ++ uv_app/logging_config.py | 9 +++++ uv_app/settings.py | 18 ++++++++++ 19 files changed, 335 insertions(+) create mode 100644 .dockerignore create mode 100644 .env.tmpl create mode 100644 .gitignore create mode 100644 AGENTS.md create mode 100644 Dockerfile.main create mode 100644 Dockerfile.test create mode 100644 README.md create mode 100644 compose.yml create mode 100644 notebooks/.gitkeep create mode 100644 pyproject.toml create mode 100644 tests/__init__.py create mode 100644 tests/example_test.py create mode 100644 uv_app/__init__.py create mode 100644 uv_app/__main__.py create mode 100644 uv_app/core/__init__.py create mode 100644 uv_app/core/db.py create mode 100644 uv_app/example_util.py create mode 100644 uv_app/logging_config.py create mode 100644 uv_app/settings.py 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()