Move to Python 3.13, bump Werkzeug to 3.1.8, and refresh/pin all dependencies - #22
Merged
Merged
Conversation
Bumps [werkzeug](https://github.com/pallets/werkzeug) from 2.2.2 to 3.1.6. - [Release notes](https://github.com/pallets/werkzeug/releases) - [Changelog](https://github.com/pallets/werkzeug/blob/main/CHANGES.rst) - [Commits](pallets/werkzeug@2.2.2...3.1.6) --- updated-dependencies: - dependency-name: werkzeug dependency-version: 3.1.6 dependency-type: direct:production ... Signed-off-by: dependabot[bot] <support@github.com>
dependabot
Bot
force-pushed
the
dependabot/pip/api/werkzeug-3.1.6
branch
from
August 20, 2026 11:10
1467ca1 to
88e1cae
Compare
The Dependabot change bumped Werkzeug 2.2.2 -> 3.1.6 in /api but left
Flask==2.2.2 in place. Flask 2.2.2 does `from werkzeug.urls import url_quote`
at module scope, and url_quote was removed in Werkzeug 3.0. Flask 2.2.2
declares `Werkzeug>=2.2.2` with no upper bound, so pip resolves it and the
image builds -- then gunicorn dies at startup with:
ImportError: cannot import name 'url_quote' from 'werkzeug.urls'
The only workflow triggers on pushes to prod-changes rather than on pull
requests, so this would not have been caught before deploy.
api:
Flask 2.2.2 -> 3.1.3, Werkzeug 3.1.6 -> 3.1.8, gunicorn now pinned, and
Azure SDK / requests / cryptography / pyjwt / flask-caching refreshed.
front_end:
Same Flask/Werkzeug bump (it was still entirely on 2.2.2 and shared the
latent break), plus Flask-WTF 1.2.1 -> 1.3.0.
task:
Previously unpinned deps are now pinned, and `azure.identity` is normalised
to `azure-identity`. azure-functions is held at 1.25.0 because 2.x requires
Python >=3.13 while the image is python3.11.
dev:
pytest 9.1.1 for both services, PyYAML 6.0.3.
Verified: 122 tests pass (44 api, 78 front_end); all three services build on
their real base images for linux/python3.11 and boot under gunicorn --
front_end returns 200 on /health and /favicon.ico (exercising
send_from_directory, the safe_join CVE surface), api returns a clean 503 on
/health without a database and 404s unknown routes.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
azure-functions 2.x requires Python >=3.13, which was the reason the task app was held back at azure-functions 1.25.0. Moving the runtime to 3.13 unblocks it. - api/Dockerfile, front_end/Dockerfile: python:3.11-slim -> python:3.13-slim - task/Dockerfile: azure-functions/python:4-python3.11 -> 4-python3.13 - Both workflows (app tests + prod deploy): python-version 3.11 -> 3.13 - task: azure-functions 1.25.0 -> 2.2.0 azure-functions 2.0 carries the same SDK content as 1.25.0b3, so there are no programming-model changes; the only requirements are Python >=3.13 and Functions runtime 4.x. The bicep already sets FUNCTIONS_EXTENSION_VERSION '~4' and the function app is container-deployed, so no infrastructure change is needed. Every other pin was re-resolved against Python 3.13 and was already at the newest version available, so no other requirement changed. Verified on Python 3.13.15: - 122 tests pass (44 api, 78 front_end), run the same way CI does -- a clean `pip install -r requirements.txt -r requirements-dev.txt` on a bare image with no system packages, confirming the cp313 wheels for pymssql and cryptography resolve. - All three images build on their real base images. - api and front_end boot under gunicorn: front_end returns 200 on /health and /favicon.ico, api binds and returns a clean 503 on /health without a database. - task function app loads and registers all 3 timer triggers. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
This was referenced Aug 20, 2026
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.
Why this changed shape
Dependabot bumped Werkzeug
2.2.2 → 3.1.6in/apibut leftFlask==2.2.2in place. As originally proposed, this PR would have taken the API down.Flask 2.2.2 does
from werkzeug.urls import url_quoteat module scope, andurl_quotewas removed in Werkzeug 3.0. Flask 2.2.2 declaresWerkzeug>=2.2.2with no upper bound, so pip resolves the combination happily and the image builds fine — then gunicorn dies on boot:The bump is still worth doing — Werkzeug 2.2.2 carries debugger-PIN RCE, multipart DoS, cookie-parsing, and Windows
safe_joinCVEs. It just needed Flask carried along with it. Scope was then widened to refresh and pin everything, and to move all services to Python 3.13.Python 3.13
api/Dockerfilepython:3.11-slimpython:3.13-slimfront_end/Dockerfilepython:3.11-slimpython:3.13-slimtask/Dockerfileazure-functions/python:4-python3.114-python3.13front-end-tests.yml(both jobs)3.113.13prod-changes_byavdlinuxbrokerapi.yml3.113.13This unblocks
azure-functions1.25.0 → 2.2.0, which requires Python>=3.13— the sole reason the task app was previously held back. azure-functions 2.0 ships the same SDK content as 1.25.0b3, so there are no programming-model changes; it only requires Python ≥3.13 and Functions runtime 4.x. The bicep already setsFUNCTIONS_EXTENSION_VERSION: '~4'and the function app is container-deployed, so no infrastructure change was needed.Dependency changes
api/requirements.txtfront_end/requirements.txt— was still entirely on Flask/Werkzeug 2.2.2 and shared the same latent break. Flask → 3.1.3, Werkzeug → 3.1.8, gunicorn pinned 26.0.0, azure-monitor-opentelemetry → 1.8.9, requests → 2.34.2, Flask-WTF 1.2.1 → 1.3.0.task/requirements.txt— everything was unpinned. Nowazure-functions==2.2.0,requests==2.34.2,azure-identity==1.25.3, withazure.identitynormalised toazure-identity.Dev —
pytest==9.1.1for both services,PyYAML==6.0.3.Every pin was re-resolved against Python 3.13. Versions reflect what is currently retrievable from the internal package feed, which is intentional.
Verification
All on Python 3.13.15:
api, 78front_end— executed the same way CI does: a cleanpip install -r requirements.txt -r requirements-dev.txton a bare image with no system packages, confirming the cp313 wheels forpymssqlandcryptographyresolve without build tooling.front_endboots under gunicorn →/health200,/favicon.ico200 (exercisessend_from_directory, thesafe_joinCVE surface)apiboots under gunicorn → binds,/healthreturns a clean 503 without a database, unknown routes 404taskfunction app loads and registers all 3 timer triggersCompatibility notes
No removed Flask 2.x APIs (
before_first_request,JSONEncoder,_app_ctx_stack) are used anywhere. Werkzeug 3.1's new 500 kB form-field cap is a non-issue — the api is JSON-only and front_end's form fields are small scalars with no uploads. Theazure-mgmt-compute33 → 38 jump was checked directly:virtual_machines.begin_startandbegin_power_offare both still present.Follow-up worth filing
front_end/function_authentication.pyusesdatetime.utcnow().timestamp(), which now emits aDeprecationWarningon 3.13 and is scheduled for removal. It is also subtly incorrect —utcnow()returns a naive datetime and.timestamp()interprets naive values as local time, so it only produces the right epoch on a UTC host. Harmless in these containers, but worth fixing outside a dependency PR since it touches token-expiry logic.