From ffc7886ed4cdf0474c1974326eeb6569019af20f Mon Sep 17 00:00:00 2001 From: Daniel Schadt Date: Fri, 14 Nov 2025 22:44:16 +0100 Subject: fix SQLAlchemy types See https://docs.sqlalchemy.org/en/20/changelog/whatsnew_20.html#whatsnew-20-orm-declarative-typing The Mapped[] annotations help mypy to find the right types for the instance attributes. Some of the mapped_column() definitions are superfluous, but I think it's nice to have them explicit. --- fietsboek/models/badge.py | 10 ++--- fietsboek/models/comment.py | 17 ++++---- fietsboek/models/image.py | 12 +++--- fietsboek/models/meta.py | 14 +++++-- fietsboek/models/track.py | 97 +++++++++++++++++++++++---------------------- fietsboek/models/user.py | 36 ++++++++--------- poetry.lock | 9 ++--- pyproject.toml | 2 +- 8 files changed, 103 insertions(+), 94 deletions(-) diff --git a/fietsboek/models/badge.py b/fietsboek/models/badge.py index 2a6ef95..d80d9fb 100644 --- a/fietsboek/models/badge.py +++ b/fietsboek/models/badge.py @@ -3,8 +3,8 @@ from typing import TYPE_CHECKING from pyramid.httpexceptions import HTTPNotFound -from sqlalchemy import Column, Integer, LargeBinary, Text, select -from sqlalchemy.orm import Mapped, relationship +from sqlalchemy import Integer, LargeBinary, Text, select +from sqlalchemy.orm import Mapped, mapped_column, relationship from .meta import Base @@ -29,9 +29,9 @@ class Badge(Base): # pylint: disable=too-few-public-methods __tablename__ = "badges" - id = Column(Integer, primary_key=True) - title = Column(Text) - image = Column(LargeBinary) + id: Mapped[int] = mapped_column(Integer, primary_key=True) + title: Mapped[str] = mapped_column(Text) + image: Mapped[bytes] = mapped_column(LargeBinary) tracks: Mapped[list["Track"]] = relationship( "Track", secondary="track_badge_assoc", back_populates="badges" diff --git a/fietsboek/models/comment.py b/fietsboek/models/comment.py index e1762d5..a06b595 100644 --- a/fietsboek/models/comment.py +++ b/fietsboek/models/comment.py @@ -1,9 +1,10 @@ """Comment model.""" +import datetime from typing import TYPE_CHECKING -from sqlalchemy import Column, DateTime, ForeignKey, Integer, Text -from sqlalchemy.orm import Mapped, relationship +from sqlalchemy import DateTime, ForeignKey, Integer, Text +from sqlalchemy.orm import Mapped, mapped_column, relationship from .meta import Base @@ -35,12 +36,12 @@ class Comment(Base): # pylint: disable=too-few-public-methods __tablename__ = "comments" - id = Column(Integer, primary_key=True) - author_id = Column(Integer, ForeignKey("users.id")) - track_id = Column(Integer, ForeignKey("tracks.id")) - date = Column(DateTime(False)) - title = Column(Text) - text = Column(Text) + id: Mapped[int] = mapped_column(Integer, primary_key=True) + author_id: Mapped[int | None] = mapped_column(Integer, ForeignKey("users.id")) + track_id: Mapped[int | None] = mapped_column(Integer, ForeignKey("tracks.id")) + date: Mapped[datetime.datetime | None] = mapped_column(DateTime(False)) + title: Mapped[str | None] = mapped_column(Text) + text: Mapped[str | None] = mapped_column(Text) author: Mapped["User"] = relationship("User", back_populates="comments") track: Mapped["Track"] = relationship("Track", back_populates="comments") diff --git a/fietsboek/models/image.py b/fietsboek/models/image.py index dfa9ffb..1f0d4a9 100644 --- a/fietsboek/models/image.py +++ b/fietsboek/models/image.py @@ -6,8 +6,8 @@ image description here. from typing import TYPE_CHECKING -from sqlalchemy import Column, ForeignKey, Integer, Text, UniqueConstraint, select -from sqlalchemy.orm import Mapped, relationship +from sqlalchemy import ForeignKey, Integer, Text, UniqueConstraint, select +from sqlalchemy.orm import Mapped, mapped_column, relationship from .meta import Base @@ -32,10 +32,10 @@ class ImageMetadata(Base): # pylint: disable=too-few-public-methods __tablename__ = "image_metadata" - id = Column(Integer, primary_key=True) - track_id = Column(Integer, ForeignKey("tracks.id"), nullable=False) - image_name = Column(Text, nullable=False) - description = Column(Text) + id: Mapped[int] = mapped_column(Integer, primary_key=True) + track_id: Mapped[int] = mapped_column(Integer, ForeignKey("tracks.id"), nullable=False) + image_name: Mapped[str] = mapped_column(Text, nullable=False) + description: Mapped[str] = mapped_column(Text) track: Mapped["Track"] = relationship("Track", back_populates="images") diff --git a/fietsboek/models/meta.py b/fietsboek/models/meta.py index 45723fd..0e7dd15 100644 --- a/fietsboek/models/meta.py +++ b/fietsboek/models/meta.py @@ -1,6 +1,6 @@ """Base metadata definition for the SQLAlchemy models.""" -from sqlalchemy.orm import declarative_base +from sqlalchemy.orm import DeclarativeBase from sqlalchemy.schema import MetaData # Recommended naming convention used by Alembic, as various different database @@ -14,8 +14,14 @@ NAMING_CONVENTION = { "pk": "pk_%(table_name)s", } -metadata = MetaData(naming_convention=NAMING_CONVENTION) -Base = declarative_base(metadata=metadata) +sqla_metadata = MetaData(naming_convention=NAMING_CONVENTION) -__all__ = ["NAMING_CONVENTION", "metadata", "Base"] +class Base(DeclarativeBase): + """Base class for SQLAlchemy model definitions.""" + + # pylint: disable=too-few-public-methods + metadata = sqla_metadata + + +__all__ = ["NAMING_CONVENTION", "sqla_metadata", "Base"] diff --git a/fietsboek/models/track.py b/fietsboek/models/track.py index bd4bd15..9c758c6 100644 --- a/fietsboek/models/track.py +++ b/fietsboek/models/track.py @@ -49,7 +49,7 @@ from sqlalchemy import ( Text, select, ) -from sqlalchemy.orm import Mapped, relationship +from sqlalchemy.orm import Mapped, mapped_column, relationship from .. import geo, util from .meta import Base @@ -92,8 +92,8 @@ class Tag(Base): # pylint: disable=too-few-public-methods __tablename__ = "tags" - track_id = Column(Integer, ForeignKey("tracks.id"), primary_key=True) - tag = Column(Text, primary_key=True) + track_id: Mapped[int] = mapped_column(Integer, ForeignKey("tracks.id"), primary_key=True) + tag: Mapped[str] = mapped_column(Text, primary_key=True) track: Mapped["Track"] = relationship("Track", back_populates="tags") @@ -163,13 +163,13 @@ class Waypoint(Base): # pylint: disable=too-few-public-methods __tablename__ = "waypoints" - id = Column(Integer, primary_key=True) - track_id = Column(Integer, ForeignKey("tracks.id"), nullable=False) - longitude = Column(Float) - latitude = Column(Float) - elevation = Column(Float) - name = Column(Text) - description = Column(Text) + id: Mapped[int] = mapped_column(Integer, primary_key=True) + track_id: Mapped[int] = mapped_column(Integer, ForeignKey("tracks.id"), nullable=False) + longitude: Mapped[float | None] = mapped_column(Float) + latitude: Mapped[float | None] = mapped_column(Float) + elevation: Mapped[float | None] = mapped_column(Float) + name: Mapped[str | None] = mapped_column(Text) + description: Mapped[str | None] = mapped_column(Text) track: Mapped["Track"] = relationship("Track", back_populates="waypoints") @@ -179,12 +179,12 @@ class TrackPoint(Base): # pylint: disable=too-few-public-methods __tablename__ = "track_points" - track_id = Column(Integer, ForeignKey("tracks.id"), primary_key=True) - index = Column(Integer, primary_key=True) - longitude = Column(Float) - latitude = Column(Float) - elevation = Column(Float) - time_offset = Column(Float) + track_id: Mapped[int] = mapped_column(Integer, ForeignKey("tracks.id"), primary_key=True) + index: Mapped[int] = mapped_column(Integer, primary_key=True) + longitude: Mapped[float | None] = mapped_column(Float) + latitude: Mapped[float | None] = mapped_column(Float) + elevation: Mapped[float | None] = mapped_column(Float) + time_offset: Mapped[float | None] = mapped_column(Float) track: Mapped["Track"] = relationship("Track", back_populates="points") @@ -194,10 +194,10 @@ class TrackPoint(Base): :return: The converted point. """ return geo.Point( - latitude=self.latitude, - longitude=self.longitude, - elevation=self.elevation, - time_offset=self.time_offset, + latitude=self.latitude or 0.0, + longitude=self.longitude or 0.0, + elevation=self.elevation or 0.0, + time_offset=self.time_offset or 0.0, ) @@ -273,16 +273,16 @@ class Track(Base): """ __tablename__ = "tracks" - id = Column(Integer, primary_key=True) - owner_id = Column(Integer, ForeignKey("users.id")) - title = Column(Text) - description = Column(Text) - date_raw = Column(DateTime(False)) - date_tz = Column(Integer) - visibility = Column(Enum(Visibility)) - link_secret = Column(Text) - type = Column(Enum(TrackType)) - transformers = Column(JsonText) + id: Mapped[int] = mapped_column(Integer, primary_key=True) + owner_id: Mapped[int] = mapped_column(Integer, ForeignKey("users.id")) + title: Mapped[str | None] = mapped_column(Text) + description: Mapped[str | None] = mapped_column(Text) + date_raw: Mapped[datetime.datetime | None] = mapped_column(DateTime(False)) + date_tz: Mapped[int | None] = mapped_column(Integer) + visibility: Mapped[Visibility | None] = mapped_column(Enum(Visibility)) + link_secret: Mapped[str | None] = mapped_column(Text) + type: Mapped[TrackType | None] = mapped_column(Enum(TrackType)) + transformers: Mapped[list | dict | bool | str | int | float | None] = mapped_column(JsonText) owner: Mapped["models.User"] = relationship("User", back_populates="tracks") points: Mapped[list["TrackPoint"]] = relationship( @@ -575,6 +575,9 @@ class Track(Base): if not self.transformers: return None + if not isinstance(self.transformers, dict): + return None + for t_id, settings in self.transformers: if t_id == transformer_id: return settings @@ -596,7 +599,7 @@ class TrackWithMetadata: def __init__(self, track: Track): self.track = track self.cache = track.cache - self._cached_meta: Optional[dict] = None + self._cached_meta: Optional[geo.MovementData] = None def _meta(self): # Already loaded, we're done @@ -882,18 +885,18 @@ class TrackCache(Base): # pylint: disable=too-many-instance-attributes,too-few-public-methods __tablename__ = "track_cache" - track_id = Column(Integer, ForeignKey("tracks.id"), primary_key=True) - length = Column(Float) - uphill = Column(Float) - downhill = Column(Float) - moving_time = Column(Float) - stopped_time = Column(Float) - max_speed = Column(Float) - avg_speed = Column(Float) - start_time_raw = Column(DateTime(False)) - start_time_tz = Column(Integer) - end_time_raw = Column(DateTime(False)) - end_time_tz = Column(Integer) + track_id: Mapped[int] = mapped_column(Integer, ForeignKey("tracks.id"), primary_key=True) + length: Mapped[float | None] = mapped_column(Float) + uphill: Mapped[float | None] = mapped_column(Float) + downhill: Mapped[float | None] = mapped_column(Float) + moving_time: Mapped[float | None] = mapped_column(Float) + stopped_time: Mapped[float | None] = mapped_column(Float) + max_speed: Mapped[float | None] = mapped_column(Float) + avg_speed: Mapped[float | None] = mapped_column(Float) + start_time_raw: Mapped[datetime.datetime | None] = mapped_column(DateTime(False)) + start_time_tz: Mapped[int | None] = mapped_column(Integer) + end_time_raw: Mapped[datetime.datetime | None] = mapped_column(DateTime(False)) + end_time_tz: Mapped[int | None] = mapped_column(Integer) track: Mapped["Track"] = relationship("Track", back_populates="cache") @@ -974,10 +977,10 @@ class Upload(Base): # pylint: disable=too-many-instance-attributes,too-few-public-methods __tablename__ = "uploads" - id = Column(Integer, primary_key=True) - uploaded_at = Column(DateTime(False)) - owner_id = Column(Integer, ForeignKey("users.id")) - gpx = Column(LargeBinary) + id: Mapped[int] = mapped_column(Integer, primary_key=True) + uploaded_at: Mapped[datetime.datetime | None] = mapped_column(DateTime(False)) + owner_id: Mapped[int | None] = mapped_column(Integer, ForeignKey("users.id")) + gpx: Mapped[bytes | None] = mapped_column(LargeBinary) owner: Mapped["models.User"] = relationship("User", back_populates="uploads") diff --git a/fietsboek/models/user.py b/fietsboek/models/user.py index 725fb3a..69d6972 100644 --- a/fietsboek/models/user.py +++ b/fietsboek/models/user.py @@ -31,7 +31,7 @@ from sqlalchemy import ( union, ) from sqlalchemy.exc import NoResultFound -from sqlalchemy.orm import Mapped, Session, relationship, with_parent +from sqlalchemy.orm import Mapped, Session, mapped_column, relationship, with_parent from sqlalchemy.orm.attributes import flag_dirty from sqlalchemy.orm.session import object_session @@ -112,14 +112,14 @@ class User(Base): """ __tablename__ = "users" - id = Column(Integer, primary_key=True) - name = Column(Text) - password = Column(LargeBinary) - salt = Column(LargeBinary) - email = Column(Text) - session_secret = Column(LargeBinary(SESSION_SECRET_LENGTH)) - is_admin = Column(Boolean, default=False) - is_verified = Column(Boolean, default=False) + id: Mapped[int] = mapped_column(Integer, primary_key=True) + name: Mapped[str | None] = mapped_column(Text) + password: Mapped[bytes | None] = mapped_column(LargeBinary) + salt: Mapped[bytes | None] = mapped_column(LargeBinary) + email: Mapped[str | None] = mapped_column(Text) + session_secret: Mapped[bytes | None] = mapped_column(LargeBinary(SESSION_SECRET_LENGTH)) + is_admin: Mapped[bool | None] = mapped_column(Boolean, default=False) + is_verified: Mapped[bool | None] = mapped_column(Boolean, default=False) tracks: Mapped[list["Track"]] = relationship( "Track", back_populates="owner", cascade="all, delete-orphan" @@ -486,10 +486,10 @@ class FriendRequest(Base): # pylint: disable=too-few-public-methods __tablename__ = "friend_requests" - id = Column(Integer, primary_key=True) - sender_id = Column(Integer, ForeignKey("users.id")) - recipient_id = Column(Integer, ForeignKey("users.id")) - date = Column(DateTime(False)) + id: Mapped[int] = mapped_column(Integer, primary_key=True) + sender_id: Mapped[int | None] = mapped_column(Integer, ForeignKey("users.id")) + recipient_id: Mapped[int | None] = mapped_column(Integer, ForeignKey("users.id")) + date: Mapped[datetime.datetime | None] = mapped_column(DateTime(False)) sender: Mapped["User"] = relationship( "User", primaryjoin="User.id == FriendRequest.sender_id", backref="outgoing_requests" @@ -540,11 +540,11 @@ class Token(Base): # pylint: disable=too-few-public-methods __tablename__ = "tokens" - id = Column(Integer, primary_key=True) - user_id = Column(Integer, ForeignKey("users.id")) - uuid = Column(Text) - token_type = Column(Enum(TokenType)) - date = Column(DateTime(False)) + id: Mapped[int] = mapped_column(Integer, primary_key=True) + user_id: Mapped[int | None] = mapped_column(Integer, ForeignKey("users.id")) + uuid: Mapped[str | None] = mapped_column(Text) + token_type: Mapped[TokenType | None] = mapped_column(Enum(TokenType)) + date: Mapped[datetime.datetime | None] = mapped_column(DateTime(False)) user: Mapped["User"] = relationship("User", back_populates="tokens") diff --git a/poetry.lock b/poetry.lock index 3763009..c5ba468 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1115,7 +1115,7 @@ version = "1.18.2" description = "Optional static typing for Python" optional = false python-versions = ">=3.9" -groups = ["main", "types"] +groups = ["types"] files = [ {file = "mypy-1.18.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c1eab0cf6294dafe397c261a75f96dc2c31bffe3b944faa24db5def4e2b0f77c"}, {file = "mypy-1.18.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7a780ca61fc239e4865968ebc5240bb3bf610ef59ac398de9a7421b54e4a207e"}, @@ -1175,7 +1175,7 @@ version = "1.1.0" description = "Type system extensions for programs checked with the mypy type checker." optional = false python-versions = ">=3.8" -groups = ["main", "linters", "types"] +groups = ["linters", "types"] files = [ {file = "mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505"}, {file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"}, @@ -1252,7 +1252,7 @@ version = "0.12.1" description = "Utility library for gitignore style pattern matching of file paths." optional = false python-versions = ">=3.8" -groups = ["main", "linters", "types"] +groups = ["linters", "types"] files = [ {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, @@ -2252,7 +2252,6 @@ files = [ [package.dependencies] greenlet = {version = ">=1", markers = "platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\""} -mypy = {version = ">=0.910", optional = true, markers = "extra == \"mypy\""} typing-extensions = ">=4.6.0" [package.extras] @@ -2677,4 +2676,4 @@ hittekaart = ["hittekaart-py"] [metadata] lock-version = "2.1" python-versions = ">=3.11" -content-hash = "b2bd0fd69ceab70c448a4e4b517ce30c65c17148f0ac9698eaadd4673b66f36a" +content-hash = "d845a9c1ae4a96f42c85dd2d73f364f22b4bfe14779bf0ad902557faffa6a7db" diff --git a/pyproject.toml b/pyproject.toml index cea085f..891b5e3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ dependencies = [ "pyramid_tm (>=2.5, <3.0)", "waitress (>=3, <4)", - "SQLAlchemy[mypy] (>=2.0.15, <3.0.0)", + "SQLAlchemy (>=2.0.15, <3.0.0)", "alembic (>=1.8, <2.0)", "transaction (>=5, <6)", "zope.sqlalchemy (>=4.0, <5.0)", -- cgit v1.2.3