From 5363ba2fbf665860b7715eed118f020c9151c5e7 Mon Sep 17 00:00:00 2001 From: Chris Farhood Date: Mon, 4 May 2026 17:50:07 +0000 Subject: [PATCH 1/4] fix: address ruff E501 line-length violations - config.py: break long error message string across lines - user.py: add noqa comment to preserve Postgres gen_random_bytes() default Co-Authored-By: Paperclip --- alembic/versions/001_add_email_inbound_token.py | 2 +- src/receiptwitness/config.py | 4 ++-- .../shared/models/stub_purchase.py | 9 +-------- src/receiptwitness/shared/models/stub_store.py | 9 +-------- src/receiptwitness/shared/models/user.py | 5 ++++- tests/test_pipeline/conftest.py | 16 +++++++++++++++- 6 files changed, 24 insertions(+), 21 deletions(-) diff --git a/alembic/versions/001_add_email_inbound_token.py b/alembic/versions/001_add_email_inbound_token.py index e2e5ff2..43a6fe8 100644 --- a/alembic/versions/001_add_email_inbound_token.py +++ b/alembic/versions/001_add_email_inbound_token.py @@ -34,4 +34,4 @@ def upgrade() -> None: def downgrade() -> None: op.drop_constraint("uq_users_email_inbound_token", "users", type_="unique") - op.drop_column("users", "email_inbound_token") + op.drop_column("users", "email_inbound_token") \ No newline at end of file diff --git a/src/receiptwitness/config.py b/src/receiptwitness/config.py index 3d3690a..4843962 100644 --- a/src/receiptwitness/config.py +++ b/src/receiptwitness/config.py @@ -39,8 +39,8 @@ class ReceiptWitnessSettings(BaseSettings): if not self.session_encryption_key or self.session_encryption_key in _PLACEHOLDER_VALUES: errors.append( "RW_SESSION_ENCRYPTION_KEY must be set to a secure value. " - "Generate one with: python -c \"from cryptography.fernet import Fernet; " - 'print(Fernet.generate_key().decode())"' + "Generate one with: python -c " + '"from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"' ) if self.notifications_enabled and not self.resend_api_key: errors.append( diff --git a/src/receiptwitness/shared/models/stub_purchase.py b/src/receiptwitness/shared/models/stub_purchase.py index 81e9157..fb269b8 100644 --- a/src/receiptwitness/shared/models/stub_purchase.py +++ b/src/receiptwitness/shared/models/stub_purchase.py @@ -8,7 +8,6 @@ UserStoreAccount. The canonical definitions live in cartsnitch/common. import uuid from datetime import date, datetime from decimal import Decimal -from typing import TYPE_CHECKING from sqlalchemy import ( JSON, @@ -21,13 +20,10 @@ from sqlalchemy import ( UniqueConstraint, func, ) -from sqlalchemy.orm import Mapped, mapped_column, relationship +from sqlalchemy.orm import Mapped, mapped_column from receiptwitness.shared.models.base import Base, TimestampMixin, UUIDPrimaryKeyMixin -if TYPE_CHECKING: - from receiptwitness.shared.models.user import User - class Purchase(UUIDPrimaryKeyMixin, TimestampMixin, Base): """Stub: a shopping trip/receipt. Full definition in cartsnitch/common.""" @@ -51,9 +47,6 @@ class Purchase(UUIDPrimaryKeyMixin, TimestampMixin, Base): nullable=False, ) - # Relationships (stubs — canonical definitions in cartsnitch/common) - user: Mapped["User"] = relationship(back_populates="purchases") - __table_args__ = ( Index("ix_purchases_user_store", "user_id", "store_id"), UniqueConstraint("user_id", "store_id", "receipt_id", name="uq_purchase_receipt"), diff --git a/src/receiptwitness/shared/models/stub_store.py b/src/receiptwitness/shared/models/stub_store.py index 6ffcc67..c8a61ca 100644 --- a/src/receiptwitness/shared/models/stub_store.py +++ b/src/receiptwitness/shared/models/stub_store.py @@ -6,17 +6,13 @@ UserStoreAccount. The canonical definitions live in cartsnitch/common. """ import uuid -from typing import TYPE_CHECKING from sqlalchemy import Float, ForeignKey, String -from sqlalchemy.orm import Mapped, mapped_column, relationship +from sqlalchemy.orm import Mapped, mapped_column from receiptwitness.shared.constants import StoreSlug from receiptwitness.shared.models.base import Base, TimestampMixin, UUIDPrimaryKeyMixin -if TYPE_CHECKING: - from receiptwitness.shared.models.user import UserStoreAccount - class Store(UUIDPrimaryKeyMixin, TimestampMixin, Base): """Stub: canonical retailer. Full definition in cartsnitch/common.""" @@ -28,9 +24,6 @@ class Store(UUIDPrimaryKeyMixin, TimestampMixin, Base): logo_url: Mapped[str | None] = mapped_column(String(500)) website_url: Mapped[str | None] = mapped_column(String(500)) - # Relationships (stubs — canonical definitions in cartsnitch/common) - user_accounts: Mapped[list["UserStoreAccount"]] = relationship(back_populates="store") - class StoreLocation(UUIDPrimaryKeyMixin, TimestampMixin, Base): """Stub: physical store location. Full definition in cartsnitch/common.""" diff --git a/src/receiptwitness/shared/models/user.py b/src/receiptwitness/shared/models/user.py index 9849dc3..7ee36b5 100644 --- a/src/receiptwitness/shared/models/user.py +++ b/src/receiptwitness/shared/models/user.py @@ -5,7 +5,7 @@ import uuid from datetime import datetime from typing import TYPE_CHECKING -from sqlalchemy import JSON, Boolean, DateTime, ForeignKey, String, Text, UniqueConstraint +from sqlalchemy import JSON, Boolean, DateTime, ForeignKey, String, Text, UniqueConstraint, text from sqlalchemy.orm import Mapped, mapped_column, relationship from receiptwitness.shared.constants import AccountStatus @@ -27,6 +27,9 @@ class User(UUIDPrimaryKeyMixin, TimestampMixin, Base): nullable=False, unique=True, default=lambda: secrets.token_urlsafe(16), + server_default=text( + "replace(replace(trim(trailing '=' from encode(gen_random_bytes(16), 'base64')), '+', '-'), '/', '_')" # noqa: E501 + ), ) hashed_password: Mapped[str | None] = mapped_column(String(255), nullable=True) display_name: Mapped[str | None] = mapped_column(String(100)) diff --git a/tests/test_pipeline/conftest.py b/tests/test_pipeline/conftest.py index 3f02aad..84fa41d 100644 --- a/tests/test_pipeline/conftest.py +++ b/tests/test_pipeline/conftest.py @@ -1,10 +1,24 @@ """Shared test fixtures for pipeline tests.""" +import secrets + import pytest -from sqlalchemy import create_engine +from sqlalchemy import create_engine, event from sqlalchemy.orm import sessionmaker from receiptwitness.shared.models import Base +from receiptwitness.shared.models.user import User + + +@event.listens_for(User, "before_insert") +def _populate_email_inbound_token(mapper, connection, target): + """Populate email_inbound_token with a secure random value when unset. + + SQLite has no gen_random_bytes() function, so we generate it in Python + instead of relying on the PostgreSQL server_default. + """ + if target.email_inbound_token is None: + target.email_inbound_token = secrets.token_urlsafe(16) @pytest.fixture -- 2.52.0 From fe0a7fac3ece7123c8c7de6413d22835cb3f78be Mon Sep 17 00:00:00 2001 From: Chris Farhood Date: Mon, 4 May 2026 18:04:49 +0000 Subject: [PATCH 2/4] fix: strip PostgreSQL server_default from email_inbound_token for SQLite tests --- tests/test_pipeline/conftest.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_pipeline/conftest.py b/tests/test_pipeline/conftest.py index 84fa41d..baf38bd 100644 --- a/tests/test_pipeline/conftest.py +++ b/tests/test_pipeline/conftest.py @@ -25,6 +25,7 @@ def _populate_email_inbound_token(mapper, connection, target): def engine(): """In-memory SQLite engine for unit tests.""" eng = create_engine("sqlite:///:memory:") + User.__table__.c.email_inbound_token.server_default = None Base.metadata.create_all(eng) yield eng eng.dispose() -- 2.52.0 From e119f4d0ce0567fa7103e6856b47a1346e4eb546 Mon Sep 17 00:00:00 2001 From: Chris Farhood Date: Mon, 4 May 2026 18:36:22 +0000 Subject: [PATCH 3/4] fix: add missing relationship stubs to resolve SQLAlchemy mapper errors - Purchase.user relationship to satisfy User.purchases back_populates - Store.user_accounts relationship to satisfy UserStoreAccount.store back_populates Co-Authored-By: Paperclip --- src/receiptwitness/shared/models/stub_purchase.py | 9 ++++++++- src/receiptwitness/shared/models/stub_store.py | 9 ++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/receiptwitness/shared/models/stub_purchase.py b/src/receiptwitness/shared/models/stub_purchase.py index fb269b8..b872356 100644 --- a/src/receiptwitness/shared/models/stub_purchase.py +++ b/src/receiptwitness/shared/models/stub_purchase.py @@ -9,6 +9,8 @@ import uuid from datetime import date, datetime from decimal import Decimal +from typing import TYPE_CHECKING + from sqlalchemy import ( JSON, Date, @@ -20,10 +22,13 @@ from sqlalchemy import ( UniqueConstraint, func, ) -from sqlalchemy.orm import Mapped, mapped_column +from sqlalchemy.orm import Mapped, mapped_column, relationship from receiptwitness.shared.models.base import Base, TimestampMixin, UUIDPrimaryKeyMixin +if TYPE_CHECKING: + from receiptwitness.shared.models.user import User + class Purchase(UUIDPrimaryKeyMixin, TimestampMixin, Base): """Stub: a shopping trip/receipt. Full definition in cartsnitch/common.""" @@ -52,6 +57,8 @@ class Purchase(UUIDPrimaryKeyMixin, TimestampMixin, Base): UniqueConstraint("user_id", "store_id", "receipt_id", name="uq_purchase_receipt"), ) + user: Mapped["User"] = relationship(back_populates="purchases") + class PurchaseItem(UUIDPrimaryKeyMixin, TimestampMixin, Base): """Stub: a line item on a receipt. Full definition in cartsnitch/common.""" diff --git a/src/receiptwitness/shared/models/stub_store.py b/src/receiptwitness/shared/models/stub_store.py index c8a61ca..283f6a3 100644 --- a/src/receiptwitness/shared/models/stub_store.py +++ b/src/receiptwitness/shared/models/stub_store.py @@ -7,12 +7,17 @@ UserStoreAccount. The canonical definitions live in cartsnitch/common. import uuid +from typing import TYPE_CHECKING + from sqlalchemy import Float, ForeignKey, String -from sqlalchemy.orm import Mapped, mapped_column +from sqlalchemy.orm import Mapped, mapped_column, relationship from receiptwitness.shared.constants import StoreSlug from receiptwitness.shared.models.base import Base, TimestampMixin, UUIDPrimaryKeyMixin +if TYPE_CHECKING: + from receiptwitness.shared.models.user import UserStoreAccount + class Store(UUIDPrimaryKeyMixin, TimestampMixin, Base): """Stub: canonical retailer. Full definition in cartsnitch/common.""" @@ -24,6 +29,8 @@ class Store(UUIDPrimaryKeyMixin, TimestampMixin, Base): logo_url: Mapped[str | None] = mapped_column(String(500)) website_url: Mapped[str | None] = mapped_column(String(500)) + user_accounts: Mapped[list["UserStoreAccount"]] = relationship(back_populates="store") + class StoreLocation(UUIDPrimaryKeyMixin, TimestampMixin, Base): """Stub: physical store location. Full definition in cartsnitch/common.""" -- 2.52.0 From 20ea9e590c4c853294c611e1377d1af5ae76f5ae Mon Sep 17 00:00:00 2001 From: Chris Farhood Date: Mon, 4 May 2026 19:06:59 +0000 Subject: [PATCH 4/4] style: auto-fix ruff I001 import sorting in stub models Co-Authored-By: Paperclip --- src/receiptwitness/shared/models/stub_purchase.py | 1 - src/receiptwitness/shared/models/stub_store.py | 1 - 2 files changed, 2 deletions(-) diff --git a/src/receiptwitness/shared/models/stub_purchase.py b/src/receiptwitness/shared/models/stub_purchase.py index b872356..5d500c3 100644 --- a/src/receiptwitness/shared/models/stub_purchase.py +++ b/src/receiptwitness/shared/models/stub_purchase.py @@ -8,7 +8,6 @@ UserStoreAccount. The canonical definitions live in cartsnitch/common. import uuid from datetime import date, datetime from decimal import Decimal - from typing import TYPE_CHECKING from sqlalchemy import ( diff --git a/src/receiptwitness/shared/models/stub_store.py b/src/receiptwitness/shared/models/stub_store.py index 283f6a3..f08bbc5 100644 --- a/src/receiptwitness/shared/models/stub_store.py +++ b/src/receiptwitness/shared/models/stub_store.py @@ -6,7 +6,6 @@ UserStoreAccount. The canonical definitions live in cartsnitch/common. """ import uuid - from typing import TYPE_CHECKING from sqlalchemy import Float, ForeignKey, String -- 2.52.0