What the agent does
In a Flask app like miguelgrinberg/microblog, the agent first looks for where the Python version is set and finds almost nothing to edit. There is no pyproject.toml, no .python-version and no CI workflow. The Dockerfile starts FROM python:slim, a moving tag that already means whatever Python is newest, and the Vagrantfile and Procfile name no version. So the pull request pins the image to a 3.14 slim tag, adds a version file the team can read, and says plainly that CI did not exist before rather than claiming to update it.
The real work is requirements.txt: 54 exact pins from late 2023, including Flask==3.0.0, Werkzeug==3.0.1, SQLAlchemy==2.0.23, Flask-SQLAlchemy==3.1.1, alembic==1.12.1 and setuptools==68.2.2. Several of them ship compiled code, such as greenlet==3.0.1, psycopg2-binary==2.9.9, MarkupSafe==2.1.3 and multidict==6.0.4, and old releases of compiled packages usually have no wheel for a newer Python. The agent runs pip install -r requirements.txt on 3.14, reads which packages fall back to building from source or fail, and moves only those pins to the first release that installs, so the diff stays reviewable.
Then it runs the tests with warnings as errors: python -W error -m unittest tests, since tests.py is a unittest module with four test methods on UserModelCase and an in-memory sqlite:// database set in its TestConfig. The models already use timezone-aware datetime.now(timezone.utc), so the agent checks the comparisons that mix aware and naive values, like the datetime(1900, 1, 1) default in User.unread_message_count, rather than assuming they are fine. Each warning is fixed in the code that raised it, or listed as coming from a third-party package.
Last, it builds the image. The Dockerfile runs pip install for requirements.txt, then pip install gunicorn pymysql cryptography with no pins, then flask translate compile, and boot.sh runs flask db upgrade before starting gunicorn. The agent builds it on the 3.14 base, runs boot.sh against the default SQLite database to see the migrations apply and the server start, and suggests pinning those three unpinned packages so the next build matches this one.
Start it with one API call
POST https://api.prix.dev/o/{org}/sessions with this body:
{
"agent": "claude",
"input": "Upgrade this Flask project to Python 3.14. Find every place the version is or should be pinned (Dockerfile, pyproject, .python-version, CI) and pin it to 3.14. Install requirements.txt on 3.14 and bump only the packages that have no 3.14 wheel or fail to install, to the first release that works. Run the tests with python -W error -m unittest and fix each failure or deprecation without weakening a test. Build the Docker image, run boot.sh against SQLite and confirm migrations apply and gunicorn starts. Open a pull request listing each pin change with its reason, each fix with the warning it removed, and anything you could not verify.",
"resources": [
{
"url": "https://github.com/your-org/your-app"
}
]
}- 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.
- 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.
- 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
The pull request changes the Dockerfile's FROM line, requirements.txt for the pins that had to move, any application file where a warning was fixed, and adds a version file. Its description has one row per moved package (old pin, new pin, the pip error or missing wheel that forced it), one row per fixed warning, and a short note that this repository had no CI before, with an offer to add a workflow in a separate change.
A reviewer reads the moved pins first. SQLAlchemy, Flask-SQLAlchemy and alembic touch the database layer and migrations, so their bumps deserve a look at the changelogs even when the four unit tests pass. They also check that the image builds on the same tag they deploy and that boot.sh reached gunicorn in the transcript, which GET /o/{org}/sessions/{id}/messages returns with every command and its output.
Run it on every Python release
Run one upgrade per Flask service when a new Python minor version ships. A scheduled job reads your list of repositories and sends POST /o/{org}/sessions with each repository in resources and the target version in the prompt, so every service gets its own pull request instead of one large change.
A webhook on the key tells your endpoint when each run finishes, with the session_url to read the result. Runs that stop on a package with no wheel yet report it, and a follow-up message to the same session later picks up from the sandbox it left parked.
The use case this job follows, with its automation and trigger: Upgrade dependencies weekly.
Why run it on Rush?
This kind of upgrade is mostly waiting on pip: reinstalling dozens of pinned packages, watching which ones try to compile, bumping one, trying again. In a Rush sandbox the agent does those cycles on a fresh Python 3.14 without touching the interpreter your other projects use, and every pip, unittest and docker build command is kept with its output so the pull request can point at the exact error behind each bump.
Old Flask apps tend to share the same aging pins, so the same fix repeats across repositories. One run per repository, in parallel up to the key's concurrency limit, gives each its own pull request and its own record, and the ones that cannot finish yet name the package that blocks them.
Frequently asked questions
My Dockerfile uses python:slim with no version. Is that a problem?
It means every rebuild can change your Python without a commit. The agent pins the image to a 3.14 slim tag so the version is chosen in the repository and visible in review, and the description explains the change.
Will it upgrade all 54 packages to their latest versions?
No. It moves only the pins that fail to install or have no wheel on 3.14, and lists why for each one. A general refresh of every dependency is a separate, larger change that is easier to review on its own.
The app has only four unit tests. How do I know it really works?
The agent also builds the Docker image, runs flask db upgrade and starts gunicorn through boot.sh, so a broken migration or import shows up even where tests.py has no coverage. Anything it could not run, such as Elasticsearch search or the Redis task queue, is listed as not verified.
Can it add a CI workflow too?
Ask for it in the prompt and it will add a workflow that installs on 3.14 and runs the tests. By default it notes the missing CI instead, since adding a workflow is a separate decision from changing the Python version.