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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,15 +88,15 @@ from each adapter's own `ADAPTER_TIER` (`adapters/*/adapter.env`) — see

| Tier | Adapters | CI runs it | Guarantee |
| --- | --- | --- | --- |
| A | `nextjs`, `nestjs`, `laravel-api` | every pull request, and nightly | stays green through every dependency bump |
| A | `nextjs`, `nestjs`, `laravel-api`, `flask` | every pull request, and nightly | stays green through every dependency bump |
| B | `laravel-inertia` | when `adapters/laravel-inertia/**` changes, and weekly | verified regularly, not on every push — a full generation measures ~5 minutes per test |
| C | none currently | not automatically verified | may rot; no guarantee at all |

## Services

A database or cache is a directory under `services/`, not an adapter — see
[ADR-0019](docs/decisions/0019-services-are-not-adapters.md). Each ships a
driver per adapter family (`laravel`, `nest`, `next`); `scaffold lint`
driver per adapter family (`laravel`, `nest`, `next`, `flask`); `scaffold lint`
requires the full matrix before an adapter in a new family can merge.

| Slot | Services | Default |
Expand Down
9 changes: 9 additions & 0 deletions adapters/flask/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.venv
__pycache__
*.pyc
.pytest_cache
.mypy_cache
.ruff_cache
.env
.env.*
.git
3 changes: 3 additions & 0 deletions adapters/flask/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# off by default: Flask's debug console executes arbitrary code from the browser
FLASK_DEBUG=0
# the database variables are written by the selected service's driver
1 change: 1 addition & 0 deletions adapters/flask/.python-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.13
23 changes: 23 additions & 0 deletions adapters/flask/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# syntax=docker/dockerfile:1

# python3.13-bookworm-slim is not published for this uv release — only
# python3.13-trixie-slim is; python:3.13-slim below already resolves to
# slim-trixie, so this keeps both stages on the same Debian release.
FROM ghcr.io/astral-sh/uv:0.12.13-python3.13-trixie-slim@sha256:c0ba49559fc5622531fd05a5747b52afb49ffa883574bbf8eb719ebd103efb84 AS deps
WORKDIR /app
COPY pyproject.toml uv.lock .python-version ./
RUN uv sync --locked --no-dev --no-install-project

FROM python:3.13-slim@sha256:9d2e5553305c7c7b0097999bb17187c69b921ccd6bc9d40e4bb5ebe652c00285 AS runtime
# @SERVICE_SETUP@
WORKDIR /app
COPY --from=deps /app/.venv ./.venv
COPY . .
ENV PATH="/app/.venv/bin:${PATH}"
RUN useradd --create-home --uid 10001 app
USER app
EXPOSE 8080
# python, not wget or curl: the slim image ships neither.
HEALTHCHECK --interval=30s --timeout=3s \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8080/health/live')" || exit 1
CMD ["gunicorn", "--bind", "0.0.0.0:8080", "app:create_app()"]
22 changes: 22 additions & 0 deletions adapters/flask/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# API

A Flask application. It implements this project's task contract, so every
check runs the same way here as in any other config root:

```sh
mise run //<this-root>:ci-unit # install, format, lint, check, test
```

`mise.toml` in this directory is the whole story of how those tasks are wired.

## conftest.py is intentionally empty

It exists only so pytest's default `prepend` import mode adds this directory
to `sys.path`; without it, `from app import create_app` in the tests raises
`ModuleNotFoundError`. There is nothing to configure, so the file is empty.

## Why python is pinned in .python-version, not mise.toml

uv resolves its own managed interpreter and reads `.python-version` for the
pin; a `python` tool entry in `mise.toml` would install a second interpreter
that uv never uses.
15 changes: 15 additions & 0 deletions adapters/flask/adapter.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
ADAPTER_NAME="flask"
ADAPTER_ROLE="api"
ADAPTER_TIER="A"
ADAPTER_LANGUAGE="python"
ADAPTER_FAMILY="flask"
# --bare writes pyproject.toml and nothing else; the default template adds a
# .git directory inside the app, a src/<dir-name> package and a README the
# overlay would have to delete. uv comes through `mise x` because the generator
# runs in the project root, whose mise.toml pins node and pnpm only.
ADAPTER_GENERATOR='mise x uv@0.12.13 -- uv init --bare --vcs none --author-from none --no-workspace --python 3.13 "$APP_DIR"'
# The grep pair turns a resolution that reported success but wrote nothing into
# a build failure instead of an ImportError at container start.
ADAPTER_POST_GENERATE='uv add flask gunicorn && uv add --dev ruff mypy pytest && { grep -q "flask" pyproject.toml && grep -q "gunicorn" pyproject.toml; } || { echo "post-generate: flask or gunicorn missing from pyproject.toml after uv add" >&2; exit 1; }'
ADAPTER_LIVENESS_PATH="/health/live"
ADAPTER_READINESS_PATH="/health/ready"
9 changes: 9 additions & 0 deletions adapters/flask/app/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from flask import Flask

from .health import health


def create_app() -> Flask:
app = Flask(__name__)
app.register_blueprint(health)
return app
21 changes: 21 additions & 0 deletions adapters/flask/app/health.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from flask import Blueprint, Response, jsonify

# @DB_ENGINE@

health = Blueprint("health", __name__)

Reply = Response | tuple[Response, int]


@health.get("/health/live")
def live() -> Reply:
return jsonify(status="ok")


@health.get("/health/ready")
def ready() -> Reply:
try:
# @DB_PROBE@
raise RuntimeError("no database is configured for this project")
except Exception as error: # noqa: BLE001
return jsonify(status="unavailable", reason=str(error)), 503
Empty file added adapters/flask/conftest.py
Empty file.
7 changes: 7 additions & 0 deletions adapters/flask/lefthook.fragment.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
pre-commit:
commands:
ruff:
glob: "*.py"
root: "@APP_ROOT@"
run: uv run ruff format {staged_files}
stage_fixed: true
45 changes: 45 additions & 0 deletions adapters/flask/mise.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# uv alone: uv resolves its own managed interpreter, so a `python` entry here
# would be installed and then ignored. .python-version is the pin uv reads.
[tools]
uv = "0.12.13"

[tasks.install]
run = "uv sync --locked"

[tasks.format]
run = "uv run ruff format --check ."

[tasks."format-fix"]
run = "uv run ruff format ."

[tasks.lint]
run = "uv run ruff check ."

[tasks.check]
run = "uv run mypy --strict app tests"

[tasks.test]
run = "uv run pytest -q"

[tasks.build]
# The production sync drops ruff, which the pre-commit hook calls directly, so
# the second sync puts it back; `status=$?` keeps the production exit code.
run = "uv sync --locked --no-dev; status=$?; uv sync --locked --quiet; exit $status"

[tasks.migrate]
# alembic.ini exists only for postgres/mysql, app/migrate.py only for mongodb;
# --db none or a cache-only project ships neither, so this must not assume one
# of the two is always there.
run = "if [ -f alembic.ini ]; then uv run alembic upgrade head; elif [ -f app/migrate.py ]; then uv run python -m app.migrate; else echo 'nothing to migrate'; fi"

[tasks.ci-unit]
run = [
{ task = ":install" },
{ task = ":format" },
{ task = ":lint" },
{ task = ":check" },
{ task = ":test" },
]

[tasks.checklist]
run = [{ task = ":ci-unit" }, { task = ":build" }]
12 changes: 12 additions & 0 deletions adapters/flask/tests/test_health.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from app import create_app


def test_live_reports_ok() -> None:
response = create_app().test_client().get("/health/live")
assert response.status_code == 200
assert response.get_json() == {"status": "ok"}


def test_ready_route_is_registered() -> None:
rules = create_app().url_map.iter_rules()
assert any(rule.rule == "/health/ready" for rule in rules)
2 changes: 2 additions & 0 deletions docs/PROVENANCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ Excluded, and why:
| `common/.dockerignore` | `.dockerignore` | adapted | same excludes as the per-adapter `.dockerignore` files (`node_modules`, build output, `.env*`, `.git`), only read when the build context is the workspace root; immich's own root `.dockerignore` excludes the same categories (`**/node_modules/`, `**/dist/`, `.env*`, `.git/`) for the same reason. |
| `adapters/*/Dockerfile`'s base image, `USER`/`EXPOSE`/`HEALTHCHECK` shape, `adapters/*/.env.example`, `adapters/*/adapter.env`, `adapters/*/lefthook.fragment.yml`, `adapters/{nestjs,nextjs}/.prettierignore` | — | original | per-stack overlay files invoking each framework's own generator (ADR-0003), not vendored from immich's own `Dockerfile`/`docker/example.env`/`.prettierignore` — checked against all three, no meaningful resemblance; the `.prettierignore` pair shares only one coincidental line (`pnpm-lock.yaml`) with immich's much larger ignore lists. The two laravel Dockerfiles build with composer, not pnpm, and are untouched by the row above. |
| `scaffold`, `lib/*.sh` | — | original | immich has no scaffolding tool. |
| `adapters/flask/mise.toml` | `machine-learning/mise.toml` | adapted | the ADR-0011 task vocabulary and the uv shape (`uv sync --locked`, `uv run ruff`, `uv run mypy --strict`, `uv run pytest`) come from immich's only Python service; the tools differ — uv is pinned here and python is not, because uv resolves its own interpreter. |
| `adapters/flask/` (every other file), `services/{mysql,postgres,mongodb,redis}/drivers/flask.sh`, `services/shared/flask.sh` | — | original | immich runs FastAPI, has no Flask application, no application factory and no service-driver mechanism at all, so none of these has an upstream file to adapt — checked by name and content against `machine-learning/`, including `grep -r flask`, which surfaces the name only as a transitive `locust` dev-dependency in `uv.lock`, never imported by immich's own code. One exception: `adapters/flask/.python-version` is byte-identical to `machine-learning/.python-version` (`3.13`) — both independently pin the current stable interpreter; coincidence, not a copy, and not the same relationship the `mise.toml` row above describes. |

## Out of scope, checked and rejected as rows

Expand Down
Loading