Initial project scaffold
Full-stack Dutch supermarket price tracker with FastAPI backend, PostgreSQL/SQLAlchemy, Albert Heijn scraper, and Next.js frontend. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
from datetime import datetime
|
||||
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from .database import Base
|
||||
|
||||
|
||||
class Store(Base):
|
||||
__tablename__ = "stores"
|
||||
|
||||
id: Mapped[int] = mapped_column(sa.Integer, primary_key=True)
|
||||
name: Mapped[str] = mapped_column(sa.String(100), nullable=False)
|
||||
slug: Mapped[str] = mapped_column(sa.String(50), unique=True, nullable=False)
|
||||
country: Mapped[str] = mapped_column(sa.String(2), default="NL")
|
||||
website: Mapped[str | None] = mapped_column(sa.String(255))
|
||||
|
||||
products: Mapped[list["Product"]] = relationship(back_populates="store")
|
||||
scrape_runs: Mapped[list["ScrapeRun"]] = relationship(back_populates="store")
|
||||
|
||||
|
||||
class Product(Base):
|
||||
__tablename__ = "products"
|
||||
__table_args__ = (
|
||||
sa.UniqueConstraint("store_id", "external_id", name="uq_products_store_external"),
|
||||
)
|
||||
|
||||
id: Mapped[int] = mapped_column(sa.Integer, primary_key=True)
|
||||
store_id: Mapped[int] = mapped_column(sa.Integer, sa.ForeignKey("stores.id"), nullable=False)
|
||||
external_id: Mapped[str] = mapped_column(sa.String(50), nullable=False)
|
||||
ean: Mapped[str | None] = mapped_column(sa.String(20), index=True)
|
||||
name: Mapped[str] = mapped_column(sa.String(255), nullable=False)
|
||||
brand: Mapped[str | None] = mapped_column(sa.String(100))
|
||||
category: Mapped[str | None] = mapped_column(sa.String(100))
|
||||
unit_size: Mapped[str | None] = mapped_column(sa.String(50))
|
||||
url: Mapped[str | None] = mapped_column(sa.String(500))
|
||||
created_at: Mapped[datetime] = mapped_column(sa.DateTime, default=datetime.utcnow)
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
sa.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow
|
||||
)
|
||||
|
||||
store: Mapped["Store"] = relationship(back_populates="products")
|
||||
price_snapshots: Mapped[list["PriceSnapshot"]] = relationship(back_populates="product")
|
||||
|
||||
|
||||
class ScrapeRun(Base):
|
||||
__tablename__ = "scrape_runs"
|
||||
|
||||
id: Mapped[int] = mapped_column(sa.Integer, primary_key=True)
|
||||
store_id: Mapped[int] = mapped_column(sa.Integer, sa.ForeignKey("stores.id"), nullable=False)
|
||||
query: Mapped[str] = mapped_column(sa.String(255), nullable=False)
|
||||
started_at: Mapped[datetime] = mapped_column(sa.DateTime, default=datetime.utcnow)
|
||||
finished_at: Mapped[datetime | None] = mapped_column(sa.DateTime)
|
||||
status: Mapped[str] = mapped_column(sa.String(20), default="running")
|
||||
products_found: Mapped[int] = mapped_column(sa.Integer, default=0)
|
||||
error_message: Mapped[str | None] = mapped_column(sa.String(1000))
|
||||
|
||||
store: Mapped["Store"] = relationship(back_populates="scrape_runs")
|
||||
price_snapshots: Mapped[list["PriceSnapshot"]] = relationship(back_populates="scrape_run")
|
||||
|
||||
|
||||
class PriceSnapshot(Base):
|
||||
__tablename__ = "price_snapshots"
|
||||
|
||||
id: Mapped[int] = mapped_column(sa.Integer, primary_key=True)
|
||||
product_id: Mapped[int] = mapped_column(
|
||||
sa.Integer, sa.ForeignKey("products.id"), nullable=False
|
||||
)
|
||||
scrape_run_id: Mapped[int] = mapped_column(
|
||||
sa.Integer, sa.ForeignKey("scrape_runs.id"), nullable=False
|
||||
)
|
||||
price: Mapped[int] = mapped_column(sa.Integer, nullable=False) # euro cents
|
||||
unit_price: Mapped[int | None] = mapped_column(sa.Integer) # euro cents
|
||||
unit_description: Mapped[str | None] = mapped_column(sa.String(50))
|
||||
currency: Mapped[str] = mapped_column(sa.String(3), default="EUR")
|
||||
discount_label: Mapped[str | None] = mapped_column(sa.String(100))
|
||||
discount_description: Mapped[str | None] = mapped_column(sa.String(255))
|
||||
was_price: Mapped[int | None] = mapped_column(sa.Integer) # euro cents, original price
|
||||
is_on_sale: Mapped[bool] = mapped_column(sa.Boolean, default=False)
|
||||
timestamp: Mapped[datetime] = mapped_column(sa.DateTime, default=datetime.utcnow, index=True)
|
||||
|
||||
product: Mapped["Product"] = relationship(back_populates="price_snapshots")
|
||||
scrape_run: Mapped["ScrapeRun"] = relationship(back_populates="price_snapshots")
|
||||
Reference in New Issue
Block a user