Merge pull request #440 from SamR1/flask-sqlalchemy-update-rollback

API - FlaskSQLAlchemy update rollback
This commit is contained in:
Sam 2023-10-04 15:59:08 +02:00 committed by GitHub
commit abdf6c6cee
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 221 additions and 266 deletions

View File

@ -20,7 +20,6 @@ from flask_limiter.util import get_remote_address
from flask_migrate import Migrate from flask_migrate import Migrate
from flask_sqlalchemy import SQLAlchemy from flask_sqlalchemy import SQLAlchemy
from sqlalchemy.exc import ProgrammingError from sqlalchemy.exc import ProgrammingError
from sqlalchemy.orm import DeclarativeBase
from werkzeug.middleware.proxy_fix import ProxyFix from werkzeug.middleware.proxy_fix import ProxyFix
from fittrackee.emails.email import EmailService from fittrackee.emails.email import EmailService
@ -39,12 +38,7 @@ logging.basicConfig(
) )
appLog = logging.getLogger('fittrackee') appLog = logging.getLogger('fittrackee')
db = SQLAlchemy()
class Base(DeclarativeBase):
pass
db = SQLAlchemy(model_class=Base)
bcrypt = Bcrypt() bcrypt = Bcrypt()
migrate = Migrate() migrate = Migrate()
email_service = EmailService() email_service = EmailService()

View File

@ -5,30 +5,29 @@ from flask import current_app
from sqlalchemy import exc from sqlalchemy import exc
from sqlalchemy.engine.base import Connection from sqlalchemy.engine.base import Connection
from sqlalchemy.event import listens_for from sqlalchemy.event import listens_for
from sqlalchemy.orm import mapped_column from sqlalchemy.ext.declarative import DeclarativeMeta
from sqlalchemy.orm.mapper import Mapper from sqlalchemy.orm.mapper import Mapper
from sqlalchemy.orm.session import Session from sqlalchemy.orm.session import Session
from sqlalchemy.sql import text
from fittrackee import VERSION, db from fittrackee import VERSION, db
from fittrackee.users.models import User from fittrackee.users.models import User
BaseModel: DeclarativeMeta = db.Model
class AppConfig(db.Model): # type: ignore
class AppConfig(BaseModel):
__tablename__ = 'app_config' __tablename__ = 'app_config'
id = mapped_column(db.Integer, primary_key=True, autoincrement=True) id = db.Column(db.Integer, primary_key=True, autoincrement=True)
max_users = mapped_column(db.Integer, default=0, nullable=False) max_users = db.Column(db.Integer, default=0, nullable=False)
gpx_limit_import = mapped_column(db.Integer, default=10, nullable=False) gpx_limit_import = db.Column(db.Integer, default=10, nullable=False)
max_single_file_size = mapped_column( max_single_file_size = db.Column(
db.Integer, default=1048576, nullable=False db.Integer, default=1048576, nullable=False
) )
max_zip_file_size = mapped_column( max_zip_file_size = db.Column(db.Integer, default=10485760, nullable=False)
db.Integer, default=10485760, nullable=False admin_contact = db.Column(db.String(255), nullable=True)
) privacy_policy_date = db.Column(db.DateTime, nullable=True)
admin_contact = mapped_column(db.String(255), nullable=True) privacy_policy = db.Column(db.Text, nullable=True)
privacy_policy_date = mapped_column(db.DateTime, nullable=True) about = db.Column(db.Text, nullable=True)
privacy_policy = mapped_column(db.Text, nullable=True)
about = mapped_column(db.Text, nullable=True)
@property @property
def is_registration_enabled(self) -> bool: def is_registration_enabled(self) -> bool:
@ -37,9 +36,8 @@ class AppConfig(db.Model): # type: ignore
except exc.ProgrammingError as e: except exc.ProgrammingError as e:
# workaround for user model related migrations # workaround for user model related migrations
if 'psycopg2.errors.UndefinedColumn' in str(e): if 'psycopg2.errors.UndefinedColumn' in str(e):
query = db.session.execute(text("SELECT COUNT(*) FROM users;")) result = db.engine.execute("SELECT COUNT(*) FROM users;")
result = query.fetchone() nb_users = result.fetchone()[0]
nb_users = result[0] if result else 0
else: else:
raise e raise e
return self.max_users == 0 or nb_users < self.max_users return self.max_users == 0 or nb_users < self.max_users

View File

@ -3,7 +3,6 @@ import shutil
import click import click
from flask_migrate import upgrade from flask_migrate import upgrade
from sqlalchemy.sql import text
from fittrackee import db from fittrackee import db
from fittrackee.cli.app import app from fittrackee.cli.app import app
@ -37,7 +36,7 @@ def drop_db() -> None:
err=True, err=True,
) )
return return
db.session.execute(text("DROP TABLE IF EXISTS alembic_version;")) db.engine.execute("DROP TABLE IF EXISTS alembic_version;")
db.drop_all() db.drop_all()
db.session.commit() db.session.commit()
click.echo('Database dropped.') click.echo('Database dropped.')

View File

@ -4,6 +4,6 @@ from fittrackee.utils import clean
def clean_tokens(days: int) -> int: def clean_tokens(days: int) -> int:
sql = """ sql = """
DELETE FROM oauth2_token DELETE FROM oauth2_token
WHERE oauth2_token.issued_at + oauth2_token.expires_in < :limit; WHERE oauth2_token.issued_at + oauth2_token.expires_in < %(limit)s;
""" """
return clean(sql, days) return clean(sql, days)

View File

@ -8,22 +8,23 @@ from authlib.integrations.sqla_oauth2 import (
) )
from sqlalchemy.engine.base import Connection from sqlalchemy.engine.base import Connection
from sqlalchemy.event import listens_for from sqlalchemy.event import listens_for
from sqlalchemy.orm import mapped_column, relationship from sqlalchemy.ext.declarative import DeclarativeMeta
from sqlalchemy.orm.mapper import Mapper from sqlalchemy.orm.mapper import Mapper
from sqlalchemy.orm.session import Session from sqlalchemy.orm.session import Session
from sqlalchemy.sql import text
from fittrackee import db from fittrackee import db
BaseModel: DeclarativeMeta = db.Model
class OAuth2Client(OAuth2ClientMixin, db.Model): # type: ignore
class OAuth2Client(BaseModel, OAuth2ClientMixin):
__tablename__ = 'oauth2_client' __tablename__ = 'oauth2_client'
id = mapped_column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
user_id = mapped_column( user_id = db.Column(
db.Integer, db.ForeignKey('users.id', ondelete='CASCADE'), index=True db.Integer, db.ForeignKey('users.id', ondelete='CASCADE'), index=True
) )
user = relationship('User') user = db.relationship('User')
def serialize(self, with_secret: bool = False) -> Dict: def serialize(self, with_secret: bool = False) -> Dict:
client = { client = {
@ -62,9 +63,7 @@ def on_old_oauth2_delete(
).delete(synchronize_session=False) ).delete(synchronize_session=False)
class OAuth2AuthorizationCode( class OAuth2AuthorizationCode(BaseModel, OAuth2AuthorizationCodeMixin):
OAuth2AuthorizationCodeMixin, db.Model # type: ignore
):
__tablename__ = 'oauth2_code' __tablename__ = 'oauth2_code'
__table_args__ = ( __table_args__ = (
db.Index( db.Index(
@ -73,21 +72,21 @@ class OAuth2AuthorizationCode(
), ),
) )
id = mapped_column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
user_id = mapped_column( user_id = db.Column(
db.Integer, db.ForeignKey('users.id', ondelete='CASCADE'), index=True db.Integer, db.ForeignKey('users.id', ondelete='CASCADE'), index=True
) )
user = relationship('User') user = db.relationship('User')
class OAuth2Token(OAuth2TokenMixin, db.Model): # type: ignore class OAuth2Token(BaseModel, OAuth2TokenMixin):
__tablename__ = 'oauth2_token' __tablename__ = 'oauth2_token'
id = mapped_column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
user_id = mapped_column( user_id = db.Column(
db.Integer, db.ForeignKey('users.id', ondelete='CASCADE'), index=True db.Integer, db.ForeignKey('users.id', ondelete='CASCADE'), index=True
) )
user = relationship('User') user = db.relationship('User')
def is_refresh_token_active(self) -> bool: def is_refresh_token_active(self) -> bool:
if self.is_revoked(): if self.is_revoked():
@ -99,10 +98,10 @@ class OAuth2Token(OAuth2TokenMixin, db.Model): # type: ignore
def revoke_client_tokens(cls, client_id: str) -> None: def revoke_client_tokens(cls, client_id: str) -> None:
sql = """ sql = """
UPDATE oauth2_token UPDATE oauth2_token
SET access_token_revoked_at = :revoked_at SET access_token_revoked_at = %(revoked_at)s
WHERE client_id = :client_id; WHERE client_id = %(client_id)s;
""" """
db.session.execute( db.engine.execute(
text(sql), {'client_id': client_id, 'revoked_at': int(time.time())} sql, {'client_id': client_id, 'revoked_at': int(time.time())}
) )
db.session.commit() db.session.commit()

View File

@ -23,29 +23,6 @@ class TestUserModel:
) -> None: ) -> None:
assert '<User \'test\'>' == str(user_1) assert '<User \'test\'>' == str(user_1)
def test_it_returns_workout_count_when_no_workouts(
self,
app: Flask,
user_1: User,
user_2: User,
sport_1_cycling: Sport,
workout_cycling_user_2: Workout,
) -> None:
assert user_1.workouts_count == 0
def test_it_returns_workout_count_when_user_has_workout(
self,
app: Flask,
user_1: User,
user_2: User,
sport_1_cycling: Sport,
sport_2_running: Sport,
workout_cycling_user_1: Workout,
workout_running_user_1: Workout,
workout_cycling_user_2: Workout,
) -> None:
assert user_1.workouts_count == 2
class UserModelAssertMixin: class UserModelAssertMixin:
@staticmethod @staticmethod

View File

@ -7,8 +7,8 @@ from flask import current_app
from sqlalchemy import func from sqlalchemy import func
from sqlalchemy.engine.base import Connection from sqlalchemy.engine.base import Connection
from sqlalchemy.event import listens_for from sqlalchemy.event import listens_for
from sqlalchemy.ext.declarative import DeclarativeMeta
from sqlalchemy.ext.hybrid import hybrid_property from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.orm import mapped_column, relationship
from sqlalchemy.orm.mapper import Mapper from sqlalchemy.orm.mapper import Mapper
from sqlalchemy.orm.session import Session from sqlalchemy.orm.session import Session
from sqlalchemy.sql.expression import select from sqlalchemy.sql.expression import select
@ -21,48 +21,48 @@ from .exceptions import UserNotFoundException
from .roles import UserRole from .roles import UserRole
from .utils.token import decode_user_token, get_user_token from .utils.token import decode_user_token, get_user_token
BaseModel: DeclarativeMeta = db.Model
class User(db.Model): # type: ignore
class User(BaseModel):
__tablename__ = 'users' __tablename__ = 'users'
id = mapped_column(db.Integer, primary_key=True, autoincrement=True) id = db.Column(db.Integer, primary_key=True, autoincrement=True)
username = mapped_column(db.String(255), unique=True, nullable=False) username = db.Column(db.String(255), unique=True, nullable=False)
email = mapped_column(db.String(255), unique=True, nullable=False) email = db.Column(db.String(255), unique=True, nullable=False)
password = mapped_column(db.String(255), nullable=False) password = db.Column(db.String(255), nullable=False)
created_at = mapped_column(db.DateTime, nullable=False) created_at = db.Column(db.DateTime, nullable=False)
admin = mapped_column(db.Boolean, default=False, nullable=False) admin = db.Column(db.Boolean, default=False, nullable=False)
first_name = mapped_column(db.String(80), nullable=True) first_name = db.Column(db.String(80), nullable=True)
last_name = mapped_column(db.String(80), nullable=True) last_name = db.Column(db.String(80), nullable=True)
birth_date = mapped_column(db.DateTime, nullable=True) birth_date = db.Column(db.DateTime, nullable=True)
location = mapped_column(db.String(80), nullable=True) location = db.Column(db.String(80), nullable=True)
bio = mapped_column(db.String(200), nullable=True) bio = db.Column(db.String(200), nullable=True)
picture = mapped_column(db.String(255), nullable=True) picture = db.Column(db.String(255), nullable=True)
timezone = mapped_column(db.String(50), nullable=True) timezone = db.Column(db.String(50), nullable=True)
date_format = mapped_column(db.String(50), nullable=True) date_format = db.Column(db.String(50), nullable=True)
# does the week start Monday? # does the week start Monday?
weekm = mapped_column(db.Boolean, default=False, nullable=False) weekm = db.Column(db.Boolean, default=False, nullable=False)
workouts = relationship( workouts = db.relationship(
'Workout', 'Workout',
lazy=True, lazy=True,
backref=db.backref('user', lazy='joined', single_parent=True), backref=db.backref('user', lazy='joined', single_parent=True),
) )
records = relationship( records = db.relationship(
'Record', 'Record',
lazy=True, lazy=True,
backref=db.backref('user', lazy='joined', single_parent=True), backref=db.backref('user', lazy='joined', single_parent=True),
) )
language = mapped_column(db.String(50), nullable=True) language = db.Column(db.String(50), nullable=True)
imperial_units = mapped_column(db.Boolean, default=False, nullable=False) imperial_units = db.Column(db.Boolean, default=False, nullable=False)
is_active = mapped_column(db.Boolean, default=False, nullable=False) is_active = db.Column(db.Boolean, default=False, nullable=False)
email_to_confirm = mapped_column(db.String(255), nullable=True) email_to_confirm = db.Column(db.String(255), nullable=True)
confirmation_token = mapped_column(db.String(255), nullable=True) confirmation_token = db.Column(db.String(255), nullable=True)
display_ascent = mapped_column(db.Boolean, default=True, nullable=False) display_ascent = db.Column(db.Boolean, default=True, nullable=False)
accepted_policy_date = mapped_column(db.DateTime, nullable=True) accepted_policy_date = db.Column(db.DateTime, nullable=True)
start_elevation_at_zero = mapped_column( start_elevation_at_zero = db.Column(
db.Boolean, default=True, nullable=False db.Boolean, default=True, nullable=False
) )
use_raw_gpx_speed = mapped_column( use_raw_gpx_speed = db.Column(db.Boolean, default=False, nullable=False)
db.Boolean, default=False, nullable=False
)
def __repr__(self) -> str: def __repr__(self) -> str:
return f'<User {self.username!r}>' return f'<User {self.username!r}>'
@ -138,7 +138,7 @@ class User(db.Model): # type: ignore
@workouts_count.expression # type: ignore @workouts_count.expression # type: ignore
def workouts_count(self) -> int: def workouts_count(self) -> int:
return ( return (
select(func.count(Workout.id)) select([func.count(Workout.id)])
.where(Workout.user_id == self.id) .where(Workout.user_id == self.id)
.label('workouts_count') .label('workouts_count')
) )
@ -225,24 +225,22 @@ class User(db.Model): # type: ignore
return serialized_user return serialized_user
class UserSportPreference(db.Model): # type: ignore class UserSportPreference(BaseModel):
__tablename__ = 'users_sports_preferences' __tablename__ = 'users_sports_preferences'
user_id = mapped_column( user_id = db.Column(
db.Integer, db.Integer,
db.ForeignKey('users.id'), db.ForeignKey('users.id'),
primary_key=True, primary_key=True,
) )
sport_id = mapped_column( sport_id = db.Column(
db.Integer, db.Integer,
db.ForeignKey('sports.id'), db.ForeignKey('sports.id'),
primary_key=True, primary_key=True,
) )
color = mapped_column(db.String(50), nullable=True) color = db.Column(db.String(50), nullable=True)
is_active = mapped_column(db.Boolean, default=True, nullable=False) is_active = db.Column(db.Boolean, default=True, nullable=False)
stopped_speed_threshold = mapped_column( stopped_speed_threshold = db.Column(db.Float, default=1.0, nullable=False)
db.Float, default=1.0, nullable=False
)
def __init__( def __init__(
self, self,
@ -265,13 +263,13 @@ class UserSportPreference(db.Model): # type: ignore
} }
class BlacklistedToken(db.Model): # type: ignore class BlacklistedToken(BaseModel):
__tablename__ = 'blacklisted_tokens' __tablename__ = 'blacklisted_tokens'
id = mapped_column(db.Integer, primary_key=True, autoincrement=True) id = db.Column(db.Integer, primary_key=True, autoincrement=True)
token = mapped_column(db.String(500), unique=True, nullable=False) token = db.Column(db.String(500), unique=True, nullable=False)
expired_at = mapped_column(db.Integer, nullable=False) expired_at = db.Column(db.Integer, nullable=False)
blacklisted_on = mapped_column(db.DateTime, nullable=False) blacklisted_on = db.Column(db.DateTime, nullable=False)
def __init__( def __init__(
self, token: str, blacklisted_on: Optional[datetime] = None self, token: str, blacklisted_on: Optional[datetime] = None
@ -292,25 +290,25 @@ class BlacklistedToken(db.Model): # type: ignore
return cls.query.filter_by(token=str(auth_token)).first() is not None return cls.query.filter_by(token=str(auth_token)).first() is not None
class UserDataExport(db.Model): # type: ignore class UserDataExport(BaseModel):
__tablename__ = 'users_data_export' __tablename__ = 'users_data_export'
id = mapped_column(db.Integer, primary_key=True, autoincrement=True) id = db.Column(db.Integer, primary_key=True, autoincrement=True)
user_id = mapped_column( user_id = db.Column(
db.Integer, db.Integer,
db.ForeignKey('users.id', ondelete='CASCADE'), db.ForeignKey('users.id', ondelete='CASCADE'),
index=True, index=True,
unique=True, unique=True,
) )
created_at = mapped_column( created_at = db.Column(
db.DateTime, nullable=False, default=datetime.utcnow db.DateTime, nullable=False, default=datetime.utcnow
) )
updated_at = mapped_column( updated_at = db.Column(
db.DateTime, nullable=True, onupdate=datetime.utcnow db.DateTime, nullable=True, onupdate=datetime.utcnow
) )
completed = mapped_column(db.Boolean, nullable=False, default=False) completed = db.Column(db.Boolean, nullable=False, default=False)
file_name = mapped_column(db.String(100), nullable=True) file_name = db.Column(db.String(100), nullable=True)
file_size = mapped_column(db.Integer, nullable=True) file_size = db.Column(db.Integer, nullable=True)
def __init__( def __init__(
self, self,

View File

@ -55,6 +55,6 @@ def clean_blacklisted_tokens(days: int) -> int:
""" """
sql = """ sql = """
DELETE FROM blacklisted_tokens DELETE FROM blacklisted_tokens
WHERE blacklisted_tokens.expired_at < :limit; WHERE blacklisted_tokens.expired_at < %(limit)s;
""" """
return clean(sql, days) return clean(sql, days)

View File

@ -3,7 +3,6 @@ from datetime import timedelta
from typing import Optional from typing import Optional
import humanize import humanize
from sqlalchemy.sql import text
from fittrackee import db from fittrackee import db
@ -27,6 +26,5 @@ def get_readable_duration(duration: int, locale: Optional[str] = None) -> str:
def clean(sql: str, days: int) -> int: def clean(sql: str, days: int) -> int:
limit = int(time.time()) - (days * 86400) limit = int(time.time()) - (days * 86400)
result = db.session.execute(text(sql), {'limit': limit}) result = db.engine.execute(sql, {'limit': limit})
# DELETE statement returns CursorResult with the rowcount attribute return result.rowcount
return result.rowcount # type: ignore

View File

@ -1,13 +1,13 @@
import datetime
import os import os
from datetime import datetime, timedelta
from typing import Any, Dict, Optional, Union from typing import Any, Dict, Optional, Union
from uuid import UUID, uuid4 from uuid import UUID, uuid4
from sqlalchemy.dialects import postgresql from sqlalchemy.dialects import postgresql
from sqlalchemy.engine.base import Connection from sqlalchemy.engine.base import Connection
from sqlalchemy.event import listens_for from sqlalchemy.event import listens_for
from sqlalchemy.ext.declarative import DeclarativeMeta
from sqlalchemy.ext.hybrid import hybrid_property from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.orm import mapped_column, relationship
from sqlalchemy.orm.mapper import Mapper from sqlalchemy.orm.mapper import Mapper
from sqlalchemy.orm.session import Session, object_session from sqlalchemy.orm.session import Session, object_session
from sqlalchemy.types import JSON, Enum from sqlalchemy.types import JSON, Enum
@ -18,6 +18,7 @@ from fittrackee.files import get_absolute_file_path
from .utils.convert import convert_in_duration, convert_value_to_integer from .utils.convert import convert_in_duration, convert_value_to_integer
from .utils.short_id import encode_uuid from .utils.short_id import encode_uuid
BaseModel: DeclarativeMeta = db.Model
record_types = [ record_types = [
'AS', # 'Best Average Speed' 'AS', # 'Best Average Speed'
'FD', # 'Farthest Distance' 'FD', # 'Farthest Distance'
@ -66,20 +67,18 @@ def update_records(
) )
class Sport(db.Model): # type: ignore class Sport(BaseModel):
__tablename__ = 'sports' __tablename__ = 'sports'
id = mapped_column(db.Integer, primary_key=True, autoincrement=True) id = db.Column(db.Integer, primary_key=True, autoincrement=True)
label = mapped_column(db.String(50), unique=True, nullable=False) label = db.Column(db.String(50), unique=True, nullable=False)
is_active = mapped_column(db.Boolean, default=True, nullable=False) is_active = db.Column(db.Boolean, default=True, nullable=False)
stopped_speed_threshold = mapped_column( stopped_speed_threshold = db.Column(db.Float, default=1.0, nullable=False)
db.Float, default=1.0, nullable=False workouts = db.relationship(
)
workouts = relationship(
'Workout', 'Workout',
lazy=True, lazy=True,
backref=db.backref('sport', lazy='joined', single_parent=True), backref=db.backref('sport', lazy='joined', single_parent=True),
) )
records = relationship( records = db.relationship(
'Record', 'Record',
lazy=True, lazy=True,
backref=db.backref('sport', lazy='joined', single_parent=True), backref=db.backref('sport', lazy='joined', single_parent=True),
@ -121,49 +120,51 @@ class Sport(db.Model): # type: ignore
return serialized_sport return serialized_sport
class Workout(db.Model): # type: ignore class Workout(BaseModel):
__tablename__ = 'workouts' __tablename__ = 'workouts'
id = mapped_column(db.Integer, primary_key=True, autoincrement=True) id = db.Column(db.Integer, primary_key=True, autoincrement=True)
uuid = mapped_column( uuid = db.Column(
postgresql.UUID(as_uuid=True), postgresql.UUID(as_uuid=True),
default=uuid4, default=uuid4,
unique=True, unique=True,
nullable=False, nullable=False,
) )
user_id = mapped_column( user_id = db.Column(
db.Integer, db.ForeignKey('users.id'), index=True, nullable=False db.Integer, db.ForeignKey('users.id'), index=True, nullable=False
) )
sport_id = mapped_column( sport_id = db.Column(
db.Integer, db.ForeignKey('sports.id'), index=True, nullable=False db.Integer, db.ForeignKey('sports.id'), index=True, nullable=False
) )
title = mapped_column(db.String(255), nullable=True) title = db.Column(db.String(255), nullable=True)
gpx = mapped_column(db.String(255), nullable=True) gpx = db.Column(db.String(255), nullable=True)
creation_date = mapped_column(db.DateTime, default=datetime.utcnow) creation_date = db.Column(db.DateTime, default=datetime.datetime.utcnow)
modification_date = mapped_column(db.DateTime, onupdate=datetime.utcnow) modification_date = db.Column(
workout_date = mapped_column(db.DateTime, index=True, nullable=False) db.DateTime, onupdate=datetime.datetime.utcnow
duration = mapped_column(db.Interval, nullable=False) )
pauses = mapped_column(db.Interval, nullable=True) workout_date = db.Column(db.DateTime, index=True, nullable=False)
moving = mapped_column(db.Interval, nullable=True) duration = db.Column(db.Interval, nullable=False)
distance = mapped_column(db.Numeric(6, 3), nullable=True) # kilometers pauses = db.Column(db.Interval, nullable=True)
min_alt = mapped_column(db.Numeric(6, 2), nullable=True) # meters moving = db.Column(db.Interval, nullable=True)
max_alt = mapped_column(db.Numeric(6, 2), nullable=True) # meters distance = db.Column(db.Numeric(6, 3), nullable=True) # kilometers
descent = mapped_column(db.Numeric(8, 3), nullable=True) # meters min_alt = db.Column(db.Numeric(6, 2), nullable=True) # meters
ascent = mapped_column(db.Numeric(8, 3), nullable=True) # meters max_alt = db.Column(db.Numeric(6, 2), nullable=True) # meters
max_speed = mapped_column(db.Numeric(6, 2), nullable=True) # km/h descent = db.Column(db.Numeric(8, 3), nullable=True) # meters
ave_speed = mapped_column(db.Numeric(6, 2), nullable=True) # km/h ascent = db.Column(db.Numeric(8, 3), nullable=True) # meters
bounds = mapped_column(postgresql.ARRAY(db.Float), nullable=True) max_speed = db.Column(db.Numeric(6, 2), nullable=True) # km/h
map = mapped_column(db.String(255), nullable=True) ave_speed = db.Column(db.Numeric(6, 2), nullable=True) # km/h
map_id = mapped_column(db.String(50), index=True, nullable=True) bounds = db.Column(postgresql.ARRAY(db.Float), nullable=True)
weather_start = mapped_column(JSON, nullable=True) map = db.Column(db.String(255), nullable=True)
weather_end = mapped_column(JSON, nullable=True) map_id = db.Column(db.String(50), index=True, nullable=True)
notes = mapped_column(db.String(500), nullable=True) weather_start = db.Column(JSON, nullable=True)
segments = relationship( weather_end = db.Column(JSON, nullable=True)
notes = db.Column(db.String(500), nullable=True)
segments = db.relationship(
'WorkoutSegment', 'WorkoutSegment',
lazy=True, lazy=True,
cascade='all, delete', cascade='all, delete',
backref=db.backref('workout', lazy='joined', single_parent=True), backref=db.backref('workout', lazy='joined', single_parent=True),
) )
records = relationship( records = db.relationship(
'Record', 'Record',
lazy=True, lazy=True,
cascade='all, delete', cascade='all, delete',
@ -177,9 +178,9 @@ class Workout(db.Model): # type: ignore
self, self,
user_id: int, user_id: int,
sport_id: int, sport_id: int,
workout_date: datetime, workout_date: datetime.datetime,
distance: float, distance: float,
duration: timedelta, duration: datetime.timedelta,
) -> None: ) -> None:
self.user_id = user_id self.user_id = user_id
self.sport_id = sport_id self.sport_id = sport_id
@ -241,10 +242,11 @@ class Workout(db.Model): # type: ignore
Workout.sport_id == sport_id if sport_id else True, Workout.sport_id == sport_id if sport_id else True,
Workout.workout_date <= self.workout_date, Workout.workout_date <= self.workout_date,
Workout.workout_date Workout.workout_date
>= datetime.strptime(date_from, '%Y-%m-%d') >= datetime.datetime.strptime(date_from, '%Y-%m-%d')
if date_from if date_from
else True, else True,
Workout.workout_date <= datetime.strptime(date_to, '%Y-%m-%d') Workout.workout_date
<= datetime.datetime.strptime(date_to, '%Y-%m-%d')
if date_to if date_to
else True, else True,
Workout.distance >= float(distance_from) Workout.distance >= float(distance_from)
@ -282,10 +284,11 @@ class Workout(db.Model): # type: ignore
Workout.sport_id == sport_id if sport_id else True, Workout.sport_id == sport_id if sport_id else True,
Workout.workout_date >= self.workout_date, Workout.workout_date >= self.workout_date,
Workout.workout_date Workout.workout_date
>= datetime.strptime(date_from, '%Y-%m-%d') >= datetime.datetime.strptime(date_from, '%Y-%m-%d')
if date_from if date_from
else True, else True,
Workout.workout_date <= datetime.strptime(date_to, '%Y-%m-%d') Workout.workout_date
<= datetime.datetime.strptime(date_to, '%Y-%m-%d')
if date_to if date_to
else True, else True,
Workout.distance >= float(distance_from) Workout.distance >= float(distance_from)
@ -377,10 +380,9 @@ def on_workout_insert(
def on_workout_update( def on_workout_update(
mapper: Mapper, connection: Connection, workout: Workout mapper: Mapper, connection: Connection, workout: Workout
) -> None: ) -> None:
workout_session = object_session(workout) if object_session(workout).is_modified(
if workout_session is not None and workout_session.is_modified(
workout, include_collections=True workout, include_collections=True
): ): # noqa
@listens_for(db.Session, 'after_flush', once=True) @listens_for(db.Session, 'after_flush', once=True)
def receive_after_flush(session: Session, context: Any) -> None: def receive_after_flush(session: Session, context: Any) -> None:
@ -411,23 +413,23 @@ def on_workout_delete(
appLog.error('gpx file not found when deleting workout') appLog.error('gpx file not found when deleting workout')
class WorkoutSegment(db.Model): # type: ignore class WorkoutSegment(BaseModel):
__tablename__ = 'workout_segments' __tablename__ = 'workout_segments'
workout_id = mapped_column( workout_id = db.Column(
db.Integer, db.ForeignKey('workouts.id'), primary_key=True db.Integer, db.ForeignKey('workouts.id'), primary_key=True
) )
workout_uuid = mapped_column(postgresql.UUID(as_uuid=True), nullable=False) workout_uuid = db.Column(postgresql.UUID(as_uuid=True), nullable=False)
segment_id = mapped_column(db.Integer, primary_key=True) segment_id = db.Column(db.Integer, primary_key=True)
duration = mapped_column(db.Interval, nullable=False) duration = db.Column(db.Interval, nullable=False)
pauses = mapped_column(db.Interval, nullable=True) pauses = db.Column(db.Interval, nullable=True)
moving = mapped_column(db.Interval, nullable=True) moving = db.Column(db.Interval, nullable=True)
distance = mapped_column(db.Numeric(6, 3), nullable=True) # kilometers distance = db.Column(db.Numeric(6, 3), nullable=True) # kilometers
min_alt = mapped_column(db.Numeric(6, 2), nullable=True) # meters min_alt = db.Column(db.Numeric(6, 2), nullable=True) # meters
max_alt = mapped_column(db.Numeric(6, 2), nullable=True) # meters max_alt = db.Column(db.Numeric(6, 2), nullable=True) # meters
descent = mapped_column(db.Numeric(8, 3), nullable=True) # meters descent = db.Column(db.Numeric(8, 3), nullable=True) # meters
ascent = mapped_column(db.Numeric(8, 3), nullable=True) # meters ascent = db.Column(db.Numeric(8, 3), nullable=True) # meters
max_speed = mapped_column(db.Numeric(6, 2), nullable=True) # km/h max_speed = db.Column(db.Numeric(6, 2), nullable=True) # km/h
ave_speed = mapped_column(db.Numeric(6, 2), nullable=True) # km/h ave_speed = db.Column(db.Numeric(6, 2), nullable=True) # km/h
def __str__(self) -> str: def __str__(self) -> str:
return ( return (
@ -459,27 +461,25 @@ class WorkoutSegment(db.Model): # type: ignore
} }
class Record(db.Model): # type: ignore class Record(BaseModel):
__tablename__ = "records" __tablename__ = "records"
__table_args__ = ( __table_args__ = (
db.UniqueConstraint( db.UniqueConstraint(
'user_id', 'sport_id', 'record_type', name='user_sports_records' 'user_id', 'sport_id', 'record_type', name='user_sports_records'
), ),
) )
id = mapped_column(db.Integer, primary_key=True, autoincrement=True) id = db.Column(db.Integer, primary_key=True, autoincrement=True)
user_id = mapped_column( user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
db.Integer, db.ForeignKey('users.id'), nullable=False sport_id = db.Column(
)
sport_id = mapped_column(
db.Integer, db.ForeignKey('sports.id'), nullable=False db.Integer, db.ForeignKey('sports.id'), nullable=False
) )
workout_id = mapped_column( workout_id = db.Column(
db.Integer, db.ForeignKey('workouts.id'), nullable=False db.Integer, db.ForeignKey('workouts.id'), nullable=False
) )
workout_uuid = mapped_column(postgresql.UUID(as_uuid=True), nullable=False) workout_uuid = db.Column(postgresql.UUID(as_uuid=True), nullable=False)
record_type = mapped_column(Enum(*record_types, name="record_types")) record_type = db.Column(Enum(*record_types, name="record_types"))
workout_date = mapped_column(db.DateTime, nullable=False) workout_date = db.Column(db.DateTime, nullable=False)
_value = mapped_column("value", db.Integer, nullable=True) _value = db.Column("value", db.Integer, nullable=True)
def __str__(self) -> str: def __str__(self) -> str:
return ( return (
@ -497,11 +497,11 @@ class Record(db.Model): # type: ignore
self.workout_date = workout.workout_date self.workout_date = workout.workout_date
@hybrid_property @hybrid_property
def value(self) -> Optional[Union[timedelta, float]]: def value(self) -> Optional[Union[datetime.timedelta, float]]:
if self._value is None: if self._value is None:
return None return None
if self.record_type == 'LD': if self.record_type == 'LD':
return timedelta(seconds=self._value) return datetime.timedelta(seconds=self._value)
elif self.record_type in ['AS', 'MS']: elif self.record_type in ['AS', 'MS']:
return float(self._value / 100) return float(self._value / 100)
else: # 'FD' or 'HA' else: # 'FD' or 'HA'

117
poetry.lock generated
View File

@ -738,18 +738,18 @@ Flask-SQLAlchemy = ">=1.0"
[[package]] [[package]]
name = "flask-sqlalchemy" name = "flask-sqlalchemy"
version = "3.1.1" version = "3.0.5"
description = "Add SQLAlchemy support to your Flask application." description = "Add SQLAlchemy support to your Flask application."
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.7"
files = [ files = [
{file = "flask_sqlalchemy-3.1.1-py3-none-any.whl", hash = "sha256:4ba4be7f419dc72f4efd8802d69974803c37259dd42f3913b0dcf75c9447e0a0"}, {file = "flask_sqlalchemy-3.0.5-py3-none-any.whl", hash = "sha256:cabb6600ddd819a9f859f36515bb1bd8e7dbf30206cc679d2b081dff9e383283"},
{file = "flask_sqlalchemy-3.1.1.tar.gz", hash = "sha256:e4b68bb881802dda1a7d878b2fc84c06d1ee57fb40b874d3dc97dabfa36b8312"}, {file = "flask_sqlalchemy-3.0.5.tar.gz", hash = "sha256:c5765e58ca145401b52106c0f46178569243c5da25556be2c231ecc60867c5b1"},
] ]
[package.dependencies] [package.dependencies]
flask = ">=2.2.5" flask = ">=2.2.5"
sqlalchemy = ">=2.0.16" sqlalchemy = ">=1.4.18"
[[package]] [[package]]
name = "freezegun" name = "freezegun"
@ -2293,80 +2293,73 @@ test = ["pytest"]
[[package]] [[package]]
name = "sqlalchemy" name = "sqlalchemy"
version = "2.0.21" version = "1.4.49"
description = "Database Abstraction Library" description = "Database Abstraction Library"
optional = false optional = false
python-versions = ">=3.7" python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7"
files = [ files = [
{file = "SQLAlchemy-2.0.21-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1e7dc99b23e33c71d720c4ae37ebb095bebebbd31a24b7d99dfc4753d2803ede"}, {file = "SQLAlchemy-1.4.49-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:2e126cf98b7fd38f1e33c64484406b78e937b1a280e078ef558b95bf5b6895f6"},
{file = "SQLAlchemy-2.0.21-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7f0c4ee579acfe6c994637527c386d1c22eb60bc1c1d36d940d8477e482095d4"}, {file = "SQLAlchemy-1.4.49-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:03db81b89fe7ef3857b4a00b63dedd632d6183d4ea5a31c5d8a92e000a41fc71"},
{file = "SQLAlchemy-2.0.21-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f7d57a7e140efe69ce2d7b057c3f9a595f98d0bbdfc23fd055efdfbaa46e3a5"}, {file = "SQLAlchemy-1.4.49-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:95b9df9afd680b7a3b13b38adf6e3a38995da5e162cc7524ef08e3be4e5ed3e1"},
{file = "SQLAlchemy-2.0.21-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ca38746eac23dd7c20bec9278d2058c7ad662b2f1576e4c3dbfcd7c00cc48fa"}, {file = "SQLAlchemy-1.4.49-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a63e43bf3f668c11bb0444ce6e809c1227b8f067ca1068898f3008a273f52b09"},
{file = "SQLAlchemy-2.0.21-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:3cf229704074bce31f7f47d12883afee3b0a02bb233a0ba45ddbfe542939cca4"}, {file = "SQLAlchemy-1.4.49-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f835c050ebaa4e48b18403bed2c0fda986525896efd76c245bdd4db995e51a4c"},
{file = "SQLAlchemy-2.0.21-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fb87f763b5d04a82ae84ccff25554ffd903baafba6698e18ebaf32561f2fe4aa"}, {file = "SQLAlchemy-1.4.49-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c21b172dfb22e0db303ff6419451f0cac891d2e911bb9fbf8003d717f1bcf91"},
{file = "SQLAlchemy-2.0.21-cp310-cp310-win32.whl", hash = "sha256:89e274604abb1a7fd5c14867a412c9d49c08ccf6ce3e1e04fffc068b5b6499d4"}, {file = "SQLAlchemy-1.4.49-cp310-cp310-win32.whl", hash = "sha256:5fb1ebdfc8373b5a291485757bd6431de8d7ed42c27439f543c81f6c8febd729"},
{file = "SQLAlchemy-2.0.21-cp310-cp310-win_amd64.whl", hash = "sha256:e36339a68126ffb708dc6d1948161cea2a9e85d7d7b0c54f6999853d70d44430"}, {file = "SQLAlchemy-1.4.49-cp310-cp310-win_amd64.whl", hash = "sha256:f8a65990c9c490f4651b5c02abccc9f113a7f56fa482031ac8cb88b70bc8ccaa"},
{file = "SQLAlchemy-2.0.21-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:bf8eebccc66829010f06fbd2b80095d7872991bfe8415098b9fe47deaaa58063"}, {file = "SQLAlchemy-1.4.49-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8923dfdf24d5aa8a3adb59723f54118dd4fe62cf59ed0d0d65d940579c1170a4"},
{file = "SQLAlchemy-2.0.21-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b977bfce15afa53d9cf6a632482d7968477625f030d86a109f7bdfe8ce3c064a"}, {file = "SQLAlchemy-1.4.49-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9ab2c507a7a439f13ca4499db6d3f50423d1d65dc9b5ed897e70941d9e135b0"},
{file = "SQLAlchemy-2.0.21-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ff3dc2f60dbf82c9e599c2915db1526d65415be323464f84de8db3e361ba5b9"}, {file = "SQLAlchemy-1.4.49-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5debe7d49b8acf1f3035317e63d9ec8d5e4d904c6e75a2a9246a119f5f2fdf3d"},
{file = "SQLAlchemy-2.0.21-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:44ac5c89b6896f4740e7091f4a0ff2e62881da80c239dd9408f84f75a293dae9"}, {file = "SQLAlchemy-1.4.49-cp311-cp311-win32.whl", hash = "sha256:82b08e82da3756765c2e75f327b9bf6b0f043c9c3925fb95fb51e1567fa4ee87"},
{file = "SQLAlchemy-2.0.21-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:87bf91ebf15258c4701d71dcdd9c4ba39521fb6a37379ea68088ce8cd869b446"}, {file = "SQLAlchemy-1.4.49-cp311-cp311-win_amd64.whl", hash = "sha256:171e04eeb5d1c0d96a544caf982621a1711d078dbc5c96f11d6469169bd003f1"},
{file = "SQLAlchemy-2.0.21-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:b69f1f754d92eb1cc6b50938359dead36b96a1dcf11a8670bff65fd9b21a4b09"}, {file = "SQLAlchemy-1.4.49-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:36e58f8c4fe43984384e3fbe6341ac99b6b4e083de2fe838f0fdb91cebe9e9cb"},
{file = "SQLAlchemy-2.0.21-cp311-cp311-win32.whl", hash = "sha256:af520a730d523eab77d754f5cf44cc7dd7ad2d54907adeb3233177eeb22f271b"}, {file = "SQLAlchemy-1.4.49-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b31e67ff419013f99ad6f8fc73ee19ea31585e1e9fe773744c0f3ce58c039c30"},
{file = "SQLAlchemy-2.0.21-cp311-cp311-win_amd64.whl", hash = "sha256:141675dae56522126986fa4ca713739d00ed3a6f08f3c2eb92c39c6dfec463ce"}, {file = "SQLAlchemy-1.4.49-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c14b29d9e1529f99efd550cd04dbb6db6ba5d690abb96d52de2bff4ed518bc95"},
{file = "SQLAlchemy-2.0.21-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:7614f1eab4336df7dd6bee05bc974f2b02c38d3d0c78060c5faa4cd1ca2af3b8"}, {file = "SQLAlchemy-1.4.49-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c40f3470e084d31247aea228aa1c39bbc0904c2b9ccbf5d3cfa2ea2dac06f26d"},
{file = "SQLAlchemy-2.0.21-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d59cb9e20d79686aa473e0302e4a82882d7118744d30bb1dfb62d3c47141b3ec"}, {file = "SQLAlchemy-1.4.49-cp36-cp36m-win32.whl", hash = "sha256:706bfa02157b97c136547c406f263e4c6274a7b061b3eb9742915dd774bbc264"},
{file = "SQLAlchemy-2.0.21-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a95aa0672e3065d43c8aa80080cdd5cc40fe92dc873749e6c1cf23914c4b83af"}, {file = "SQLAlchemy-1.4.49-cp36-cp36m-win_amd64.whl", hash = "sha256:a7f7b5c07ae5c0cfd24c2db86071fb2a3d947da7bd487e359cc91e67ac1c6d2e"},
{file = "SQLAlchemy-2.0.21-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:8c323813963b2503e54d0944813cd479c10c636e3ee223bcbd7bd478bf53c178"}, {file = "SQLAlchemy-1.4.49-cp37-cp37m-macosx_11_0_x86_64.whl", hash = "sha256:4afbbf5ef41ac18e02c8dc1f86c04b22b7a2125f2a030e25bbb4aff31abb224b"},
{file = "SQLAlchemy-2.0.21-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:419b1276b55925b5ac9b4c7044e999f1787c69761a3c9756dec6e5c225ceca01"}, {file = "SQLAlchemy-1.4.49-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:24e300c0c2147484a002b175f4e1361f102e82c345bf263242f0449672a4bccf"},
{file = "SQLAlchemy-2.0.21-cp37-cp37m-win32.whl", hash = "sha256:4615623a490e46be85fbaa6335f35cf80e61df0783240afe7d4f544778c315a9"}, {file = "SQLAlchemy-1.4.49-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:201de072b818f8ad55c80d18d1a788729cccf9be6d9dc3b9d8613b053cd4836d"},
{file = "SQLAlchemy-2.0.21-cp37-cp37m-win_amd64.whl", hash = "sha256:cca720d05389ab1a5877ff05af96551e58ba65e8dc65582d849ac83ddde3e231"}, {file = "SQLAlchemy-1.4.49-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7653ed6817c710d0c95558232aba799307d14ae084cc9b1f4c389157ec50df5c"},
{file = "SQLAlchemy-2.0.21-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b4eae01faee9f2b17f08885e3f047153ae0416648f8e8c8bd9bc677c5ce64be9"}, {file = "SQLAlchemy-1.4.49-cp37-cp37m-win32.whl", hash = "sha256:647e0b309cb4512b1f1b78471fdaf72921b6fa6e750b9f891e09c6e2f0e5326f"},
{file = "SQLAlchemy-2.0.21-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3eb7c03fe1cd3255811cd4e74db1ab8dca22074d50cd8937edf4ef62d758cdf4"}, {file = "SQLAlchemy-1.4.49-cp37-cp37m-win_amd64.whl", hash = "sha256:ab73ed1a05ff539afc4a7f8cf371764cdf79768ecb7d2ec691e3ff89abbc541e"},
{file = "SQLAlchemy-2.0.21-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c2d494b6a2a2d05fb99f01b84cc9af9f5f93bf3e1e5dbdafe4bed0c2823584c1"}, {file = "SQLAlchemy-1.4.49-cp38-cp38-macosx_11_0_x86_64.whl", hash = "sha256:37ce517c011560d68f1ffb28af65d7e06f873f191eb3a73af5671e9c3fada08a"},
{file = "SQLAlchemy-2.0.21-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b19ae41ef26c01a987e49e37c77b9ad060c59f94d3b3efdfdbf4f3daaca7b5fe"}, {file = "SQLAlchemy-1.4.49-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1878ce508edea4a879015ab5215546c444233881301e97ca16fe251e89f1c55"},
{file = "SQLAlchemy-2.0.21-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:fc6b15465fabccc94bf7e38777d665b6a4f95efd1725049d6184b3a39fd54880"}, {file = "SQLAlchemy-1.4.49-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0e8e608983e6f85d0852ca61f97e521b62e67969e6e640fe6c6b575d4db68557"},
{file = "SQLAlchemy-2.0.21-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:014794b60d2021cc8ae0f91d4d0331fe92691ae5467a00841f7130fe877b678e"}, {file = "SQLAlchemy-1.4.49-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ccf956da45290df6e809ea12c54c02ace7f8ff4d765d6d3dfb3655ee876ce58d"},
{file = "SQLAlchemy-2.0.21-cp38-cp38-win32.whl", hash = "sha256:0268256a34806e5d1c8f7ee93277d7ea8cc8ae391f487213139018b6805aeaf6"}, {file = "SQLAlchemy-1.4.49-cp38-cp38-win32.whl", hash = "sha256:f167c8175ab908ce48bd6550679cc6ea20ae169379e73c7720a28f89e53aa532"},
{file = "SQLAlchemy-2.0.21-cp38-cp38-win_amd64.whl", hash = "sha256:73c079e21d10ff2be54a4699f55865d4b275fd6c8bd5d90c5b1ef78ae0197301"}, {file = "SQLAlchemy-1.4.49-cp38-cp38-win_amd64.whl", hash = "sha256:45806315aae81a0c202752558f0df52b42d11dd7ba0097bf71e253b4215f34f4"},
{file = "SQLAlchemy-2.0.21-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:785e2f2c1cb50d0a44e2cdeea5fd36b5bf2d79c481c10f3a88a8be4cfa2c4615"}, {file = "SQLAlchemy-1.4.49-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:b6d0c4b15d65087738a6e22e0ff461b407533ff65a73b818089efc8eb2b3e1de"},
{file = "SQLAlchemy-2.0.21-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c111cd40910ffcb615b33605fc8f8e22146aeb7933d06569ac90f219818345ef"}, {file = "SQLAlchemy-1.4.49-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a843e34abfd4c797018fd8d00ffffa99fd5184c421f190b6ca99def4087689bd"},
{file = "SQLAlchemy-2.0.21-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9cba4e7369de663611ce7460a34be48e999e0bbb1feb9130070f0685e9a6b66"}, {file = "SQLAlchemy-1.4.49-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1c890421651b45a681181301b3497e4d57c0d01dc001e10438a40e9a9c25ee77"},
{file = "SQLAlchemy-2.0.21-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50a69067af86ec7f11a8e50ba85544657b1477aabf64fa447fd3736b5a0a4f67"}, {file = "SQLAlchemy-1.4.49-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d26f280b8f0a8f497bc10573849ad6dc62e671d2468826e5c748d04ed9e670d5"},
{file = "SQLAlchemy-2.0.21-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ccb99c3138c9bde118b51a289d90096a3791658da9aea1754667302ed6564f6e"}, {file = "SQLAlchemy-1.4.49-cp39-cp39-win32.whl", hash = "sha256:ec2268de67f73b43320383947e74700e95c6770d0c68c4e615e9897e46296294"},
{file = "SQLAlchemy-2.0.21-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:513fd5b6513d37e985eb5b7ed89da5fd9e72354e3523980ef00d439bc549c9e9"}, {file = "SQLAlchemy-1.4.49-cp39-cp39-win_amd64.whl", hash = "sha256:bbdf16372859b8ed3f4d05f925a984771cd2abd18bd187042f24be4886c2a15f"},
{file = "SQLAlchemy-2.0.21-cp39-cp39-win32.whl", hash = "sha256:f9fefd6298433b6e9188252f3bff53b9ff0443c8fde27298b8a2b19f6617eeb9"}, {file = "SQLAlchemy-1.4.49.tar.gz", hash = "sha256:06ff25cbae30c396c4b7737464f2a7fc37a67b7da409993b182b024cec80aed9"},
{file = "SQLAlchemy-2.0.21-cp39-cp39-win_amd64.whl", hash = "sha256:2e617727fe4091cedb3e4409b39368f424934c7faa78171749f704b49b4bb4ce"},
{file = "SQLAlchemy-2.0.21-py3-none-any.whl", hash = "sha256:ea7da25ee458d8f404b93eb073116156fd7d8c2a776d8311534851f28277b4ce"},
{file = "SQLAlchemy-2.0.21.tar.gz", hash = "sha256:05b971ab1ac2994a14c56b35eaaa91f86ba080e9ad481b20d99d77f381bb6258"},
] ]
[package.dependencies] [package.dependencies]
greenlet = {version = "!=0.4.17", markers = "platform_machine == \"win32\" or platform_machine == \"WIN32\" or platform_machine == \"AMD64\" or platform_machine == \"amd64\" or platform_machine == \"x86_64\" or platform_machine == \"ppc64le\" or platform_machine == \"aarch64\""} greenlet = {version = "!=0.4.17", markers = "python_version >= \"3\" and (platform_machine == \"win32\" or platform_machine == \"WIN32\" or platform_machine == \"AMD64\" or platform_machine == \"amd64\" or platform_machine == \"x86_64\" or platform_machine == \"ppc64le\" or platform_machine == \"aarch64\")"}
typing-extensions = ">=4.2.0"
[package.extras] [package.extras]
aiomysql = ["aiomysql (>=0.2.0)", "greenlet (!=0.4.17)"] aiomysql = ["aiomysql", "greenlet (!=0.4.17)"]
aiosqlite = ["aiosqlite", "greenlet (!=0.4.17)", "typing-extensions (!=3.10.0.1)"] aiosqlite = ["aiosqlite", "greenlet (!=0.4.17)", "typing-extensions (!=3.10.0.1)"]
asyncio = ["greenlet (!=0.4.17)"] asyncio = ["greenlet (!=0.4.17)"]
asyncmy = ["asyncmy (>=0.2.3,!=0.2.4,!=0.2.6)", "greenlet (!=0.4.17)"] asyncmy = ["asyncmy (>=0.2.3,!=0.2.4)", "greenlet (!=0.4.17)"]
mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2,!=1.1.5)"] mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2)"]
mssql = ["pyodbc"] mssql = ["pyodbc"]
mssql-pymssql = ["pymssql"] mssql-pymssql = ["pymssql"]
mssql-pyodbc = ["pyodbc"] mssql-pyodbc = ["pyodbc"]
mypy = ["mypy (>=0.910)"] mypy = ["mypy (>=0.910)", "sqlalchemy2-stubs"]
mysql = ["mysqlclient (>=1.4.0)"] mysql = ["mysqlclient (>=1.4.0)", "mysqlclient (>=1.4.0,<2)"]
mysql-connector = ["mysql-connector-python"] mysql-connector = ["mysql-connector-python"]
oracle = ["cx-oracle (>=7)"] oracle = ["cx-oracle (>=7)", "cx-oracle (>=7,<8)"]
oracle-oracledb = ["oracledb (>=1.0.1)"]
postgresql = ["psycopg2 (>=2.7)"] postgresql = ["psycopg2 (>=2.7)"]
postgresql-asyncpg = ["asyncpg", "greenlet (!=0.4.17)"] postgresql-asyncpg = ["asyncpg", "greenlet (!=0.4.17)"]
postgresql-pg8000 = ["pg8000 (>=1.29.1)"] postgresql-pg8000 = ["pg8000 (>=1.16.6,!=1.29.0)"]
postgresql-psycopg = ["psycopg (>=3.0.7)"]
postgresql-psycopg2binary = ["psycopg2-binary"] postgresql-psycopg2binary = ["psycopg2-binary"]
postgresql-psycopg2cffi = ["psycopg2cffi"] postgresql-psycopg2cffi = ["psycopg2cffi"]
postgresql-psycopgbinary = ["psycopg[binary] (>=3.0.7)"] pymysql = ["pymysql", "pymysql (<1)"]
pymysql = ["pymysql"]
sqlcipher = ["sqlcipher3-binary"] sqlcipher = ["sqlcipher3-binary"]
[[package]] [[package]]
@ -2720,4 +2713,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = "^3.8.1" python-versions = "^3.8.1"
content-hash = "fe398b8d9c8bb4223284e01983948f0837ef862554b2673705fb8de963f3ed4b" content-hash = "8455a7b70d2a966152da804dcbaa3780bb516953105ce2b73c7e2442432f3ae6"

View File

@ -33,7 +33,6 @@ flask = "^3.0"
flask-bcrypt = "^1.0" flask-bcrypt = "^1.0"
flask-dramatiq = "^0.6" flask-dramatiq = "^0.6"
flask-limiter = {version = "^3.5", extras = ["redis"]} flask-limiter = {version = "^3.5", extras = ["redis"]}
flask-sqlalchemy = "^3.1"
flask-migrate = "^4.0" flask-migrate = "^4.0"
gpxpy = "=1.5.0" gpxpy = "=1.5.0"
gunicorn = "^21.0" gunicorn = "^21.0"
@ -44,7 +43,7 @@ pyopenssl = "^23.2"
pytz = "^2023.3" pytz = "^2023.3"
shortuuid = "^1.0.11" shortuuid = "^1.0.11"
staticmap = "^0.5.7" staticmap = "^0.5.7"
sqlalchemy = "=2.0.21" sqlalchemy = "=1.4.49"
ua-parser = "^0.18.0" ua-parser = "^0.18.0"
[tool.poetry.group.dev.dependencies] [tool.poetry.group.dev.dependencies]