diff --git a/monitor_utils/config.py b/monitor_utils/config.py index c981083..0342123 100644 --- a/monitor_utils/config.py +++ b/monitor_utils/config.py @@ -1,6 +1,8 @@ import json from enum import Enum from functools import cache +import pathlib + from pydantic_settings import BaseSettings, SettingsConfigDict from monitor_utils.constants import PROXIES_PATH, PROJECT_ROOT @@ -22,6 +24,13 @@ class Settings(BaseSettings): model_config = SettingsConfigDict(env_file=(".env", ".env.local"), extra="ignore") + @property + def bot_token(self) -> str: + if self.development_mode == DevelopmentMode.PRODUCTION: + return self.production_bot_token + + return self.development_bot_token + @cache def get_settings() -> Settings: @@ -39,11 +48,14 @@ def get_proxies() -> list[str]: return proxies -def get_config(config_name: str, use_production_path: bool = False) -> dict: - config_name = config_name.replace(".json", "") +def get_config( + monitor_name: str, use_production_path: bool = False +) -> tuple[dict, pathlib.Path]: + monitor_name = monitor_name.replace(".json", "") if use_production_path: - config_path = PROJECT_ROOT / "configs" / f"{config_name}.json" + config_path = PROJECT_ROOT / "configs" / f"{monitor_name}.json" else: - config_path = PROJECT_ROOT / "tests" / "configs" / f"{config_name}.json" - with open(config_path) as config: - return json.load(config) + config_path = PROJECT_ROOT / "tests" / "configs" / f"{monitor_name}.json" + + with open(config_path) as f: + return json.load(f), config_path diff --git a/monitor_utils/constants.py b/monitor_utils/constants.py index 3efb164..ec4342c 100644 --- a/monitor_utils/constants.py +++ b/monitor_utils/constants.py @@ -11,6 +11,10 @@ PROXIES_PATH = pathlib.Path("proxies.txt") RETRY_LIMIT = 5 RETRY_WAIT = 3 +BACKOFF_DELAY = 0.001 +FAILURE_DELAY = 0.001 +MAX_BACKOFF = 10 + TIMEOUT_SECONDS = 30 CLIENT_IDENTIFIERS = ["chrome", "chrome_android", "safari", "safari_ios"] CLIENT_TIMEOUT = aiohttp.ClientTimeout( @@ -32,6 +36,10 @@ BUCKET_WEBHOOK_MAP = { "price_drop": "40-49", } MONITOR_NAMES = ["bestbuy", "woot", "lg", "hp", "samsung"] +STATUS_PENDING = "pending" +STATUS_SENT = "sent" +STATUS_SKIPPED = "skipped" +STATUS_FAILED = "failed" EXPECTED_TASK_ERRORS = ( RequestsError, diff --git a/monitor_utils/database.py b/monitor_utils/database.py index 6a35232..d66ae08 100644 --- a/monitor_utils/database.py +++ b/monitor_utils/database.py @@ -55,20 +55,20 @@ async def add_products_to_database( for product in products: document = convert_product_to_mongo(product, category_identifier) doc_id = document["_id"] - old_price = existing_prices.get(doc_id) new_price = document.get("currentPrice") price_dropped = old_price is None or ( new_price is not None and new_price < old_price ) + if price_dropped: - document["webhookSent"] = False + document["status"] = STATUS_PENDING document["priceBeforeDrop"] = old_price set_on_insert = {"discoveredDate": datetime.now(timezone.utc)} if not price_dropped: - set_on_insert["webhookSent"] = False + set_on_insert["status"] = STATUS_PENDING ops.append( UpdateOne( @@ -81,7 +81,7 @@ async def add_products_to_database( try: await collection.bulk_write(ops, ordered=False) except PyMongoError: - LOGGER.exception(f"{category_identifier} - Failed to add products to database!") + LOGGER.exception(f"{category_identifier} - failed to add products to database") raise @@ -91,7 +91,6 @@ def convert_product_to_mongo(product: Product, category_identifier: str) -> dict timing = data["timing"] pricing = data["pricing"] availability = data["availability"] - current_price = pricing["current_price"] original_price = pricing["original_price"] if isinstance(current_price, str): diff --git a/monitor_utils/models.py b/monitor_utils/models.py index 64978a3..b70d97a 100644 --- a/monitor_utils/models.py +++ b/monitor_utils/models.py @@ -3,6 +3,28 @@ from dataclasses import field from pydantic.dataclasses import dataclass +@dataclass +class ConsumerConfig: + # used claude to refactor the consumer building in the discord bot + """Configuration for a single retailer's consumer. + + Attributes: + monitor_name: Retailer name (matches setup_config key). + collection: MongoDB collection for this retailer's products. + channel_ids: Discord webhook/channel routing config. + min_percentage: Minimum discount % to trigger a ping. + ping_threshold: Price floor for the secondary 40% rule (e.g. Woot pings + on 40%+ discounts if the item is >= this price). None disables + the secondary rule. + """ + + monitor_name: str + collection: object + channel_ids: dict + min_percentage: int + ping_threshold: int | None = None + + @dataclass class PricingInfo: original_price: float @@ -43,7 +65,7 @@ class Product: product_id: str title: str url: str + pricing: PricingInfo metadata: ProductMetadata = field(default_factory=ProductMetadata) timing: ProductTiming = field(default_factory=ProductTiming) - pricing: PricingInfo = field(default_factory=PricingInfo) availability: AvailabilityInfo = field(default_factory=AvailabilityInfo) diff --git a/monitor_utils/runner.py b/monitor_utils/runner.py index 19d04e8..683712f 100644 --- a/monitor_utils/runner.py +++ b/monitor_utils/runner.py @@ -33,16 +33,16 @@ def configure_logging(): ) -def setup_config(config_name: str) -> tuple[list[str], dict, AsyncIOMotorCollection]: +def setup_config(monitor_name: str) -> tuple[list[str], dict, AsyncIOMotorCollection]: is_production = get_settings().development_mode == "PRODUCTION" - config = get_config( - config_name=config_name, + config, _ = get_config( + monitor_name=monitor_name, use_production_path=is_production, ) category_names = config["categories"] collection = get_collection( - config_name if is_production else f"{config_name}_testing" + monitor_name if is_production else f"{monitor_name}_testing" ) return category_names, config, collection