feat: decouple market from shared_lib, add app-owned models
Phase 1-3 of decoupling: - path_setup.py adds project root to sys.path - Market-owned models in market/models/ (market, market_place) - All imports updated: shared.infrastructure, shared.db, shared.browser, etc. - MarketPlace uses container_type/container_id instead of post_id FK Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
0
__init__.py
Normal file
0
__init__.py
Normal file
40
app.py
40
app.py
@@ -7,11 +7,10 @@ from quart import g, abort, render_template, make_response
|
||||
from jinja2 import FileSystemLoader, ChoiceLoader
|
||||
from sqlalchemy import select
|
||||
|
||||
from shared.factory import create_base_app
|
||||
from shared.cart_loader import load_cart
|
||||
from config import config
|
||||
from shared.infrastructure.factory import create_base_app
|
||||
from shared.config import config
|
||||
|
||||
from suma_browser.app.bp import register_market_bp
|
||||
from bp import register_market_bp
|
||||
|
||||
|
||||
async def market_context() -> dict:
|
||||
@@ -21,8 +20,8 @@ async def market_context() -> dict:
|
||||
- menu_items: fetched from coop internal API
|
||||
- cart_count/cart_total: fetched from cart internal API
|
||||
"""
|
||||
from shared.context import base_context
|
||||
from shared.internal_api import get as api_get, dictobj
|
||||
from shared.infrastructure.context import base_context
|
||||
from shared.infrastructure.internal_api import get as api_get, dictobj
|
||||
|
||||
ctx = await base_context()
|
||||
|
||||
@@ -44,9 +43,9 @@ async def market_context() -> dict:
|
||||
|
||||
def create_app() -> "Quart":
|
||||
from models.market_place import MarketPlace
|
||||
from models.ghost_content import Post
|
||||
from blog.models.ghost_content import Post
|
||||
|
||||
app = create_base_app("market", context_fn=market_context, before_request_fns=[load_cart])
|
||||
app = create_base_app("market", context_fn=market_context)
|
||||
|
||||
# App-specific templates override shared templates
|
||||
app_templates = str(Path(__file__).resolve().parent / "templates")
|
||||
@@ -111,12 +110,13 @@ def create_app() -> "Quart":
|
||||
},
|
||||
}
|
||||
|
||||
# Load market scoped to post
|
||||
# Load market scoped to post (container pattern)
|
||||
market = (
|
||||
await g.s.execute(
|
||||
select(MarketPlace).where(
|
||||
MarketPlace.slug == market_slug,
|
||||
MarketPlace.post_id == post.id,
|
||||
MarketPlace.container_type == "page",
|
||||
MarketPlace.container_id == post.id,
|
||||
MarketPlace.deleted_at.is_(None),
|
||||
)
|
||||
)
|
||||
@@ -135,16 +135,24 @@ def create_app() -> "Quart":
|
||||
# --- Root route: market listing ---
|
||||
@app.get("/")
|
||||
async def markets_listing():
|
||||
from sqlalchemy.orm import selectinload
|
||||
|
||||
markets = (
|
||||
rows = (
|
||||
await g.s.execute(
|
||||
select(MarketPlace)
|
||||
select(MarketPlace, Post)
|
||||
.join(
|
||||
Post,
|
||||
(MarketPlace.container_type == "page")
|
||||
& (MarketPlace.container_id == Post.id),
|
||||
)
|
||||
.where(MarketPlace.deleted_at.is_(None))
|
||||
.options(selectinload(MarketPlace.post))
|
||||
.order_by(MarketPlace.name)
|
||||
)
|
||||
).scalars().all()
|
||||
).all()
|
||||
|
||||
# Attach the joined post to each market for template access
|
||||
markets = []
|
||||
for market, post in rows:
|
||||
market.page = post
|
||||
markets.append(market)
|
||||
|
||||
html = await render_template(
|
||||
"_types/market/markets_listing.html",
|
||||
|
||||
@@ -27,8 +27,8 @@ from models.market import (
|
||||
ProductAllergen,
|
||||
)
|
||||
|
||||
from suma_browser.app.redis_cacher import clear_cache
|
||||
from suma_browser.app.csrf import csrf_exempt
|
||||
from shared.browser.app.redis_cacher import clear_cache
|
||||
from shared.browser.app.csrf import csrf_exempt
|
||||
|
||||
|
||||
products_api = Blueprint("products_api", __name__, url_prefix="/api/products")
|
||||
|
||||
@@ -10,7 +10,7 @@ from quart import (
|
||||
make_response,
|
||||
current_app,
|
||||
)
|
||||
from config import config
|
||||
from shared.config import config
|
||||
from .services.nav import category_context, get_nav
|
||||
from .services.blacklist.category import is_category_blocked
|
||||
|
||||
@@ -21,8 +21,8 @@ from .services import (
|
||||
_current_url_without_page,
|
||||
)
|
||||
|
||||
from suma_browser.app.redis_cacher import cache_page
|
||||
from suma_browser.app.utils.htmx import is_htmx_request
|
||||
from shared.browser.app.redis_cacher import cache_page
|
||||
from shared.browser.app.utils.htmx import is_htmx_request
|
||||
|
||||
def register():
|
||||
browse_bp = Blueprint("browse", __name__)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# suma_browser/category_blacklist.py
|
||||
from __future__ import annotations
|
||||
from typing import Optional
|
||||
from config import config
|
||||
from shared.config import config
|
||||
|
||||
def _norm(s: str) -> str:
|
||||
return (s or "").strip().lower().strip("/")
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from typing import Set, Optional
|
||||
from ..slugs import canonical_html_slug
|
||||
from config import config
|
||||
from shared.config import config
|
||||
|
||||
_blocked: Set[str] = set()
|
||||
_mtime: Optional[float] = None
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import re
|
||||
from config import config
|
||||
from shared.config import config
|
||||
|
||||
def _norm_title_key(t: str) -> str:
|
||||
t = (t or "").strip().lower()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from __future__ import annotations
|
||||
import os, json
|
||||
from typing import List, Optional
|
||||
from config import config
|
||||
from shared.config import config
|
||||
from .blacklist.product import is_product_blocked
|
||||
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ from typing import Dict, List, Optional
|
||||
from sqlalchemy import select, and_
|
||||
from sqlalchemy.orm import selectinload
|
||||
|
||||
from config import config # if unused elsewhere, you can remove this import
|
||||
from shared.config import config # if unused elsewhere, you can remove this import
|
||||
|
||||
# ORM models
|
||||
from models.market import (
|
||||
|
||||
@@ -5,7 +5,7 @@ import re
|
||||
from typing import Dict, List, Tuple, Optional
|
||||
from urllib.parse import urlparse, urljoin
|
||||
|
||||
from config import config
|
||||
from shared.config import config
|
||||
from . import db_backend as cb
|
||||
from .blacklist.category import is_category_blocked # Reverse map: slug -> label
|
||||
|
||||
|
||||
@@ -6,11 +6,11 @@ from quart import (
|
||||
g,
|
||||
request,
|
||||
)
|
||||
from config import config
|
||||
from shared.config import config
|
||||
from .products import products, products_nocounts
|
||||
from .blacklist.product_details import is_blacklisted_heading
|
||||
|
||||
from utils import host_url
|
||||
from shared.utils import host_url
|
||||
|
||||
|
||||
from sqlalchemy import select
|
||||
@@ -163,7 +163,7 @@ def _massage_product(d):
|
||||
|
||||
|
||||
# Re-export from canonical shared location
|
||||
from shared.http_utils import vary as _vary, current_url_without_page as _current_url_without_page
|
||||
from shared.infrastructure.http_utils import vary as _vary, current_url_without_page as _current_url_without_page
|
||||
|
||||
async def _is_liked(user_id: int | None, slug: str) -> bool:
|
||||
"""
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import re
|
||||
from urllib.parse import urljoin, urlparse
|
||||
from config import config
|
||||
from shared.config import config
|
||||
|
||||
def product_slug_from_href(href: str) -> str:
|
||||
p = urlparse(href)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Re-export from canonical shared location
|
||||
from shared.cart_identity import CartIdentity, current_cart_identity
|
||||
from shared.infrastructure.cart_identity import CartIdentity, current_cart_identity
|
||||
|
||||
__all__ = ["CartIdentity", "current_cart_identity"]
|
||||
|
||||
@@ -5,7 +5,7 @@ from quart import (
|
||||
)
|
||||
|
||||
|
||||
from suma_browser.app.authz import require_admin
|
||||
from shared.browser.app.authz import require_admin
|
||||
|
||||
|
||||
def register():
|
||||
@@ -15,7 +15,7 @@ def register():
|
||||
@bp.get("/")
|
||||
@require_admin
|
||||
async def admin():
|
||||
from suma_browser.app.utils.htmx import is_htmx_request
|
||||
from shared.browser.app.utils.htmx import is_htmx_request
|
||||
|
||||
# Determine which template to use based on request type
|
||||
if not is_htmx_request():
|
||||
|
||||
@@ -2,10 +2,10 @@ from quart import request
|
||||
|
||||
from typing import Iterable, Optional, Union
|
||||
|
||||
from suma_browser.app.filters.qs_base import (
|
||||
from shared.browser.app.filters.qs_base import (
|
||||
KEEP, _norm, make_filter_set, build_qs,
|
||||
)
|
||||
from suma_browser.app.filters.query_types import MarketQuery
|
||||
from shared.browser.app.filters.query_types import MarketQuery
|
||||
|
||||
|
||||
def decode() -> MarketQuery:
|
||||
|
||||
@@ -15,8 +15,8 @@ from ..browse.services.slugs import canonical_html_slug
|
||||
from ..browse.services.blacklist.product import is_product_blocked
|
||||
from ..browse.services import db_backend as cb
|
||||
from ..browse.services import _massage_product
|
||||
from utils import host_url
|
||||
from suma_browser.app.redis_cacher import cache_page, clear_cache
|
||||
from shared.utils import host_url
|
||||
from shared.browser.app.redis_cacher import cache_page, clear_cache
|
||||
from ..cart.services import total
|
||||
from .services.product_operations import toggle_product_like, massage_full_product
|
||||
|
||||
@@ -94,7 +94,7 @@ def register():
|
||||
@bp.get("/")
|
||||
@cache_page(tag="browse")
|
||||
async def product_detail(slug: str):
|
||||
from suma_browser.app.utils.htmx import is_htmx_request
|
||||
from shared.browser.app.utils.htmx import is_htmx_request
|
||||
|
||||
# Determine which template to use based on request type
|
||||
if not is_htmx_request():
|
||||
@@ -136,11 +136,11 @@ def register():
|
||||
)
|
||||
return html
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@bp.get("/admin/")
|
||||
async def admin(slug: str):
|
||||
from suma_browser.app.utils.htmx import is_htmx_request
|
||||
from shared.browser.app.utils.htmx import is_htmx_request
|
||||
|
||||
if not is_htmx_request():
|
||||
# Normal browser request: full page with layout
|
||||
@@ -152,8 +152,8 @@ def register():
|
||||
return await make_response(html)
|
||||
|
||||
|
||||
from suma_browser.app.bp.cart.services.identity import current_cart_identity
|
||||
#from suma_browser.app.bp.cart.routes import view_cart
|
||||
from bp.cart.services.identity import current_cart_identity
|
||||
#from bp.cart.routes import view_cart
|
||||
from models.market import CartItem
|
||||
from quart import request, url_for
|
||||
|
||||
@@ -242,7 +242,7 @@ def register():
|
||||
)
|
||||
|
||||
# normal POST: go to cart page
|
||||
from shared.urls import cart_url
|
||||
from shared.infrastructure.urls import cart_url
|
||||
return redirect(cart_url("/"))
|
||||
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ def massage_full_product(product: Product) -> dict:
|
||||
Convert a Product ORM model to a dictionary with all fields.
|
||||
Used for rendering product detail pages.
|
||||
"""
|
||||
from suma_browser.app.bp.browse.services import _massage_product
|
||||
from bp.browse.services import _massage_product
|
||||
|
||||
gallery = []
|
||||
if product.image:
|
||||
|
||||
8
models/__init__.py
Normal file
8
models/__init__.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from .market import (
|
||||
Product, ProductLike, ProductImage, ProductSection,
|
||||
NavTop, NavSub, Listing, ListingItem,
|
||||
LinkError, LinkExternal, SubcategoryRedirect, ProductLog,
|
||||
ProductLabel, ProductSticker, ProductAttribute, ProductNutrition, ProductAllergen,
|
||||
CartItem,
|
||||
)
|
||||
from .market_place import MarketPlace
|
||||
441
models/market.py
Normal file
441
models/market.py
Normal file
@@ -0,0 +1,441 @@
|
||||
# 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"),
|
||||
)
|
||||
52
models/market_place.py
Normal file
52
models/market_place.py
Normal file
@@ -0,0 +1,52 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from typing import Optional, List
|
||||
|
||||
from sqlalchemy import (
|
||||
Integer, String, Text, DateTime, ForeignKey, Index, func, text,
|
||||
)
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from shared.db.base import Base
|
||||
|
||||
|
||||
def utcnow() -> datetime:
|
||||
return datetime.now(timezone.utc)
|
||||
|
||||
|
||||
class MarketPlace(Base):
|
||||
__tablename__ = "market_places"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
container_type: Mapped[str] = mapped_column(
|
||||
String(32), nullable=False, server_default=text("'page'"),
|
||||
)
|
||||
container_id: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
slug: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
description: Mapped[Optional[str]] = mapped_column(Text, nullable=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), nullable=True,
|
||||
)
|
||||
|
||||
nav_tops: Mapped[List["NavTop"]] = relationship(
|
||||
"NavTop", back_populates="market",
|
||||
)
|
||||
|
||||
__table_args__ = (
|
||||
Index("ix_market_places_container", "container_type", "container_id"),
|
||||
Index(
|
||||
"ux_market_places_slug_active",
|
||||
func.lower(slug),
|
||||
unique=True,
|
||||
postgresql_where=text("deleted_at IS NULL"),
|
||||
),
|
||||
)
|
||||
@@ -1,7 +1,9 @@
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Add the shared library submodule to the Python path
|
||||
_shared = os.path.join(os.path.dirname(os.path.abspath(__file__)), "shared_lib")
|
||||
if _shared not in sys.path:
|
||||
sys.path.insert(0, _shared)
|
||||
_app_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
_project_root = os.path.dirname(_app_dir)
|
||||
|
||||
for _p in (_project_root, _app_dir):
|
||||
if _p not in sys.path:
|
||||
sys.path.insert(0, _p)
|
||||
|
||||
@@ -7,9 +7,9 @@ from typing import Dict, Set
|
||||
from ..http_client import configure_cookies
|
||||
from ..get_auth import login
|
||||
|
||||
from config import config
|
||||
from shared.config import config
|
||||
|
||||
from utils import log
|
||||
from shared.utils import log
|
||||
|
||||
# DB: persistence helpers
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ from bs4 import BeautifulSoup
|
||||
from urllib.parse import urlparse, urljoin
|
||||
|
||||
from ._anchor_text import _anchor_text
|
||||
from suma_browser.app.bp.browse.services.slugs import product_slug_from_href
|
||||
from bp.browse.services.slugs import product_slug_from_href
|
||||
from .APP_ROOT_PLACEHOLDER import APP_ROOT_PLACEHOLDER
|
||||
|
||||
def _rewrite_links_fragment(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from urllib.parse import urljoin
|
||||
from config import config
|
||||
from utils import log
|
||||
from shared.config import config
|
||||
from shared.utils import log
|
||||
from ...listings import scrape_products
|
||||
|
||||
async def capture_category(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from typing import Dict, Set
|
||||
from .capture_category import capture_category
|
||||
from .capture_sub import capture_sub
|
||||
from config import config
|
||||
from shared.config import config
|
||||
|
||||
|
||||
async def capture_product_slugs(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from urllib.parse import urljoin
|
||||
from urllib.parse import urljoin
|
||||
from config import config
|
||||
from utils import log
|
||||
from shared.config import config
|
||||
from shared.utils import log
|
||||
from ...listings import scrape_products
|
||||
|
||||
async def capture_sub(
|
||||
|
||||
@@ -6,12 +6,12 @@ import httpx
|
||||
|
||||
|
||||
from ...html_utils import to_fragment
|
||||
from suma_browser.app.bp.browse.services.slugs import suma_href_from_html_slug
|
||||
|
||||
from bp.browse.services.slugs import suma_href_from_html_slug
|
||||
|
||||
from config import config
|
||||
|
||||
from utils import log
|
||||
from shared.config import config
|
||||
|
||||
from shared.utils import log
|
||||
|
||||
# DB: persistence helpers
|
||||
from ...product.product_detail import scrape_product_detail
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import asyncio
|
||||
from typing import Dict, List, Set
|
||||
from config import config
|
||||
from utils import log
|
||||
from shared.config import config
|
||||
from shared.utils import log
|
||||
from .fetch_and_upsert_product import fetch_and_upsert_product
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
|
||||
from typing import Dict
|
||||
from urllib.parse import urljoin
|
||||
from config import config
|
||||
from shared.config import config
|
||||
|
||||
def rewrite_nav(nav: Dict[str, Dict], nav_redirects:Dict[str, str]):
|
||||
if nav_redirects:
|
||||
|
||||
@@ -2,7 +2,7 @@ from typing import Optional, Dict, Any, List
|
||||
from urllib.parse import urljoin
|
||||
import httpx
|
||||
from bs4 import BeautifulSoup
|
||||
from config import config
|
||||
from shared.config import config
|
||||
|
||||
class LoginFailed(Exception):
|
||||
def __init__(self, message: str, *, debug: Dict[str, Any]):
|
||||
|
||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
from typing import Optional
|
||||
from bs4 import BeautifulSoup
|
||||
from urllib.parse import urljoin
|
||||
from config import config
|
||||
from shared.config import config
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import secrets
|
||||
from typing import Optional, Dict
|
||||
|
||||
import httpx
|
||||
from config import config
|
||||
from shared.config import config
|
||||
|
||||
_CLIENT: httpx.AsyncClient | None = None
|
||||
|
||||
|
||||
@@ -7,8 +7,8 @@ from urllib.parse import parse_qsl, urlencode, urljoin, urlparse, urlunparse
|
||||
|
||||
|
||||
from .http_client import fetch
|
||||
from suma_browser.app.bp.browse.services.slugs import product_slug_from_href
|
||||
from suma_browser.app.bp.browse.services.state import (
|
||||
from bp.browse.services.slugs import product_slug_from_href
|
||||
from bp.browse.services.state import (
|
||||
KNOWN_PRODUCT_SLUGS,
|
||||
_listing_page_cache,
|
||||
_listing_page_ttl,
|
||||
@@ -16,8 +16,8 @@ from suma_browser.app.bp.browse.services.state import (
|
||||
_listing_variant_ttl,
|
||||
now,
|
||||
)
|
||||
from utils import normalize_text, soup_of
|
||||
from config import config
|
||||
from shared.utils import normalize_text, soup_of
|
||||
from shared.config import config
|
||||
|
||||
|
||||
def parse_total_pages_from_text(text: str) -> Optional[int]:
|
||||
|
||||
@@ -5,7 +5,7 @@ from typing import Dict, List, Tuple, Optional
|
||||
from urllib.parse import urlparse, urljoin
|
||||
|
||||
from bs4 import BeautifulSoup
|
||||
from config import config
|
||||
from shared.config import config
|
||||
from .http_client import fetch # only fetch; define soup_of locally
|
||||
#from .. import cache_backend as cb
|
||||
#from ..blacklist.category import is_category_blocked # Reverse map: slug -> label
|
||||
|
||||
@@ -17,7 +17,7 @@ from models.market import (
|
||||
Listing,
|
||||
ListingItem,
|
||||
)
|
||||
from db.session import get_session
|
||||
from shared.db.session import get_session
|
||||
|
||||
# --- Models are unchanged, see original code ---
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ from typing import Dict
|
||||
from models.market import (
|
||||
ProductLog,
|
||||
)
|
||||
from db.session import get_session
|
||||
from shared.db.session import get_session
|
||||
|
||||
|
||||
async def log_product_result(ok: bool, payload: Dict) -> None:
|
||||
|
||||
@@ -7,7 +7,7 @@ from models.market import (
|
||||
LinkError,
|
||||
LinkExternal,
|
||||
)
|
||||
from db.session import get_session
|
||||
from shared.db.session import get_session
|
||||
|
||||
# --- Models are unchanged, see original code ---
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ from models.market import (
|
||||
NavTop,
|
||||
NavSub,
|
||||
)
|
||||
from db.session import get_session
|
||||
from shared.db.session import get_session
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ from sqlalchemy import (
|
||||
from models.market import (
|
||||
SubcategoryRedirect,
|
||||
)
|
||||
from db.session import get_session
|
||||
from shared.db.session import get_session
|
||||
|
||||
# --- Models are unchanged, see original code ---
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ from models.market import (
|
||||
ProductNutrition,
|
||||
ProductAllergen
|
||||
)
|
||||
from db.session import get_session
|
||||
from shared.db.session import get_session
|
||||
|
||||
from ._get import _get
|
||||
from .log_product_result import _log_product_result
|
||||
|
||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
from typing import Dict, List, Union
|
||||
from urllib.parse import urlparse
|
||||
from bs4 import BeautifulSoup
|
||||
from utils import normalize_text
|
||||
from shared.utils import normalize_text
|
||||
from ..registry import extractor
|
||||
|
||||
@extractor
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
from __future__ import annotations
|
||||
from typing import Dict, List
|
||||
from bs4 import BeautifulSoup
|
||||
from utils import normalize_text
|
||||
from shared.utils import normalize_text
|
||||
from ...html_utils import absolutize_fragment
|
||||
from ..registry import extractor
|
||||
from ..helpers.desc import (
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
from __future__ import annotations
|
||||
from typing import Dict, Union
|
||||
from bs4 import BeautifulSoup
|
||||
from utils import normalize_text
|
||||
from shared.utils import normalize_text
|
||||
from ..registry import extractor
|
||||
from ..helpers.price import parse_price, parse_case_size
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
from __future__ import annotations
|
||||
from typing import Dict, List
|
||||
from bs4 import BeautifulSoup
|
||||
from utils import normalize_text
|
||||
from shared.utils import normalize_text
|
||||
from ..registry import extractor
|
||||
|
||||
@extractor
|
||||
|
||||
@@ -2,7 +2,7 @@ from __future__ import annotations
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
import re
|
||||
from bs4 import BeautifulSoup
|
||||
from utils import normalize_text
|
||||
from shared.utils import normalize_text
|
||||
from ..registry import extractor
|
||||
from ..helpers.desc import (
|
||||
split_description_container, find_description_container,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
from __future__ import annotations
|
||||
from typing import Dict
|
||||
from bs4 import BeautifulSoup
|
||||
from utils import normalize_text
|
||||
from shared.utils import normalize_text
|
||||
from ..registry import extractor
|
||||
|
||||
@extractor
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
from __future__ import annotations
|
||||
from typing import Dict
|
||||
from bs4 import BeautifulSoup
|
||||
from utils import normalize_text
|
||||
from shared.utils import normalize_text
|
||||
from ..registry import extractor
|
||||
|
||||
@extractor
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
from __future__ import annotations
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
from bs4 import BeautifulSoup, NavigableString, Tag
|
||||
from utils import normalize_text
|
||||
from shared.utils import normalize_text
|
||||
from ...html_utils import absolutize_fragment
|
||||
from .text import clean_title, is_blacklisted_heading
|
||||
from config import config
|
||||
from shared.config import config
|
||||
|
||||
|
||||
def split_description_container(desc_el: Tag) -> Tuple[str, List[Dict]]:
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
from __future__ import annotations
|
||||
from typing import List, Optional
|
||||
from urllib.parse import urljoin, urlparse
|
||||
from config import config
|
||||
from shared.config import config
|
||||
|
||||
def first_from_srcset(val: str) -> Optional[str]:
|
||||
if not val:
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
|
||||
from __future__ import annotations
|
||||
import re
|
||||
from utils import normalize_text
|
||||
from config import config
|
||||
from shared.utils import normalize_text
|
||||
from shared.config import config
|
||||
|
||||
def clean_title(t: str) -> str:
|
||||
t = normalize_text(t)
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
|
||||
from __future__ import annotations
|
||||
from typing import Dict, Tuple, Union
|
||||
from utils import soup_of
|
||||
from shared.utils import soup_of
|
||||
from ..http_client import fetch
|
||||
from ..html_utils import absolutize_fragment
|
||||
from suma_browser.app.bp.browse.services.slugs import product_slug_from_href
|
||||
from bp.browse.services.slugs import product_slug_from_href
|
||||
from .registry import REGISTRY, merge_missing
|
||||
from . import extractors as _auto_register # noqa: F401 (import-time side effects)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user