Files
rose-ash/shared/models/market.py
giles f42042ccb7
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m5s
Monorepo: consolidate 7 repos into one
Combines shared, blog, market, cart, events, federation, and account
into a single repository. Eliminates submodule sync, sibling model
copying at build time, and per-app CI orchestration.

Changes:
- Remove per-app .git, .gitmodules, .gitea, submodule shared/ dirs
- Remove stale sibling model copies from each app
- Update all 6 Dockerfiles for monorepo build context (root = .)
- Add build directives to docker-compose.yml
- Add single .gitea/workflows/ci.yml with change detection
- Add .dockerignore for monorepo build context
- Create __init__.py for federation and account (cross-app imports)
2026-02-24 19:44:17 +00:00

442 lines
20 KiB
Python

# at top of persist_snapshot.py:
from datetime import datetime
from typing import Optional, List
from sqlalchemy.orm import Mapped, mapped_column, relationship
from typing import List, Optional
from sqlalchemy import (
String, Text, Integer, ForeignKey, DateTime, Boolean, Numeric,
UniqueConstraint, Index, func
)
from shared.db.base import Base # you already import Base in app.py
class Product(Base):
__tablename__ = "products"
id: Mapped[int] = mapped_column(primary_key=True)
slug: Mapped[str] = mapped_column(String(255), unique=True, index=True, nullable=False)
title: Mapped[Optional[str]] = mapped_column(String(512))
image: Mapped[Optional[str]] = mapped_column(Text)
description_short: Mapped[Optional[str]] = mapped_column(Text)
description_html: Mapped[Optional[str]] = mapped_column(Text)
suma_href: Mapped[Optional[str]] = mapped_column(Text)
brand: Mapped[Optional[str]] = mapped_column(String(255))
rrp: Mapped[Optional[float]] = mapped_column(Numeric(12, 2))
rrp_currency: Mapped[Optional[str]] = mapped_column(String(16))
rrp_raw: Mapped[Optional[str]] = mapped_column(String(128))
price_per_unit: Mapped[Optional[float]] = mapped_column(Numeric(12, 4))
price_per_unit_currency: Mapped[Optional[str]] = mapped_column(String(16))
price_per_unit_raw: Mapped[Optional[str]] = mapped_column(String(128))
special_price: Mapped[Optional[float]] = mapped_column(Numeric(12, 2))
special_price_currency: Mapped[Optional[str]] = mapped_column(String(16))
special_price_raw: Mapped[Optional[str]] = mapped_column(String(128))
regular_price: Mapped[Optional[float]] = mapped_column(Numeric(12, 2))
regular_price_currency: Mapped[Optional[str]] = mapped_column(String(16))
regular_price_raw: Mapped[Optional[str]] = mapped_column(String(128))
oe_list_price: Mapped[Optional[float]] = mapped_column(Numeric(12, 2))
case_size_count: Mapped[Optional[int]] = mapped_column(Integer)
case_size_item_qty: Mapped[Optional[float]] = mapped_column(Numeric(12, 3))
case_size_item_unit: Mapped[Optional[str]] = mapped_column(String(32))
case_size_raw: Mapped[Optional[str]] = mapped_column(String(128))
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
nullable=False,
server_default=func.now(),
)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
nullable=False,
server_default=func.now(),
)
deleted_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
images: Mapped[List["ProductImage"]] = relationship(
back_populates="product",
cascade="all, delete-orphan",
passive_deletes=True,
)
sections: Mapped[List["ProductSection"]] = relationship(
back_populates="product",
cascade="all, delete-orphan",
passive_deletes=True,
)
labels: Mapped[List["ProductLabel"]] = relationship(
cascade="all, delete-orphan",
passive_deletes=True,
)
stickers: Mapped[List["ProductSticker"]] = relationship(
cascade="all, delete-orphan",
passive_deletes=True,
)
ean: Mapped[Optional[str]] = mapped_column(String(64))
sku: Mapped[Optional[str]] = mapped_column(String(128))
unit_size: Mapped[Optional[str]] = mapped_column(String(128))
pack_size: Mapped[Optional[str]] = mapped_column(String(128))
attributes = relationship(
"ProductAttribute",
back_populates="product",
lazy="selectin",
cascade="all, delete-orphan",
)
nutrition = relationship(
"ProductNutrition",
back_populates="product",
lazy="selectin",
cascade="all, delete-orphan",
)
allergens = relationship(
"ProductAllergen",
back_populates="product",
lazy="selectin",
cascade="all, delete-orphan",
)
likes = relationship(
"ProductLike",
back_populates="product",
cascade="all, delete-orphan",
)
cart_items: Mapped[List["CartItem"]] = relationship(
"CartItem",
back_populates="product",
cascade="all, delete-orphan",
)
# NEW: all order items that reference this product
order_items: Mapped[List["OrderItem"]] = relationship(
"OrderItem",
back_populates="product",
cascade="all, delete-orphan",
)
from sqlalchemy import Column
class ProductLike(Base):
__tablename__ = "product_likes"
id = Column(Integer, primary_key=True, autoincrement=True)
user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
product_slug: Mapped[str] = mapped_column(ForeignKey("products.slug", ondelete="CASCADE"))
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now())
deleted_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
product: Mapped["Product"] = relationship("Product", back_populates="likes", foreign_keys=[product_slug])
user = relationship("User", back_populates="liked_products") # optional, if you want reverse access
class ProductImage(Base):
__tablename__ = "product_images"
id: Mapped[int] = mapped_column(primary_key=True)
product_id: Mapped[int] = mapped_column(ForeignKey("products.id", ondelete="CASCADE"), index=True, nullable=False)
url: Mapped[str] = mapped_column(Text, nullable=False)
position: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
kind: Mapped[str] = mapped_column(String(16), nullable=False, default="gallery")
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now())
deleted_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
product: Mapped["Product"] = relationship(back_populates="images")
__table_args__ = (
UniqueConstraint("product_id", "url", "kind", name="uq_product_images_product_url_kind"),
Index("ix_product_images_position", "position"),
)
class ProductSection(Base):
__tablename__ = "product_sections"
id: Mapped[int] = mapped_column(primary_key=True)
product_id: Mapped[int] = mapped_column(
ForeignKey("products.id", ondelete="CASCADE"),
index=True,
nullable=False,
)
title: Mapped[str] = mapped_column(String(255), nullable=False)
html: Mapped[str] = mapped_column(Text, nullable=False)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now())
deleted_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
product: Mapped["Product"] = relationship(back_populates="sections")
__table_args__ = (
UniqueConstraint("product_id", "title", name="uq_product_sections_product_title"),
)
# --- Nav & listings ---
class NavTop(Base):
__tablename__ = "nav_tops"
id: Mapped[int] = mapped_column(primary_key=True)
label: Mapped[str] = mapped_column(String(255), nullable=False)
slug: Mapped[str] = mapped_column(String(255), nullable=False, index=True)
market_id: Mapped[Optional[int]] = mapped_column(
Integer,
ForeignKey("market_places.id", ondelete="SET NULL"),
nullable=True,
index=True,
)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now())
deleted_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
listings: Mapped[List["Listing"]] = relationship(back_populates="top", cascade="all, delete-orphan")
market = relationship("MarketPlace", back_populates="nav_tops")
__table_args__ = (UniqueConstraint("label", "slug", name="uq_nav_tops_label_slug"),)
class NavSub(Base):
__tablename__ = "nav_subs"
id: Mapped[int] = mapped_column(primary_key=True)
top_id: Mapped[int] = mapped_column(ForeignKey("nav_tops.id", ondelete="CASCADE"), index=True, nullable=False)
label: Mapped[Optional[str]] = mapped_column(String(255))
slug: Mapped[str] = mapped_column(String(255), nullable=False, index=True)
href: Mapped[Optional[str]] = mapped_column(Text)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now())
deleted_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
listings: Mapped[List["Listing"]] = relationship(back_populates="sub", cascade="all, delete-orphan")
__table_args__ = (UniqueConstraint("top_id", "slug", name="uq_nav_subs_top_slug"),)
class Listing(Base):
__tablename__ = "listings"
id: Mapped[int] = mapped_column(primary_key=True)
# Old slug-based fields (optional: remove)
# top_slug: Mapped[str] = mapped_column(String(255), nullable=False, index=True)
# sub_slug: Mapped[Optional[str]] = mapped_column(String(255), index=True)
top_id: Mapped[int] = mapped_column(ForeignKey("nav_tops.id", ondelete="CASCADE"), index=True, nullable=False)
sub_id: Mapped[Optional[int]] = mapped_column(ForeignKey("nav_subs.id", ondelete="CASCADE"), index=True)
total_pages: Mapped[Optional[int]] = mapped_column(Integer)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now())
deleted_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
top: Mapped["NavTop"] = relationship(back_populates="listings")
sub: Mapped[Optional["NavSub"]] = relationship(back_populates="listings")
__table_args__ = (
UniqueConstraint("top_id", "sub_id", name="uq_listings_top_sub"),
)
class ListingItem(Base):
__tablename__ = "listing_items"
id: Mapped[int] = mapped_column(primary_key=True)
listing_id: Mapped[int] = mapped_column(ForeignKey("listings.id", ondelete="CASCADE"), index=True, nullable=False)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now())
deleted_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
slug: Mapped[str] = mapped_column(String(255), nullable=False, index=True)
__table_args__ = (UniqueConstraint("listing_id", "slug", name="uq_listing_items_listing_slug"),)
# --- Reports / redirects / logs ---
class LinkError(Base):
__tablename__ = "link_errors"
id: Mapped[int] = mapped_column(primary_key=True)
product_slug: Mapped[Optional[str]] = mapped_column(String(255), index=True)
href: Mapped[Optional[str]] = mapped_column(Text)
text: Mapped[Optional[str]] = mapped_column(Text)
top: Mapped[Optional[str]] = mapped_column(String(255))
sub: Mapped[Optional[str]] = mapped_column(String(255))
target_slug: Mapped[Optional[str]] = mapped_column(String(255))
type: Mapped[Optional[str]] = mapped_column(String(255))
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now())
deleted_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
class LinkExternal(Base):
__tablename__ = "link_externals"
id: Mapped[int] = mapped_column(primary_key=True)
product_slug: Mapped[Optional[str]] = mapped_column(String(255), index=True)
href: Mapped[Optional[str]] = mapped_column(Text)
text: Mapped[Optional[str]] = mapped_column(Text)
host: Mapped[Optional[str]] = mapped_column(String(255))
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now())
deleted_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
class SubcategoryRedirect(Base):
__tablename__ = "subcategory_redirects"
id: Mapped[int] = mapped_column(primary_key=True)
old_path: Mapped[str] = mapped_column(String(512), nullable=False, index=True)
new_path: Mapped[str] = mapped_column(String(512), nullable=False)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now())
deleted_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
class ProductLog(Base):
__tablename__ = "product_logs"
id: Mapped[int] = mapped_column(primary_key=True)
slug: Mapped[Optional[str]] = mapped_column(String(255), index=True)
href_tried: Mapped[Optional[str]] = mapped_column(Text)
ok: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default="false")
error_type: Mapped[Optional[str]] = mapped_column(String(255))
error_message: Mapped[Optional[str]] = mapped_column(Text)
http_status: Mapped[Optional[int]] = mapped_column(Integer)
final_url: Mapped[Optional[str]] = mapped_column(Text)
transport_error: Mapped[Optional[bool]] = mapped_column(Boolean)
title: Mapped[Optional[str]] = mapped_column(String(512))
has_description_html: Mapped[Optional[bool]] = mapped_column(Boolean)
has_description_short: Mapped[Optional[bool]] = mapped_column(Boolean)
sections_count: Mapped[Optional[int]] = mapped_column(Integer)
images_count: Mapped[Optional[int]] = mapped_column(Integer)
embedded_images_count: Mapped[Optional[int]] = mapped_column(Integer)
all_images_count: Mapped[Optional[int]] = mapped_column(Integer)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now())
# ...existing models...
class ProductLabel(Base):
__tablename__ = "product_labels"
id: Mapped[int] = mapped_column(primary_key=True)
product_id: Mapped[int] = mapped_column(ForeignKey("products.id", ondelete="CASCADE"), index=True, nullable=False)
name: Mapped[str] = mapped_column(String(255), nullable=False)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now())
deleted_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
product: Mapped["Product"] = relationship(back_populates="labels")
__table_args__ = (UniqueConstraint("product_id", "name", name="uq_product_labels_product_name"),)
class ProductSticker(Base):
__tablename__ = "product_stickers"
id: Mapped[int] = mapped_column(primary_key=True)
product_id: Mapped[int] = mapped_column(ForeignKey("products.id", ondelete="CASCADE"), index=True, nullable=False)
name: Mapped[str] = mapped_column(String(255), nullable=False)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now())
deleted_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
product: Mapped["Product"] = relationship(back_populates="stickers")
__table_args__ = (UniqueConstraint("product_id", "name", name="uq_product_stickers_product_name"),)
class ProductAttribute(Base):
__tablename__ = "product_attributes"
id: Mapped[int] = mapped_column(primary_key=True)
product_id: Mapped[int] = mapped_column(ForeignKey("products.id", ondelete="CASCADE"), index=True, nullable=False)
key: Mapped[str] = mapped_column(String(255), nullable=False)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now())
deleted_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
value: Mapped[Optional[str]] = mapped_column(Text)
product = relationship("Product", back_populates="attributes")
__table_args__ = (UniqueConstraint("product_id", "key", name="uq_product_attributes_product_key"),)
class ProductNutrition(Base):
__tablename__ = "product_nutrition"
id: Mapped[int] = mapped_column(primary_key=True)
product_id: Mapped[int] = mapped_column(ForeignKey("products.id", ondelete="CASCADE"), index=True, nullable=False)
key: Mapped[str] = mapped_column(String(255), nullable=False)
value: Mapped[Optional[str]] = mapped_column(String(255))
unit: Mapped[Optional[str]] = mapped_column(String(64))
product = relationship("Product", back_populates="nutrition")
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now())
deleted_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
__table_args__ = (UniqueConstraint("product_id", "key", name="uq_product_nutrition_product_key"),)
class ProductAllergen(Base):
__tablename__ = "product_allergens"
id: Mapped[int] = mapped_column(primary_key=True)
product_id: Mapped[int] = mapped_column(ForeignKey("products.id", ondelete="CASCADE"), index=True, nullable=False)
name: Mapped[str] = mapped_column(String(255), nullable=False)
contains: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default="false")
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now())
deleted_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
product: Mapped["Product"] = relationship(back_populates="allergens")
__table_args__ = (UniqueConstraint("product_id", "name", name="uq_product_allergens_product_name"),)
class CartItem(Base):
__tablename__ = "cart_items"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
# Either a logged-in user OR an anonymous session
user_id: Mapped[int | None] = mapped_column(
ForeignKey("users.id", ondelete="CASCADE"),
nullable=True,
)
session_id: Mapped[str | None] = mapped_column(
String(128),
nullable=True,
)
# IMPORTANT: link to product *id*, not slug
product_id: Mapped[int] = mapped_column(
ForeignKey("products.id", ondelete="CASCADE"),
nullable=False,
)
quantity: Mapped[int] = mapped_column(
Integer,
nullable=False,
default=1,
server_default="1",
)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
nullable=False,
server_default=func.now(),
)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
nullable=False,
server_default=func.now(),
)
market_place_id: Mapped[int | None] = mapped_column(
ForeignKey("market_places.id", ondelete="SET NULL"),
nullable=True,
index=True,
)
deleted_at: Mapped[datetime | None] = mapped_column(
DateTime(timezone=True),
nullable=True,
)
# Relationships
market_place: Mapped["MarketPlace | None"] = relationship(
"MarketPlace",
foreign_keys=[market_place_id],
)
product: Mapped["Product"] = relationship(
"Product",
back_populates="cart_items",
)
user: Mapped["User | None"] = relationship("User", back_populates="cart_items")
__table_args__ = (
Index("ix_cart_items_user_product", "user_id", "product_id"),
Index("ix_cart_items_session_product", "session_id", "product_id"),
)