WUT_Computer_Science/Programming/ERSMS-project/backend/models.py
Krzysztof kuhy Rudnicki 2a59de6363 The six mypy errors are gone: four were fixable, two are scoped
The note in TODO-refactor-remaining.md said ERSMS's four were structural and
that a fix "would rewrite both models". Both halves were wrong.
flask-sqlalchemy 3.1.1 does ship py.typed; the real blocker was that
`class User(db.Model)` is attribute access, which mypy cannot resolve in a
base-class position.

backend/models.py now declares a named DeclarativeBase with SQLAlchemy 2.0
Mapped[] columns. The Model mixin goes away with the base, so the six legacy
Model.query call sites became db.session selects and the two table names are
spelled out rather than derived.

Verified by driving the real routes through Flask's test client with Firebase
stubbed out -- 16 scenarios covering every one of the six call sites, plus
both CREATE TABLE statements, byte-identical to the submitted version. That
harness caught the one thing review would not have: mapped_column(ForeignKey(
"user.uid")) silently infers VARCHAR(80) from the referenced column, which
had changed the submitted `user_id INTEGER`. An explicit Integer puts it back.

app.py went over the 250-line cap on the way, hence the split; models.py takes
the shebang + exec bit that NLP's nine import-only modules already use, since
__init__.py would make `from models import` unresolvable to mypy while it
still works at runtime. backend/Dockerfile copied only app.py, so it needed
the new file too.

PSD's two are pyflink's MapFunction and KeyedProcessFunction, which ship no
types and are not installed in that venv at all -- a hand-written .pyi would
assert an API nothing here can check. A [[tool.mypy.overrides]] scoped to that
one module turns off disallow_subclassing_any and nothing else.

ERSMS 4 -> 0, PSD 2 -> 0, under --strict. ruff and ruff format clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0172dj34FDpao6wsix8ZuCzE
2026-08-29 11:00:28 +02:00

53 lines
2.1 KiB
Python
Executable File

#!/usr/bin/env python3
"""The two persisted tables: an account, and one account's score for one film.
Split out of `app.py`, which went over this repo's 250-line cap when the models
were migrated to SQLAlchemy 2.0's typed declarative style. They import nothing
from `app`, so there is no cycle: `app.py` builds the `SQLAlchemy` object from
`Base` rather than the models being built from it.
"""
from __future__ import annotations
from sqlalchemy import ForeignKey, Integer, String
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
class Base(DeclarativeBase):
"""Declarative base for this app's two models.
`class User(db.Model)` was attribute access, which mypy cannot resolve in a
base-class position -- that was four `--strict` errors. A named base fixes
it. The trade is that flask-sqlalchemy's `Model` mixin goes with it, so
table names are spelled out here rather than derived from the class name.
"""
class User(Base):
"""One Firebase account, remembered so ratings have an owner."""
__tablename__ = "user"
id: Mapped[int] = mapped_column(primary_key=True)
uid: Mapped[str] = mapped_column(String(80), unique=True)
email: Mapped[str] = mapped_column(String(80), unique=True)
# Nullable with a default, exactly as the submitted schema had it.
is_admin: Mapped[bool | None] = mapped_column(default=False)
class Rating(Base):
"""One user's score for one film."""
__tablename__ = "rating"
id: Mapped[int] = mapped_column(primary_key=True)
# As handed in: the foreign key points at user.uid (a VARCHAR) while the
# column itself is declared INTEGER, and what actually gets stored is the
# Firebase uid string -- SQLite is dynamically typed, so this worked. The
# explicit Integer keeps the submitted DDL byte-identical, because
# mapped_column otherwise infers VARCHAR(80) from the referenced column;
# the `str` annotation is what the code really puts in it.
user_id: Mapped[str] = mapped_column(Integer, ForeignKey("user.uid"))
movie_id: Mapped[int]
value: Mapped[int]