Skip to content

Commit decc0b8

Browse files
committed
build(python): upgrade to Flask 3.x (reanahub#678)
Upgrade Flask from `<2.3.0` to `>=3.0.0` and Werkzeug from `<2.3.0` to `>=3.0.0`, in line with the Invenio package upgrade in reana-server. Bump marshmallow from `<3.0.0` to `>=3.5.0,<4.0.0` and webargs to `>=8.0`, since the new Invenio stack requires marshmallow 3 and the corresponding webargs 8. Adapt the REST endpoints to the new marshmallow 3 and webargs 8 APIs. Replace the deprecated `missing=` keyword on schema fields with `load_default=`. Add `unknown=marshmallow.EXCLUDE` to all `@use_kwargs` and `@use_args` decorators that parse query parameters, so that REANA-specific extras such as `access_token` are silently ignored, matching the pre-upgrade webargs 5 behaviour. Replace all remaining `Model.query` call sites in the REST handlers, the workflow run manager, and the test suite with explicit `Session.query(Model)` queries, since SQLAlchemy 2.x no longer supports the `Base.query` shortcut. Replace the deprecated `FLASK_ENV` setting with `FLASK_DEBUG`, both in the runtime checks of the workflow run manager and in the `DEBUG_ENV_VARS` injected into spawned workflow engine containers. Update the test configuration to use `DEBUG` instead of `FLASK_ENV`. Remove pinned `reana-commons`, `reana-db`, and `kubernetes` entries from `requirements.txt` so that `pip install .` resolves them freshly from `setup.py` against the locally-mounted `modules/reana-db` and `modules/reana-commons` shared sources during development, avoiding version conflicts with PyPI versions that still carry the old pins. Closes reanahub/reana-server#770
1 parent dc8bb22 commit decc0b8

13 files changed

Lines changed: 59 additions & 62 deletions

reana_workflow_controller/config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,7 @@ def compose_reana_url(hostname: str, hostport: int) -> str:
227227
"value": os.getenv("WDB_SOCKET_SERVER", f"{REANA_COMPONENT_PREFIX}-wdb"),
228228
},
229229
{"name": "WDB_NO_BROWSER_AUTO_OPEN", "value": "True"},
230-
{"name": "FLASK_ENV", "value": "development"},
230+
{"name": "FLASK_DEBUG", "value": "1"},
231231
)
232232
"""Common to all workflow engines environment variables for debug mode."""
233233

reana_workflow_controller/rest/utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -266,7 +266,7 @@ def delete_workflow(workflow, all_runs=False, workspace=False):
266266
remove_workflow_workspace(workflow.workspace_path)
267267
# 3. update the disk usage of the user
268268
disk_resource = get_default_quota_resource(ResourceType.disk.name)
269-
workflow_disk_resource = WorkflowResource.query.filter(
269+
workflow_disk_resource = Session.query(WorkflowResource).filter(
270270
WorkflowResource.workflow_id == workflow.id_,
271271
WorkflowResource.resource_id == disk_resource.id_,
272272
).one_or_none()

reana_workflow_controller/rest/workflows.py

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from sqlalchemy import and_, nullslast, or_, select
2020
from sqlalchemy.orm import aliased
2121
from sqlalchemy.exc import IntegrityError
22+
import marshmallow
2223
from webargs import fields, validate
2324
from webargs.flaskparser import use_args, use_kwargs
2425
from reana_commons.config import WORKFLOW_TIME_FORMAT
@@ -80,18 +81,19 @@
8081
{
8182
"include_progress": fields.Bool(),
8283
"include_workspace_size": fields.Bool(),
83-
"search": fields.String(missing=""),
84-
"sort": fields.String(missing="desc"),
85-
"status": fields.String(missing=""),
84+
"search": fields.String(load_default=""),
85+
"sort": fields.String(load_default="desc"),
86+
"status": fields.String(load_default=""),
8687
"type": fields.String(required=True),
8788
"user": fields.String(required=True),
88-
"verbose": fields.Bool(missing=False),
89+
"verbose": fields.Bool(load_default=False),
8990
"workflow_id_or_name": fields.String(),
90-
"shared": fields.Bool(missing=False),
91+
"shared": fields.Bool(load_default=False),
9192
"shared_by": fields.String(),
9293
"shared_with": fields.String(),
9394
},
9495
location="query",
96+
unknown=marshmallow.EXCLUDE,
9597
)
9698
def get_workflows(args, paginate=None): # noqa
9799
r"""Get all workflows.
@@ -318,7 +320,7 @@ def get_workflows(args, paginate=None): # noqa
318320

319321
try:
320322

321-
user = User.query.filter(User.id_ == user_uuid).first()
323+
user = Session.query(User).filter(User.id_ == user_uuid).first()
322324
if not user:
323325
return jsonify({"message": "User {} does not exist".format(user_uuid)}), 404
324326

@@ -590,7 +592,7 @@ def create_workflow(): # noqa
590592
"""
591593
try:
592594
user_uuid = request.args["user"]
593-
user = User.query.filter(User.id_ == user_uuid).first()
595+
user = Session.query(User).filter(User.id_ == user_uuid).first()
594596
if not user:
595597
return (
596598
jsonify(
@@ -946,7 +948,7 @@ def get_workflow_diff(workflow_id_or_name_a, workflow_id_or_name_b): # noqa
946948

947949

948950
@blueprint.route("/workflows/<workflow_id_or_name>/retention_rules")
949-
@use_kwargs({"user": fields.Str(required=True)}, location="query")
951+
@use_kwargs({"user": fields.Str(required=True)}, location="query", unknown=marshmallow.EXCLUDE)
950952
def get_workflow_retention_rules(workflow_id_or_name: str, user: str):
951953
r"""Get the retention rules of a workflow.
952954
@@ -1058,7 +1060,7 @@ def get_workflow_retention_rules(workflow_id_or_name: str, user: str):
10581060

10591061

10601062
@blueprint.route("/workflows/<workflow_id_or_name>/share", methods=["POST"])
1061-
@use_kwargs({"user": fields.Str(required=True)}, location="query")
1063+
@use_kwargs({"user": fields.Str(required=True)}, location="query", unknown=marshmallow.EXCLUDE)
10621064
@use_kwargs(
10631065
{
10641066
"user_email_to_share_with": fields.Str(required=True),
@@ -1170,7 +1172,7 @@ def share_workflow(
11701172
valid_until = kwargs.get("valid_until")
11711173

11721174
try:
1173-
sharer = User.query.filter(User.id_ == user).first()
1175+
sharer = Session.query(User).filter(User.id_ == user).first()
11741176
if not sharer:
11751177
return (
11761178
jsonify({"message": f"User with id '{user}' does not exist."}),
@@ -1243,6 +1245,7 @@ def share_workflow(
12431245
"user_email_to_unshare_with": fields.Str(required=True),
12441246
},
12451247
location="query",
1248+
unknown=marshmallow.EXCLUDE,
12461249
)
12471250
def unshare_workflow(
12481251
workflow_id_or_name: str, user: str, user_email_to_unshare_with: str
@@ -1362,7 +1365,7 @@ def unshare_workflow(
13621365
}
13631366
"""
13641367
try:
1365-
sharer = User.query.filter(User.id_ == user).first()
1368+
sharer = Session.query(User).filter(User.id_ == user).first()
13661369
if not sharer:
13671370
return (
13681371
jsonify({"message": f"User with id '{sharer}' does not exist."}),
@@ -1411,7 +1414,7 @@ def unshare_workflow(
14111414

14121415

14131416
@blueprint.route("/workflows/<workflow_id_or_name>/share-status", methods=["GET"])
1414-
@use_kwargs({"user": fields.Str(required=True)}, location="query")
1417+
@use_kwargs({"user": fields.Str(required=True)}, location="query", unknown=marshmallow.EXCLUDE)
14151418
def get_workflow_share_status(
14161419
workflow_id_or_name: str,
14171420
user: str,

reana_workflow_controller/rest/workflows_session.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
"""REANA Workflow Controller interactive sessions REST API."""
1010

11+
import marshmallow
1112
from flask import Blueprint, jsonify, request
1213
from webargs import fields
1314
from webargs.flaskparser import use_kwargs
@@ -24,7 +25,7 @@
2425
"/workflows/<workflow_id_or_name>/open/<interactive_session_type>",
2526
methods=["POST"],
2627
)
27-
@use_kwargs({"user": fields.Str(required=True)}, location="query")
28+
@use_kwargs({"user": fields.Str(required=True)}, location="query", unknown=marshmallow.EXCLUDE)
2829
@use_kwargs({"image": fields.Str()}, location="json")
2930
def open_interactive_session(
3031
workflow_id_or_name, interactive_session_type, user, **kwargs

reana_workflow_controller/rest/workflows_status.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import json
1212

1313
from flask import Blueprint, jsonify, request
14+
import marshmallow
1415
from webargs import fields
1516
from webargs.flaskparser import use_kwargs
1617

@@ -362,6 +363,7 @@ def get_workflow_status(workflow_id_or_name): # noqa
362363
"status": fields.Str(required=True),
363364
},
364365
location="query",
366+
unknown=marshmallow.EXCLUDE,
365367
)
366368
def set_workflow_status(
367369
workflow_id_or_name: str, user: str, status: str, **parameters: dict

reana_workflow_controller/rest/workflows_workspace.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424

2525
from reana_commons import workspace
2626
from reana_commons.errors import REANAWorkspaceError
27+
from reana_db.database import Session
2728
from reana_db.models import User
2829
from reana_db.utils import (
2930
_get_workflow_with_uuid_or_name,
@@ -248,7 +249,7 @@ def download_file(workflow_id_or_name, file_name): # noqa
248249
"""
249250
try:
250251
user_uuid = request.args["user"]
251-
user = User.query.filter(User.id_ == user_uuid).first()
252+
user = Session.query(User).filter(User.id_ == user_uuid).first()
252253
if not user:
253254
return jsonify({"message": "User {} does not exist".format(user)}), 404
254255

@@ -337,7 +338,7 @@ def delete_file(workflow_id_or_name, file_name): # noqa
337338
"""
338339
try:
339340
user_uuid = request.args["user"]
340-
user = User.query.filter(User.id_ == user_uuid).first()
341+
user = Session.query(User).filter(User.id_ == user_uuid).first()
341342
if not user:
342343
return jsonify({"message": "User {} does not exist".format(user)}), 404
343344

@@ -471,7 +472,7 @@ def get_files(workflow_id_or_name, paginate=None): # noqa
471472
try:
472473
user_uuid = request.args["user"]
473474
search = request.args.get("search")
474-
user = User.query.filter(User.id_ == user_uuid).first()
475+
user = Session.query(User).filter(User.id_ == user_uuid).first()
475476
if not user:
476477
return jsonify({"message": "User {} does not exist".format(user)}), 404
477478

reana_workflow_controller/workflow_run_manager.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ def _validate_interactive_session_image(type_: str, user_image: Optional[str]) -
167167
class WorkflowRunManager:
168168
"""Interface which specifies how to manage workflow runs."""
169169

170-
if os.getenv("FLASK_ENV") == "development":
170+
if os.getenv("FLASK_DEBUG", "").lower() in ("1", "true"):
171171
WORKFLOW_ENGINE_COMMON_ENV_VARS.extend(DEBUG_ENV_VARS)
172172

173173
engine_mapping = {
@@ -533,7 +533,7 @@ def start_interactive_session(self, interactive_session_type, image=None, **kwar
533533

534534
def stop_interactive_session(self, interactive_session_id):
535535
"""Stop an interactive workflow run."""
536-
int_session = InteractiveSession.query.filter_by(
536+
int_session = Session.query(InteractiveSession).filter_by(
537537
id_=interactive_session_id
538538
).first()
539539

@@ -867,7 +867,7 @@ def _create_job_spec(
867867
# filter out volumes with the same name
868868
spec.template.spec.volumes = list({v["name"]: v for v in volumes}.values())
869869

870-
if os.getenv("FLASK_ENV") == "development":
870+
if os.getenv("FLASK_DEBUG", "").lower() in ("1", "true"):
871871
code_volume_name = "reana-code"
872872
code_mount_path = "/code"
873873
k8s_code_volume = client.V1Volume(name=code_volume_name)

requirements.txt

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
#
55
# pip-compile --annotation-style=line --output-file=requirements.txt setup.py
66
#
7-
alembic==1.18.4 # via reana-db
87
amqp==5.3.1 # via kombu
98
appdirs==1.4.4 # via fs
109
arrow==1.4.0 # via isoduration
@@ -19,29 +18,23 @@ checksumdir==1.1.9 # via reana-commons
1918
click==8.3.1 # via flask, reana-commons
2019
cryptography==46.0.6 # via google-auth, sqlalchemy-utils
2120
events==0.5 # via opensearch-py
22-
flask==2.2.5 # via reana-workflow-controller (setup.py)
2321
fqdn==1.5.1 # via jsonschema
2422
fs==2.4.16 # via reana-commons
2523
gherkin-official==39.0.0 # via reana-commons
2624
gitdb==4.0.12 # via gitpython
2725
gitpython==3.1.46 # via reana-workflow-controller (setup.py)
2826
google-auth==2.49.1 # via kubernetes
29-
greenlet==3.3.2 # via sqlalchemy
3027
idna==3.11 # via jsonschema, requests
3128
importlib-resources==6.5.2 # via swagger-spec-validator
3229
isoduration==20.11.0 # via jsonschema
33-
itsdangerous==2.2.0 # via flask
34-
jinja2==3.1.6 # via flask
3530
jsonpickle==4.1.1 # via reana-workflow-controller (setup.py)
3631
jsonpointer==3.1.1 # via jsonschema
3732
jsonref==1.1.0 # via bravado-core
3833
jsonschema[format]==4.26.0 # via bravado-core, reana-commons, swagger-spec-validator
3934
jsonschema-specifications==2025.9.1 # via jsonschema
4035
kombu==5.6.2 # via reana-commons
41-
kubernetes==26.1.0 # via reana-commons
4236
mako==1.3.10 # via alembic
4337
markupsafe==3.0.3 # via jinja2, mako, werkzeug
44-
marshmallow==2.21.0 # via reana-workflow-controller (setup.py), webargs
4538
mock==3.0.5 # via reana-commons
4639
monotonic==1.6 # via bravado
4740
msgpack==1.1.2 # via bravado-core
@@ -58,8 +51,6 @@ python-dateutil==2.9.0.post0 # via arrow, bravado, bravado-core, kubernetes, op
5851
pytz==2026.1.post1 # via bravado-core
5952
pyuwsgi==2.0.30.post1 # via reana-workflow-controller (setup.py)
6053
pyyaml==6.0.3 # via bravado, bravado-core, kubernetes, reana-commons, swagger-spec-validator
61-
reana-commons[kubernetes]==0.95.0a14 # via reana-db, reana-workflow-controller (setup.py)
62-
reana-db==0.95.0a6 # via reana-workflow-controller (setup.py)
6354
referencing==0.37.0 # via jsonschema, jsonschema-specifications
6455
requests==2.33.0 # via bravado, bravado-core, kubernetes, opensearch-py, reana-workflow-controller (setup.py), requests-oauthlib
6556
requests-oauthlib==2.0.0 # via kubernetes
@@ -69,8 +60,6 @@ rpds-py==0.30.0 # via jsonschema, referencing
6960
simplejson==3.20.2 # via bravado, bravado-core
7061
six==1.17.0 # via bravado, bravado-core, fs, kubernetes, mock, python-dateutil, rfc3339-validator
7162
smmap==5.0.3 # via gitdb
72-
sqlalchemy==1.4.54 # via alembic, reana-db, sqlalchemy-utils
73-
sqlalchemy-utils[encrypted]==0.42.1 # via reana-db, reana-workflow-controller (setup.py)
7463
swagger-spec-validator==3.0.4 # via bravado-core
7564
typing-extensions==4.15.0 # via alembic, bravado, gherkin-official, referencing, swagger-spec-validator
7665
tzdata==2025.3 # via arrow, kombu
@@ -80,10 +69,8 @@ uwsgi-tools==1.1.1 # via reana-workflow-controller (setup.py)
8069
uwsgitop==0.12 # via reana-workflow-controller (setup.py)
8170
vine==5.1.0 # via amqp, kombu
8271
wcmatch==8.4.1 # via reana-commons
83-
webargs==6.1.1 # via reana-workflow-controller (setup.py)
8472
webcolors==25.10.0 # via jsonschema
8573
websocket-client==1.9.0 # via kubernetes
86-
werkzeug==2.2.3 # via flask, reana-commons, reana-workflow-controller (setup.py)
8774

8875
# The following packages are considered to be unsafe in a requirements file:
8976
# setuptools

setup.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,11 @@
4444
extras_require["all"].extend(reqs)
4545

4646
install_requires = [
47-
"Flask>=2.1.1,<2.3.0", # same upper pin as invenio-base/reana-server
48-
"Werkzeug>=2.1.0,<2.3.0", # same upper pin as invenio-base
47+
"Flask>=3.0.0,<4.0.0",
48+
"Werkzeug>=3.0.0",
4949
"gitpython>=2.1",
5050
"jsonpickle>=0.9.6",
51-
"marshmallow>2.13.0,<3.0.0", # same upper pin as reana-server
51+
"marshmallow>=3.5.0,<4.0.0",
5252
"opensearch-py>=2.7.0,<2.8.0",
5353
"packaging>=18.0",
5454
"reana-commons[kubernetes]>=0.95.0a14,<0.96.0",
@@ -58,7 +58,7 @@
5858
"uwsgi-tools>=1.1.1",
5959
"pyuwsgi>=2.0.17",
6060
"uwsgitop>=0.10",
61-
"webargs>=6.1.0,<7.0.0",
61+
"webargs>=8.0",
6262
]
6363

6464
packages = find_packages()

tests/conftest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ def base_app(tmp_shared_volume_path):
5151
"SHARED_VOLUME_PATH": tmp_shared_volume_path,
5252
"SQLALCHEMY_DATABASE_URI": os.getenv("REANA_SQLALCHEMY_DATABASE_URI"),
5353
"SQLALCHEMY_TRACK_MODIFICATIONS": False,
54-
"FLASK_ENV": "development",
54+
"DEBUG": True,
5555
"ORGANIZATIONS": ["default"],
5656
}
5757
app_ = create_app(config_mapping=config_mapping)

0 commit comments

Comments
 (0)