fixed price drops/moved logging outside of init_monitor
This commit is contained in:
1 parent
7d070dec40
commit
00d3c12ee0
3 files changed
+98
-27
No files matched your search
@@ -94,10 +94,12 @@ async def add_products_to_database(
|
|||||||
):
|
):
|
||||||
"""Upsert products, tracking deduplicated price history and emitting alerts.
|
"""Upsert products, tracking deduplicated price history and emitting alerts.
|
||||||
|
|
||||||
Rewrites current fields on each upsert. Prices are recorded in a
|
Rewrites current fields on each upsert. priceBeforeDrop is derived here,
|
||||||
pastPrices array of price/date entries; a price already present in the
|
not by monitors: it is set to the previous stored currentPrice when the new
|
||||||
array is never recorded again, so each distinct price keeps only its
|
price is lower (a drop) and cleared otherwise, so any price_before_drop on
|
||||||
first-seen timestamp. New products and never-before-recorded prices
|
the incoming product is ignored. Prices are recorded in a pastPrices array
|
||||||
|
of price/date entries; a price already present in the array is never
|
||||||
|
recorded again, so each distinct price keeps only its first-seen timestamp. New products and never-before-recorded prices
|
||||||
upsert an alert referencing the product; a price returning to a value
|
upsert an alert referencing the product; a price returning to a value
|
||||||
already in pastPrices emits nothing, so per-source price oscillation
|
already in pastPrices emits nothing, so per-source price oscillation
|
||||||
(member vs non-member quotes for the same SKU) alerts once per distinct
|
(member vs non-member quotes for the same SKU) alerts once per distinct
|
||||||
@@ -133,6 +135,11 @@ async def add_products_to_database(
|
|||||||
document["_id"] = doc_id
|
document["_id"] = doc_id
|
||||||
existing_doc = existing.get(doc_id)
|
existing_doc = existing.get(doc_id)
|
||||||
new_price = document.get("currentPrice")
|
new_price = document.get("currentPrice")
|
||||||
|
previous_price = existing_doc.get("currentPrice") if existing_doc else None
|
||||||
|
document["priceBeforeDrop"] = resolve_price_before_drop(
|
||||||
|
previous_price, new_price
|
||||||
|
)
|
||||||
|
|
||||||
recorded_prices = extract_recorded_prices(existing_doc)
|
recorded_prices = extract_recorded_prices(existing_doc)
|
||||||
never_seen_price = new_price is not None and new_price not in recorded_prices
|
never_seen_price = new_price is not None and new_price not in recorded_prices
|
||||||
set_on_insert = {"discoveredDate": now}
|
set_on_insert = {"discoveredDate": now}
|
||||||
@@ -193,6 +200,29 @@ async def write_alert_ops(
|
|||||||
raise
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_price_before_drop(
|
||||||
|
previous_price: float | None, new_price: float | None
|
||||||
|
) -> float | None:
|
||||||
|
"""Return the pre-drop price when new_price is a drop from previous_price.
|
||||||
|
|
||||||
|
Owns price-drop detection so monitors do not: a drop is a new price strictly
|
||||||
|
below the previously stored one, and its priceBeforeDrop is that prior price.
|
||||||
|
Any other case (no prior price, unchanged, or a rise) is not a drop.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
previous_price: The product's last stored currentPrice, or None when new.
|
||||||
|
new_price: The incoming currentPrice.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
previous_price on a drop, otherwise None.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if previous_price is not None and new_price is not None and new_price < previous_price:
|
||||||
|
return previous_price
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def extract_recorded_prices(existing_doc: dict | None) -> set[float]:
|
def extract_recorded_prices(existing_doc: dict | None) -> set[float]:
|
||||||
"""Return the set of prices already recorded in a document's pastPrices.
|
"""Return the set of prices already recorded in a document's pastPrices.
|
||||||
|
|
||||||
|
|||||||
+34
-23
@@ -28,34 +28,19 @@ from monitor_utils.sessions import get_base_session, get_tls_session
|
|||||||
LOGGER = logging.getLogger(__name__)
|
LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
async def init_monitor(
|
def setup_monitor_logging(monitor_name: str, logging_level=LOGGING_LEVEL):
|
||||||
monitor_name: str,
|
"""Configure JSON logging for a monitor and quiet noisy libraries.
|
||||||
development_mode: DevelopmentMode,
|
|
||||||
production_proxy_path: str = "proxies.txt",
|
|
||||||
testing_proxy_path: str = "configs/proxies.txt",
|
|
||||||
logging_level=LOGGING_LEVEL,
|
|
||||||
) -> tuple[list[str] | dict[str, str], AsyncIOMotorCollection, AioProxies]:
|
|
||||||
"""Run shared monitor startup and return categories, collection, and proxies.
|
|
||||||
|
|
||||||
Configures JSON logging under the monitor's name, applies logger levels,
|
Call once at module import, at global scope, before creating module loggers,
|
||||||
loads the monitor config and Mongo collection, and loads proxies from the
|
so logging is configured synchronously at startup rather than inside the
|
||||||
production or testing path based on development_mode. Async because the
|
async init_monitor. Logs go to logs/latest.log with rotated history keyed by
|
||||||
config now lives in Mongo; call it from inside the monitor's event loop
|
monitor_name.
|
||||||
(in main), not at module import.
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
monitor_name: Monitor identifier used for logging and config lookup.
|
monitor_name: Monitor name used as the rotated-history base.
|
||||||
development_mode: Selects production or testing config, collection,
|
logging_level: Root logging level.
|
||||||
and proxy path.
|
|
||||||
production_proxy_path: Proxy file path used in production mode.
|
|
||||||
testing_proxy_path: Proxy file path used in testing mode.
|
|
||||||
logging_level: Level passed to setup_logging.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Tuple of category names, Mongo collection, and loaded proxies.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
use_production = is_production(development_mode)
|
|
||||||
setup_logging(
|
setup_logging(
|
||||||
name="logs/latest.log",
|
name="logs/latest.log",
|
||||||
history_name=monitor_name,
|
history_name=monitor_name,
|
||||||
@@ -63,6 +48,32 @@ async def init_monitor(
|
|||||||
output="json",
|
output="json",
|
||||||
)
|
)
|
||||||
set_logger_levels()
|
set_logger_levels()
|
||||||
|
|
||||||
|
|
||||||
|
async def init_monitor(
|
||||||
|
monitor_name: str,
|
||||||
|
development_mode: DevelopmentMode,
|
||||||
|
production_proxy_path: str = "proxies.txt",
|
||||||
|
testing_proxy_path: str = "configs/proxies.txt",
|
||||||
|
) -> tuple[list[str] | dict[str, str], AsyncIOMotorCollection, AioProxies]:
|
||||||
|
"""Load a monitor's config, collection, and proxies for the mode.
|
||||||
|
|
||||||
|
Async because config now lives in Mongo; call it from inside the monitor's
|
||||||
|
event loop (in main), not at module import. Logging is configured separately
|
||||||
|
by setup_monitor_logging at module scope and is not touched here.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
monitor_name: Monitor identifier used for config and collection lookup.
|
||||||
|
development_mode: Selects production or testing config, collection,
|
||||||
|
and proxy path.
|
||||||
|
production_proxy_path: Proxy file path used in production mode.
|
||||||
|
testing_proxy_path: Proxy file path used in testing mode.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of category names, Mongo collection, and loaded proxies.
|
||||||
|
"""
|
||||||
|
|
||||||
|
use_production = is_production(development_mode)
|
||||||
category_names, _, collection = await setup_config(
|
category_names, _, collection = await setup_config(
|
||||||
monitor_name=monitor_name, development_mode=development_mode
|
monitor_name=monitor_name, development_mode=development_mode
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
"""unit test: resolve_price_before_drop detects a drop from the prior stored price
|
||||||
|
|
||||||
|
run from the repo root:
|
||||||
|
uv run python tests/test_price_before_drop.py
|
||||||
|
|
||||||
|
pure and offline — no Mongo, no product construction; just the drop rule
|
||||||
|
add_products_to_database relies on
|
||||||
|
"""
|
||||||
|
|
||||||
|
from monitor_utils.database import resolve_price_before_drop
|
||||||
|
|
||||||
|
|
||||||
|
def test_drop_returns_previous_price():
|
||||||
|
"""a strictly lower new price is a drop; the prior price is returned"""
|
||||||
|
|
||||||
|
assert resolve_price_before_drop(200.0, 150.0) == 200.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_non_drops_return_none():
|
||||||
|
"""no prior price, an unchanged price, or a rise are not drops"""
|
||||||
|
|
||||||
|
assert resolve_price_before_drop(None, 150.0) is None # new product
|
||||||
|
assert resolve_price_before_drop(150.0, 150.0) is None # unchanged
|
||||||
|
assert resolve_price_before_drop(150.0, 200.0) is None # price rose
|
||||||
|
assert resolve_price_before_drop(200.0, None) is None # missing new price
|
||||||
|
|
||||||
|
|
||||||
|
test_drop_returns_previous_price()
|
||||||
|
test_non_drops_return_none()
|
||||||
|
print("price before drop test passed")
|
||||||
Reference in new issue
Block a user