API - update to latest version of Flask-SQLAlchemy

(and to SQLAlchemy 2.x)
This commit is contained in:
Sam 2023-10-02 18:57:53 +02:00
parent fecd93a7b4
commit 7e65eb8334
12 changed files with 265 additions and 221 deletions

View File

@ -20,6 +20,7 @@ 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
@ -38,7 +39,12 @@ 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,29 +5,30 @@ 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.ext.declarative import DeclarativeMeta from sqlalchemy.orm import mapped_column
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 = db.Column(db.Integer, primary_key=True, autoincrement=True) id = mapped_column(db.Integer, primary_key=True, autoincrement=True)
max_users = db.Column(db.Integer, default=0, nullable=False) max_users = mapped_column(db.Integer, default=0, nullable=False)
gpx_limit_import = db.Column(db.Integer, default=10, nullable=False) gpx_limit_import = mapped_column(db.Integer, default=10, nullable=False)
max_single_file_size = db.Column( max_single_file_size = mapped_column(
db.Integer, default=1048576, nullable=False db.Integer, default=1048576, nullable=False
) )
max_zip_file_size = db.Column(db.Integer, default=10485760, nullable=False) max_zip_file_size = mapped_column(
admin_contact = db.Column(db.String(255), nullable=True) db.Integer, default=10485760, nullable=False
privacy_policy_date = db.Column(db.DateTime, nullable=True) )
privacy_policy = db.Column(db.Text, nullable=True) admin_contact = mapped_column(db.String(255), nullable=True)
about = db.Column(db.Text, nullable=True) privacy_policy_date = mapped_column(db.DateTime, 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:
@ -36,8 +37,9 @@ class AppConfig(BaseModel):
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):
result = db.engine.execute("SELECT COUNT(*) FROM users;") query = db.session.execute(text("SELECT COUNT(*) FROM users;"))
nb_users = result.fetchone()[0] result = query.fetchone()
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,6 +3,7 @@ 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
@ -36,7 +37,7 @@ def drop_db() -> None:
err=True, err=True,
) )
return return
db.engine.execute("DROP TABLE IF EXISTS alembic_version;") db.session.execute(text("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)s; WHERE oauth2_token.issued_at + oauth2_token.expires_in < :limit;
""" """
return clean(sql, days) return clean(sql, days)

View File

@ -8,23 +8,22 @@ 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.ext.declarative import DeclarativeMeta 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 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 = db.Column(db.Integer, primary_key=True) id = mapped_column(db.Integer, primary_key=True)
user_id = db.Column( user_id = mapped_column(
db.Integer, db.ForeignKey('users.id', ondelete='CASCADE'), index=True db.Integer, db.ForeignKey('users.id', ondelete='CASCADE'), index=True
) )
user = db.relationship('User') user = relationship('User')
def serialize(self, with_secret: bool = False) -> Dict: def serialize(self, with_secret: bool = False) -> Dict:
client = { client = {
@ -63,7 +62,9 @@ def on_old_oauth2_delete(
).delete(synchronize_session=False) ).delete(synchronize_session=False)
class OAuth2AuthorizationCode(BaseModel, OAuth2AuthorizationCodeMixin): class OAuth2AuthorizationCode(
OAuth2AuthorizationCodeMixin, db.Model # type: ignore
):
__tablename__ = 'oauth2_code' __tablename__ = 'oauth2_code'
__table_args__ = ( __table_args__ = (
db.Index( db.Index(
@ -72,21 +73,21 @@ class OAuth2AuthorizationCode(BaseModel, OAuth2AuthorizationCodeMixin):
), ),
) )
id = db.Column(db.Integer, primary_key=True) id = mapped_column(db.Integer, primary_key=True)
user_id = db.Column( user_id = mapped_column(
db.Integer, db.ForeignKey('users.id', ondelete='CASCADE'), index=True db.Integer, db.ForeignKey('users.id', ondelete='CASCADE'), index=True
) )
user = db.relationship('User') user = relationship('User')
class OAuth2Token(BaseModel, OAuth2TokenMixin): class OAuth2Token(OAuth2TokenMixin, db.Model): # type: ignore
__tablename__ = 'oauth2_token' __tablename__ = 'oauth2_token'
id = db.Column(db.Integer, primary_key=True) id = mapped_column(db.Integer, primary_key=True)
user_id = db.Column( user_id = mapped_column(
db.Integer, db.ForeignKey('users.id', ondelete='CASCADE'), index=True db.Integer, db.ForeignKey('users.id', ondelete='CASCADE'), index=True
) )
user = db.relationship('User') user = relationship('User')
def is_refresh_token_active(self) -> bool: def is_refresh_token_active(self) -> bool:
if self.is_revoked(): if self.is_revoked():
@ -98,10 +99,10 @@ class OAuth2Token(BaseModel, OAuth2TokenMixin):
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)s SET access_token_revoked_at = :revoked_at
WHERE client_id = %(client_id)s; WHERE client_id = :client_id;
""" """
db.engine.execute( db.session.execute(
sql, {'client_id': client_id, 'revoked_at': int(time.time())} text(sql), {'client_id': client_id, 'revoked_at': int(time.time())}
) )
db.session.commit() db.session.commit()

View File

@ -23,6 +23,29 @@ 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 = db.Column(db.Integer, primary_key=True, autoincrement=True) id = mapped_column(db.Integer, primary_key=True, autoincrement=True)
username = db.Column(db.String(255), unique=True, nullable=False) username = mapped_column(db.String(255), unique=True, nullable=False)
email = db.Column(db.String(255), unique=True, nullable=False) email = mapped_column(db.String(255), unique=True, nullable=False)
password = db.Column(db.String(255), nullable=False) password = mapped_column(db.String(255), nullable=False)
created_at = db.Column(db.DateTime, nullable=False) created_at = mapped_column(db.DateTime, nullable=False)
admin = db.Column(db.Boolean, default=False, nullable=False) admin = mapped_column(db.Boolean, default=False, nullable=False)
first_name = db.Column(db.String(80), nullable=True) first_name = mapped_column(db.String(80), nullable=True)
last_name = db.Column(db.String(80), nullable=True) last_name = mapped_column(db.String(80), nullable=True)
birth_date = db.Column(db.DateTime, nullable=True) birth_date = mapped_column(db.DateTime, nullable=True)
location = db.Column(db.String(80), nullable=True) location = mapped_column(db.String(80), nullable=True)
bio = db.Column(db.String(200), nullable=True) bio = mapped_column(db.String(200), nullable=True)
picture = db.Column(db.String(255), nullable=True) picture = mapped_column(db.String(255), nullable=True)
timezone = db.Column(db.String(50), nullable=True) timezone = mapped_column(db.String(50), nullable=True)
date_format = db.Column(db.String(50), nullable=True) date_format = mapped_column(db.String(50), nullable=True)
# does the week start Monday? # does the week start Monday?
weekm = db.Column(db.Boolean, default=False, nullable=False) weekm = mapped_column(db.Boolean, default=False, nullable=False)
workouts = db.relationship( workouts = 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 = db.relationship( records = 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 = db.Column(db.String(50), nullable=True) language = mapped_column(db.String(50), nullable=True)
imperial_units = db.Column(db.Boolean, default=False, nullable=False) imperial_units = mapped_column(db.Boolean, default=False, nullable=False)
is_active = db.Column(db.Boolean, default=False, nullable=False) is_active = mapped_column(db.Boolean, default=False, nullable=False)
email_to_confirm = db.Column(db.String(255), nullable=True) email_to_confirm = mapped_column(db.String(255), nullable=True)
confirmation_token = db.Column(db.String(255), nullable=True) confirmation_token = mapped_column(db.String(255), nullable=True)
display_ascent = db.Column(db.Boolean, default=True, nullable=False) display_ascent = mapped_column(db.Boolean, default=True, nullable=False)
accepted_policy_date = db.Column(db.DateTime, nullable=True) accepted_policy_date = mapped_column(db.DateTime, nullable=True)
start_elevation_at_zero = db.Column( start_elevation_at_zero = mapped_column(
db.Boolean, default=True, nullable=False db.Boolean, default=True, nullable=False
) )
use_raw_gpx_speed = db.Column(db.Boolean, default=False, nullable=False) use_raw_gpx_speed = mapped_column(
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(BaseModel):
@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,22 +225,24 @@ class User(BaseModel):
return serialized_user return serialized_user
class UserSportPreference(BaseModel): class UserSportPreference(db.Model): # type: ignore
__tablename__ = 'users_sports_preferences' __tablename__ = 'users_sports_preferences'
user_id = db.Column( user_id = mapped_column(
db.Integer, db.Integer,
db.ForeignKey('users.id'), db.ForeignKey('users.id'),
primary_key=True, primary_key=True,
) )
sport_id = db.Column( sport_id = mapped_column(
db.Integer, db.Integer,
db.ForeignKey('sports.id'), db.ForeignKey('sports.id'),
primary_key=True, primary_key=True,
) )
color = db.Column(db.String(50), nullable=True) color = mapped_column(db.String(50), nullable=True)
is_active = db.Column(db.Boolean, default=True, nullable=False) is_active = mapped_column(db.Boolean, default=True, nullable=False)
stopped_speed_threshold = db.Column(db.Float, default=1.0, nullable=False) stopped_speed_threshold = mapped_column(
db.Float, default=1.0, nullable=False
)
def __init__( def __init__(
self, self,
@ -263,13 +265,13 @@ class UserSportPreference(BaseModel):
} }
class BlacklistedToken(BaseModel): class BlacklistedToken(db.Model): # type: ignore
__tablename__ = 'blacklisted_tokens' __tablename__ = 'blacklisted_tokens'
id = db.Column(db.Integer, primary_key=True, autoincrement=True) id = mapped_column(db.Integer, primary_key=True, autoincrement=True)
token = db.Column(db.String(500), unique=True, nullable=False) token = mapped_column(db.String(500), unique=True, nullable=False)
expired_at = db.Column(db.Integer, nullable=False) expired_at = mapped_column(db.Integer, nullable=False)
blacklisted_on = db.Column(db.DateTime, nullable=False) blacklisted_on = mapped_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
@ -290,25 +292,25 @@ class BlacklistedToken(BaseModel):
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(BaseModel): class UserDataExport(db.Model): # type: ignore
__tablename__ = 'users_data_export' __tablename__ = 'users_data_export'
id = db.Column(db.Integer, primary_key=True, autoincrement=True) id = mapped_column(db.Integer, primary_key=True, autoincrement=True)
user_id = db.Column( user_id = mapped_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 = db.Column( created_at = mapped_column(
db.DateTime, nullable=False, default=datetime.utcnow db.DateTime, nullable=False, default=datetime.utcnow
) )
updated_at = db.Column( updated_at = mapped_column(
db.DateTime, nullable=True, onupdate=datetime.utcnow db.DateTime, nullable=True, onupdate=datetime.utcnow
) )
completed = db.Column(db.Boolean, nullable=False, default=False) completed = mapped_column(db.Boolean, nullable=False, default=False)
file_name = db.Column(db.String(100), nullable=True) file_name = mapped_column(db.String(100), nullable=True)
file_size = db.Column(db.Integer, nullable=True) file_size = mapped_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)s; WHERE blacklisted_tokens.expired_at < :limit;
""" """
return clean(sql, days) return clean(sql, days)

View File

@ -3,6 +3,7 @@ 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
@ -26,5 +27,6 @@ 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.engine.execute(sql, {'limit': limit}) result = db.session.execute(text(sql), {'limit': limit})
return result.rowcount # DELETE statement returns CursorResult with the rowcount attribute
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,7 +18,6 @@ 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'
@ -67,18 +66,20 @@ def update_records(
) )
class Sport(BaseModel): class Sport(db.Model): # type: ignore
__tablename__ = 'sports' __tablename__ = 'sports'
id = db.Column(db.Integer, primary_key=True, autoincrement=True) id = mapped_column(db.Integer, primary_key=True, autoincrement=True)
label = db.Column(db.String(50), unique=True, nullable=False) label = mapped_column(db.String(50), unique=True, nullable=False)
is_active = db.Column(db.Boolean, default=True, nullable=False) is_active = mapped_column(db.Boolean, default=True, nullable=False)
stopped_speed_threshold = db.Column(db.Float, default=1.0, nullable=False) stopped_speed_threshold = mapped_column(
workouts = db.relationship( db.Float, default=1.0, nullable=False
)
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 = db.relationship( records = 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),
@ -120,51 +121,49 @@ class Sport(BaseModel):
return serialized_sport return serialized_sport
class Workout(BaseModel): class Workout(db.Model): # type: ignore
__tablename__ = 'workouts' __tablename__ = 'workouts'
id = db.Column(db.Integer, primary_key=True, autoincrement=True) id = mapped_column(db.Integer, primary_key=True, autoincrement=True)
uuid = db.Column( uuid = mapped_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 = db.Column( user_id = mapped_column(
db.Integer, db.ForeignKey('users.id'), index=True, nullable=False db.Integer, db.ForeignKey('users.id'), index=True, nullable=False
) )
sport_id = db.Column( sport_id = mapped_column(
db.Integer, db.ForeignKey('sports.id'), index=True, nullable=False db.Integer, db.ForeignKey('sports.id'), index=True, nullable=False
) )
title = db.Column(db.String(255), nullable=True) title = mapped_column(db.String(255), nullable=True)
gpx = db.Column(db.String(255), nullable=True) gpx = mapped_column(db.String(255), nullable=True)
creation_date = db.Column(db.DateTime, default=datetime.datetime.utcnow) creation_date = mapped_column(db.DateTime, default=datetime.utcnow)
modification_date = db.Column( modification_date = mapped_column(db.DateTime, onupdate=datetime.utcnow)
db.DateTime, onupdate=datetime.datetime.utcnow workout_date = mapped_column(db.DateTime, index=True, nullable=False)
) duration = mapped_column(db.Interval, nullable=False)
workout_date = db.Column(db.DateTime, index=True, nullable=False) pauses = mapped_column(db.Interval, nullable=True)
duration = db.Column(db.Interval, nullable=False) moving = mapped_column(db.Interval, nullable=True)
pauses = db.Column(db.Interval, nullable=True) distance = mapped_column(db.Numeric(6, 3), nullable=True) # kilometers
moving = db.Column(db.Interval, nullable=True) min_alt = mapped_column(db.Numeric(6, 2), nullable=True) # meters
distance = db.Column(db.Numeric(6, 3), nullable=True) # kilometers max_alt = mapped_column(db.Numeric(6, 2), nullable=True) # meters
min_alt = db.Column(db.Numeric(6, 2), nullable=True) # meters descent = mapped_column(db.Numeric(8, 3), nullable=True) # meters
max_alt = db.Column(db.Numeric(6, 2), nullable=True) # meters ascent = mapped_column(db.Numeric(8, 3), nullable=True) # meters
descent = db.Column(db.Numeric(8, 3), nullable=True) # meters max_speed = mapped_column(db.Numeric(6, 2), nullable=True) # km/h
ascent = db.Column(db.Numeric(8, 3), nullable=True) # meters ave_speed = mapped_column(db.Numeric(6, 2), nullable=True) # km/h
max_speed = db.Column(db.Numeric(6, 2), nullable=True) # km/h bounds = mapped_column(postgresql.ARRAY(db.Float), nullable=True)
ave_speed = db.Column(db.Numeric(6, 2), nullable=True) # km/h map = mapped_column(db.String(255), nullable=True)
bounds = db.Column(postgresql.ARRAY(db.Float), nullable=True) map_id = mapped_column(db.String(50), index=True, nullable=True)
map = db.Column(db.String(255), nullable=True) weather_start = mapped_column(JSON, nullable=True)
map_id = db.Column(db.String(50), index=True, nullable=True) weather_end = mapped_column(JSON, nullable=True)
weather_start = db.Column(JSON, nullable=True) notes = mapped_column(db.String(500), nullable=True)
weather_end = db.Column(JSON, nullable=True) segments = relationship(
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 = db.relationship( records = relationship(
'Record', 'Record',
lazy=True, lazy=True,
cascade='all, delete', cascade='all, delete',
@ -178,9 +177,9 @@ class Workout(BaseModel):
self, self,
user_id: int, user_id: int,
sport_id: int, sport_id: int,
workout_date: datetime.datetime, workout_date: datetime,
distance: float, distance: float,
duration: datetime.timedelta, duration: timedelta,
) -> None: ) -> None:
self.user_id = user_id self.user_id = user_id
self.sport_id = sport_id self.sport_id = sport_id
@ -242,11 +241,10 @@ class Workout(BaseModel):
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.datetime.strptime(date_from, '%Y-%m-%d') >= datetime.strptime(date_from, '%Y-%m-%d')
if date_from if date_from
else True, else True,
Workout.workout_date Workout.workout_date <= datetime.strptime(date_to, '%Y-%m-%d')
<= 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)
@ -284,11 +282,10 @@ class Workout(BaseModel):
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.datetime.strptime(date_from, '%Y-%m-%d') >= datetime.strptime(date_from, '%Y-%m-%d')
if date_from if date_from
else True, else True,
Workout.workout_date Workout.workout_date <= datetime.strptime(date_to, '%Y-%m-%d')
<= 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)
@ -380,9 +377,10 @@ 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:
if object_session(workout).is_modified( workout_session = object_session(workout)
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:
@ -413,23 +411,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(BaseModel): class WorkoutSegment(db.Model): # type: ignore
__tablename__ = 'workout_segments' __tablename__ = 'workout_segments'
workout_id = db.Column( workout_id = mapped_column(
db.Integer, db.ForeignKey('workouts.id'), primary_key=True db.Integer, db.ForeignKey('workouts.id'), primary_key=True
) )
workout_uuid = db.Column(postgresql.UUID(as_uuid=True), nullable=False) workout_uuid = mapped_column(postgresql.UUID(as_uuid=True), nullable=False)
segment_id = db.Column(db.Integer, primary_key=True) segment_id = mapped_column(db.Integer, primary_key=True)
duration = db.Column(db.Interval, nullable=False) duration = mapped_column(db.Interval, nullable=False)
pauses = db.Column(db.Interval, nullable=True) pauses = mapped_column(db.Interval, nullable=True)
moving = db.Column(db.Interval, nullable=True) moving = mapped_column(db.Interval, nullable=True)
distance = db.Column(db.Numeric(6, 3), nullable=True) # kilometers distance = mapped_column(db.Numeric(6, 3), nullable=True) # kilometers
min_alt = db.Column(db.Numeric(6, 2), nullable=True) # meters min_alt = mapped_column(db.Numeric(6, 2), nullable=True) # meters
max_alt = db.Column(db.Numeric(6, 2), nullable=True) # meters max_alt = mapped_column(db.Numeric(6, 2), nullable=True) # meters
descent = db.Column(db.Numeric(8, 3), nullable=True) # meters descent = mapped_column(db.Numeric(8, 3), nullable=True) # meters
ascent = db.Column(db.Numeric(8, 3), nullable=True) # meters ascent = mapped_column(db.Numeric(8, 3), nullable=True) # meters
max_speed = db.Column(db.Numeric(6, 2), nullable=True) # km/h max_speed = mapped_column(db.Numeric(6, 2), nullable=True) # km/h
ave_speed = db.Column(db.Numeric(6, 2), nullable=True) # km/h ave_speed = mapped_column(db.Numeric(6, 2), nullable=True) # km/h
def __str__(self) -> str: def __str__(self) -> str:
return ( return (
@ -461,25 +459,27 @@ class WorkoutSegment(BaseModel):
} }
class Record(BaseModel): class Record(db.Model): # type: ignore
__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 = db.Column(db.Integer, primary_key=True, autoincrement=True) id = mapped_column(db.Integer, primary_key=True, autoincrement=True)
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False) user_id = mapped_column(
sport_id = db.Column( db.Integer, db.ForeignKey('users.id'), nullable=False
)
sport_id = mapped_column(
db.Integer, db.ForeignKey('sports.id'), nullable=False db.Integer, db.ForeignKey('sports.id'), nullable=False
) )
workout_id = db.Column( workout_id = mapped_column(
db.Integer, db.ForeignKey('workouts.id'), nullable=False db.Integer, db.ForeignKey('workouts.id'), nullable=False
) )
workout_uuid = db.Column(postgresql.UUID(as_uuid=True), nullable=False) workout_uuid = mapped_column(postgresql.UUID(as_uuid=True), nullable=False)
record_type = db.Column(Enum(*record_types, name="record_types")) record_type = mapped_column(Enum(*record_types, name="record_types"))
workout_date = db.Column(db.DateTime, nullable=False) workout_date = mapped_column(db.DateTime, nullable=False)
_value = db.Column("value", db.Integer, nullable=True) _value = mapped_column("value", db.Integer, nullable=True)
def __str__(self) -> str: def __str__(self) -> str:
return ( return (
@ -497,11 +497,11 @@ class Record(BaseModel):
self.workout_date = workout.workout_date self.workout_date = workout.workout_date
@hybrid_property @hybrid_property
def value(self) -> Optional[Union[datetime.timedelta, float]]: def value(self) -> Optional[Union[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 datetime.timedelta(seconds=self._value) return 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.0.5" version = "3.1.1"
description = "Add SQLAlchemy support to your Flask application." description = "Add SQLAlchemy support to your Flask application."
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.8"
files = [ files = [
{file = "flask_sqlalchemy-3.0.5-py3-none-any.whl", hash = "sha256:cabb6600ddd819a9f859f36515bb1bd8e7dbf30206cc679d2b081dff9e383283"}, {file = "flask_sqlalchemy-3.1.1-py3-none-any.whl", hash = "sha256:4ba4be7f419dc72f4efd8802d69974803c37259dd42f3913b0dcf75c9447e0a0"},
{file = "flask_sqlalchemy-3.0.5.tar.gz", hash = "sha256:c5765e58ca145401b52106c0f46178569243c5da25556be2c231ecc60867c5b1"}, {file = "flask_sqlalchemy-3.1.1.tar.gz", hash = "sha256:e4b68bb881802dda1a7d878b2fc84c06d1ee57fb40b874d3dc97dabfa36b8312"},
] ]
[package.dependencies] [package.dependencies]
flask = ">=2.2.5" flask = ">=2.2.5"
sqlalchemy = ">=1.4.18" sqlalchemy = ">=2.0.16"
[[package]] [[package]]
name = "freezegun" name = "freezegun"
@ -2291,73 +2291,80 @@ test = ["pytest"]
[[package]] [[package]]
name = "sqlalchemy" name = "sqlalchemy"
version = "1.4.49" version = "2.0.21"
description = "Database Abstraction Library" description = "Database Abstraction Library"
optional = false optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" python-versions = ">=3.7"
files = [ files = [
{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_10_9_x86_64.whl", hash = "sha256:1e7dc99b23e33c71d720c4ae37ebb095bebebbd31a24b7d99dfc4753d2803ede"},
{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-macosx_11_0_arm64.whl", hash = "sha256:7f0c4ee579acfe6c994637527c386d1c22eb60bc1c1d36d940d8477e482095d4"},
{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_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f7d57a7e140efe69ce2d7b057c3f9a595f98d0bbdfc23fd055efdfbaa46e3a5"},
{file = "SQLAlchemy-1.4.49-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a63e43bf3f668c11bb0444ce6e809c1227b8f067ca1068898f3008a273f52b09"}, {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_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_aarch64.whl", hash = "sha256:3cf229704074bce31f7f47d12883afee3b0a02bb233a0ba45ddbfe542939cca4"},
{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-musllinux_1_1_x86_64.whl", hash = "sha256:fb87f763b5d04a82ae84ccff25554ffd903baafba6698e18ebaf32561f2fe4aa"},
{file = "SQLAlchemy-1.4.49-cp310-cp310-win32.whl", hash = "sha256:5fb1ebdfc8373b5a291485757bd6431de8d7ed42c27439f543c81f6c8febd729"}, {file = "SQLAlchemy-2.0.21-cp310-cp310-win32.whl", hash = "sha256:89e274604abb1a7fd5c14867a412c9d49c08ccf6ce3e1e04fffc068b5b6499d4"},
{file = "SQLAlchemy-1.4.49-cp310-cp310-win_amd64.whl", hash = "sha256:f8a65990c9c490f4651b5c02abccc9f113a7f56fa482031ac8cb88b70bc8ccaa"}, {file = "SQLAlchemy-2.0.21-cp310-cp310-win_amd64.whl", hash = "sha256:e36339a68126ffb708dc6d1948161cea2a9e85d7d7b0c54f6999853d70d44430"},
{file = "SQLAlchemy-1.4.49-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8923dfdf24d5aa8a3adb59723f54118dd4fe62cf59ed0d0d65d940579c1170a4"}, {file = "SQLAlchemy-2.0.21-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:bf8eebccc66829010f06fbd2b80095d7872991bfe8415098b9fe47deaaa58063"},
{file = "SQLAlchemy-1.4.49-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9ab2c507a7a439f13ca4499db6d3f50423d1d65dc9b5ed897e70941d9e135b0"}, {file = "SQLAlchemy-2.0.21-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b977bfce15afa53d9cf6a632482d7968477625f030d86a109f7bdfe8ce3c064a"},
{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_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ff3dc2f60dbf82c9e599c2915db1526d65415be323464f84de8db3e361ba5b9"},
{file = "SQLAlchemy-1.4.49-cp311-cp311-win32.whl", hash = "sha256:82b08e82da3756765c2e75f327b9bf6b0f043c9c3925fb95fb51e1567fa4ee87"}, {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-win_amd64.whl", hash = "sha256:171e04eeb5d1c0d96a544caf982621a1711d078dbc5c96f11d6469169bd003f1"}, {file = "SQLAlchemy-2.0.21-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:87bf91ebf15258c4701d71dcdd9c4ba39521fb6a37379ea68088ce8cd869b446"},
{file = "SQLAlchemy-1.4.49-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:36e58f8c4fe43984384e3fbe6341ac99b6b4e083de2fe838f0fdb91cebe9e9cb"}, {file = "SQLAlchemy-2.0.21-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:b69f1f754d92eb1cc6b50938359dead36b96a1dcf11a8670bff65fd9b21a4b09"},
{file = "SQLAlchemy-1.4.49-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b31e67ff419013f99ad6f8fc73ee19ea31585e1e9fe773744c0f3ce58c039c30"}, {file = "SQLAlchemy-2.0.21-cp311-cp311-win32.whl", hash = "sha256:af520a730d523eab77d754f5cf44cc7dd7ad2d54907adeb3233177eeb22f271b"},
{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-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_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c40f3470e084d31247aea228aa1c39bbc0904c2b9ccbf5d3cfa2ea2dac06f26d"}, {file = "SQLAlchemy-2.0.21-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:7614f1eab4336df7dd6bee05bc974f2b02c38d3d0c78060c5faa4cd1ca2af3b8"},
{file = "SQLAlchemy-1.4.49-cp36-cp36m-win32.whl", hash = "sha256:706bfa02157b97c136547c406f263e4c6274a7b061b3eb9742915dd774bbc264"}, {file = "SQLAlchemy-2.0.21-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d59cb9e20d79686aa473e0302e4a82882d7118744d30bb1dfb62d3c47141b3ec"},
{file = "SQLAlchemy-1.4.49-cp36-cp36m-win_amd64.whl", hash = "sha256:a7f7b5c07ae5c0cfd24c2db86071fb2a3d947da7bd487e359cc91e67ac1c6d2e"}, {file = "SQLAlchemy-2.0.21-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a95aa0672e3065d43c8aa80080cdd5cc40fe92dc873749e6c1cf23914c4b83af"},
{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_aarch64.whl", hash = "sha256:8c323813963b2503e54d0944813cd479c10c636e3ee223bcbd7bd478bf53c178"},
{file = "SQLAlchemy-1.4.49-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:24e300c0c2147484a002b175f4e1361f102e82c345bf263242f0449672a4bccf"}, {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_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-win32.whl", hash = "sha256:4615623a490e46be85fbaa6335f35cf80e61df0783240afe7d4f544778c315a9"},
{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-cp37-cp37m-win_amd64.whl", hash = "sha256:cca720d05389ab1a5877ff05af96551e58ba65e8dc65582d849ac83ddde3e231"},
{file = "SQLAlchemy-1.4.49-cp37-cp37m-win32.whl", hash = "sha256:647e0b309cb4512b1f1b78471fdaf72921b6fa6e750b9f891e09c6e2f0e5326f"}, {file = "SQLAlchemy-2.0.21-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b4eae01faee9f2b17f08885e3f047153ae0416648f8e8c8bd9bc677c5ce64be9"},
{file = "SQLAlchemy-1.4.49-cp37-cp37m-win_amd64.whl", hash = "sha256:ab73ed1a05ff539afc4a7f8cf371764cdf79768ecb7d2ec691e3ff89abbc541e"}, {file = "SQLAlchemy-2.0.21-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3eb7c03fe1cd3255811cd4e74db1ab8dca22074d50cd8937edf4ef62d758cdf4"},
{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_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c2d494b6a2a2d05fb99f01b84cc9af9f5f93bf3e1e5dbdafe4bed0c2823584c1"},
{file = "SQLAlchemy-1.4.49-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1878ce508edea4a879015ab5215546c444233881301e97ca16fe251e89f1c55"}, {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_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_aarch64.whl", hash = "sha256:fc6b15465fabccc94bf7e38777d665b6a4f95efd1725049d6184b3a39fd54880"},
{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-musllinux_1_1_x86_64.whl", hash = "sha256:014794b60d2021cc8ae0f91d4d0331fe92691ae5467a00841f7130fe877b678e"},
{file = "SQLAlchemy-1.4.49-cp38-cp38-win32.whl", hash = "sha256:f167c8175ab908ce48bd6550679cc6ea20ae169379e73c7720a28f89e53aa532"}, {file = "SQLAlchemy-2.0.21-cp38-cp38-win32.whl", hash = "sha256:0268256a34806e5d1c8f7ee93277d7ea8cc8ae391f487213139018b6805aeaf6"},
{file = "SQLAlchemy-1.4.49-cp38-cp38-win_amd64.whl", hash = "sha256:45806315aae81a0c202752558f0df52b42d11dd7ba0097bf71e253b4215f34f4"}, {file = "SQLAlchemy-2.0.21-cp38-cp38-win_amd64.whl", hash = "sha256:73c079e21d10ff2be54a4699f55865d4b275fd6c8bd5d90c5b1ef78ae0197301"},
{file = "SQLAlchemy-1.4.49-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:b6d0c4b15d65087738a6e22e0ff461b407533ff65a73b818089efc8eb2b3e1de"}, {file = "SQLAlchemy-2.0.21-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:785e2f2c1cb50d0a44e2cdeea5fd36b5bf2d79c481c10f3a88a8be4cfa2c4615"},
{file = "SQLAlchemy-1.4.49-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a843e34abfd4c797018fd8d00ffffa99fd5184c421f190b6ca99def4087689bd"}, {file = "SQLAlchemy-2.0.21-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c111cd40910ffcb615b33605fc8f8e22146aeb7933d06569ac90f219818345ef"},
{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_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9cba4e7369de663611ce7460a34be48e999e0bbb1feb9130070f0685e9a6b66"},
{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-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50a69067af86ec7f11a8e50ba85544657b1477aabf64fa447fd3736b5a0a4f67"},
{file = "SQLAlchemy-1.4.49-cp39-cp39-win32.whl", hash = "sha256:ec2268de67f73b43320383947e74700e95c6770d0c68c4e615e9897e46296294"}, {file = "SQLAlchemy-2.0.21-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ccb99c3138c9bde118b51a289d90096a3791658da9aea1754667302ed6564f6e"},
{file = "SQLAlchemy-1.4.49-cp39-cp39-win_amd64.whl", hash = "sha256:bbdf16372859b8ed3f4d05f925a984771cd2abd18bd187042f24be4886c2a15f"}, {file = "SQLAlchemy-2.0.21-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:513fd5b6513d37e985eb5b7ed89da5fd9e72354e3523980ef00d439bc549c9e9"},
{file = "SQLAlchemy-1.4.49.tar.gz", hash = "sha256:06ff25cbae30c396c4b7737464f2a7fc37a67b7da409993b182b024cec80aed9"}, {file = "SQLAlchemy-2.0.21-cp39-cp39-win32.whl", hash = "sha256:f9fefd6298433b6e9188252f3bff53b9ff0443c8fde27298b8a2b19f6617eeb9"},
{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 = "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\")"} 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\""}
typing-extensions = ">=4.2.0"
[package.extras] [package.extras]
aiomysql = ["aiomysql", "greenlet (!=0.4.17)"] aiomysql = ["aiomysql (>=0.2.0)", "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)", "greenlet (!=0.4.17)"] asyncmy = ["asyncmy (>=0.2.3,!=0.2.4,!=0.2.6)", "greenlet (!=0.4.17)"]
mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2)"] mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2,!=1.1.5)"]
mssql = ["pyodbc"] mssql = ["pyodbc"]
mssql-pymssql = ["pymssql"] mssql-pymssql = ["pymssql"]
mssql-pyodbc = ["pyodbc"] mssql-pyodbc = ["pyodbc"]
mypy = ["mypy (>=0.910)", "sqlalchemy2-stubs"] mypy = ["mypy (>=0.910)"]
mysql = ["mysqlclient (>=1.4.0)", "mysqlclient (>=1.4.0,<2)"] mysql = ["mysqlclient (>=1.4.0)"]
mysql-connector = ["mysql-connector-python"] mysql-connector = ["mysql-connector-python"]
oracle = ["cx-oracle (>=7)", "cx-oracle (>=7,<8)"] oracle = ["cx-oracle (>=7)"]
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.16.6,!=1.29.0)"] postgresql-pg8000 = ["pg8000 (>=1.29.1)"]
postgresql-psycopg = ["psycopg (>=3.0.7)"]
postgresql-psycopg2binary = ["psycopg2-binary"] postgresql-psycopg2binary = ["psycopg2-binary"]
postgresql-psycopg2cffi = ["psycopg2cffi"] postgresql-psycopg2cffi = ["psycopg2cffi"]
pymysql = ["pymysql", "pymysql (<1)"] postgresql-psycopgbinary = ["psycopg[binary] (>=3.0.7)"]
pymysql = ["pymysql"]
sqlcipher = ["sqlcipher3-binary"] sqlcipher = ["sqlcipher3-binary"]
[[package]] [[package]]
@ -2711,4 +2718,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 = "269f524bf7a13303da047ed0c2757b60ff5fc344f842648d3d9fa7a729e0812a" content-hash = "f85f8c64c0d6b2f490a0adde219d5729c80a8a200ee84211898ea5c396164279"

View File

@ -41,7 +41,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 = "=1.4.49" sqlalchemy = "=2.0.21"
ua-parser = "^0.18.0" ua-parser = "^0.18.0"
[tool.poetry.group.dev.dependencies] [tool.poetry.group.dev.dependencies]