feat: add a flask adapter - #20
Merged
Merged
Conversation
added 16 commits
September 13, 2026 22:36
alembic for postgres/mysql, a app/migrate.py index-ensure step for mongodb — common/install.sh refuses to start a project with a database service and no migrate service, so an empty migrate command was never a valid shape.
sed exits 0 whether or not it matched, so only the sqlalchemy.url splice was guarded; the import splice could silently no-op and leave a NameError behind.
flask (2m9s) is faster than laravel-api (9m28s), so ADAPTER_TIER stays A.
…ext already was splice_flask_probe's guard greps for jsonify(status="ok"), which live() already contains before any splice runs, so the check passes even if the raise substitution silently fails to fire. Grep for the removed sentinel instead. Also escapes a raw % in DATABASE_URL before alembic's set_main_option, whose ConfigParser backing applies pyformat interpolation on read.
Every other tier A smoke suite already asserts the generated app passes its own ci-unit; flask's whole promise is staying green through dependency bumps and had no such test. Also asserts the generated app is wired to its database — the DATABASE_URL the mysql driver writes, the spliced health probe with the unconfigured sentinel gone, and no leftover @SERVICE_SETUP@ anchor — the test that would have caught the vacuous splice guard.
… runs uv resolved the deps stage's interpreter against requires-python alone, withholding the same .python-version pin the rest of the adapter's story depends on; it could pick a managed interpreter the runtime image's system python does not match. chown -R app:app /app also made the source tree and venv writable by the process that runs them for no reason this adapter has — nothing under /app is written at runtime, so narrow it to none rather than laravel-api's targeted chown of the paths that actually need it.
… vars mise.toml's migrate task assumed alembic.ini or app/migrate.py was always present and died with ModuleNotFoundError when neither is — legitimate for --db none or a cache-only project. Give it a third arm that says there is nothing to migrate and exits 0. .env.example also lacked the note laravel-api, laravel-inertia and nestjs all carry, that the database variables come from the selected service's driver.
It's consumed as a single uv add argument — "$FLASK_PACKAGES" would pass "a b" as one bogus package name the day a service needs two. Matches the sibling LARAVEL_PACKAGE and the header comment, which was already singular.
flask is a fourth adapter family alongside laravel, nest and next; the services section's driver-per-family sentence had not been updated to say so.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this changes
scaffold new demo --api flasknow produces a Python project. It is the fifthadapter and the first Python one, so it brings a new
ADAPTER_FAMILYand, withit, a driver in every service.
The adapter (
adapters/flask/) is generated byuv init --bare, whichwrites one file —
pyproject.toml— and leaves everything else to the overlay,the way every other adapter works. The default
uv inittemplate was notusable: it writes a
.gitdirectory inside the application, asrc/<dir-name>package keyed to the directory it lands in, a
README.mdand a[project.scripts]entry, four of which the overlay would have had to delete.The application is Flask's own documented shape — an application factory and one
blueprint — with gunicorn in front of it, and
install,format,format-fix,lint,check,test,build,ci-unitandchecklistimplemented throughuv, ruff, mypy
--strictand pytest. The toolchain comes fromimmich/machine-learning/mise.toml, immich's only Python service, which alreadywrites the ADR-0011 vocabulary. The framework does not: immich runs FastAPI.
Python is pinned by
.python-version, not bymise.toml. uv resolves itsown managed interpreter, so a
pythonentry in the application'smise.tomlwould be installed and then ignored — two pins, one of them a lie.
.python-versionis the file uv actually reads, and the application'smise.tomlpinsuvalone. This mirrorsadapters/laravel-api/mise.toml,which pins composer alone and takes php from outside (ADR-0016), and it keeps
the root
mise.tomlunaware the project contains Python. No CI step and noambient installation is needed:
ADAPTER_GENERATORreaches uv throughmise x uv@0.12.13, so nothing had to be added to.github/workflows/.Four drivers, not three. The intent was to ship the SQL pair plus redis and
add mongodb later, but
lint_services(lib/lint.sh:207) collects every familyany adapter declares and fails a service that has no driver for one of them.
Declaring a combination unsupported would have been new mechanism; the pymongo
driver was less code. The split copies
laravelexactly: a shared SQLAlchemybody for postgres and mysql, mongodb and redis self-contained.
The plan was wrong about migrations, and this fixes it forward. Both said
service_driver_compose_migrateprints nothing, on the reasoning that thisadapter ships no models.
common/install.sh'srun_migrations— the installerthat ships to every generated project — refuses to start a stack that has a
database service and no migrate service ("refusing to start with unapplied
schema"), and
scripts/deploy-check.sh:298enforces the same invariant underADR-0021. So a flask project with a database would not have started at all, and
a client would have had to add migrations by hand on day one, which no other
adapter requires. Neither gate was weakened: postgres and mysql get alembic, and
mongodb ships
app/migrate.pyand runs it — the same honesty as nest+mongodb'sprisma db push --skip-generateagainst an empty schema, plus a named seam for aclient's own indexes.
ADAPTER_TIERisA, measured rather than assumed, per ADR-0012:tests/new-flask.batsruns in 3m47s againsttests/new-laravel-api.bats's8m58s.
How it was verified
Structural checks, run here rather than taken on report:
mise run lintand./scaffold lint— clean.scaffold lintis the one thatproves all four drivers define the four required functions, including the
mongodb driver, which inherits two of them from the shared body it sources.
mise run test-runner— both lanes underCI=true GITHUB_ACTIONS=true MISE_YES=1, the environment a runner actually has.bats tests/new-flask.bats— 8 tests. Two of them were added after review:the generated application running its own
ci-unit, which every sibling suitehas and this one did not, and an assertion that the api is wired to the
project's database. The second is what would have caught the splice guard
below.
./scripts/deploy-check.sh flaskagainst postgres, mysql and mongodb — eachbuilds the image, starts the released stack, applies migrations, and gets 200
from both
/health/liveand/health/ready.ci-unit— ruff, mypy
--strictand pytest against the files the drivers spliced, notonly against the files the adapter ships.
Four defects the review found and this branch fixes, recorded because each is
the kind that passes every check until it reaches a client:
splice_flask_probe's verificationgrepcould never fail — it looked forreturn jsonify(status="ok"), which thelive()handler already containsbefore any splice runs. It now proves the unconfigured sentinel is gone. A
drifted
raiseline would otherwise have shipped a readiness probe that waspermanently 503, silently.
services/shared/nest.sh:96has the identical holefor the identical reason and is deliberately untouched here.
%in a generatedDB_PASSWORDbrokealembic upgrade head:set_main_optionwrites throughConfigParser, which applies pyformatinterpolation on read. Escaped.
.python-version, so the one placethe interpreter pin was withheld was the image build.
chown -R app:app /appmade the application's own source and venv writable bythe process running it. Removed — nothing under
/appis written at runtime.Checklist
mise run lintpassesmise run test-runnerpasses