Merge pull request #4 from cartsnitch/dev

chore: promote dev to uat — receiptwitness migration CI fixes
This commit was merged in pull request #4.
This commit is contained in:
cartsnitch-ceo[bot]
2026-04-19 13:25:40 +00:00
committed by GitHub
14 changed files with 49 additions and 27 deletions
@@ -34,4 +34,4 @@ def upgrade() -> None:
def downgrade() -> None: def downgrade() -> None:
op.drop_constraint("uq_users_email_inbound_token", "users", type_="unique") 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")
+2 -2
View File
@@ -3,7 +3,6 @@
from pydantic import model_validator from pydantic import model_validator
from pydantic_settings import BaseSettings from pydantic_settings import BaseSettings
_PLACEHOLDER_VALUES = {"change-me-in-production"} _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: if not self.session_encryption_key or self.session_encryption_key in _PLACEHOLDER_VALUES:
errors.append( errors.append(
"RW_SESSION_ENCRYPTION_KEY must be set to a secure value. " "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: if self.notifications_enabled and not self.resend_api_key:
errors.append( errors.append(
+2 -2
View File
@@ -7,12 +7,12 @@ from datetime import UTC, datetime
from decimal import Decimal from decimal import Decimal
import redis.asyncio as aioredis 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 sqlalchemy import select
from receiptwitness.config import settings from receiptwitness.config import settings
from receiptwitness.notifications.email import send_receipt_notification 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__) logger = logging.getLogger(__name__)
+3 -3
View File
@@ -7,9 +7,6 @@ and batch matching for purchase ingestion.
import uuid import uuid
from dataclasses import dataclass 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 sqlalchemy.orm import Session
from receiptwitness.pipeline.normalization import ( from receiptwitness.pipeline.normalization import (
@@ -18,6 +15,9 @@ from receiptwitness.pipeline.normalization import (
extract_size_info, extract_size_info,
normalize_product, normalize_product,
) )
from receiptwitness.shared.constants import MatchConfidence
from receiptwitness.shared.models import NormalizedProduct
from receiptwitness.shared.schemas import PurchaseItemCreate
# Re-export for convenience # Re-export for convenience
ConfidenceLevel = MatchConfidence ConfidenceLevel = MatchConfidence
+3 -2
View File
@@ -10,11 +10,12 @@ import re
from dataclasses import dataclass from dataclasses import dataclass
from enum import StrEnum from enum import StrEnum
from receiptwitness.shared.models import NormalizedProduct from sqlalchemy import String, cast, func, select
from sqlalchemy import cast, func, select, String
from sqlalchemy.dialects.postgresql import JSONB from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from receiptwitness.shared.models import NormalizedProduct
class MatchMethod(StrEnum): class MatchMethod(StrEnum):
"""How a product match was determined.""" """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.base import Base, TimestampMixin, UUIDPrimaryKeyMixin
from receiptwitness.shared.models.product import NormalizedProduct 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. # Stub models — needed for relationship resolution but not directly used by receiptwitness.
# Full definitions live in cartsnitch/common. # Full definitions live in cartsnitch/common.
from receiptwitness.shared.models.stub_store import Store, StoreLocation 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__ = [ __all__ = [
"Base", "Base",
@@ -8,12 +8,26 @@ UserStoreAccount. The canonical definitions live in cartsnitch/common.
import uuid import uuid
from datetime import date, datetime from datetime import date, datetime
from decimal import Decimal from decimal import Decimal
from typing import TYPE_CHECKING
from sqlalchemy import JSON, Date, DateTime, ForeignKey, Index, Numeric, String, UniqueConstraint, func from sqlalchemy import (
from sqlalchemy.orm import Mapped, mapped_column 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 from receiptwitness.shared.models.base import Base, TimestampMixin, UUIDPrimaryKeyMixin
if TYPE_CHECKING:
from receiptwitness.shared.models.user import User
class Purchase(UUIDPrimaryKeyMixin, TimestampMixin, Base): class Purchase(UUIDPrimaryKeyMixin, TimestampMixin, Base):
"""Stub: a shopping trip/receipt. Full definition in cartsnitch/common.""" """Stub: a shopping trip/receipt. Full definition in cartsnitch/common."""
@@ -37,6 +51,9 @@ class Purchase(UUIDPrimaryKeyMixin, TimestampMixin, Base):
nullable=False, nullable=False,
) )
# Relationships (stubs — canonical definitions in cartsnitch/common)
user: Mapped["User"] = relationship(back_populates="purchases")
__table_args__ = ( __table_args__ = (
Index("ix_purchases_user_store", "user_id", "store_id"), Index("ix_purchases_user_store", "user_id", "store_id"),
UniqueConstraint("user_id", "store_id", "receipt_id", name="uq_purchase_receipt"), UniqueConstraint("user_id", "store_id", "receipt_id", name="uq_purchase_receipt"),
@@ -6,6 +6,7 @@ UserStoreAccount. The canonical definitions live in cartsnitch/common.
""" """
import uuid import uuid
from typing import TYPE_CHECKING
from sqlalchemy import Float, ForeignKey, String from sqlalchemy import Float, ForeignKey, String
from sqlalchemy.orm import Mapped, mapped_column, relationship 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.constants import StoreSlug
from receiptwitness.shared.models.base import Base, TimestampMixin, UUIDPrimaryKeyMixin from receiptwitness.shared.models.base import Base, TimestampMixin, UUIDPrimaryKeyMixin
if TYPE_CHECKING:
from receiptwitness.shared.models.user import UserStoreAccount
class Store(UUIDPrimaryKeyMixin, TimestampMixin, Base): class Store(UUIDPrimaryKeyMixin, TimestampMixin, Base):
"""Stub: canonical retailer. Full definition in cartsnitch/common.""" """Stub: canonical retailer. Full definition in cartsnitch/common."""
@@ -24,6 +28,9 @@ class Store(UUIDPrimaryKeyMixin, TimestampMixin, Base):
logo_url: Mapped[str | None] = mapped_column(String(500)) logo_url: Mapped[str | None] = mapped_column(String(500))
website_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): class StoreLocation(UUIDPrimaryKeyMixin, TimestampMixin, Base):
"""Stub: physical store location. Full definition in cartsnitch/common.""" """Stub: physical store location. Full definition in cartsnitch/common."""
+1 -4
View File
@@ -5,7 +5,7 @@ import uuid
from datetime import datetime from datetime import datetime
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from sqlalchemy import JSON, Boolean, DateTime, ForeignKey, String, Text, UniqueConstraint, text from sqlalchemy import JSON, Boolean, DateTime, ForeignKey, String, Text, UniqueConstraint
from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.orm import Mapped, mapped_column, relationship
from receiptwitness.shared.constants import AccountStatus from receiptwitness.shared.constants import AccountStatus
@@ -27,9 +27,6 @@ class User(UUIDPrimaryKeyMixin, TimestampMixin, Base):
nullable=False, nullable=False,
unique=True, unique=True,
default=lambda: secrets.token_urlsafe(16), default=lambda: secrets.token_urlsafe(16),
server_default=text(
"replace(replace(trim(trailing '=' from encode(gen_random_bytes(16), 'base64')), '+', '-'), '/', '_')"
),
) )
hashed_password: Mapped[str | None] = mapped_column(String(255), nullable=True) hashed_password: Mapped[str | None] = mapped_column(String(255), nullable=True)
display_name: Mapped[str | None] = mapped_column(String(100)) display_name: Mapped[str | None] = mapped_column(String(100))
+2 -2
View File
@@ -3,8 +3,6 @@
import asyncio import asyncio
import logging import logging
from receiptwitness.shared.database import get_async_session_factory
from receiptwitness.shared.models import User
from sqlalchemy import select from sqlalchemy import select
from receiptwitness.config import settings 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.meijer import MeijerEmailParser
from receiptwitness.parsers.email.target import TargetEmailParser from receiptwitness.parsers.email.target import TargetEmailParser
from receiptwitness.queue.email import ack_email, consume_emails, get_redis 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__) logger = logging.getLogger(__name__)
+1
View File
@@ -1,4 +1,5 @@
import pytest import pytest
from receiptwitness.config import ReceiptWitnessSettings from receiptwitness.config import ReceiptWitnessSettings
+2 -1
View File
@@ -1,10 +1,11 @@
"""Shared test fixtures for pipeline tests.""" """Shared test fixtures for pipeline tests."""
import pytest import pytest
from receiptwitness.shared.models import Base
from sqlalchemy import create_engine from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker from sqlalchemy.orm import sessionmaker
from receiptwitness.shared.models import Base
@pytest.fixture @pytest.fixture
def engine(): def engine():
+3 -4
View File
@@ -4,16 +4,15 @@ import uuid
from datetime import UTC, datetime from datetime import UTC, datetime
from decimal import Decimal 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 ( from receiptwitness.pipeline.matching import (
ProductMatcher, ProductMatcher,
classify_confidence, classify_confidence,
match_purchase_item, match_purchase_item,
) )
from receiptwitness.pipeline.normalization import MatchMethod 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: class TestClassifyConfidence:
+1 -2
View File
@@ -3,8 +3,6 @@
import uuid import uuid
from datetime import UTC, datetime from datetime import UTC, datetime
from receiptwitness.shared.models import NormalizedProduct
from receiptwitness.pipeline.normalization import ( from receiptwitness.pipeline.normalization import (
MatchMethod, MatchMethod,
clean_name, clean_name,
@@ -14,6 +12,7 @@ from receiptwitness.pipeline.normalization import (
match_by_upc, match_by_upc,
normalize_product, normalize_product,
) )
from receiptwitness.shared.models import NormalizedProduct
class TestCleanName: class TestCleanName: