Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions docs/source/deal_queries.rst
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,25 @@ Use :class:`keepa.models.backend.DealRequest` to construct the request and
response = api.deals(request, typed=True)
asins = [deal.asin for deal in response.dr or [] if deal is not None]

Deal Deltas and Previous Values
-------------------------------
Deal arrays such as ``current``, ``delta``, ``deltaPercent``, and
``deltaLast`` are indexed by Keepa's product CSV type order. Use
``keepa.csv_indices`` to map a price type name to the array position.

.. code-block:: python

deal = response.dr[0]
csv_index = next(index for index, name, _ in keepa.csv_indices if name == "NEW")

current_new = deal.current[csv_index]
change_from_last = deal.deltaLast[csv_index]
previous_new = current_new - change_from_last

Only compute a previous value when both array entries are present and Keepa
supplies a signed delta for that price type. Keepa uses sentinel values such
as ``-1`` for unavailable prices, so filter those values before arithmetic.

Async Usage
-----------

Expand Down
2 changes: 1 addition & 1 deletion docs/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ when you want products that recently changed and match deal filters.
params = keepa.ProductParams(
author="jim butcher",
current_SALES_lte=50_000,
sort=["current_SALES", "asc"],
sort=[["current_SALES", "asc"]],
perPage=100,
)
asins = api.product_finder(params)
Expand Down
19 changes: 19 additions & 0 deletions docs/source/offer_queries.rst
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,25 @@ offers. Not every historical offer is active, and the field can be absent.
for offer in active_offers:
times, prices = keepa.convert_offer_history(offer["offerCSV"])

Offer Stock
-----------
Set ``stock=True`` together with ``offers`` when you need current offer stock
data. Keepa can only collect stock for live offers, and the backend caps
reported offer stock at 10 units.

.. code-block:: python

product = api.query("1454857935", offers=20, stock=True)[0]

for index in product.get("liveOffersOrder", []):
offer = product["offers"][index]
stock_history = offer.get("stockCSV")
latest_stock = offer.get("stock")

Existing ``stockCSV`` history can be present even when ``stock=True`` is not
set, but ``stock=True`` asks Keepa to refresh stock for the current live
offers and consumes additional tokens.

.. figure:: images/Offer_History.png
:alt: Active marketplace offer histories
:width: 700px
Expand Down
27 changes: 25 additions & 2 deletions docs/source/product_finder.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,14 @@ Basic Search

parameters = {
"author": "jim butcher",
"sort": ["current_SALES", "asc"],
"sort": [["current_SALES", "asc"]],
"perPage": 100,
}
asins = api.product_finder(parameters)

Sort values follow the backend schema: pass a list of ``[field, direction]``
pairs, where direction is ``"asc"`` or ``"desc"``.

Validated Parameters
--------------------
:class:`keepa.ProductParams` validates backend field names before a request
Expand All @@ -25,7 +28,7 @@ consumes tokens. Unknown names raise a validation error.

parameters = keepa.ProductParams(
author="jim butcher",
sort=["current_SALES", "asc"],
sort=[["current_SALES", "asc"]],
perPage=100,
)
asins = api.product_finder(parameters)
Expand All @@ -47,6 +50,26 @@ The backend exposes more than a thousand filters. Inspect them through
``ProductParams.model_json_schema()``, or
``keepa.backend_models.ProductFinderRequest.model_json_schema()``.

Backend Filter Shapes
---------------------
Keep product-finder field names exactly as Keepa documents them. Range filters
use suffixes such as ``_gte`` and ``_lte``; list-like filters accept either a
single string or a list of strings when the backend supports multiple values.

.. code-block:: python

request = keepa.ProductParams(
buyBoxSellerId=["A2L77EE7U53NWQ", "ATVPDKIKX0DER"],
partNumber=["MX-1000", "MX-1001"],
current_SALES_lte=50000,
sort=[["current_SALES", "desc"]],
)
asins = api.product_finder(request)

This preserves backend payloads for fields such as ``buyBoxSellerId``,
``partNumber``, ``categories_include``, and ``sort`` instead of rewriting them
into client-specific names.

Result Limits and Pages
-----------------------
``n_products`` sets ``perPage`` only when the supplied parameters do not
Expand Down
8 changes: 7 additions & 1 deletion docs/source/product_history.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ under ``product["data"]``. Each available value array has a matching
History Availability
--------------------
Products do not necessarily contain every history type. Use ``get`` when
availability is not guaranteed.
availability is not guaranteed. A key can be absent when Keepa has no history
for that product, when the product type does not support that history, or when
the request disables history parsing with ``history=False``.

.. code-block:: python

Expand Down Expand Up @@ -37,6 +39,10 @@ Key Meaning
``COUNT_REVIEWS`` Review count history
========================== =================================================

If ``NEW_FBA`` or ``NEW_FBM_SHIPPING`` is absent for a product, query with
``history=True`` and treat the missing key as unavailable backend data rather
than a client parsing failure.

Plotting
--------
History values are discontinuous and are best represented as step plots.
Expand Down
14 changes: 14 additions & 0 deletions docs/source/product_query.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,20 @@ Keepa requests require a paid API key from `Keepa Data Access
by default; use ``wait=False`` only when your application manages token
availability itself.

Timeouts and Token Use
======================
``Keepa(accesskey, timeout=...)`` controls how long the client waits for the
backend to send a response; it is not a total runtime limit for a large batch.
The client chunks product queries into backend-sized requests, but token cost
is calculated by Keepa and can be more than one token per ASIN when options
such as ``offers``, ``stock``, ``buybox``, ``rating``, ``stats``, or forced
updates are enabled.

For large batches, keep ``wait=True``, use smaller chunks when debugging, and
inspect ``api.tokens_left`` or ``api.status`` between calls. If a request is
timing out, reduce expensive options first and then increase ``timeout`` only
when the backend legitimately needs longer to produce the requested data.

.. code-block:: python

import keepa
Expand Down
8 changes: 4 additions & 4 deletions src/keepa/keepa_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -1321,8 +1321,8 @@ def product_finder(
Notes
-----
When using the ``'sort'`` key in the ``product_parms`` parameter, use a
compatible key along with the type of sort. For example:
``["current_SALES", "asc"]``
list of ``[field, direction]`` pairs. For example:
``[["current_SALES", "asc"]]``

Examples
--------
Expand All @@ -1333,7 +1333,7 @@ def product_finder(
>>> api = keepa.Keepa("<ENTER_ACTUAL_KEY_HERE>")
>>> product_parms = {
... "author": "jim butcher",
... "sort": ["current_SALES", "asc"],
... "sort": [["current_SALES", "asc"]],
... }
>>> asins = api.product_finder(product_parms, n_products=100)
>>> asins
Expand All @@ -1349,7 +1349,7 @@ def product_finder(

>>> product_parms = keepa.ProductParams(
... author="jim butcher",
... sort=["current_SALES", "asc"],
... sort=[["current_SALES", "asc"]],
... )
>>> asins = api.product_finder(product_parms, n_products=100)

Expand Down
30 changes: 30 additions & 0 deletions tests/test_backend_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,36 @@ def fake_request(request_type: str, payload: dict[str, Any], **kwargs: Any) -> d
assert asins == ["B000HRMAR2"]


def test_product_finder_preserves_backend_filter_shapes(
monkeypatch: pytest.MonkeyPatch,
) -> None:
api = _ready_api()

def fake_request(request_type: str, payload: dict[str, Any], **kwargs: Any) -> dict[str, Any]:
assert request_type == "query"
selection = json.loads(payload["selection"])
assert selection["sort"] == [["current_SALES", "desc"]]
assert selection["buyBoxSellerId"] == ["A2L77EE7U53NWQ", "ATVPDKIKX0DER"]
assert selection["partNumber"] == ["MX-1000", "MX-1001"]
assert selection["categories_include"] == ["2619533011"]
assert selection["perPage"] == 75
return {"asinList": ["B000HRMAR2"]}

monkeypatch.setattr(api, "_request", fake_request)

asins = api.product_finder(
{
"sort": [["current_SALES", "desc"]],
"buyBoxSellerId": ["A2L77EE7U53NWQ", "ATVPDKIKX0DER"],
"partNumber": ["MX-1000", "MX-1001"],
"categories_include": ["2619533011"],
"perPage": 75,
}
)

assert asins == ["B000HRMAR2"]


def test_best_sellers_typed_response(monkeypatch: pytest.MonkeyPatch) -> None:
api = _ready_api()

Expand Down
8 changes: 8 additions & 0 deletions tests/test_backend_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -445,14 +445,22 @@ def test_product_params_accept_backend_fields_and_reject_unknown_fields() -> Non
availabilityAmazonMinDelayInDays_gte=2,
buyBoxEligibleOfferCountsNewFBA_lte=[[1, 2]],
buyBoxStatsTopSellerId365="A2L77EE7U53NWQ",
buyBoxSellerId=["A2L77EE7U53NWQ", "ATVPDKIKX0DER"],
categories_include=["2619533011"],
hasAPlus=True,
historicalSellerIds=["A2L77EE7U53NWQ"],
partNumber=["MX-1000", "MX-1001"],
sort=[["current_SALES", "desc"]],
websiteDisplayGroup="kitchen_display_on_website",
srAvg211_lte=1000,
)

dumped = params.model_dump(exclude_none=True)
assert dumped["activeIngredients"] == ["ceramide"]
assert dumped["buyBoxSellerId"] == ["A2L77EE7U53NWQ", "ATVPDKIKX0DER"]
assert dumped["categories_include"] == ["2619533011"]
assert dumped["partNumber"] == ["MX-1000", "MX-1001"]
assert dumped["sort"] == [["current_SALES", "desc"]]
assert dumped["srAvg211_lte"] == 1000

with pytest.raises(ValueError):
Expand Down
Loading