Skip to content

Commit 9895576

Browse files
committed
build(python): upgrade to Flask 3.x (#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 9895576

13 files changed

Lines changed: 136 additions & 91 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: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -266,10 +266,14 @@ 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(
270-
WorkflowResource.workflow_id == workflow.id_,
271-
WorkflowResource.resource_id == disk_resource.id_,
272-
).one_or_none()
269+
workflow_disk_resource = (
270+
Session.query(WorkflowResource)
271+
.filter(
272+
WorkflowResource.workflow_id == workflow.id_,
273+
WorkflowResource.resource_id == disk_resource.id_,
274+
)
275+
.one_or_none()
276+
)
273277
disk_usage = None
274278
if workflow_disk_resource:
275279
disk_usage = workflow_disk_resource.quota_used

reana_workflow_controller/rest/workflows.py

Lines changed: 21 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,9 @@ 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(
952+
{"user": fields.Str(required=True)}, location="query", unknown=marshmallow.EXCLUDE
953+
)
950954
def get_workflow_retention_rules(workflow_id_or_name: str, user: str):
951955
r"""Get the retention rules of a workflow.
952956
@@ -1058,7 +1062,9 @@ def get_workflow_retention_rules(workflow_id_or_name: str, user: str):
10581062

10591063

10601064
@blueprint.route("/workflows/<workflow_id_or_name>/share", methods=["POST"])
1061-
@use_kwargs({"user": fields.Str(required=True)}, location="query")
1065+
@use_kwargs(
1066+
{"user": fields.Str(required=True)}, location="query", unknown=marshmallow.EXCLUDE
1067+
)
10621068
@use_kwargs(
10631069
{
10641070
"user_email_to_share_with": fields.Str(required=True),
@@ -1170,7 +1176,7 @@ def share_workflow(
11701176
valid_until = kwargs.get("valid_until")
11711177

11721178
try:
1173-
sharer = User.query.filter(User.id_ == user).first()
1179+
sharer = Session.query(User).filter(User.id_ == user).first()
11741180
if not sharer:
11751181
return (
11761182
jsonify({"message": f"User with id '{user}' does not exist."}),
@@ -1243,6 +1249,7 @@ def share_workflow(
12431249
"user_email_to_unshare_with": fields.Str(required=True),
12441250
},
12451251
location="query",
1252+
unknown=marshmallow.EXCLUDE,
12461253
)
12471254
def unshare_workflow(
12481255
workflow_id_or_name: str, user: str, user_email_to_unshare_with: str
@@ -1362,7 +1369,7 @@ def unshare_workflow(
13621369
}
13631370
"""
13641371
try:
1365-
sharer = User.query.filter(User.id_ == user).first()
1372+
sharer = Session.query(User).filter(User.id_ == user).first()
13661373
if not sharer:
13671374
return (
13681375
jsonify({"message": f"User with id '{sharer}' does not exist."}),
@@ -1411,7 +1418,9 @@ def unshare_workflow(
14111418

14121419

14131420
@blueprint.route("/workflows/<workflow_id_or_name>/share-status", methods=["GET"])
1414-
@use_kwargs({"user": fields.Str(required=True)}, location="query")
1421+
@use_kwargs(
1422+
{"user": fields.Str(required=True)}, location="query", unknown=marshmallow.EXCLUDE
1423+
)
14151424
def get_workflow_share_status(
14161425
workflow_id_or_name: str,
14171426
user: str,

reana_workflow_controller/rest/workflows_session.py

Lines changed: 4 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,9 @@
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(
29+
{"user": fields.Str(required=True)}, location="query", unknown=marshmallow.EXCLUDE
30+
)
2831
@use_kwargs({"image": fields.Str()}, location="json")
2932
def open_interactive_session(
3033
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: 7 additions & 5 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,9 +533,11 @@ 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(
537-
id_=interactive_session_id
538-
).first()
536+
int_session = (
537+
Session.query(InteractiveSession)
538+
.filter_by(id_=interactive_session_id)
539+
.first()
540+
)
539541

540542
if not int_session:
541543
raise REANAInteractiveSessionError(
@@ -867,7 +869,7 @@ def _create_job_spec(
867869
# filter out volumes with the same name
868870
spec.template.spec.volumes = list({v["name"]: v for v in volumes}.values())
869871

870-
if os.getenv("FLASK_ENV") == "development":
872+
if os.getenv("FLASK_DEBUG", "").lower() in ("1", "true"):
871873
code_volume_name = "reana-code"
872874
code_mount_path = "/code"
873875
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: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
"sphinxcontrib-redoc>=1.5.1",
3434
],
3535
"tests": [
36-
"pytest-reana>=0.95.0a4,<0.96.0",
36+
"pytest-reana>=0.95.0a8,<0.96.0",
3737
],
3838
}
3939

@@ -44,21 +44,21 @@
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",
54-
"reana-commons[kubernetes]>=0.95.0a14,<0.96.0",
55-
"reana-db>=0.95.0a6,<0.96.0",
54+
"reana-commons[kubernetes]>=0.95.0a15,<0.96.0",
55+
"reana-db>=0.95.0a7,<0.96.0",
5656
"requests>=2.25.0",
5757
"sqlalchemy-utils>=0.31.0",
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)