Skip to content

Commit b230bb2

Browse files
committed
Chore: Upgrade Flask to 3.x and Sqlalchemy to 2.x
Minimal changes to get server working without any migrations to alchemy new API
1 parent bac1498 commit b230bb2

19 files changed

+2024
-1104
lines changed

server/Pipfile

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,25 +4,25 @@ verify_ssl = true
44
name = "pypi"
55

66
[packages]
7-
connexion = {extras = ["swagger-ui"],version = "==2.14.1"}
8-
flask = "==2.2.5"
7+
connexion = {extras = ["swagger-ui"],version = "==2.15.1"}
8+
flask = "==3.1.2"
99
python-dateutil = "==2.8.2"
10-
marshmallow = "==3.20.1"
11-
flask-marshmallow = "==0.14.0"
12-
marshmallow-sqlalchemy = "==1.1.0"
10+
marshmallow = "==3.26.1"
11+
flask-marshmallow = "==0.15.0"
12+
marshmallow-sqlalchemy = "==1.4.1"
1313
psycopg2-binary = "==2.9.9"
1414
itsdangerous = "==2.2.0"
15-
Flask-SQLAlchemy = "==2.5.1"
16-
sqlalchemy = "==1.4.53"
17-
gunicorn = {extras = ["gevent"],version = "==19.9"}
15+
Flask-SQLAlchemy = "==3.1.1"
16+
sqlalchemy = "==2.0.44"
17+
gunicorn = {extras = ["gevent"],version = "==23.0"}
1818
python-dotenv = "==0.20.0"
19-
flask-login = "==0.6.2"
19+
flask-login = "==0.6.3"
2020
bcrypt = "==4.2.0"
21-
wtforms = {extras = ["email"],version = "==3.1.2"}
22-
flask-wtf = "==1.0.1"
21+
wtforms = {extras = ["email"],version = "==3.2.1"}
22+
flask-wtf = "==1.2.2"
2323
flask-mail = "==0.10.0"
2424
safe = "==0.4"
25-
flask-migrate = "==2.6.0" # 3.1.0
25+
flask-migrate = "==3.1.0"
2626
wtforms-json = "==0.3.5"
2727
pytz = "==2022.2.1"
2828
scikit-build = "==0.18.1"

server/Pipfile.lock

Lines changed: 1899 additions & 1003 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

server/mergin/app.py

Lines changed: 14 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import gevent
1111
from marshmallow import fields
1212
from sqlalchemy.schema import MetaData
13+
from sqlalchemy import text
1314
from flask_sqlalchemy import SQLAlchemy
1415
from flask_marshmallow import Marshmallow
1516
from flask import (
@@ -27,7 +28,6 @@
2728
from flask_wtf.csrf import generate_csrf, CSRFProtect
2829
from flask_migrate import Migrate
2930
from flask_mail import Mail
30-
from connexion.apps.flask_app import FlaskJSONEncoder
3131
from flask_wtf import FlaskForm
3232
from wtforms import StringField
3333
from pathlib import Path
@@ -37,7 +37,6 @@
3737
from werkzeug.exceptions import HTTPException
3838
from typing import List, Dict, Optional, Tuple
3939

40-
from .sync.utils import get_blacklisted_dirs, get_blacklisted_files
4140
from .config import Configuration
4241
from .commands import add_commands as server_commands
4342

@@ -139,7 +138,6 @@ def create_simple_app() -> Flask:
139138
app = connexion.FlaskApp(__name__, specification_dir=os.path.join(this_dir))
140139
flask_app = app.app
141140

142-
flask_app.json_encoder = FlaskJSONEncoder
143141
flask_app.config.from_object(Configuration)
144142
db.init_app(flask_app)
145143
ma.init_app(flask_app)
@@ -155,54 +153,36 @@ def create_simple_app() -> Flask:
155153
def create_app(public_keys: List[str] = None) -> Flask:
156154
"""Factory function to create Flask app instance"""
157155
from itsdangerous import BadTimeSignature, BadSignature
158-
from .auth import auth_required, decode_token
159-
from .auth.models import User
160156

161-
# from .celery import celery
162-
from .sync.db_events import register_events
163-
from .sync.workspace import GlobalWorkspaceHandler
164-
from .sync.config import Configuration as SyncConfig
165-
from .sync.commands import add_commands
166-
from .auth import register as register_auth
157+
from .auth import auth_required, decode_token, register as register_auth
158+
from .auth.models import User
159+
from .sync.app import register as register_sync
167160
from .sync.project_handler import ProjectHandler
161+
from .sync.utils import get_blacklisted_dirs, get_blacklisted_files
162+
from .sync.workspace import GlobalWorkspaceHandler
168163

169164
app = create_simple_app().connexion_app
170165

171-
app.add_api(
172-
"sync/public_api.yaml",
173-
arguments={"title": "Mergin"},
174-
options={"swagger_ui": Configuration.SWAGGER_UI},
175-
validate_responses=True,
176-
)
177-
app.add_api(
178-
"sync/public_api_v2.yaml",
179-
arguments={"title": "Mergin"},
180-
options={"swagger_ui": Configuration.SWAGGER_UI},
181-
validate_responses=True,
182-
)
183-
app.add_api(
184-
"sync/private_api.yaml",
185-
base_path="/app",
186-
arguments={"title": "Mergin"},
187-
options={"swagger_ui": False, "serve_spec": False},
188-
validate_responses=True,
189-
)
190166
app.add_api(
191167
"api.yaml",
192168
arguments={"title": "Mergin"},
193169
options={"swagger_ui": False, "serve_spec": False},
194170
validate_responses=True,
195171
)
172+
app.app.blueprints["/"].name = "main"
173+
app.app.blueprints["main"] = app.app.blueprints.pop("/")
196174

197-
app.app.config.from_object(SyncConfig)
198-
app.app.connexion_app = app
175+
# register sync module
176+
register_sync(app.app)
199177

178+
# initialize extensions
200179
mail.init_app(app.app)
201-
app.mail = mail
202180
csrf.init_app(app.app)
203181
login_manager.init_app(app.app)
182+
204183
# register auth blueprint
205184
register_auth(app.app)
185+
206186
server_commands(app.app)
207187

208188
# adjust login manager
@@ -228,8 +208,6 @@ def load_user_from_header(header_val): # pylint: disable=W0613,W0612
228208
except (BadSignature, BadTimeSignature, KeyError):
229209
pass
230210

231-
# csrf = app.app.extensions['csrf']
232-
233211
@app.app.before_request
234212
def check_maintenance():
235213
allowed_endpoints = ["/project/by_names", "/auth/login", "/alive"]
@@ -275,9 +253,6 @@ def get_startup_data():
275253
}
276254
return data
277255

278-
# update celery config with flask app config
279-
# celery.conf.update(app.app.config)
280-
281256
@app.route("/alive", methods=["POST"])
282257
@csrf.exempt
283258
def alive(): # pylint: disable=E0722
@@ -287,7 +262,7 @@ def alive(): # pylint: disable=E0722
287262
start_time = time.time()
288263
try:
289264
with db.engine.connect() as con:
290-
rs = con.execute("SELECT 2 * 2")
265+
rs = con.execute(text("SELECT 2 * 2"))
291266
assert rs.fetchone()[0] == 4
292267
except:
293268
"""Although bad form, we have deliberate left this except broad. When we have an uncaught exception in
@@ -388,7 +363,6 @@ def init(): # pylint: disable=W0612
388363
response.headers.set("X-CSRF-Token", generate_csrf())
389364
return response
390365

391-
register_events()
392366
application = app.app
393367

394368
@application.errorhandler(Exception)
@@ -467,8 +441,6 @@ def config():
467441
cfg["build_hash"] = application.config["BUILD_HASH"]
468442
return jsonify(cfg), 200
469443

470-
# append project commands (from default sync module)
471-
add_commands(application)
472444
return application
473445

474446

server/mergin/auth/controller.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -396,7 +396,7 @@ def register_user(): # pylint: disable=W0613,W0612
396396

397397
form = UserRegistrationForm()
398398
form.username.data = User.generate_username(form.email.data)
399-
if form.validate_on_submit():
399+
if form.is_submitted() and form.validate():
400400
user = User.create(form.username.data, form.email.data, form.password.data)
401401
user_created.send(user, source="admin")
402402
token = generate_confirmation_token(
@@ -497,8 +497,9 @@ def get_paginated_users(
497497
elif not descending and order_by:
498498
users = users.order_by(asc(User.__table__.c[order_by]))
499499

500-
result = users.paginate(page, per_page).items
501-
total = users.paginate(page, per_page).total
500+
paginate = users.paginate(page=page, per_page=per_page)
501+
result = paginate.items
502+
total = paginate.total
502503

503504
result_users = UserSchema(many=True).dump(result)
504505

server/mergin/auth/tasks.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
@celery.task
1515
def anonymize_removed_users():
1616
"""Permanently 'delete' users marked for removal by removing personal information"""
17-
db.session.info = {"msg": "anonymize_removed_users"}
17+
db.session.info["msg"] = "anonymize_removed_users"
1818
before_expiration = datetime.today() - timedelta(Configuration.ACCOUNT_EXPIRATION)
1919
users = User.query.filter(
2020
isnot(User.active, True),

server/mergin/commands.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import random
88
import string
99
import os
10+
from sqlalchemy import inspect
1011

1112

1213
def _echo_title(title):
@@ -136,7 +137,8 @@ def _check_server(app: Flask): # pylint: disable=W0612
136137
else:
137138
_echo_error("No service ID set.")
138139

139-
tables = db.engine.table_names()
140+
inspect_engine = inspect(db.engine)
141+
tables = inspect_engine.get_table_names()
140142
if not tables:
141143
_echo_error("Database not initialized. Run flask init-db command")
142144
else:
@@ -157,9 +159,9 @@ def _init_db(app: Flask):
157159
label="Creating database", length=4, show_eta=False
158160
) as progress_bar:
159161
progress_bar.update(0)
160-
db.drop_all(bind=None)
162+
db.drop_all(bind_key=None)
161163
progress_bar.update(1)
162-
db.create_all(bind=None)
164+
db.create_all(bind_key=None)
163165
progress_bar.update(2)
164166
db.session.commit()
165167
progress_bar.update(3)
@@ -202,7 +204,8 @@ def init(email: str, recreate: bool):
202204
"""Initialize database if does not exist or -r is provided. Perform check of server configuration. Send statistics, respecting your setup."""
203205
from .auth.models import User, UserProfile
204206

205-
tables = db.engine.table_names()
207+
inspect_engine = inspect(db.engine)
208+
tables = inspect_engine.get_table_names()
206209
if recreate and tables:
207210
click.confirm(
208211
"Are you sure you want to recreate database and admin user? This will remove all data!",

server/mergin/controller.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
1-
import json
2-
import logging
3-
import os
1+
# Copyright (C) Lutra Consulting Limited
2+
#
3+
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-MerginMaps-Commercial
4+
45
from flask import abort, current_app, request
56
from flask_login import current_user
67
from magic import from_buffer
7-
import time
8-
98
import requests
109

1110
from .utils import save_diagnostic_log_file
12-
from .app import parse_version_string, db
11+
from .app import parse_version_string
1312

1413

1514
def get_latest_version():

server/mergin/stats/tasks.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,12 @@
44

55
from dataclasses import asdict
66
import requests
7-
from datetime import datetime, timedelta, timezone
7+
from datetime import datetime, timedelta
88
import json
99
import logging
1010
from flask import current_app
1111
from sqlalchemy.sql.operators import is_
12+
from sqlalchemy import inspect
1213

1314
from .models import MerginInfo, MerginStatistics, ServerCallhomeData
1415
from ..celery import celery
@@ -71,7 +72,8 @@ def send_statistics():
7172
if not current_app.config["COLLECT_STATISTICS"]:
7273
return
7374

74-
if not db.engine.has_table("mergin_info"):
75+
inspect_engine = inspect(db.engine)
76+
if not inspect_engine.has_table("mergin_info"):
7577
logging.warning("Database not initialized")
7678
return
7779

server/mergin/sync/app.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# Copyright (C) Lutra Consulting Limited
2+
#
3+
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-MerginMaps-Commercial
4+
5+
from flask import Flask
6+
7+
from .commands import add_commands
8+
from .config import Configuration
9+
from .db_events import register_events
10+
11+
12+
def register(app: Flask):
13+
"""Register mergin sync module in Flask app
14+
Adds Flask blueprint with autogenerated Flask/Connexion routes from openAPI definition.
15+
Register db events/hooks.
16+
"""
17+
app.config.from_object(Configuration)
18+
19+
app.connexion_app.add_api(
20+
"sync/public_api.yaml",
21+
arguments={"title": "Mergin"},
22+
options={"swagger_ui": False},
23+
validate_responses=True,
24+
)
25+
26+
app.connexion_app.add_api(
27+
"sync/public_api_v2.yaml",
28+
arguments={"title": "Mergin"},
29+
options={"swagger_ui": False},
30+
validate_responses=True,
31+
)
32+
33+
app.connexion_app.add_api(
34+
"sync/private_api.yaml",
35+
base_path="/app",
36+
arguments={"title": "Mergin"},
37+
options={"swagger_ui": False, "serve_spec": False},
38+
validate_responses=True,
39+
)
40+
41+
add_commands(app)
42+
register_events()

server/mergin/sync/models.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ def cache_latest_files(self) -> None:
167167
WHERE a.project_id = pf.project_id;
168168
"""
169169
params = {"project_id": self.id, "latest_version": self.latest_version}
170-
db.session.execute(query, params)
170+
db.session.execute(text(query), params)
171171
db.session.commit()
172172

173173
@property
@@ -222,7 +222,7 @@ def files(self) -> List[ProjectFile]:
222222
else None
223223
),
224224
)
225-
for row in db.session.execute(query, params).fetchall()
225+
for row in db.session.execute(text(query), params).fetchall()
226226
]
227227
return files
228228

@@ -1506,7 +1506,7 @@ def _files_from_start(self):
15061506
WHERE fh.change != 'delete';
15071507
"""
15081508
params = {"project_id": self.project_id, "version": self.name}
1509-
return db.session.execute(query, params).fetchall()
1509+
return db.session.execute(text(query), params).fetchall()
15101510

15111511
def _files_from_end(self):
15121512
"""Calculate version files using lookup from the last version
@@ -1567,7 +1567,7 @@ def _files_from_end(self):
15671567
ORDER BY fp.path;
15681568
"""
15691569
params = {"project_id": self.project_id, "version": self.name}
1570-
return db.session.execute(query, params).fetchall()
1570+
return db.session.execute(text(query), params).fetchall()
15711571

15721572
@property
15731573
def files(self) -> List[ProjectFile]:
@@ -1673,7 +1673,7 @@ def changes_count(self) -> Dict:
16731673
"""Return number of changes by type"""
16741674
query = f"SELECT change, COUNT(change) FROM file_history WHERE version_id = :version_id GROUP BY change;"
16751675
params = {"version_id": self.id}
1676-
result = db.session.execute(query, params).fetchall()
1676+
result = db.session.execute(text(query), params).fetchall()
16771677
return {row[0]: row[1] for row in result}
16781678

16791679
@property

0 commit comments

Comments
 (0)