Subscribing to Product Page Updates
Product page data can be updated using the update methods of the ProductPage
class, namely update, update_forever, and init_update_task. It is
also possible to fetch the data without updating the inner state of the product
page instance using the fetch method.
You can subscribe to product updates by registering custom handlers for specific
ProductUpdateEvent events with the subscribe_for_update method. It is
possible to unsubscribe from updates by calling the unsubscribe_from_update
method.
Product Page Update Methods
There are several ways to fetch and update the product page data.
fetch
The fetch method of the ProductPage class is used to fetch and return
the product page data. It does not modify the inner page data of the instance.
import asyncio
from freshpointsync import ProductPage
async def main():
async with ProductPage(location_id=296) as page:
assert not page.data.products # 1. page data is empty
products = await page.fetch() # 2. fetch the products
assert products # 3. products have been fetched
assert not page.data.products # 4. page data remains empty
if __name__ == "__main__":
asyncio.run(main())
In this example, the fetch method is used to fetch the product page data.
The method returns a list of products but does not modify the data attribute
of the ProductPage instance.
Note
Providing an invalid location ID will result in an empty list of products.
update
The update methods of the ProductPage class updates the page data with
the latest information from the product page. Its silent parameter can
enable and disable triggering of update handlers during the update process,
and its await_handlers parameter can enable and disable waiting for the
handlers to finish in case they have been executed.
import asyncio
from freshpointsync import ProductPage
async def main():
async with ProductPage(location_id=296) as page:
assert not page.data.products # 1. page data is empty
await page.update(silent=True) # 2. update the products
assert page.data.products # 3. page data has been updated
if __name__ == "__main__":
asyncio.run(main())
In this example, the update method is used to fetch and update the product
page data. The update is performed silently, meaning that no registered update
handlers would be triggered. The new data is stored in the data attribute of
the ProductPage instance.
update_forever and init_update_task
The update_forever method of the ProductPage class is a wrapper around
update that runs indefinitely. It continuously polls the product page at
regular intervals and updates the data accordingly.
Note
Every update operation means a new HTTP request to the product page. Make sure to set a reasonable update interval to avoid overloading the server.
The init_update_task method is a simple convenience wrapper around
update_forever that creates a new asyncio task for the update process.
This task can be later gracefully cancelled using
the cancel_update_forever_task method.
Tip
The update_forever method is useful for long-running applications that
require real-time updates of the product page data. It is usually the last
method called in the script, since awaiting it will block the event loop.
The init_update_task method is useful for running the update process in
the background while the main application logic is executed. Calling this
method does not block the event loop, but the created task must be cancelled
manually eventually.
Subscribing to Product Updates
The ProductPage class allows you to subscribe to product updates and handle
them using custom handlers. Subscribing to updates enables you to define
specific actions that will be executed when certain update events occur.
Update Events
The range of supported update events is defined by the ProductUpdateEvent
enum. The following events are available:
Update Event |
Description |
|---|---|
|
A new product has been listed on the product page. |
|
An existing product has been updated in any way. |
|
The number of items in stock for a product has been updated. |
|
The full price and/or current price of a product have been updated. |
|
An update to a product’s metadata, such as its illustration picture. |
|
A product has been removed from the product page. |
Update Handlers
A handler must be a synchronous callable or an asynchronous coroutine that
accepts a single argument of type ProductUpdateContext. The context contains
the old and the new product data, if applicable for the update event, and
the type of event that triggered the update. It can also be used to pass
arbitrary data to the handlers. To do so, set the desired data as a key-value
pair in the context mapping of the ProductPage instance.
async def on_product_update(context: ProductUpdateContext) -> None:
"""Handle all product update events."""
product_name = context.product_new.name
if product_name in context.get('favorite_products', []):
print(f'Your favorite product "{product_name}" was updated!')
else:
print(f'Product "{product_name}" was updated.')
await asyncio.sleep(1) # simulate a delay for some IO operation
def on_product_price_update(context: ProductUpdateContext) -> None:
"""Handle all price update events."""
price_curr = f'{context.product_new.price_curr:.2f} CZK'
price_prev = f'{context.product_old.price_curr:.2f} CZK'
print(
f'Product "{context.product_old.name}" price update: '
f'{price_prev} -> {price_curr}'
)
def on_product_quantity_update(context: ProductUpdateContext) -> None:
"""Handle all quantity update events."""
quantity_curr = f'{context.product_new.quantity} items'
quantity_prev = f'{context.product_old.quantity} items'
print(
f'Product "{context.product_old.name}" quantity update: '
f'{quantity_prev} -> {quantity_curr}'
)
The handlers in this example print the product name and the type of update event
that occurred. The on_product_update handler also simulates a delay of one
second, which could correspond to some IO operation.
Note
The handlers in the example are aimed to run on a product update event,
so both context.product_new and context.product_old are guaranteed
to be present. Unfortunately, there is no way to type-check this within
the ProductUpdateContext class. You may check for the presence of these
attributes in your handlers with a simple if statement or assert or
suppress the type-checking warning with # type: ignore.
Context of a create event will contain only the new product data, while the context of a delete event will contain only the old product data.
If a handler is asynchronous, it inherently does not block the event loop. If, however, a handler is synchronous, it can be executed directly in a blocking manner or in a separate thread in a non-blocking manner. By default, synchronous handlers are executed in a blocking manner to prevent potential race conditions.
Tip
If you decide to run synchronous handlers in a non-blocking manner, make sure that the handlers do not share any data between each other or with other parts of the application. If they do, use locks or other synchronization mechanisms to avoid race conditions.
subscribe_for_update and unsubscribe_from_update
The subscribe_for_update method of the ProductPage class is used to
subscribe to product update events. It takes the following arguments:
handler: A callable that will be executed when the event occurs.event: The type of event to subscribe to. If event isNone, the handler will be executed for all events.call_safe: A boolean flag that determines whether exceptions raised by the handler should be caught or propagated.call_blocking: A boolean flag that determines whether synchronous handlers should be executed directly, blocking the event loop, or ran in a separate thread or process, allowing the event loop to continue running.handler_done_callback: An optional synchronous function that will be executed when the handler has finished processing the event. It must accept a single argument - the completed future object of the handler.
page.subscribe_for_update(
handler=on_product_update,
event=ProductUpdateEvent.PRODUCT_UPDATED,
handler_done_callback=lambda f: print('Product update handled\n.')
)
In the code snippet above, the on_product_update handler is subscribed to
the PRODUCT_UPDATED event. The handler is asynchronous, so it will not block
the event loop, and setting call_blocking has no effect. call_safe is
set to True by default. A lambda function is used as the handler done
callback. It will print a message when the handler has finished processing the
event. Note that the handler done callback must be synchronous and accept
a single argument - the completed future object of the handler.
Note
If a product update is eligible for multiple events, all handlers subscribed
to these events will be executed. PRODUCT_UPDATED is a special generic
event that is triggered for all types of product updates. For example, if a
product’s price has been updated, both PRODUCT_UPDATED and
PRICE_UPDATED event handlers will be executed.
The unsubscribe_from_update method is used to unsubscribe a handler from
product update events. It takes the following arguments:
handler: The handler to be unsubscribed from the event(s). IfNone, all handlers for the event are unsubscribed.event: The type of product update event(s) to unsubscribe from. IfNone, the handler will be unsubscribed from all events.
page.unsubscribe_from_update(
handler=on_product_update,
event=ProductUpdateEvent.PRODUCT_UPDATED
)
In the code snippet above, the on_product_update handler is unsubscribed
from the PRODUCT_UPDATED event. The handler will no longer be executed when
a product update occurs.
Complete Example
The example below demonstrates how to subscribe to product updates and handle them using custom handlers. The script will run continuously until interrupted by the user. Note that it may take some time for a product update to occur on the tracked page.
import asyncio
from freshpointsync import ProductPage, ProductUpdateEvent
from freshpointsync.update import ProductUpdateContext
async def on_product_update(context: ProductUpdateContext) -> None:
"""Handle all product update events."""
if context.product_name in context.get('favorite_products', []):
print(f'Your favorite product "{context.product_name}" was updated!')
else:
print(f'Product "{context.product_name}" was updated.')
await asyncio.sleep(1) # simulate a delay for some IO operation
def on_product_price_update(context: ProductUpdateContext) -> None:
"""Handle all price update events."""
price_curr = f'{context.product_new.price_curr:.2f} CZK'
price_prev = f'{context.product_old.price_curr:.2f} CZK'
print(
f'Product "{context.product_name}" price update: '
f'{price_prev} -> {price_curr}'
)
def on_product_quantity_update(context: ProductUpdateContext) -> None:
"""Handle all quantity update events."""
quantity_curr = f'{context.product_new.quantity} items'
quantity_prev = f'{context.product_old.quantity} items'
print(
f'Product "{context.product_name}" quantity update: '
f'{quantity_prev} -> {quantity_curr}'
)
async def main():
page = ProductPage(location_id=296)
page.context['favorite_products'] = [
'Harboe Cola',
'Club Sendvič',
'Dezert Tiramisu do kelímku',
]
try:
print('Fetching the initial product data...')
await page.start_session()
await page.update(silent=True)
print('Subscribing to updates...')
page.subscribe_for_update(
handler=on_product_update,
event=ProductUpdateEvent.PRODUCT_UPDATED,
handler_done_callback=lambda _: print('Product update handled\n.')
)
page.subscribe_for_update(
handler=on_product_price_update,
event=ProductUpdateEvent.PRICE_UPDATED,
call_blocking=False,
)
page.subscribe_for_update(
handler=on_product_quantity_update,
event=ProductUpdateEvent.QUANTITY_UPDATED,
call_blocking=False
)
print('Subscribed to updates. Press Ctrl+C to exit.')
await page.update_forever(interval=10)
except asyncio.CancelledError:
print('Exiting...')
except Exception as e:
print(f'Error: {e}')
finally:
await page.close_session()
await page.await_update_handlers()
if __name__ == "__main__":
try:
asyncio.run(main())
except KeyboardInterrupt:
pass
The example above demonstrates how to create a ProductPage instance, set
context data, subscribe to product update events, and handle these events using
custom handlers. The on_product_update handler is subscribed to the
PRODUCT_UPDATED event and prints a message when a product update occurs.
It also has a one-second delay to simulate an IO operation and a bound callback
function. The on_product_price_update and on_product_quantity_update
handlers are subscribed to the PRICE_UPDATED and QUANTITY_UPDATED
events, respectively. They print specific information about the product price
and quantity updates. The application will run continuously until interrupted by
the user.