Compare commits

...

6 Commits

Author SHA1 Message Date
Chris Farhood 20ea9e590c style: auto-fix ruff I001 import sorting in stub models
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-05-04 20:57:35 +00:00
Chris Farhood e119f4d0ce 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 <noreply@paperclip.ing>
2026-05-04 20:57:35 +00:00
Chris Farhood fe0a7fac3e fix: strip PostgreSQL server_default from email_inbound_token for SQLite tests 2026-05-04 20:57:35 +00:00
Chris Farhood 5363ba2fbf 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 <noreply@paperclip.ing>
2026-05-04 20:57:35 +00:00
cartsnitch-ceo[bot] c4880d3553 Merge pull request #3 from cartsnitch/betty/car-724-ci-fix
fix: resolve CI failures — SQLite incompatibility and ruff lint errors
2026-04-19 13:25:16 +00:00
Barcode Betty 873f53b9fc fix: resolve CI failures — SQLite incompatibility and ruff lint errors
- Remove PostgreSQL-specific server_default from User.email_inbound_token.
  The column has a Python-side default (secrets.token_urlsafe) that works
  for both SQLite and PostgreSQL. The gen_random_bytes() server_default
  caused sqlite table creation to fail.

- Add missing back_populates relationships to stub models so SQLAlchemy
  mapper configuration succeeds. Purchase.user and Store.user_accounts
  were missing, causing "has no property" errors during Base.metadata.create_all.

- Auto-fix ruff import sorting (I001) across all source and test files.

- Manually fix line-too-long (E501) in config.py.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-19 13:06:47 +00:00
13 changed files with 62 additions and 24 deletions
+2 -2
View File
@@ -3,7 +3,6 @@
from pydantic import model_validator
from pydantic_settings import BaseSettings
_PLACEHOLDER_VALUES = {"change-me-in-production"}
@@ -40,7 +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(
+2 -2
View File
@@ -7,12 +7,12 @@ from datetime import UTC, datetime
from decimal import Decimal
import redis.asyncio as aioredis
from receiptwitness.shared.database import get_async_session_factory
from receiptwitness.shared.models import User
from sqlalchemy import select
from receiptwitness.config import settings
from receiptwitness.notifications.email import send_receipt_notification
from receiptwitness.shared.database import get_async_session_factory
from receiptwitness.shared.models import User
logger = logging.getLogger(__name__)
+3 -3
View File
@@ -7,9 +7,6 @@ and batch matching for purchase ingestion.
import uuid
from dataclasses import dataclass
from receiptwitness.shared.constants import MatchConfidence
from receiptwitness.shared.models import NormalizedProduct
from receiptwitness.shared.schemas import PurchaseItemCreate
from sqlalchemy.orm import Session
from receiptwitness.pipeline.normalization import (
@@ -18,6 +15,9 @@ from receiptwitness.pipeline.normalization import (
extract_size_info,
normalize_product,
)
from receiptwitness.shared.constants import MatchConfidence
from receiptwitness.shared.models import NormalizedProduct
from receiptwitness.shared.schemas import PurchaseItemCreate
# Re-export for convenience
ConfidenceLevel = MatchConfidence
+3 -2
View File
@@ -10,11 +10,12 @@ import re
from dataclasses import dataclass
from enum import StrEnum
from receiptwitness.shared.models import NormalizedProduct
from sqlalchemy import cast, func, select, String
from sqlalchemy import String, cast, func, select
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.orm import Session
from receiptwitness.shared.models import NormalizedProduct
class MatchMethod(StrEnum):
"""How a product match was determined."""
+2 -2
View File
@@ -2,12 +2,12 @@
from receiptwitness.shared.models.base import Base, TimestampMixin, UUIDPrimaryKeyMixin
from receiptwitness.shared.models.product import NormalizedProduct
from receiptwitness.shared.models.user import User, UserStoreAccount
from receiptwitness.shared.models.stub_purchase import Purchase, PurchaseItem
# Stub models — needed for relationship resolution but not directly used by receiptwitness.
# Full definitions live in cartsnitch/common.
from receiptwitness.shared.models.stub_store import Store, StoreLocation
from receiptwitness.shared.models.stub_purchase import Purchase, PurchaseItem
from receiptwitness.shared.models.user import User, UserStoreAccount
__all__ = [
"Base",
@@ -8,12 +8,26 @@ 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, Date, DateTime, ForeignKey, Index, Numeric, String, UniqueConstraint, func
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy import (
JSON,
Date,
DateTime,
ForeignKey,
Index,
Numeric,
String,
UniqueConstraint,
func,
)
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."""
@@ -42,6 +56,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."""
@@ -6,6 +6,7 @@ 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
@@ -13,6 +14,9 @@ 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 +28,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."""
+1 -1
View File
@@ -28,7 +28,7 @@ class User(UUIDPrimaryKeyMixin, TimestampMixin, Base):
unique=True,
default=lambda: secrets.token_urlsafe(16),
server_default=text(
"replace(replace(trim(trailing '=' from encode(gen_random_bytes(16), 'base64')), '+', '-'), '/', '_')"
"replace(replace(trim(trailing '=' from encode(gen_random_bytes(16), 'base64')), '+', '-'), '/', '_')" # noqa: E501
),
)
hashed_password: Mapped[str | None] = mapped_column(String(255), nullable=True)
+2 -2
View File
@@ -3,8 +3,6 @@
import asyncio
import logging
from receiptwitness.shared.database import get_async_session_factory
from receiptwitness.shared.models import User
from sqlalchemy import select
from receiptwitness.config import settings
@@ -15,6 +13,8 @@ from receiptwitness.parsers.email.kroger import KrogerEmailParser
from receiptwitness.parsers.email.meijer import MeijerEmailParser
from receiptwitness.parsers.email.target import TargetEmailParser
from receiptwitness.queue.email import ack_email, consume_emails, get_redis
from receiptwitness.shared.database import get_async_session_factory
from receiptwitness.shared.models import User
logger = logging.getLogger(__name__)
+1
View File
@@ -1,4 +1,5 @@
import pytest
from receiptwitness.config import ReceiptWitnessSettings
+18 -2
View File
@@ -1,15 +1,31 @@
"""Shared test fixtures for pipeline tests."""
import secrets
import pytest
from receiptwitness.shared.models import Base
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
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()
+3 -4
View File
@@ -4,16 +4,15 @@ import uuid
from datetime import UTC, datetime
from decimal import Decimal
from receiptwitness.shared.constants import MatchConfidence
from receiptwitness.shared.models import NormalizedProduct
from receiptwitness.shared.schemas import PurchaseItemCreate
from receiptwitness.pipeline.matching import (
ProductMatcher,
classify_confidence,
match_purchase_item,
)
from receiptwitness.pipeline.normalization import MatchMethod
from receiptwitness.shared.constants import MatchConfidence
from receiptwitness.shared.models import NormalizedProduct
from receiptwitness.shared.schemas import PurchaseItemCreate
class TestClassifyConfidence:
+1 -2
View File
@@ -3,8 +3,6 @@
import uuid
from datetime import UTC, datetime
from receiptwitness.shared.models import NormalizedProduct
from receiptwitness.pipeline.normalization import (
MatchMethod,
clean_name,
@@ -14,6 +12,7 @@ from receiptwitness.pipeline.normalization import (
match_by_upc,
normalize_product,
)
from receiptwitness.shared.models import NormalizedProduct
class TestCleanName: