# Migrate a Flask Project to uv

Yes. An agent moves your Flask project from requirements.txt to a pyproject.toml and uv.lock, updates the Dockerfile and proves the tests pass with uv run.

[Run this on Rush](https://cloud.getrush.ai/run/migrate-to-uv/flask/), in a Project that has your repository.

## What the agent does

The agent clones the repository into its own sandbox and works out which packages the project actually asks for, because a `requirements.txt` is usually a `pip freeze` that mixes direct dependencies with everything they pulled in. In a Flask app like miguelgrinberg/microblog, `requirements.txt` pins 56 packages at exact versions, from `Flask==3.0.0`, `Flask-SQLAlchemy==3.1.1` and `SQLAlchemy==2.0.23` down to `greenlet`, `MarkupSafe` and `urllib3`. The agent reads the imports in `app/` and `microblog.py` (`flask_babel`, `flask_login`, `flask_mail`, `flask_migrate`, `flask_moment`, `flask_wtf`, `elasticsearch`, `langdetect`, `redis`, `rq`, `jwt`, `requests`) and the places that need a package without importing it: `Email()` in `app/auth/forms.py` needs `email-validator`, `app/cli.py` shells out to `pybabel` from `Babel`, `config.py` accepts a Postgres `DATABASE_URL` that `psycopg2-binary` serves, and `boot.sh` and the `Procfile` run `gunicorn` and `rq worker`.

From that it writes `pyproject.toml` with only the direct dependencies under `[project] dependencies`, each kept at the version the old file pinned so the migration changes the tool and not the libraries. Packages that only a developer uses go in a `dev` dependency group: `httpie` and `aiosmtpd` (the local mail server the tutorial runs) are in `requirements.txt` but nothing in `app/` imports them. Then `uv lock` resolves the rest, and the agent compares the locked versions of the transitive packages with the old pins and lists any that moved. It sets `requires-python` from what the code and its pins can run on, and adds a `.python-version` so `uv` and the image agree.

Then the files that installed with pip. The `Dockerfile` today starts `FROM python:slim`, runs `pip install -r requirements.txt` and then `pip install gunicorn pymysql cryptography` on top, so the image carries two packages, `pymysql` and `cryptography`, that the requirements never mention, for a MySQL `DATABASE_URL`. The agent moves those into `pyproject.toml` (or an optional extra, and says which), copies `uv` into the image, runs `uv sync --locked --no-dev` before copying the app so the dependency layer caches, and puts `.venv/bin` on `PATH` so `boot.sh`'s `flask db upgrade` and `gunicorn` and the build-time `flask translate compile` still find their commands. `deployment/supervisor/microblog.conf` and `microblog-tasks.conf` call `/home/ubuntu/microblog/venv/bin/gunicorn` and `.../venv/bin/rq`; `uv` creates `.venv` by default, so the agent updates those paths or sets `UV_PROJECT_ENVIRONMENT` and explains the choice.

Last, it proves it. The repository has no CI workflow, so the agent says that rather than inventing one, and runs the checks itself: `uv sync`, `uv run python -m unittest tests.py` (the four tests in `UserModelCase` run against an in-memory SQLite database from `TestConfig`), `uv run flask db upgrade` on a fresh database, and `docker build` followed by a container start that reaches gunicorn. It deletes `requirements.txt` only once all of that passes, or keeps a generated one with `uv export` if the README or a deploy script still reads it.

## Start it with one API call

`POST https://api.prix.dev/o/{org}/sessions`

```json
{
  "agent": "claude",
  "input": "Migrate this Flask project from pip and requirements.txt to uv. First find the direct dependencies: read the imports, the CLI commands the code shells out to, the WSGI server and worker the Procfile and boot scripts run, and any package the Dockerfile installs outside requirements.txt. Write a pyproject.toml with only those under dependencies, at the versions requirements.txt pins today, and developer-only tools in a dev dependency group. Run uv lock and list any transitive package whose version changed from the old pins. Add .python-version, update the Dockerfile to install with uv sync --locked --no-dev in a cached layer, and update any CI workflow, Procfile, boot script or process manager config that calls pip or a venv path. Run the tests with uv run, run the database migrations on a fresh database, build the image and start it. Remove requirements.txt only if nothing still reads it. Open a pull request with the dependency list and why each is direct, the version changes, and the test, migration and docker output.",
  "resources": [
    {
      "url": "https://github.com/your-org/your-app"
    }
  ]
}
```

1. **Get an API key.** In the console, open Settings, then API keys, and create a key for your org. Send it in the Authorization header as a Bearer token.
2. **Send the request.** POST the body in request.json to https://api.prix.dev/o/{org}/sessions, with your repository as the resources URL. The run starts in its own cloud sandbox.
3. **Read what came back.** Follow the run on its live stream or wait for the run.completed webhook, then read the result and its trajectory.

## What comes back

What comes back is a pull request on its own branch with a new `pyproject.toml`, `uv.lock` and `.python-version`, an updated `Dockerfile`, the venv paths fixed in `deployment/supervisor/`, and `requirements.txt` removed or regenerated. The description lists each direct dependency with the reason it is direct (the import, the command or the config that needs it), the developer-only packages moved to the `dev` group, the packages the Dockerfile used to install on the side, and every transitive package whose locked version differs from the old pin.

The proof is the transcript, which keeps every command with its output: `uv lock`, `uv sync`, the `unittest` run, `flask db upgrade` against a new database, the `docker build` and the container's first log lines showing gunicorn listening. If your repository runs CI on pull requests, those checks run against the new setup too; microblog has none, so the agent's own runs are the record.

Before merging, a reviewer checks that no package the app needs at runtime landed in the `dev` group, that the Dockerfile copies `uv.lock` and installs with `--locked` so the image matches the lock, and that every place that starts the app (the `Procfile`, `boot.sh`, supervisor, a deploy script) points at the new environment.

## Run it on every Python repository still on requirements.txt

A team moving all its Python services to uv can start one run per repository: a `POST /o/{org}/sessions` with the repository in `resources` and this prompt as `input`, with an `Idempotency-Key` per repository so a retried request returns the run already started.

Up to the key's `max_concurrent` sessions run at once (4 by default), each in its own sandbox with its own Python and its own Docker. A webhook on the API key posts `run.completed` or `run.failed` with the `session_url` when each turn finishes, so a script can collect the pull requests as they arrive.

## Why run it on Rush?

The risky part of a uv migration is not writing `pyproject.toml`, it is proving nothing else changed: the same versions, the same commands on `PATH` in the image, the same entry points for the web process and the worker. That takes a clean environment, a fresh database for the migrations and a Docker build, and a laptop that already has the old virtualenv activated hides exactly the mistakes you want to catch. A Rush sandbox starts with none of that installed.

The pull request carries the evidence a reviewer needs: why each package is direct, which transitive versions moved, and the output of the tests, the migration and the container start, each kept in the transcript with the command that produced it. If a reviewer wants `pymysql` as an optional extra instead of a dependency, one follow-up message to the same session changes it and runs everything again.

## Frequently asked questions

### Will it upgrade my dependencies?

No. The prompt keeps every direct dependency at the version `requirements.txt` pins today, so the migration changes how packages are installed, not which ones. Transitive packages are resolved by `uv lock`, and any whose version differs from the old pin is listed in the pull request.

### How does it tell direct dependencies from transitive ones?

It reads what the code imports, what it runs as a command (`pybabel`, `gunicorn`, `rq`), and what the config needs without importing, such as a database driver for `DATABASE_URL` or `email-validator` for a WTForms `Email()` field. Everything else in the freeze is left to the lock.

### What happens to my Dockerfile?

It installs `uv`, copies `pyproject.toml` and `uv.lock` first, runs `uv sync --locked --no-dev` so that layer is cached between code changes, then copies the app. The virtualenv's `bin` goes on `PATH`, so the existing entry point and boot script keep working unchanged.

### Can I keep a requirements.txt for a platform that needs one?

Yes. If the README, a deploy script or a hosting platform still reads `requirements.txt`, the agent generates it from the lock with `uv export` and says which file depends on it, so the two cannot drift.
