Merge pull request #440 from SamR1/flask-sqlalchemy-update-rollback
API - FlaskSQLAlchemy update rollback
This commit is contained in:
		@@ -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()
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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.')
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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()
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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
									
									
									
								
							
							
						
						
									
										117
									
								
								poetry.lock
									
									
									
										generated
									
									
									
								
							@@ -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"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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]
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user