diff --git a/.envrc b/.envrc
deleted file mode 100644
index 31e2911..0000000
--- a/.envrc
+++ /dev/null
@@ -1,45 +0,0 @@
-
-set +u
-
-[ -f "$HOME/.envrc" ] && source_env $HOME || true
-
-export_alias() {
- local name=$1
- shift
- local alias_dir=$PWD/.direnv/aliases
- local target="$alias_dir/$name"
- mkdir -p "$alias_dir"
- PATH_add "$alias_dir"
- echo "#!/usr/bin/env bash" > "$target"
- echo "set -e" >> "$target"
- echo "$@ \"\$@\"" >> "$target"
- chmod +x "$target"
-}
-
-export_function() {
- local name=$1
- local alias_dir=$PWD/.direnv/aliases
- mkdir -p "$alias_dir"
- PATH_add "$alias_dir"
- local target="$alias_dir/$name"
- if declare -f "$name" >/dev/null; then
- echo "#!/usr/bin/env bash" > "$target"
- declare -f "$name" >> "$target" 2>/dev/null
- echo "$name" >> "$target"
- chmod +x "$target"
- fi
-}
-
-export PROJECT_NAME=keepa
-
-export PYENV_VIRTUALENV_DISABLE_PROMPT=1
-
-PATH_add "$PWD"
-
-PYENV_ROOT=$(pyenv root)
-if [[ -d "${PYENV_ROOT}/versions/$PROJECT_NAME" ]]; then
- eval "$(pyenv init -)"
- pyenv activate $PROJECT_NAME
-fi
-
-unset PS1
diff --git a/.flake8 b/.flake8
deleted file mode 100644
index ad9b427..0000000
--- a/.flake8
+++ /dev/null
@@ -1,4 +0,0 @@
-[flake8]
-max-line-length = 120
-show-source = True
-exclude=.git,.tox,dist,*egg,build,backoffice/*/migrations/*,.*,docker/*,docs/*,keepa/__init__.py
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
deleted file mode 100644
index 5ace460..0000000
--- a/.github/dependabot.yml
+++ /dev/null
@@ -1,6 +0,0 @@
-version: 2
-updates:
- - package-ecosystem: "github-actions"
- directory: "/"
- schedule:
- interval: "weekly"
diff --git a/.github/workflows/testing-and-deployment.yml b/.github/workflows/testing-and-deployment.yml
index b600ea8..5f9100b 100644
--- a/.github/workflows/testing-and-deployment.yml
+++ b/.github/workflows/testing-and-deployment.yml
@@ -1,63 +1,92 @@
+name: CI/CD
+
on:
pull_request:
workflow_dispatch:
push:
tags:
- - "*"
+ - '*'
branches:
- - main
+ - main
jobs:
unit_testing:
+ name: Build and Testing
runs-on: ubuntu-latest
strategy:
matrix:
- python-version: ['3.8', '3.9', '3.10', '3.11']
+ python-version: ['3.10', '3.11', '3.12', '3.13', '3.14']
env:
KEEPAKEY: ${{ secrets.KEEPAKEY }}
WEAKKEEPAKEY: ${{ secrets.WEAKKEEPAKEY }}
steps:
- - uses: actions/checkout@v3
-
- - name: Set up Python ${{ matrix.python-version }}
- uses: actions/setup-python@v4
- with:
- python-version: ${{ matrix.python-version }}
- cache: 'pip'
- cache-dependency-path: |
- **/setup.py
- **/requirements*.txt
-
- - name: Install
- run: |
- python setup.py sdist
- pip install dist/*.tar.gz --disable-pip-version-check
- cd tests/
- python -c "import keepa"
-
- - name: Validate Keys
- run: |
- python -c "import os, keepa; keepa.Keepa(os.environ.get('KEEPAKEY'))"
-
- - name: Unit testing
- run: |
- pip install -r requirements_test.txt --disable-pip-version-check
- cd tests
- pytest -v --cov keepa --cov-report xml
-
- - uses: codecov/codecov-action@v3
- if: matrix.python-version == '3.11'
- name: 'Upload coverage to codecov'
-
- - name: Upload to PyPi
- if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags')
- run: |
- pip install twine
- python setup.py sdist
- twine upload --skip-existing dist/keepa*.tar.gz
- env: # Or as an environment variable
- TWINE_USERNAME: "__token__"
- TWINE_PASSWORD: ${{ secrets.TWINE_TOKEN }}
- TWINE_REPOSITORY_URL: "https://upload.pypi.org/legacy/"
+ - uses: actions/checkout@v4
+
+ - name: Set up Python ${{ matrix.python-version }}
+ uses: actions/setup-python@v5
+ with:
+ python-version: ${{ matrix.python-version }}
+ cache: pip
+
+ - name: Install
+ run: |
+ pip install .[test] --disable-pip-version-check
+ python -c "import keepa"
+
+ - name: Validate Keys
+ run: |
+ python -c "import os, keepa; keepa.Keepa(os.environ.get('KEEPAKEY'))"
+
+ - name: Unit testing
+ run: |
+ pytest -v --cov keepa --cov-report xml
+
+ - uses: codecov/codecov-action@v4
+ if: matrix.python-version == '3.13'
+ name: Upload coverage to codecov
+
+ - name: Build wheel
+ if: matrix.python-version == '3.13'
+ run: |
+ pip install build --disable-pip-version-check
+ python -m build
+
+ - name: Upload wheel
+ if: matrix.python-version == '3.13'
+ uses: actions/upload-artifact@v4
+ with:
+ name: keepa-wheel
+ path: dist/
+ retention-days: 1
+
+ release:
+ name: Upload release to PyPI
+ if: github.event_name == 'push' && contains(github.ref, 'refs/tags')
+ needs: [unit_testing]
+ runs-on: ubuntu-latest
+ environment:
+ name: pypi
+ url: https://pypi.org/p/keepa
+ permissions:
+ id-token: write # Required for PyPI publishing
+ contents: write # Required for creating GitHub releases
+ steps:
+ - uses: actions/download-artifact@v4
+ with:
+ path: dist/
+ - name: Flatten directory structure
+ run: |
+ mv dist/*/* dist/
+ rm -rf dist/keepa-wheel
+ - name: Display structure of downloaded files
+ run: ls -R
+ - name: Publish package distributions to PyPI
+ uses: pypa/gh-action-pypi-publish@release/v1
+ - name: Create GitHub Release
+ uses: softprops/action-gh-release@v2
+ with:
+ generate_release_notes: true
+ files: |
+ ./**/*.whl
diff --git a/.gitignore b/.gitignore
index 6546b6b..6416e03 100644
--- a/.gitignore
+++ b/.gitignore
@@ -20,7 +20,7 @@ dist/
keepa/__pycache__/
# testing
-Testing/
+test-scripts/
test.sh
.pytest_cache/
tests/.coverage
@@ -28,6 +28,7 @@ tests/htmlcov/
*,cover
.coverage
.hypothesis
+.venv
# key storage
tests/key
diff --git a/.isort.cfg b/.isort.cfg
deleted file mode 100644
index b0f2963..0000000
--- a/.isort.cfg
+++ /dev/null
@@ -1,8 +0,0 @@
-[settings]
-profile = black
-line_length = 80
-# Sort by name, don't cluster "from" vs "import"
-force_sort_within_sections = true
-# Combines "as" imports on the same line
-combine_as_imports = true
-skip_glob = "femorph/__init__.py"
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 552d47a..0407ebf 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,25 +1,23 @@
# Integration with GitHub Actions
# See https://pre-commit.ci/
ci:
- autofix_prs: true
- autoupdate_schedule: monthly
+ autofix_prs: true
+ autoupdate_schedule: quarterly
repos:
-- repo: https://github.com/psf/black
- rev: 23.3.0
+- repo: https://github.com/keewis/blackdoc
+ rev: v0.4.5
hooks:
- - id: black
- exclude: "^({{cookiecutter.project_slug}}/)"
-- repo: https://github.com/pycqa/isort
- rev: 5.12.0
+ - id: blackdoc
+ files: \.py$
+- repo: https://github.com/astral-sh/ruff-pre-commit
+ rev: v0.14.3
hooks:
- - id: isort
-- repo: https://github.com/PyCQA/flake8
- rev: 6.0.0
- hooks:
- - id: flake8
- exclude: "^({{cookiecutter.project_slug}}/)"
+ - id: ruff-check
+ args: [--fix, --exit-non-zero-on-fix]
+ exclude: ^(docs/|tests)
+ - id: ruff-format
- repo: https://github.com/codespell-project/codespell
- rev: v2.2.4
+ rev: v2.4.1
hooks:
- id: codespell
args: [-S ./docs/\*]
@@ -28,4 +26,24 @@ repos:
hooks:
- id: pydocstyle
additional_dependencies: [toml]
- exclude: "tests/"
+ exclude: tests/
+- repo: https://github.com/asottile/pyupgrade
+ rev: v3.21.0
+ hooks:
+ - id: pyupgrade
+ args: [--py39-plus, --keep-runtime-typing]
+- repo: https://github.com/pre-commit/pre-commit-hooks
+ rev: v6.0.0
+ hooks:
+ - id: check-merge-conflict
+ - id: debug-statements
+ - id: no-commit-to-branch
+ args: [--branch, main]
+ - id: requirements-txt-fixer
+- repo: https://github.com/macisamuele/language-formatters-pre-commit-hooks
+ rev: v2.15.0
+ hooks:
+ - id: pretty-format-toml
+ args: [--autofix]
+ - id: pretty-format-yaml
+ args: [--autofix, --indent, '2']
diff --git a/.readthedocs.yaml b/.readthedocs.yaml
index 5986b90..532c98d 100644
--- a/.readthedocs.yaml
+++ b/.readthedocs.yaml
@@ -9,11 +9,11 @@ version: 2
build:
os: ubuntu-22.04
tools:
- python: "3.11"
+ python: '3.11'
# Build documentation in the docs/ directory with Sphinx
sphinx:
- configuration: docs/source/conf.py
+ configuration: docs/source/conf.py
# If using Sphinx, optionally build your docs in additional formats such as PDF
# formats:
@@ -21,7 +21,7 @@ sphinx:
# Optionally declare the Python requirements required to build your docs
python:
- install:
- - requirements: requirements_docs.txt
- - method: pip
- path: .
\ No newline at end of file
+ install:
+ - requirements: requirements_docs.txt
+ - method: pip
+ path: .
diff --git a/README.rst b/README.rst
index 68ba430..9979e32 100644
--- a/README.rst
+++ b/README.rst
@@ -20,17 +20,17 @@ Python keepa Client Library
This Python library allows you to interface with the API at `Keepa
`_ to query for Amazon product information and
-history. It also contains a plotting module to allow for plotting of
+history. It also contains a plotting module to allow for plotting of
a product.
-See API pricing at `Keepa API `_.
+Sign up for `Keepa Data Access `_.
-Documentation can be found on readthedocs at `keepa Documentation `_.
+Documentation can be found at `Keepa Documentation `_.
Requirements
------------
-This library is compatible with Python >= 3.7 and requires:
+This library is compatible with Python >= 3.10 and requires:
- ``numpy``
- ``aiohttp``
@@ -41,7 +41,7 @@ Product history can be plotted from the raw data when ``matplotlib``
is installed.
Interfacing with the ``keepa`` requires an access key and a monthly
-subscription from `Keepa API `_
+subscription from `Keepa API `_.
Installation
------------
@@ -53,8 +53,10 @@ Module can be installed from `PyPi `_ with:
Source code can also be downloaded from `GitHub
-`_ and installed using:
-``python setup.py install`` or ``pip install .``
+`_ and installed using::
+
+ cd keepa
+ pip install .
Brief Example
@@ -62,11 +64,11 @@ Brief Example
.. code:: python
import keepa
- accesskey = 'XXXXXXXXXXXXXXXX' # enter real access key here
+ accesskey = 'XXXXXXXXXXXXXXXX' # enter real access key here from https://get.keepa.com/d7vrq
api = keepa.Keepa(accesskey)
# Single ASIN query
- products = api.query('B0088PUEPK') # returns list of product data
+ products = api.query('B0088PUEPK') # returns list of product data
# Plot result (requires matplotlib)
keepa.plot_product(products[0])
@@ -247,25 +249,51 @@ If you plan to do a lot of simulatneous query, you might want to speedup query u
products = await api.query('059035342X', wait=False)
+Buy Box Statistics
+~~~~~~~~~~~~~~~~~~
+To load used buy box statistics, you have to enable ``offers``. This example
+loads in product offers and converts the buy box data into a
+``pandas.DataFrame``.
+
+.. code:: pycon
+
+ >>> import keepa
+ >>> key = ''
+ >>> api = keepa.Keepa(key)
+ >>> response = api.query('B0088PUEPK', offers=20)
+ >>> product = response[0]
+ >>> buybox_info = product['buyBoxUsedHistory']
+ >>> df = keepa.process_used_buybox(buybox_info)
+ datetime user_id condition isFBA
+ 0 2022-11-02 16:46:00 A1QUAC68EAM09F Used - Like New True
+ 1 2022-11-13 10:36:00 A18WXU4I7YR6UA Used - Very Good False
+ 2 2022-11-15 23:50:00 AYUGEV9WZ4X5O Used - Like New False
+ 3 2022-11-17 06:16:00 A18WXU4I7YR6UA Used - Very Good False
+ 4 2022-11-17 10:56:00 AYUGEV9WZ4X5O Used - Like New False
+ .. ... ... ... ...
+ 115 2023-10-23 10:00:00 AYUGEV9WZ4X5O Used - Like New False
+ 116 2023-10-25 21:14:00 A1U9HDFCZO1A84 Used - Like New False
+ 117 2023-10-26 04:08:00 AYUGEV9WZ4X5O Used - Like New False
+ 118 2023-10-27 08:14:00 A1U9HDFCZO1A84 Used - Like New False
+ 119 2023-10-27 12:34:00 AYUGEV9WZ4X5O Used - Like New False
+
Contributing
------------
Contribute to this repository by forking this repository and installing in
development mode with::
git clone https://github.com//keepa
- pip install -e .
+ pip install -e .[test]
You can then add your feature or commit your bug fix and then run your unit
testing with::
- pip install requirements_test.txt
pytest
Unit testing will automatically enforce minimum code coverage standards.
Next, to ensure your code meets minimum code styling standards, run::
- pip install pre-commit
pre-commit run --all-files
Finally, `create a pull request`_ from your fork and I'll be sure to review it.
diff --git a/codecov.yml b/codecov.yml
index 782e550..f09a49f 100644
--- a/codecov.yml
+++ b/codecov.yml
@@ -8,14 +8,14 @@ coverage:
# basic
target: 85%
threshold: 80%
- base: auto
- flags:
- - unit
- paths:
- - "src"
+ base: auto
+ flags:
+ - unit
+ paths:
+ - src
# advanced
- branches:
- - master
+ branches:
+ - master
if_not_found: success
if_ci_failed: error
informational: false
@@ -25,18 +25,18 @@ coverage:
# basic
target: 90
threshold: 90
- base: auto
+ base: auto
# advanced
- branches:
- - master
+ branches:
+ - master
if_no_uploads: error
if_not_found: success
if_ci_failed: error
only_pulls: false
- flags:
- - "unit"
- paths:
- - "src"
+ flags:
+ - unit
+ paths:
+ - src
parsers:
@@ -48,6 +48,6 @@ parsers:
macro: no
comment:
- layout: "reach,diff,flags,tree"
+ layout: reach,diff,flags,tree
behavior: default
require_changes: no
diff --git a/docs/source/api_methods.rst b/docs/source/api_methods.rst
index fde87a4..d500ed2 100644
--- a/docs/source/api_methods.rst
+++ b/docs/source/api_methods.rst
@@ -2,6 +2,22 @@
keepa.Api Methods
-----------------
+These are the core ``keepa`` classes.
+
.. autoclass:: keepa.Keepa
:members:
+Types
+-----
+These types and enumerators are used by ``keepa`` for data validation.
+
+.. autoclass:: keepa.Domain
+ :members:
+ :undoc-members:
+ :member-order: bysource
+
+.. autoclass:: keepa.ProductParams
+ :members:
+ :undoc-members:
+ :member-order: bysource
+ :exclude-members: model_computed_fields, model_config, model_fields, construct,dict,from_orm,json,parse_file,parse_obj,parse_raw,schema,schema_json,update_forward_refs,validate,copy,model_construct,model_copy,model_dump,model_dump_json,model_json_schema,model_parametrized_name,model_post_init,model_rebuild,model_validate,model_validate_json,model_validate_strings, model_extra, model_fields_set
diff --git a/docs/source/conf.py b/docs/source/conf.py
index c5868ae..59303f9 100644
--- a/docs/source/conf.py
+++ b/docs/source/conf.py
@@ -1,17 +1,9 @@
"""Sphinx configuration file for keepaapi."""
+
# import pydata_sphinx_theme # noqa
from datetime import datetime
-from io import open as io_open
-import os
-
-__version__ = None
-version_file = os.path.join(
- os.path.dirname(__file__), "..", "..", "keepa", "_version.py"
-)
-
-with io_open(version_file, mode="r") as fd:
- exec(fd.read())
+from keepa import __version__
# If your documentation needs a minimal Sphinx version, state it here.
#
@@ -20,7 +12,18 @@
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
-extensions = ["sphinx.ext.autodoc", "sphinx.ext.napoleon"]
+extensions = [
+ "sphinx.ext.autodoc",
+ "numpydoc",
+ "sphinx.ext.intersphinx",
+]
+
+intersphinx_mapping = {
+ "python": (
+ "https://docs.python.org/3.11",
+ (None, "../intersphinx/python-objects.inv"),
+ ),
+}
# Add any paths that contain templates here, relative to this directory.
templates_path = ["_templates"]
@@ -87,6 +90,7 @@
"show_prev_next": False,
"github_url": "https://github.com/akaszynski/keepa",
"collapse_navigation": True,
+ "navigation_with_keys": False,
"use_edit_page_button": True,
"logo": {
"image_light": "keepa-logo.png",
diff --git a/keepa/__init__.py b/keepa/__init__.py
deleted file mode 100644
index f26ae6b..0000000
--- a/keepa/__init__.py
+++ /dev/null
@@ -1,5 +0,0 @@
-"""Keepaapi module."""
-
-from keepa._version import __version__
-from keepa.interface import *
-from keepa.plotting import *
diff --git a/keepa/_version.py b/keepa/_version.py
deleted file mode 100644
index 18f4a12..0000000
--- a/keepa/_version.py
+++ /dev/null
@@ -1,13 +0,0 @@
-"""Version number for keepaapi.
-
-On the ``main`` branch, use 'dev0' to denote a development version.
-For example:
-
-version_info = 0, 27, 'dev0'
-
-
-"""
-# major, minor, patch
-version_info = 1, 4, "dev0"
-
-__version__ = ".".join(map(str, version_info))
diff --git a/keepa/interface.py b/keepa/interface.py
deleted file mode 100644
index 3b05e6f..0000000
--- a/keepa/interface.py
+++ /dev/null
@@ -1,3083 +0,0 @@
-"""Interface module to download Amazon product and history data from keepa.com."""
-import asyncio
-import datetime
-import json
-import logging
-import time
-
-import aiohttp
-import numpy as np
-import pandas as pd
-import requests
-from tqdm import tqdm
-
-from keepa.query_keys import DEAL_REQUEST_KEYS, PRODUCT_REQUEST_KEYS
-
-
-def is_documented_by(original):
- """Avoid copying the documentation."""
-
- def wrapper(target):
- target.__doc__ = original.__doc__
- return target
-
- return wrapper
-
-
-log = logging.getLogger(__name__)
-
-# hardcoded ordinal time from
-KEEPA_ST_ORDINAL = np.datetime64("2011-01-01")
-
-# Request limit
-REQUEST_LIMIT = 100
-
-# Status code dictionary/key
-SCODES = {
- "400": "REQUEST_REJECTED",
- "402": "PAYMENT_REQUIRED",
- "405": "METHOD_NOT_ALLOWED",
- "429": "NOT_ENOUGH_TOKEN",
-}
-
-# domain codes
-# Valid values: [ 1: com | 2: co.uk | 3: de | 4: fr | 5:
-# co.jp | 6: ca | 7: cn | 8: it | 9: es | 10: in | 11: com.mx ]
-DCODES = ["RESERVED", "US", "GB", "DE", "FR", "JP", "CA", "CN", "IT", "ES", "IN", "MX"]
-
-# csv indices. used when parsing csv and stats fields.
-# https://github.com/keepacom/api_backend
-# see api_backend/src/main/java/com/keepa/api/backend/structs/Product.java
-# [index in csv, key name, isfloat(is price or rating)]
-csv_indices = [
- [0, "AMAZON", True],
- [1, "NEW", True],
- [2, "USED", True],
- [3, "SALES", False],
- [4, "LISTPRICE", True],
- [5, "COLLECTIBLE", True],
- [6, "REFURBISHED", True],
- [7, "NEW_FBM_SHIPPING", True],
- [8, "LIGHTNING_DEAL", True],
- [9, "WAREHOUSE", True],
- [10, "NEW_FBA", True],
- [11, "COUNT_NEW", False],
- [12, "COUNT_USED", False],
- [13, "COUNT_REFURBISHED", False],
- [14, "CollectableOffers", False],
- [15, "EXTRA_INFO_UPDATES", False],
- [16, "RATING", True],
- [17, "COUNT_REVIEWS", False],
- [18, "BUY_BOX_SHIPPING", True],
- [19, "USED_NEW_SHIPPING", True],
- [20, "USED_VERY_GOOD_SHIPPING", True],
- [21, "USED_GOOD_SHIPPING", True],
- [22, "USED_ACCEPTABLE_SHIPPING", True],
- [23, "COLLECTIBLE_NEW_SHIPPING", True],
- [24, "COLLECTIBLE_VERY_GOOD_SHIPPING", True],
- [25, "COLLECTIBLE_GOOD_SHIPPING", True],
- [26, "COLLECTIBLE_ACCEPTABLE_SHIPPING", True],
- [27, "REFURBISHED_SHIPPING", True],
- [28, "EBAY_NEW_SHIPPING", True],
- [29, "EBAY_USED_SHIPPING", True],
- [30, "TRADE_IN", True],
- [31, "RENT", False],
-]
-
-
-def _parse_stats(stats, to_datetime):
- """Parse numeric stats object.
-
- There is no need to parse strings or list of strings. Keepa stats object
- response documentation:
- https://keepa.com/#!discuss/t/statistics-object/1308
- """
- stats_keys_parse_not_required = {
- "buyBoxSellerId",
- "sellerIdsLowestFBA",
- "sellerIdsLowestFBM",
- "buyBoxShippingCountry",
- "buyBoxAvailabilityMessage",
- }
- stats_parsed = {}
-
- for stat_key, stat_value in stats.items():
- if stat_key in stats_keys_parse_not_required:
- stat_value = None
-
- elif (
- isinstance(stat_value, int) and stat_value < 0
- ): # -1 or -2 means not exist. 0 doesn't mean not exist.
- stat_value = None
-
- if stat_value is not None:
- if stat_key == "lastOffersUpdate":
- stats_parsed[stat_key] = keepa_minutes_to_time(
- [stat_value], to_datetime
- )[0]
- elif isinstance(stat_value, list) and len(stat_value) > 0:
- stat_value_dict = {}
- convert_time_in_value_pair = any(
- map(lambda v: v is not None and isinstance(v, list), stat_value)
- )
-
- for ind, key, isfloat in csv_indices:
- stat_value_item = stat_value[ind] if ind < len(stat_value) else None
-
- def normalize_value(v):
- if v < 0:
- return None
-
- if isfloat:
- v = float(v) / 100
- if key == "RATING":
- v = v * 10
-
- return v
-
- if stat_value_item is not None:
- if convert_time_in_value_pair:
- stat_value_time, stat_value_item = stat_value_item
- stat_value_item = normalize_value(stat_value_item)
- if stat_value_item is not None:
- stat_value_time = keepa_minutes_to_time(
- [stat_value_time], to_datetime
- )[0]
- stat_value_item = (stat_value_time, stat_value_item)
- else:
- stat_value_item = normalize_value(stat_value_item)
-
- if stat_value_item is not None:
- stat_value_dict[key] = stat_value_item
-
- if len(stat_value_dict) > 0:
- stats_parsed[stat_key] = stat_value_dict
- else:
- stats_parsed[stat_key] = stat_value
-
- return stats_parsed
-
-
-_seller_time_data_keys = ["trackedSince", "lastUpdate"]
-
-
-def _parse_seller(seller_raw_response, to_datetime):
- sellers = list(seller_raw_response.values())
- for seller in sellers:
-
- def convert_time_data(key):
- date_val = seller.get(key, None)
- if date_val is not None:
- return (key, keepa_minutes_to_time([date_val], to_datetime)[0])
- else:
- return None
-
- seller.update(
- filter(
- lambda p: p is not None, map(convert_time_data, _seller_time_data_keys)
- )
- )
-
- return dict(map(lambda seller: (seller["sellerId"], seller), sellers))
-
-
-def parse_csv(csv, to_datetime=True, out_of_stock_as_nan=True):
- """Parse csv list from keepa into a python dictionary.
-
- Parameters
- ----------
- csv : list
- csv list from keepa
-
- to_datetime : bool, optional
- Modifies numpy minutes to datetime.datetime values.
- Default True.
-
- out_of_stock_as_nan : bool, optional
- When True, prices are NAN when price category is out of stock.
- When False, prices are -0.01
- Default True
-
- Returns
- -------
- product_data : dict
- Dictionary containing the following fields with timestamps:
-
- AMAZON: Amazon price history
-
- NEW: Marketplace/3rd party New price history - Amazon is
- considered to be part of the marketplace as well, so if
- Amazon has the overall lowest new (!) price, the
- marketplace new price in the corresponding time interval
- will be identical to the Amazon price (except if there is
- only one marketplace offer). Shipping and Handling costs
- not included!
-
- USED: Marketplace/3rd party Used price history
-
- SALES: Sales Rank history. Not every product has a Sales Rank.
-
- LISTPRICE: List Price history
-
- 5 COLLECTIBLE: Collectible Price history
-
- 6 REFURBISHED: Refurbished Price history
-
- 7 NEW_FBM_SHIPPING: 3rd party (not including Amazon) New price
- history including shipping costs, only fulfilled by
- merchant (FBM).
-
- 8 LIGHTNING_DEAL: 3rd party (not including Amazon) New price
- history including shipping costs, only fulfilled by
- merchant (FBM).
-
- 9 WAREHOUSE: Amazon Warehouse Deals price history. Mostly of
- used condition, rarely new.
-
- 10 NEW_FBA: Price history of the lowest 3rd party (not
- including Amazon/Warehouse) New offer that is fulfilled
- by Amazon
-
- 11 COUNT_NEW: New offer count history
-
- 12 COUNT_USED: Used offer count history
-
- 13 COUNT_REFURBISHED: Refurbished offer count history
-
- 14 COUNT_COLLECTIBLE: Collectible offer count history
-
- 16 RATING: The product's rating history. A rating is an
- integer from 0 to 50 (e.g. 45 = 4.5 stars)
-
- 17 COUNT_REVIEWS: The product's review count history.
-
- 18 BUY_BOX_SHIPPING: The price history of the buy box. If no
- offer qualified for the buy box the price has the value
- -1. Including shipping costs. The ``buybox`` parameter
- must be True for this field to be in the data.
-
- 19 USED_NEW_SHIPPING: "Used - Like New" price history
- including shipping costs.
-
- 20 USED_VERY_GOOD_SHIPPING: "Used - Very Good" price history
- including shipping costs.
-
- 21 USED_GOOD_SHIPPING: "Used - Good" price history including
- shipping costs.
-
- 22 USED_ACCEPTABLE_SHIPPING: "Used - Acceptable" price history
- including shipping costs.
-
- 23 COLLECTIBLE_NEW_SHIPPING: "Collectible - Like New" price
- history including shipping costs.
-
- 24 COLLECTIBLE_VERY_GOOD_SHIPPING: "Collectible - Very Good"
- price history including shipping costs.
-
- 25 COLLECTIBLE_GOOD_SHIPPING: "Collectible - Good" price
- history including shipping costs.
-
- 26 COLLECTIBLE_ACCEPTABLE_SHIPPING: "Collectible - Acceptable"
- price history including shipping costs.
-
- 27 REFURBISHED_SHIPPING: Refurbished price history including
- shipping costs.
-
- 30 TRADE_IN: The trade in price history. Amazon trade-in is
- not available for every locale.
-
- 31 RENT: Rental price history. Requires use of the rental
- and offers parameter. Amazon Rental is only available
- for Amazon US.
-
- Notes
- -----
- Negative prices
-
- """
- product_data = {}
-
- for ind, key, isfloat in csv_indices:
- if csv[ind]: # Check if entry it exists
- if "SHIPPING" in key: # shipping price is included
- # Data goes [time0, value0, shipping0, time1, value1,
- # shipping1, ...]
- times = csv[ind][::3]
- values = np.array(csv[ind][1::3])
- values += np.array(csv[ind][2::3])
- else:
- # Data goes [time0, value0, time1, value1, ...]
- times = csv[ind][::2]
- values = np.array(csv[ind][1::2])
-
- # Convert to float price if applicable
- if isfloat:
- nan_mask = values < 0
- values = values.astype(float) / 100
- if out_of_stock_as_nan:
- values[nan_mask] = np.nan
-
- if key == "RATING":
- values *= 10
-
- timeval = keepa_minutes_to_time(times, to_datetime)
-
- product_data["%s_time" % key] = timeval
- product_data[key] = values
-
- # combine time and value into a data frame using time as index
- product_data[f"df_{key}"] = pd.DataFrame({"value": values}, index=timeval)
-
- return product_data
-
-
-def format_items(items):
- """Check if the input items are valid and formats them."""
- if isinstance(items, list) or isinstance(items, np.ndarray):
- return np.unique(items)
- elif isinstance(items, str):
- return np.asarray([items])
-
-
-class Keepa:
- r"""Support a synchronous Python interface to keepa server.
-
- Initializes API with access key. Access key can be obtained by
- signing up for a reoccurring or one time plan at:
- https://keepa.com/#!api
-
- Parameters
- ----------
- accesskey : str
- 64 character access key string.
-
- timeout : float, optional
- Default timeout when issuing any request. This is not a time
- limit on the entire response download; rather, an exception is
- raised if the server has not issued a response for timeout
- seconds. Setting this to 0 disables the timeout, but will
- cause any request to hang indefiantly should keepa.com be down
-
- logging_level: string, optional
- Logging level to use. Default is 'DEBUG'. Other options are
- 'INFO', 'WARNING', 'ERROR', and 'CRITICAL'.
-
- Examples
- --------
- Create the api object.
-
- >>> import keepa
- >>> key = ''
- >>> api = keepa.Keepa(key)
-
- Request data from two ASINs.
-
- >>> products = api.query(['0439064872', '1426208081'])
-
- Print item details.
-
- >>> print('Item 1')
- >>> print('\t ASIN: {:s}'.format(products[0]['asin']))
- >>> print('\t Title: {:s}'.format(products[0]['title']))
- Item 1
- ASIN: 0439064872
- Title: Harry Potter and the Chamber of Secrets (2)
-
- Print item price.
-
- >>> usedprice = products[0]['data']['USED']
- >>> usedtimes = products[0]['data']['USED_time']
- >>> print('\t Used price: ${:.2f}'.format(usedprice[-1]))
- >>> print('\t as of: {:s}'.format(str(usedtimes[-1])))
- Used price: $0.52
- as of: 2023-01-03 04:46:00
-
- """
-
- def __init__(self, accesskey, timeout=10, logging_level="DEBUG"):
- """Initialize server connection."""
- self.accesskey = accesskey
- self.status = None
- self.tokens_left = 0
- self._timeout = timeout
-
- # Set up logging
- levels = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
- if logging_level not in levels:
- raise TypeError("logging_level must be one of: " + ", ".join(levels))
- log.setLevel(logging_level)
- # Store user's available tokens
- log.info("Connecting to keepa using key ending in %s", accesskey[-6:])
- self.update_status()
- log.info("%d tokens remain", self.tokens_left)
-
- @property
- def time_to_refill(self) -> float:
- """Return the time to refill in seconds.
-
- Examples
- --------
- Return the time to refill. If you have tokens available, this time
- should be 0.0 seconds.
-
- >>> import keepa
- >>> key = ''
- >>> api = keepa.Keepa(key)
- >>> api.time_to_refill
- 0.0
-
- """
- # Get current timestamp in milliseconds from UNIX epoch
- now = int(time.time() * 1000)
- timeatrefile = self.status["timestamp"] + self.status["refillIn"]
-
- # wait plus one second fudge factor
- timetorefil = timeatrefile - now + 1000
- if timetorefil < 0:
- timetorefil = 0
-
- # Account for negative tokens left
- if self.tokens_left < 0:
- timetorefil += (abs(self.tokens_left) / self.status["refillRate"]) * 60000
-
- # Return value in seconds
- return timetorefil / 1000.0
-
- def update_status(self):
- """Update available tokens."""
- self.status = self._request("token", {"key": self.accesskey}, wait=False)
-
- def wait_for_tokens(self):
- """Check if there are any remaining tokens and waits if none are available."""
- self.update_status()
-
- # Wait if no tokens available
- if self.tokens_left <= 0:
- tdelay = self.time_to_refill
- log.warning("Waiting %.0f seconds for additional tokens" % tdelay)
- time.sleep(tdelay)
- self.update_status()
-
- def query(
- self,
- items,
- stats=None,
- domain="US",
- history=True,
- offers=None,
- update=None,
- to_datetime=True,
- rating=False,
- out_of_stock_as_nan=True,
- stock=False,
- product_code_is_asin=True,
- progress_bar=True,
- buybox=False,
- wait=True,
- days=None,
- only_live_offers=None,
- raw=False,
- ):
- """Perform a product query of a list, array, or single ASIN.
-
- Returns a list of product data with one entry for each
- product.
-
- Parameters
- ----------
- items : str, list, np.ndarray
- A list, array, or single asin, UPC, EAN, or ISBN-13
- identifying a product. ASINs should be 10 characters and
- match a product on Amazon. Items not matching Amazon
- product or duplicate Items will return no data. When
- using non-ASIN items, set product_code_is_asin to False
-
- stats : int or date, optional
- No extra token cost. If specified the product object will
- have a stats field with quick access to current prices,
- min/max prices and the weighted mean values. If the offers
- parameter was used it will also provide stock counts and
- buy box information.
-
- You can provide the stats parameter in two forms:
-
- Last x days (positive integer value): calculates the stats
- of the last x days, where x is the value of the stats
- parameter. Interval: You can provide a date range for the
- stats calculation. You can specify the range via two
- timestamps (unix epoch time milliseconds) or two date
- strings (ISO8601, with or without time in UTC).
-
- domain : str, optional
- One of the following Amazon domains: RESERVED, US, GB, DE,
- FR, JP, CA, CN, IT, ES, IN, MX Defaults to US.
-
- offers : int, optional
- Adds available offers to product data. Default 0. Must
- be between 20 and 100.
-
- update : int, optional
- if data is older than the input integer, keepa will
- update their database and return live data. If set to 0
- (live data), request may cost an additional token.
- Default None
-
- history : bool, optional
- When set to True includes the price, sales, and offer
- history of a product. Set to False to reduce request time
- if data is not required. Default True
-
- rating : bool, optional
- When set to to True, includes the existing RATING and
- COUNT_REVIEWS history of the csv field. Default False
-
- to_datetime : bool, optional
- Modifies numpy minutes to datetime.datetime values.
- Default True.
-
- out_of_stock_as_nan : bool, optional
- When True, prices are NAN when price category is out of
- stock. When False, prices are -0.01 Default True
-
- stock : bool, optional
- Can only be used if the offers parameter is also True. If
- True, the stock will be collected for all retrieved live
- offers. Note: We can only determine stock up 10 qty. Stock
- retrieval takes additional time, expect the request to
- take longer. Existing stock history will be included
- whether or not the stock parameter is used.
-
- product_code_is_asin : bool, optional
- The type of product code you are requesting. True when
- product code is an ASIN, an Amazon standard identification
- number, or 'code', for UPC, EAN, or ISBN-13 codes.
-
- progress_bar : bool, optional
- Display a progress bar using ``tqdm``. Defaults to
- ``True``.
-
- buybox : bool, optional
- Additional token cost: 2 per product). When true the
- product and statistics object will include all available
- buy box related data:
-
- - current price, price history, and statistical values
- - buyBoxSellerIdHistory
- - all buy box fields in the statistics object
-
- The buybox parameter
- does not trigger a fresh data collection. If the offers
- parameter is used the buybox parameter is ignored, as the
- offers parameter also provides access to all buy box
- related data. To access the statistics object the stats
- parameter is required.
-
- wait : bool, optional
- Wait available token before doing effective query,
- Defaults to ``True``.
-
- only_live_offers : bool, optional
- If set to True, the product object will only include live
- marketplace offers (when used in combination with the
- offers parameter). If you do not need historical offers
- use this to have them removed from the response. This can
- improve processing time and considerably decrease the size
- of the response. Default None
-
- days : int, optional
- Any positive integer value. If specified and has positive
- value X the product object will limit all historical data
- to the recent X days. This includes the csv,
- buyBoxSellerIdHistory, salesRanks, offers and
- offers.offerCSV fields. If you do not need old historical
- data use this to have it removed from the response. This
- can improve processing time and considerably decrease the
- size of the response. The parameter does not use calendar
- days - so 1 day equals the last 24 hours. The oldest data
- point of each field may have a date value which is out of
- the specified range. This means the value of the field has
- not changed since that date and is still active. Default
- ``None``
-
- raw : bool, optional
- When ``True``, return the raw request response. This is
- only available in the non-async class.
-
- Returns
- -------
- list
- List of products when ``raw=False``. Each product
- within the list is a dictionary. The keys of each item
- may vary, so see the keys within each product for further
- details.
-
- Each product should contain at a minimum a "data" key
- containing a formatted dictionary. For the available
- fields see the notes section
-
- When ``raw=True``, a list of unparsed responses are
- returned as :class:`requests.models.Response`.
-
- See: https://keepa.com/#!discuss/t/product-object/116
-
- Notes
- -----
- The following are some of the fields a product dictionary. For a full
- list and description, please see:
- `product-object `_
-
- AMAZON
- Amazon price history
-
- NEW
- Marketplace/3rd party New price history - Amazon is
- considered to be part of the marketplace as well, so if
- Amazon has the overall lowest new (!) price, the
- marketplace new price in the corresponding time interval
- will be identical to the Amazon price (except if there is
- only one marketplace offer). Shipping and Handling costs
- not included!
-
- USED
- Marketplace/3rd party Used price history
-
- SALES
- Sales Rank history. Not every product has a Sales Rank.
-
- LISTPRICE
- List Price history
-
- COLLECTIBLE
- Collectible Price history
-
- REFURBISHED
- Refurbished Price history
-
- NEW_FBM_SHIPPING
- 3rd party (not including Amazon) New price history
- including shipping costs, only fulfilled by merchant
- (FBM).
-
- LIGHTNING_DEAL
- 3rd party (not including Amazon) New price history
- including shipping costs, only fulfilled by merchant
- (FBM).
-
- WAREHOUSE
- Amazon Warehouse Deals price history. Mostly of used
- condition, rarely new.
-
- NEW_FBA
- Price history of the lowest 3rd party (not including
- Amazon/Warehouse) New offer that is fulfilled by Amazon
-
- COUNT_NEW
- New offer count history
-
- COUNT_USED
- Used offer count history
-
- COUNT_REFURBISHED
- Refurbished offer count history
-
- COUNT_COLLECTIBLE
- Collectible offer count history
-
- RATING
- The product's rating history. A rating is an integer from
- 0 to 50 (e.g. 45 = 4.5 stars)
-
- COUNT_REVIEWS
- The product's review count history.
-
- BUY_BOX_SHIPPING
- The price history of the buy box. If no offer qualified
- for the buy box the price has the value -1. Including
- shipping costs.
-
- USED_NEW_SHIPPING
- "Used - Like New" price history including shipping costs.
-
- USED_VERY_GOOD_SHIPPING
- "Used - Very Good" price history including shipping costs.
-
- USED_GOOD_SHIPPING
- "Used - Good" price history including shipping costs.
-
- USED_ACCEPTABLE_SHIPPING
- "Used - Acceptable" price history including shipping costs.
-
- COLLECTIBLE_NEW_SHIPPING
- "Collectible - Like New" price history including shipping
- costs.
-
- COLLECTIBLE_VERY_GOOD_SHIPPING
- "Collectible - Very Good" price history including shipping
- costs.
-
- COLLECTIBLE_GOOD_SHIPPING
- "Collectible - Good" price history including shipping
- costs.
-
- COLLECTIBLE_ACCEPTABLE_SHIPPING
- "Collectible - Acceptable" price history including
- shipping costs.
-
- REFURBISHED_SHIPPING
- Refurbished price history including shipping costs.
-
- TRADE_IN
- The trade in price history. Amazon trade-in is not
- available for every locale.
-
- BUY_BOX_SHIPPING
- The price history of the buy box. If no offer qualified
- for the buy box the price has the value -1. Including
- shipping costs. The ``buybox`` parameter must be True for
- this field to be in the data.
-
- Examples
- --------
- Query for product with ASIN ``'B0088PUEPK'`` using the synchronous
- keepa interface.
-
- >>> import keepa
- >>> key = ''
- >>> api = keepa.Keepa(key)
- >>> response = api.query('B0088PUEPK')
- >>> response[0]['title']
- 'Western Digital 1TB WD Blue PC Internal Hard Drive HDD - 7200 RPM,
- SATA 6 Gb/s, 64 MB Cache, 3.5" - WD10EZEX'
-
- Query for product with ASIN ``'B0088PUEPK'`` using the asynchronous
- keepa interface.
-
- >>> import asyncio
- >>> import keepa
- >>> async def main():
- ... key = ''
- ... api = await keepa.AsyncKeepa().create(key)
- ... return await api.query('B0088PUEPK')
- >>> response = asyncio.run(main())
- >>> response[0]['title']
- 'Western Digital 1TB WD Blue PC Internal Hard Drive HDD - 7200 RPM,
- SATA 6 Gb/s, 64 MB Cache, 3.5" - WD10EZEX'
-
- """
- # Format items into numpy array
- try:
- items = format_items(items)
- except BaseException:
- raise ValueError("Invalid product codes input")
- if not len(items):
- raise ValueError("No valid product codes")
-
- nitems = len(items)
- if nitems == 1:
- log.debug("Executing single product query")
- else:
- log.debug("Executing %d item product query", nitems)
-
- # check offer input
- if offers:
- if not isinstance(offers, int):
- raise TypeError('Parameter "offers" must be an interger')
-
- if offers > 100 or offers < 20:
- raise ValueError('Parameter "offers" must be between 20 and 100')
-
- # Report time to completion
- tcomplete = (
- float(nitems - self.tokens_left) / self.status["refillRate"]
- - (60000 - self.status["refillIn"]) / 60000.0
- )
- if tcomplete < 0.0:
- tcomplete = 0.5
- log.debug(
- "Estimated time to complete %d request(s) is %.2f minutes",
- nitems,
- tcomplete,
- )
- log.debug(
- "\twith a refill rate of %d token(s) per minute", self.status["refillRate"]
- )
-
- # product list
- products = []
-
- pbar = None
- if progress_bar:
- pbar = tqdm(total=nitems)
-
- # Number of requests is dependent on the number of items and
- # request limit. Use available tokens first
- idx = 0 # or number complete
- while idx < nitems:
- nrequest = nitems - idx
-
- # cap request
- if nrequest > REQUEST_LIMIT:
- nrequest = REQUEST_LIMIT
-
- # request from keepa and increment current position
- item_request = items[idx : idx + nrequest] # noqa: E203
- response = self._product_query(
- item_request,
- product_code_is_asin,
- stats=stats,
- domain=domain,
- stock=stock,
- offers=offers,
- update=update,
- history=history,
- rating=rating,
- to_datetime=to_datetime,
- out_of_stock_as_nan=out_of_stock_as_nan,
- buybox=buybox,
- wait=wait,
- days=days,
- only_live_offers=only_live_offers,
- raw=raw,
- )
- idx += nrequest
- if raw:
- products.append(response)
- else:
- products.extend(response["products"])
-
- if pbar is not None:
- pbar.update(nrequest)
-
- return products
-
- def _product_query(self, items, product_code_is_asin=True, **kwargs):
- """Send query to keepa server and returns parsed JSON result.
-
- Parameters
- ----------
- items : np.ndarray
- Array of asins. If UPC, EAN, or ISBN-13, as_asin must be
- False. Must be between 1 and 100 ASINs
-
- as_asin : bool, optional
- Interpret product codes as ASINs only.
-
- stats : int or date format
- Set the stats time for get sales rank inside this range
-
- domain : str
- One of the following Amazon domains:
- RESERVED, US, GB, DE, FR, JP, CA, CN, IT, ES, IN, MX
-
- offers : bool, optional
- Adds product offers to product data.
-
- update : int, optional
- If data is older than the input integer, keepa will update
- their database and return live data. If set to 0 (live
- data), then request may cost an additional token.
-
- history : bool, optional
- When set to True includes the price, sales, and offer
- history of a product. Set to False to reduce request time
- if data is not required.
-
- as_asin : bool, optional
- Queries keepa using asin codes. Otherwise, queries using
- the code key.
-
- Returns
- -------
- products : list
- List of products. Length equal to number of successful
- ASINs.
-
- refillIn : float
- Time in milliseconds to the next refill of tokens.
-
- refilRate : float
- Number of tokens refilled per minute
-
- timestamp : float
-
- tokensLeft : int
- Remaining tokens
-
- tz : int
- Timezone. 0 is UTC
-
- """
- # ASINs convert to comma joined string
- assert len(items) <= 100
-
- if product_code_is_asin:
- kwargs["asin"] = ",".join(items)
- else:
- kwargs["code"] = ",".join(items)
-
- kwargs["key"] = self.accesskey
- kwargs["domain"] = DCODES.index(kwargs["domain"])
-
- # Convert bool values to 0 and 1.
- kwargs["stock"] = int(kwargs["stock"])
- kwargs["history"] = int(kwargs["history"])
- kwargs["rating"] = int(kwargs["rating"])
- kwargs["buybox"] = int(kwargs["buybox"])
-
- if kwargs["update"] is None:
- del kwargs["update"]
- else:
- kwargs["update"] = int(kwargs["update"])
-
- if kwargs["offers"] is None:
- del kwargs["offers"]
- else:
- kwargs["offers"] = int(kwargs["offers"])
-
- if kwargs["only_live_offers"] is None:
- del kwargs["only_live_offers"]
- else:
- # Keepa's param actually doesn't use snake_case.
- kwargs["only-live-offers"] = int(kwargs.pop("only_live_offers"))
-
- if kwargs["days"] is None:
- del kwargs["days"]
- else:
- assert kwargs["days"] > 0
-
- if kwargs["stats"] is None:
- del kwargs["stats"]
-
- out_of_stock_as_nan = kwargs.pop("out_of_stock_as_nan", True)
- to_datetime = kwargs.pop("to_datetime", True)
-
- # Query and replace csv with parsed data if history enabled
- wait = kwargs.get("wait")
- kwargs.pop("wait", None)
- raw_response = kwargs.pop("raw", False)
- response = self._request(
- "product", kwargs, wait=wait, raw_response=raw_response
- )
-
- if kwargs["history"] and not raw_response:
- for product in response["products"]:
- if product["csv"]: # if data exists
- product["data"] = parse_csv(
- product["csv"], to_datetime, out_of_stock_as_nan
- )
-
- if kwargs.get("stats", None) and not raw_response:
- for product in response["products"]:
- stats = product.get("stats", None)
- if stats:
- product["stats_parsed"] = _parse_stats(stats, to_datetime)
-
- return response
-
- def best_sellers_query(self, category, rank_avg_range=0, domain="US", wait=True):
- """Retrieve an ASIN list of the most popular products.
-
- This is based on sales in a specific category or product group. See
- "search_for_categories" for information on how to get a category.
-
- Root category lists (e.g. "Home & Kitchen") or product group
- lists contain up to 100,000 ASINs.
-
- Sub-category lists (e.g. "Home Entertainment Furniture")
- contain up to 3,000 ASINs. As we only have access to the
- product's primary sales rank and not the ones of all
- categories it is listed in, the sub-category lists are created
- by us based on the product's primary sales rank and do not
- reflect the actual ordering on Amazon.
-
- Lists are ordered, starting with the best selling product.
-
- Lists are updated daily. If a product does not have an
- accessible sales rank it will not be included in the
- lists. This in particular affects many products in the
- Clothing and Sports & Outdoors categories.
-
- We can not correctly identify the sales rank reference
- category in all cases, so some products may be misplaced.
-
- Parameters
- ----------
- category : str
- The category node id of the category you want to request
- the best sellers list for. You can find category node ids
- via the category search "search_for_categories".
-
- domain : str
- Amazon locale you want to access. Must be one of the following
- RESERVED, US, GB, DE, FR, JP, CA, CN, IT, ES, IN, MX
- Default US.
-
- wait : bool, optional
- Wait available token before doing effective query.
- Defaults to ``True``.
-
- Returns
- -------
- best_sellers : list
- List of best seller ASINs
-
- Examples
- --------
- Query for the best sellers among the ``"movies"`` category.
-
- >>> import keepa
- >>> key = ''
- >>> api = keepa.Keepa(key)
- >>> categories = api.search_for_categories("movies")
- >>> category = list(categories.items())[0][0]
- >>> asins = api.best_sellers_query(category)
- >>> asins
- ['B0BF3P5XZS',
- 'B08JQN5VDT',
- 'B09SP8JPPK',
- '0999296345',
- 'B07HPG684T',
- '1984825577',
- ...
-
- Query for the best sellers among the ``"movies"`` category using the
- asynchronous keepa interface.
-
- >>> import asyncio
- >>> import keepa
- >>> async def main():
- ... key = ''
- ... api = await keepa.AsyncKeepa().create(key)
- ... categories = await api.search_for_categories("movies")
- ... category = list(categories.items())[0][0]
- ... return await api.best_sellers_query(category)
- >>> asins = asyncio.run(main())
- >>> asins
- ['B0BF3P5XZS',
- 'B08JQN5VDT',
- 'B09SP8JPPK',
- '0999296345',
- 'B07HPG684T',
- '1984825577',
- ...
-
- """
- assert domain in DCODES, "Invalid domain code"
-
- payload = {
- "key": self.accesskey,
- "domain": DCODES.index(domain),
- "category": category,
- "range": rank_avg_range,
- }
-
- response = self._request("bestsellers", payload, wait=wait)
- if "bestSellersList" in response:
- return response["bestSellersList"]["asinList"]
- else: # pragma: no cover
- log.info("Best sellers search results not yet available")
-
- def search_for_categories(self, searchterm, domain="US", wait=True) -> list:
- """Search for categories from Amazon.
-
- Parameters
- ----------
- searchterm : str
- Input search term.
-
- domain : str, default: 'US'
- Amazon locale you want to access. Must be one of the following
- RESERVED, US, GB, DE, FR, JP, CA, CN, IT, ES, IN, MX
- Default US.
-
- wait : bool, default: True
- Wait available token before doing effective query.
- Defaults to ``True``.
-
- Returns
- -------
- list
- The response contains a categories list with all matching
- categories.
-
- Examples
- --------
- Print all categories from science.
-
- >>> import keepa
- >>> key = ''
- >>> api = keepa.Keepa(key)
- >>> categories = api.search_for_categories('science')
- >>> for cat_id in categories:
- ... print(cat_id, categories[cat_id]['name'])
- 9091159011 Behavioral Sciences
- 8407535011 Fantasy, Horror & Science Fiction
- 8407519011 Sciences & Technology
- 12805 Science & Religion
- 13445 Astrophysics & Space Science
- 12038 Science Fiction & Fantasy
- 3207 Science, Nature & How It Works
- 144 Science Fiction & Fantasy
-
- """
- assert domain in DCODES, "Invalid domain code"
-
- payload = {
- "key": self.accesskey,
- "domain": DCODES.index(domain),
- "type": "category",
- "term": searchterm,
- }
-
- response = self._request("search", payload, wait=wait)
- if response["categories"] == {}: # pragma no cover
- raise RuntimeError(
- "Categories search results not yet available "
- "or no search terms found."
- )
- return response["categories"]
-
- def category_lookup(
- self, category_id, domain="US", include_parents=False, wait=True
- ):
- """Return root categories given a categoryId.
-
- Parameters
- ----------
- category_id : int
- ID for specific category or 0 to return a list of root
- categories.
-
- domain : str, default: "US"
- Amazon locale you want to access. Must be one of the following
- RESERVED, US, GB, DE, FR, JP, CA, CN, IT, ES, IN, MX
- Default US
-
- include_parents : bool, default: False
- Include parents.
-
- wait : bool, default: True
- Wait available token before doing effective query.
-
- Returns
- -------
- list
- Output format is the same as search_for_categories.
-
- Examples
- --------
- Use 0 to return all root categories.
-
- >>> import keepa
- >>> key = ''
- >>> api = keepa.Keepa(key)
- >>> categories = api.category_lookup(0)
-
- Print all root categories
-
- >>> for cat_id in categories:
- >>> print(cat_id, categories[cat_id]['name'])
- 133140011 Kindle Store
- 9013971011 Video Shorts
- 2350149011 Apps & Games
- 165796011 Baby Products
- 163856011 Digital Music
- 13727921011 Alexa Skills
- ...
-
- """
- if domain not in DCODES:
- raise ValueError("Invalid domain code")
-
- payload = {
- "key": self.accesskey,
- "domain": DCODES.index(domain),
- "category": category_id,
- "parents": int(include_parents),
- }
-
- response = self._request("category", payload, wait=wait)
- if response["categories"] == {}: # pragma no cover
- raise Exception(
- "Category lookup results not yet available or no match found."
- )
- return response["categories"]
-
- def seller_query(
- self,
- seller_id,
- domain="US",
- to_datetime=True,
- storefront=False,
- update=None,
- wait=True,
- ):
- """Receive seller information for a given seller id.
-
- If a seller is not found no tokens will be consumed.
-
- Token cost: 1 per requested seller
-
- Parameters
- ----------
- seller_id : str or list
- The seller id of the merchant you want to request. For
- batch requests, you may submit a list of 100 seller_ids.
- The seller id can also be found on Amazon on seller
- profile pages in the seller parameter of the URL as well
- as in the offers results from a product query.
-
- domain : str, optional
- One of the following Amazon domains: RESERVED, US, GB, DE,
- FR, JP, CA, CN, IT, ES, IN, MX Defaults to US.
-
- storefront : bool, optional
- If specified the seller object will contain additional
- information about what items the seller is listing on Amazon.
- This includes a list of ASINs as well as the total amount of
- items the seller has listed. The following seller object
- fields will be set if data is available: asinList,
- asinListLastSeen, totalStorefrontAsinsCSV. If no data is
- available no additional tokens will be consumed. The ASIN
- list can contain up to 100,000 items. As using the storefront
- parameter does not trigger any new collection it does not
- increase the processing time of the request, though the
- response may be much bigger in size. The total storefront
- ASIN count will not be updated, only historical data will
- be provided (when available).
-
- update : int, optional
- Positive integer value. If the last live data collection from
- the Amazon storefront page is older than update hours force a
- new collection. Use this parameter in conjunction with the
- storefront parameter. Token cost will only be applied if a new
- collection is triggered.
-
- Using this parameter you can achieve the following:
-
- - Retrieve data from Amazon: a storefront ASIN list
- containing up to 2,400 ASINs, in addition to all ASINs
- already collected through our database.
- - Force a refresh: Always retrieve live data with the
- value 0.
- - Retrieve the total number of listings of this seller:
- the totalStorefrontAsinsCSV field of the seller object
- will be updated.
-
- wait : bool, optional
- Wait available token before doing effective query.
- Defaults to ``True``.
-
- Returns
- -------
- dict
- Dictionary containing one entry per input ``seller_id``.
-
- Examples
- --------
- Return the information from seller ``'A2L77EE7U53NWQ'``.
-
- >>> import keepa
- >>> key = ''
- >>> api = keepa.Keepa(key)
- >>> seller_info = api.seller_query('A2L77EE7U53NWQ', 'US')
- >>> seller_info['A2L77EE7U53NWQ']['sellerName']
- 'Amazon Warehouse'
-
- Notes
- -----
- Seller data is not available for Amazon China.
-
- """
- if isinstance(seller_id, list):
- if len(seller_id) > 100:
- err_str = "seller_id can contain at maximum 100 sellers"
- raise RuntimeError(err_str)
- seller = ",".join(seller_id)
- else:
- seller = seller_id
-
- payload = {
- "key": self.accesskey,
- "domain": DCODES.index(domain),
- "seller": seller,
- }
-
- if storefront:
- payload["storefront"] = int(storefront)
- if update is not False:
- payload["update"] = update
-
- response = self._request("seller", payload, wait=wait)
- return _parse_seller(response["sellers"], to_datetime)
-
- def product_finder(self, product_parms, domain="US", wait=True) -> list:
- """Query the keepa product database to find products matching criteria.
-
- Almost all product fields can be searched for and sort.
-
- Parameters
- ----------
- product_parms : dict
- Dictionary containing one or more of the following keys:
-
- - ``'author': str``
- - ``'availabilityAmazon': int``
- - ``'avg180_AMAZON_lte': int``
- - ``'avg180_AMAZON_gte': int``
- - ``'avg180_BUY_BOX_SHIPPING_lte': int``
- - ``'avg180_BUY_BOX_SHIPPING_gte': int``
- - ``'avg180_COLLECTIBLE_lte': int``
- - ``'avg180_COLLECTIBLE_gte': int``
- - ``'avg180_COUNT_COLLECTIBLE_lte': int``
- - ``'avg180_COUNT_COLLECTIBLE_gte': int``
- - ``'avg180_COUNT_NEW_lte': int``
- - ``'avg180_COUNT_NEW_gte': int``
- - ``'avg180_COUNT_REFURBISHED_lte': int``
- - ``'avg180_COUNT_REFURBISHED_gte': int``
- - ``'avg180_COUNT_REVIEWS_lte': int``
- - ``'avg180_COUNT_REVIEWS_gte': int``
- - ``'avg180_COUNT_USED_lte': int``
- - ``'avg180_COUNT_USED_gte': int``
- - ``'avg180_EBAY_NEW_SHIPPING_lte': int``
- - ``'avg180_EBAY_NEW_SHIPPING_gte': int``
- - ``'avg180_EBAY_USED_SHIPPING_lte': int``
- - ``'avg180_EBAY_USED_SHIPPING_gte': int``
- - ``'avg180_LIGHTNING_DEAL_lte': int``
- - ``'avg180_LIGHTNING_DEAL_gte': int``
- - ``'avg180_LISTPRICE_lte': int``
- - ``'avg180_LISTPRICE_gte': int``
- - ``'avg180_NEW_lte': int``
- - ``'avg180_NEW_gte': int``
- - ``'avg180_NEW_FBA_lte': int``
- - ``'avg180_NEW_FBA_gte': int``
- - ``'avg180_NEW_FBM_SHIPPING_lte': int``
- - ``'avg180_NEW_FBM_SHIPPING_gte': int``
- - ``'avg180_RATING_lte': int``
- - ``'avg180_RATING_gte': int``
- - ``'avg180_REFURBISHED_lte': int``
- - ``'avg180_REFURBISHED_gte': int``
- - ``'avg180_REFURBISHED_SHIPPING_lte': int``
- - ``'avg180_REFURBISHED_SHIPPING_gte': int``
- - ``'avg180_RENT_lte': int``
- - ``'avg180_RENT_gte': int``
- - ``'avg180_SALES_lte': int``
- - ``'avg180_SALES_gte': int``
- - ``'avg180_TRADE_IN_lte': int``
- - ``'avg180_TRADE_IN_gte': int``
- - ``'avg180_USED_lte': int``
- - ``'avg180_USED_gte': int``
- - ``'avg180_USED_ACCEPTABLE_SHIPPING_lte': int``
- - ``'avg180_USED_ACCEPTABLE_SHIPPING_gte': int``
- - ``'avg180_USED_GOOD_SHIPPING_lte': int``
- - ``'avg180_USED_GOOD_SHIPPING_gte': int``
- - ``'avg180_USED_NEW_SHIPPING_lte': int``
- - ``'avg180_USED_NEW_SHIPPING_gte': int``
- - ``'avg180_USED_VERY_GOOD_SHIPPING_lte': int``
- - ``'avg180_USED_VERY_GOOD_SHIPPING_gte': int``
- - ``'avg180_WAREHOUSE_lte': int``
- - ``'avg180_WAREHOUSE_gte': int``
- - ``'avg1_AMAZON_lte': int``
- - ``'avg1_AMAZON_gte': int``
- - ``'avg1_BUY_BOX_SHIPPING_lte': int``
- - ``'avg1_BUY_BOX_SHIPPING_gte': int``
- - ``'avg1_COLLECTIBLE_lte': int``
- - ``'avg1_COLLECTIBLE_gte': int``
- - ``'avg1_COUNT_COLLECTIBLE_lte': int``
- - ``'avg1_COUNT_COLLECTIBLE_gte': int``
- - ``'avg1_COUNT_NEW_lte': int``
- - ``'avg1_COUNT_NEW_gte': int``
- - ``'avg1_COUNT_REFURBISHED_lte': int``
- - ``'avg1_COUNT_REFURBISHED_gte': int``
- - ``'avg1_COUNT_REVIEWS_lte': int``
- - ``'avg1_COUNT_REVIEWS_gte': int``
- - ``'avg1_COUNT_USED_lte': int``
- - ``'avg1_COUNT_USED_gte': int``
- - ``'avg1_EBAY_NEW_SHIPPING_lte': int``
- - ``'avg1_EBAY_NEW_SHIPPING_gte': int``
- - ``'avg1_EBAY_USED_SHIPPING_lte': int``
- - ``'avg1_EBAY_USED_SHIPPING_gte': int``
- - ``'avg1_LIGHTNING_DEAL_lte': int``
- - ``'avg1_LIGHTNING_DEAL_gte': int``
- - ``'avg1_LISTPRICE_lte': int``
- - ``'avg1_LISTPRICE_gte': int``
- - ``'avg1_NEW_lte': int``
- - ``'avg1_NEW_gte': int``
- - ``'avg1_NEW_FBA_lte': int``
- - ``'avg1_NEW_FBA_gte': int``
- - ``'avg1_NEW_FBM_SHIPPING_lte': int``
- - ``'avg1_NEW_FBM_SHIPPING_gte': int``
- - ``'avg1_RATING_lte': int``
- - ``'avg1_RATING_gte': int``
- - ``'avg1_REFURBISHED_lte': int``
- - ``'avg1_REFURBISHED_gte': int``
- - ``'avg1_REFURBISHED_SHIPPING_lte': int``
- - ``'avg1_REFURBISHED_SHIPPING_gte': int``
- - ``'avg1_RENT_lte': int``
- - ``'avg1_RENT_gte': int``
- - ``'avg1_SALES_lte': int``
- - ``'avg1_SALES_lte': int``
- - ``'avg1_SALES_gte': int``
- - ``'avg1_TRADE_IN_lte': int``
- - ``'avg1_TRADE_IN_gte': int``
- - ``'avg1_USED_lte': int``
- - ``'avg1_USED_gte': int``
- - ``'avg1_USED_ACCEPTABLE_SHIPPING_lte': int``
- - ``'avg1_USED_ACCEPTABLE_SHIPPING_gte': int``
- - ``'avg1_USED_GOOD_SHIPPING_lte': int``
- - ``'avg1_USED_GOOD_SHIPPING_gte': int``
- - ``'avg1_USED_NEW_SHIPPING_lte': int``
- - ``'avg1_USED_NEW_SHIPPING_gte': int``
- - ``'avg1_USED_VERY_GOOD_SHIPPING_lte': int``
- - ``'avg1_USED_VERY_GOOD_SHIPPING_gte': int``
- - ``'avg1_WAREHOUSE_lte': int``
- - ``'avg1_WAREHOUSE_gte': int``
- - ``'avg30_AMAZON_lte': int``
- - ``'avg30_AMAZON_gte': int``
- - ``'avg30_BUY_BOX_SHIPPING_lte': int``
- - ``'avg30_BUY_BOX_SHIPPING_gte': int``
- - ``'avg30_COLLECTIBLE_lte': int``
- - ``'avg30_COLLECTIBLE_gte': int``
- - ``'avg30_COUNT_COLLECTIBLE_lte': int``
- - ``'avg30_COUNT_COLLECTIBLE_gte': int``
- - ``'avg30_COUNT_NEW_lte': int``
- - ``'avg30_COUNT_NEW_gte': int``
- - ``'avg30_COUNT_REFURBISHED_lte': int``
- - ``'avg30_COUNT_REFURBISHED_gte': int``
- - ``'avg30_COUNT_REVIEWS_lte': int``
- - ``'avg30_COUNT_REVIEWS_gte': int``
- - ``'avg30_COUNT_USED_lte': int``
- - ``'avg30_COUNT_USED_gte': int``
- - ``'avg30_EBAY_NEW_SHIPPING_lte': int``
- - ``'avg30_EBAY_NEW_SHIPPING_gte': int``
- - ``'avg30_EBAY_USED_SHIPPING_lte': int``
- - ``'avg30_EBAY_USED_SHIPPING_gte': int``
- - ``'avg30_LIGHTNING_DEAL_lte': int``
- - ``'avg30_LIGHTNING_DEAL_gte': int``
- - ``'avg30_LISTPRICE_lte': int``
- - ``'avg30_LISTPRICE_gte': int``
- - ``'avg30_NEW_lte': int``
- - ``'avg30_NEW_gte': int``
- - ``'avg30_NEW_FBA_lte': int``
- - ``'avg30_NEW_FBA_gte': int``
- - ``'avg30_NEW_FBM_SHIPPING_lte': int``
- - ``'avg30_NEW_FBM_SHIPPING_gte': int``
- - ``'avg30_RATING_lte': int``
- - ``'avg30_RATING_gte': int``
- - ``'avg30_REFURBISHED_lte': int``
- - ``'avg30_REFURBISHED_gte': int``
- - ``'avg30_REFURBISHED_SHIPPING_lte': int``
- - ``'avg30_REFURBISHED_SHIPPING_gte': int``
- - ``'avg30_RENT_lte': int``
- - ``'avg30_RENT_gte': int``
- - ``'avg30_SALES_lte': int``
- - ``'avg30_SALES_gte': int``
- - ``'avg30_TRADE_IN_lte': int``
- - ``'avg30_TRADE_IN_gte': int``
- - ``'avg30_USED_lte': int``
- - ``'avg30_USED_gte': int``
- - ``'avg30_USED_ACCEPTABLE_SHIPPING_lte': int``
- - ``'avg30_USED_ACCEPTABLE_SHIPPING_gte': int``
- - ``'avg30_USED_GOOD_SHIPPING_lte': int``
- - ``'avg30_USED_GOOD_SHIPPING_gte': int``
- - ``'avg30_USED_NEW_SHIPPING_lte': int``
- - ``'avg30_USED_NEW_SHIPPING_gte': int``
- - ``'avg30_USED_VERY_GOOD_SHIPPING_lte': int``
- - ``'avg30_USED_VERY_GOOD_SHIPPING_gte': int``
- - ``'avg30_WAREHOUSE_lte': int``
- - ``'avg30_WAREHOUSE_gte': int``
- - ``'avg7_AMAZON_lte': int``
- - ``'avg7_AMAZON_gte': int``
- - ``'avg7_BUY_BOX_SHIPPING_lte': int``
- - ``'avg7_BUY_BOX_SHIPPING_gte': int``
- - ``'avg7_COLLECTIBLE_lte': int``
- - ``'avg7_COLLECTIBLE_gte': int``
- - ``'avg7_COUNT_COLLECTIBLE_lte': int``
- - ``'avg7_COUNT_COLLECTIBLE_gte': int``
- - ``'avg7_COUNT_NEW_lte': int``
- - ``'avg7_COUNT_NEW_gte': int``
- - ``'avg7_COUNT_REFURBISHED_lte': int``
- - ``'avg7_COUNT_REFURBISHED_gte': int``
- - ``'avg7_COUNT_REVIEWS_lte': int``
- - ``'avg7_COUNT_REVIEWS_gte': int``
- - ``'avg7_COUNT_USED_lte': int``
- - ``'avg7_COUNT_USED_gte': int``
- - ``'avg7_EBAY_NEW_SHIPPING_lte': int``
- - ``'avg7_EBAY_NEW_SHIPPING_gte': int``
- - ``'avg7_EBAY_USED_SHIPPING_lte': int``
- - ``'avg7_EBAY_USED_SHIPPING_gte': int``
- - ``'avg7_LIGHTNING_DEAL_lte': int``
- - ``'avg7_LIGHTNING_DEAL_gte': int``
- - ``'avg7_LISTPRICE_lte': int``
- - ``'avg7_LISTPRICE_gte': int``
- - ``'avg7_NEW_lte': int``
- - ``'avg7_NEW_gte': int``
- - ``'avg7_NEW_FBA_lte': int``
- - ``'avg7_NEW_FBA_gte': int``
- - ``'avg7_NEW_FBM_SHIPPING_lte': int``
- - ``'avg7_NEW_FBM_SHIPPING_gte': int``
- - ``'avg7_RATING_lte': int``
- - ``'avg7_RATING_gte': int``
- - ``'avg7_REFURBISHED_lte': int``
- - ``'avg7_REFURBISHED_gte': int``
- - ``'avg7_REFURBISHED_SHIPPING_lte': int``
- - ``'avg7_REFURBISHED_SHIPPING_gte': int``
- - ``'avg7_RENT_lte': int``
- - ``'avg7_RENT_gte': int``
- - ``'avg7_SALES_lte': int``
- - ``'avg7_SALES_gte': int``
- - ``'avg7_TRADE_IN_lte': int``
- - ``'avg7_TRADE_IN_gte': int``
- - ``'avg7_USED_lte': int``
- - ``'avg7_USED_gte': int``
- - ``'avg7_USED_ACCEPTABLE_SHIPPING_lte': int``
- - ``'avg7_USED_ACCEPTABLE_SHIPPING_gte': int``
- - ``'avg7_USED_GOOD_SHIPPING_lte': int``
- - ``'avg7_USED_GOOD_SHIPPING_gte': int``
- - ``'avg7_USED_NEW_SHIPPING_lte': int``
- - ``'avg7_USED_NEW_SHIPPING_gte': int``
- - ``'avg7_USED_VERY_GOOD_SHIPPING_lte': int``
- - ``'avg7_USED_VERY_GOOD_SHIPPING_gte': int``
- - ``'avg7_WAREHOUSE_lte': int``
- - ``'avg7_WAREHOUSE_gte': int``
- - ``'avg90_AMAZON_lte': int``
- - ``'avg90_AMAZON_gte': int``
- - ``'avg90_BUY_BOX_SHIPPING_lte': int``
- - ``'avg90_BUY_BOX_SHIPPING_gte': int``
- - ``'avg90_COLLECTIBLE_lte': int``
- - ``'avg90_COLLECTIBLE_gte': int``
- - ``'avg90_COUNT_COLLECTIBLE_lte': int``
- - ``'avg90_COUNT_COLLECTIBLE_gte': int``
- - ``'avg90_COUNT_NEW_lte': int``
- - ``'avg90_COUNT_NEW_gte': int``
- - ``'avg90_COUNT_REFURBISHED_lte': int``
- - ``'avg90_COUNT_REFURBISHED_gte': int``
- - ``'avg90_COUNT_REVIEWS_lte': int``
- - ``'avg90_COUNT_REVIEWS_gte': int``
- - ``'avg90_COUNT_USED_lte': int``
- - ``'avg90_COUNT_USED_gte': int``
- - ``'avg90_EBAY_NEW_SHIPPING_lte': int``
- - ``'avg90_EBAY_NEW_SHIPPING_gte': int``
- - ``'avg90_EBAY_USED_SHIPPING_lte': int``
- - ``'avg90_EBAY_USED_SHIPPING_gte': int``
- - ``'avg90_LIGHTNING_DEAL_lte': int``
- - ``'avg90_LIGHTNING_DEAL_gte': int``
- - ``'avg90_LISTPRICE_lte': int``
- - ``'avg90_LISTPRICE_gte': int``
- - ``'avg90_NEW_lte': int``
- - ``'avg90_NEW_gte': int``
- - ``'avg90_NEW_FBA_lte': int``
- - ``'avg90_NEW_FBA_gte': int``
- - ``'avg90_NEW_FBM_SHIPPING_lte': int``
- - ``'avg90_NEW_FBM_SHIPPING_gte': int``
- - ``'avg90_RATING_lte': int``
- - ``'avg90_RATING_gte': int``
- - ``'avg90_REFURBISHED_lte': int``
- - ``'avg90_REFURBISHED_gte': int``
- - ``'avg90_REFURBISHED_SHIPPING_lte': int``
- - ``'avg90_REFURBISHED_SHIPPING_gte': int``
- - ``'avg90_RENT_lte': int``
- - ``'avg90_RENT_gte': int``
- - ``'avg90_SALES_lte': int``
- - ``'avg90_SALES_gte': int``
- - ``'avg90_TRADE_IN_lte': int``
- - ``'avg90_TRADE_IN_gte': int``
- - ``'avg90_USED_lte': int``
- - ``'avg90_USED_gte': int``
- - ``'avg90_USED_ACCEPTABLE_SHIPPING_lte': int``
- - ``'avg90_USED_ACCEPTABLE_SHIPPING_gte': int``
- - ``'avg90_USED_GOOD_SHIPPING_lte': int``
- - ``'avg90_USED_GOOD_SHIPPING_gte': int``
- - ``'avg90_USED_NEW_SHIPPING_lte': int``
- - ``'avg90_USED_NEW_SHIPPING_gte': int``
- - ``'avg90_USED_VERY_GOOD_SHIPPING_lte': int``
- - ``'avg90_USED_VERY_GOOD_SHIPPING_gte': int``
- - ``'avg90_WAREHOUSE_lte': int``
- - ``'avg90_WAREHOUSE_gte': int``
- - ``'backInStock_AMAZON': bool``
- - ``'backInStock_BUY_BOX_SHIPPING': bool``
- - ``'backInStock_COLLECTIBLE': bool``
- - ``'backInStock_COUNT_COLLECTIBLE': bool``
- - ``'backInStock_COUNT_NEW': bool``
- - ``'backInStock_COUNT_REFURBISHED': bool``
- - ``'backInStock_COUNT_REVIEWS': bool``
- - ``'backInStock_COUNT_USED': bool``
- - ``'backInStock_EBAY_NEW_SHIPPING': bool``
- - ``'backInStock_EBAY_USED_SHIPPING': bool``
- - ``'backInStock_LIGHTNING_DEAL': bool``
- - ``'backInStock_LISTPRICE': bool``
- - ``'backInStock_NEW': bool``
- - ``'backInStock_NEW_FBA': bool``
- - ``'backInStock_NEW_FBM_SHIPPING': bool``
- - ``'backInStock_RATING': bool``
- - ``'backInStock_REFURBISHED': bool``
- - ``'backInStock_REFURBISHED_SHIPPING': bool``
- - ``'backInStock_RENT': bool``
- - ``'backInStock_SALES': bool``
- - ``'backInStock_TRADE_IN': bool``
- - ``'backInStock_USED': bool``
- - ``'backInStock_USED_ACCEPTABLE_SHIPPING': bool``
- - ``'backInStock_USED_GOOD_SHIPPING': bool``
- - ``'backInStock_USED_NEW_SHIPPING': bool``
- - ``'backInStock_USED_VERY_GOOD_SHIPPING': bool``
- - ``'backInStock_WAREHOUSE': bool``
- - ``'binding': str``
- - ``'brand': str``
- - ``'buyBoxSellerId': str``
- - ``'color': str``
- - ``'couponOneTimeAbsolute_lte': int``
- - ``'couponOneTimeAbsolute_gte': int``
- - ``'couponOneTimePercent_lte': int``
- - ``'couponOneTimePercent_gte': int``
- - ``'couponSNSAbsolute_lte': int``
- - ``'couponSNSAbsolute_gte': int``
- - ``'couponSNSPercent_lte': int``
- - ``'couponSNSPercent_gte': int``
- - ``'current_AMAZON_lte': int``
- - ``'current_AMAZON_gte': int``
- - ``'current_BUY_BOX_SHIPPING_lte': int``
- - ``'current_BUY_BOX_SHIPPING_gte': int``
- - ``'current_COLLECTIBLE_lte': int``
- - ``'current_COLLECTIBLE_gte': int``
- - ``'current_COUNT_COLLECTIBLE_lte': int``
- - ``'current_COUNT_COLLECTIBLE_gte': int``
- - ``'current_COUNT_NEW_lte': int``
- - ``'current_COUNT_NEW_gte': int``
- - ``'current_COUNT_REFURBISHED_lte': int``
- - ``'current_COUNT_REFURBISHED_gte': int``
- - ``'current_COUNT_REVIEWS_lte': int``
- - ``'current_COUNT_REVIEWS_gte': int``
- - ``'current_COUNT_USED_lte': int``
- - ``'current_COUNT_USED_gte': int``
- - ``'current_EBAY_NEW_SHIPPING_lte': int``
- - ``'current_EBAY_NEW_SHIPPING_gte': int``
- - ``'current_EBAY_USED_SHIPPING_lte': int``
- - ``'current_EBAY_USED_SHIPPING_gte': int``
- - ``'current_LIGHTNING_DEAL_lte': int``
- - ``'current_LIGHTNING_DEAL_gte': int``
- - ``'current_LISTPRICE_lte': int``
- - ``'current_LISTPRICE_gte': int``
- - ``'current_NEW_lte': int``
- - ``'current_NEW_gte': int``
- - ``'current_NEW_FBA_lte': int``
- - ``'current_NEW_FBA_gte': int``
- - ``'current_NEW_FBM_SHIPPING_lte': int``
- - ``'current_NEW_FBM_SHIPPING_gte': int``
- - ``'current_RATING_lte': int``
- - ``'current_RATING_gte': int``
- - ``'current_REFURBISHED_lte': int``
- - ``'current_REFURBISHED_gte': int``
- - ``'current_REFURBISHED_SHIPPING_lte': int``
- - ``'current_REFURBISHED_SHIPPING_gte': int``
- - ``'current_RENT_lte': int``
- - ``'current_RENT_gte': int``
- - ``'current_SALES_lte': int``
- - ``'current_SALES_gte': int``
- - ``'current_TRADE_IN_lte': int``
- - ``'current_TRADE_IN_gte': int``
- - ``'current_USED_lte': int``
- - ``'current_USED_gte': int``
- - ``'current_USED_ACCEPTABLE_SHIPPING_lte': int``
- - ``'current_USED_ACCEPTABLE_SHIPPING_gte': int``
- - ``'current_USED_GOOD_SHIPPING_lte': int``
- - ``'current_USED_GOOD_SHIPPING_gte': int``
- - ``'current_USED_NEW_SHIPPING_lte': int``
- - ``'current_USED_NEW_SHIPPING_gte': int``
- - ``'current_USED_VERY_GOOD_SHIPPING_lte': int``
- - ``'current_USED_VERY_GOOD_SHIPPING_gte': int``
- - ``'current_WAREHOUSE_lte': int``
- - ``'current_WAREHOUSE_gte': int``
- - ``'delta1_AMAZON_lte': int``
- - ``'delta1_AMAZON_gte': int``
- - ``'delta1_BUY_BOX_SHIPPING_lte': int``
- - ``'delta1_BUY_BOX_SHIPPING_gte': int``
- - ``'delta1_COLLECTIBLE_lte': int``
- - ``'delta1_COLLECTIBLE_gte': int``
- - ``'delta1_COUNT_COLLECTIBLE_lte': int``
- - ``'delta1_COUNT_COLLECTIBLE_gte': int``
- - ``'delta1_COUNT_NEW_lte': int``
- - ``'delta1_COUNT_NEW_gte': int``
- - ``'delta1_COUNT_REFURBISHED_lte': int``
- - ``'delta1_COUNT_REFURBISHED_gte': int``
- - ``'delta1_COUNT_REVIEWS_lte': int``
- - ``'delta1_COUNT_REVIEWS_gte': int``
- - ``'delta1_COUNT_USED_lte': int``
- - ``'delta1_COUNT_USED_gte': int``
- - ``'delta1_EBAY_NEW_SHIPPING_lte': int``
- - ``'delta1_EBAY_NEW_SHIPPING_gte': int``
- - ``'delta1_EBAY_USED_SHIPPING_lte': int``
- - ``'delta1_EBAY_USED_SHIPPING_gte': int``
- - ``'delta1_LIGHTNING_DEAL_lte': int``
- - ``'delta1_LIGHTNING_DEAL_gte': int``
- - ``'delta1_LISTPRICE_lte': int``
- - ``'delta1_LISTPRICE_gte': int``
- - ``'delta1_NEW_lte': int``
- - ``'delta1_NEW_gte': int``
- - ``'delta1_NEW_FBA_lte': int``
- - ``'delta1_NEW_FBA_gte': int``
- - ``'delta1_NEW_FBM_SHIPPING_lte': int``
- - ``'delta1_NEW_FBM_SHIPPING_gte': int``
- - ``'delta1_RATING_lte': int``
- - ``'delta1_RATING_gte': int``
- - ``'delta1_REFURBISHED_lte': int``
- - ``'delta1_REFURBISHED_gte': int``
- - ``'delta1_REFURBISHED_SHIPPING_lte': int``
- - ``'delta1_REFURBISHED_SHIPPING_gte': int``
- - ``'delta1_RENT_lte': int``
- - ``'delta1_RENT_gte': int``
- - ``'delta1_SALES_lte': int``
- - ``'delta1_SALES_gte': int``
- - ``'delta1_TRADE_IN_lte': int``
- - ``'delta1_TRADE_IN_gte': int``
- - ``'delta1_USED_lte': int``
- - ``'delta1_USED_gte': int``
- - ``'delta1_USED_ACCEPTABLE_SHIPPING_lte': int``
- - ``'delta1_USED_ACCEPTABLE_SHIPPING_gte': int``
- - ``'delta1_USED_GOOD_SHIPPING_lte': int``
- - ``'delta1_USED_GOOD_SHIPPING_gte': int``
- - ``'delta1_USED_NEW_SHIPPING_lte': int``
- - ``'delta1_USED_NEW_SHIPPING_gte': int``
- - ``'delta1_USED_VERY_GOOD_SHIPPING_lte': int``
- - ``'delta1_USED_VERY_GOOD_SHIPPING_gte': int``
- - ``'delta1_WAREHOUSE_lte': int``
- - ``'delta1_WAREHOUSE_gte': int``
- - ``'delta30_AMAZON_lte': int``
- - ``'delta30_AMAZON_gte': int``
- - ``'delta30_BUY_BOX_SHIPPING_lte': int``
- - ``'delta30_BUY_BOX_SHIPPING_gte': int``
- - ``'delta30_COLLECTIBLE_lte': int``
- - ``'delta30_COLLECTIBLE_gte': int``
- - ``'delta30_COUNT_COLLECTIBLE_lte': int``
- - ``'delta30_COUNT_COLLECTIBLE_gte': int``
- - ``'delta30_COUNT_NEW_lte': int``
- - ``'delta30_COUNT_NEW_gte': int``
- - ``'delta30_COUNT_REFURBISHED_lte': int``
- - ``'delta30_COUNT_REFURBISHED_gte': int``
- - ``'delta30_COUNT_REVIEWS_lte': int``
- - ``'delta30_COUNT_REVIEWS_gte': int``
- - ``'delta30_COUNT_USED_lte': int``
- - ``'delta30_COUNT_USED_gte': int``
- - ``'delta30_EBAY_NEW_SHIPPING_lte': int``
- - ``'delta30_EBAY_NEW_SHIPPING_gte': int``
- - ``'delta30_EBAY_USED_SHIPPING_lte': int``
- - ``'delta30_EBAY_USED_SHIPPING_gte': int``
- - ``'delta30_LIGHTNING_DEAL_lte': int``
- - ``'delta30_LIGHTNING_DEAL_gte': int``
- - ``'delta30_LISTPRICE_lte': int``
- - ``'delta30_LISTPRICE_gte': int``
- - ``'delta30_NEW_lte': int``
- - ``'delta30_NEW_gte': int``
- - ``'delta30_NEW_FBA_lte': int``
- - ``'delta30_NEW_FBA_gte': int``
- - ``'delta30_NEW_FBM_SHIPPING_lte': int``
- - ``'delta30_NEW_FBM_SHIPPING_gte': int``
- - ``'delta30_RATING_lte': int``
- - ``'delta30_RATING_gte': int``
- - ``'delta30_REFURBISHED_lte': int``
- - ``'delta30_REFURBISHED_gte': int``
- - ``'delta30_REFURBISHED_SHIPPING_lte': int``
- - ``'delta30_REFURBISHED_SHIPPING_gte': int``
- - ``'delta30_RENT_lte': int``
- - ``'delta30_RENT_gte': int``
- - ``'delta30_SALES_lte': int``
- - ``'delta30_SALES_gte': int``
- - ``'delta30_TRADE_IN_lte': int``
- - ``'delta30_TRADE_IN_gte': int``
- - ``'delta30_USED_lte': int``
- - ``'delta30_USED_gte': int``
- - ``'delta30_USED_ACCEPTABLE_SHIPPING_lte': int``
- - ``'delta30_USED_ACCEPTABLE_SHIPPING_gte': int``
- - ``'delta30_USED_GOOD_SHIPPING_lte': int``
- - ``'delta30_USED_GOOD_SHIPPING_gte': int``
- - ``'delta30_USED_NEW_SHIPPING_lte': int``
- - ``'delta30_USED_NEW_SHIPPING_gte': int``
- - ``'delta30_USED_VERY_GOOD_SHIPPING_lte': int``
- - ``'delta30_USED_VERY_GOOD_SHIPPING_gte': int``
- - ``'delta30_WAREHOUSE_lte': int``
- - ``'delta30_WAREHOUSE_gte': int``
- - ``'delta7_AMAZON_lte': int``
- - ``'delta7_AMAZON_gte': int``
- - ``'delta7_BUY_BOX_SHIPPING_lte': int``
- - ``'delta7_BUY_BOX_SHIPPING_gte': int``
- - ``'delta7_COLLECTIBLE_lte': int``
- - ``'delta7_COLLECTIBLE_gte': int``
- - ``'delta7_COUNT_COLLECTIBLE_lte': int``
- - ``'delta7_COUNT_COLLECTIBLE_gte': int``
- - ``'delta7_COUNT_NEW_lte': int``
- - ``'delta7_COUNT_NEW_gte': int``
- - ``'delta7_COUNT_REFURBISHED_lte': int``
- - ``'delta7_COUNT_REFURBISHED_gte': int``
- - ``'delta7_COUNT_REVIEWS_lte': int``
- - ``'delta7_COUNT_REVIEWS_gte': int``
- - ``'delta7_COUNT_USED_lte': int``
- - ``'delta7_COUNT_USED_gte': int``
- - ``'delta7_EBAY_NEW_SHIPPING_lte': int``
- - ``'delta7_EBAY_NEW_SHIPPING_gte': int``
- - ``'delta7_EBAY_USED_SHIPPING_lte': int``
- - ``'delta7_EBAY_USED_SHIPPING_gte': int``
- - ``'delta7_LIGHTNING_DEAL_lte': int``
- - ``'delta7_LIGHTNING_DEAL_gte': int``
- - ``'delta7_LISTPRICE_lte': int``
- - ``'delta7_LISTPRICE_gte': int``
- - ``'delta7_NEW_lte': int``
- - ``'delta7_NEW_gte': int``
- - ``'delta7_NEW_FBA_lte': int``
- - ``'delta7_NEW_FBA_gte': int``
- - ``'delta7_NEW_FBM_SHIPPING_lte': int``
- - ``'delta7_NEW_FBM_SHIPPING_gte': int``
- - ``'delta7_RATING_lte': int``
- - ``'delta7_RATING_gte': int``
- - ``'delta7_REFURBISHED_lte': int``
- - ``'delta7_REFURBISHED_gte': int``
- - ``'delta7_REFURBISHED_SHIPPING_lte': int``
- - ``'delta7_REFURBISHED_SHIPPING_gte': int``
- - ``'delta7_RENT_lte': int``
- - ``'delta7_RENT_gte': int``
- - ``'delta7_SALES_lte': int``
- - ``'delta7_SALES_gte': int``
- - ``'delta7_TRADE_IN_lte': int``
- - ``'delta7_TRADE_IN_gte': int``
- - ``'delta7_USED_lte': int``
- - ``'delta7_USED_gte': int``
- - ``'delta7_USED_ACCEPTABLE_SHIPPING_lte': int``
- - ``'delta7_USED_ACCEPTABLE_SHIPPING_gte': int``
- - ``'delta7_USED_GOOD_SHIPPING_lte': int``
- - ``'delta7_USED_GOOD_SHIPPING_gte': int``
- - ``'delta7_USED_NEW_SHIPPING_lte': int``
- - ``'delta7_USED_NEW_SHIPPING_gte': int``
- - ``'delta7_USED_VERY_GOOD_SHIPPING_lte': int``
- - ``'delta7_USED_VERY_GOOD_SHIPPING_gte': int``
- - ``'delta7_WAREHOUSE_lte': int``
- - ``'delta7_WAREHOUSE_gte': int``
- - ``'delta90_AMAZON_lte': int``
- - ``'delta90_AMAZON_gte': int``
- - ``'delta90_BUY_BOX_SHIPPING_lte': int``
- - ``'delta90_BUY_BOX_SHIPPING_gte': int``
- - ``'delta90_COLLECTIBLE_lte': int``
- - ``'delta90_COLLECTIBLE_gte': int``
- - ``'delta90_COUNT_COLLECTIBLE_lte': int``
- - ``'delta90_COUNT_COLLECTIBLE_gte': int``
- - ``'delta90_COUNT_NEW_lte': int``
- - ``'delta90_COUNT_NEW_gte': int``
- - ``'delta90_COUNT_REFURBISHED_lte': int``
- - ``'delta90_COUNT_REFURBISHED_gte': int``
- - ``'delta90_COUNT_REVIEWS_lte': int``
- - ``'delta90_COUNT_REVIEWS_gte': int``
- - ``'delta90_COUNT_USED_lte': int``
- - ``'delta90_COUNT_USED_gte': int``
- - ``'delta90_EBAY_NEW_SHIPPING_lte': int``
- - ``'delta90_EBAY_NEW_SHIPPING_gte': int``
- - ``'delta90_EBAY_USED_SHIPPING_lte': int``
- - ``'delta90_EBAY_USED_SHIPPING_gte': int``
- - ``'delta90_LIGHTNING_DEAL_lte': int``
- - ``'delta90_LIGHTNING_DEAL_gte': int``
- - ``'delta90_LISTPRICE_lte': int``
- - ``'delta90_LISTPRICE_gte': int``
- - ``'delta90_NEW_lte': int``
- - ``'delta90_NEW_gte': int``
- - ``'delta90_NEW_FBA_lte': int``
- - ``'delta90_NEW_FBA_gte': int``
- - ``'delta90_NEW_FBM_SHIPPING_lte': int``
- - ``'delta90_NEW_FBM_SHIPPING_gte': int``
- - ``'delta90_RATING_lte': int``
- - ``'delta90_RATING_gte': int``
- - ``'delta90_REFURBISHED_lte': int``
- - ``'delta90_REFURBISHED_gte': int``
- - ``'delta90_REFURBISHED_SHIPPING_lte': int``
- - ``'delta90_REFURBISHED_SHIPPING_gte': int``
- - ``'delta90_RENT_lte': int``
- - ``'delta90_RENT_gte': int``
- - ``'delta90_SALES_lte': int``
- - ``'delta90_SALES_gte': int``
- - ``'delta90_TRADE_IN_lte': int``
- - ``'delta90_TRADE_IN_gte': int``
- - ``'delta90_USED_lte': int``
- - ``'delta90_USED_gte': int``
- - ``'delta90_USED_ACCEPTABLE_SHIPPING_lte': int``
- - ``'delta90_USED_ACCEPTABLE_SHIPPING_gte': int``
- - ``'delta90_USED_GOOD_SHIPPING_lte': int``
- - ``'delta90_USED_GOOD_SHIPPING_gte': int``
- - ``'delta90_USED_NEW_SHIPPING_lte': int``
- - ``'delta90_USED_NEW_SHIPPING_gte': int``
- - ``'delta90_USED_VERY_GOOD_SHIPPING_lte': int``
- - ``'delta90_USED_VERY_GOOD_SHIPPING_gte': int``
- - ``'delta90_WAREHOUSE_lte': int``
- - ``'delta90_WAREHOUSE_gte': int``
- - ``'deltaLast_AMAZON_lte': int``
- - ``'deltaLast_AMAZON_gte': int``
- - ``'deltaLast_BUY_BOX_SHIPPING_lte': int``
- - ``'deltaLast_BUY_BOX_SHIPPING_gte': int``
- - ``'deltaLast_COLLECTIBLE_lte': int``
- - ``'deltaLast_COLLECTIBLE_gte': int``
- - ``'deltaLast_COUNT_COLLECTIBLE_lte': int``
- - ``'deltaLast_COUNT_COLLECTIBLE_gte': int``
- - ``'deltaLast_COUNT_NEW_lte': int``
- - ``'deltaLast_COUNT_NEW_gte': int``
- - ``'deltaLast_COUNT_REFURBISHED_lte': int``
- - ``'deltaLast_COUNT_REFURBISHED_gte': int``
- - ``'deltaLast_COUNT_REVIEWS_lte': int``
- - ``'deltaLast_COUNT_REVIEWS_gte': int``
- - ``'deltaLast_COUNT_USED_lte': int``
- - ``'deltaLast_COUNT_USED_gte': int``
- - ``'deltaLast_EBAY_NEW_SHIPPING_lte': int``
- - ``'deltaLast_EBAY_NEW_SHIPPING_gte': int``
- - ``'deltaLast_EBAY_USED_SHIPPING_lte': int``
- - ``'deltaLast_EBAY_USED_SHIPPING_gte': int``
- - ``'deltaLast_LIGHTNING_DEAL_lte': int``
- - ``'deltaLast_LIGHTNING_DEAL_gte': int``
- - ``'deltaLast_LISTPRICE_lte': int``
- - ``'deltaLast_LISTPRICE_gte': int``
- - ``'deltaLast_NEW_lte': int``
- - ``'deltaLast_NEW_gte': int``
- - ``'deltaLast_NEW_FBA_lte': int``
- - ``'deltaLast_NEW_FBA_gte': int``
- - ``'deltaLast_NEW_FBM_SHIPPING_lte': int``
- - ``'deltaLast_NEW_FBM_SHIPPING_gte': int``
- - ``'deltaLast_RATING_lte': int``
- - ``'deltaLast_RATING_gte': int``
- - ``'deltaLast_REFURBISHED_lte': int``
- - ``'deltaLast_REFURBISHED_gte': int``
- - ``'deltaLast_REFURBISHED_SHIPPING_lte': int``
- - ``'deltaLast_REFURBISHED_SHIPPING_gte': int``
- - ``'deltaLast_RENT_lte': int``
- - ``'deltaLast_RENT_gte': int``
- - ``'deltaLast_SALES_lte': int``
- - ``'deltaLast_SALES_gte': int``
- - ``'deltaLast_TRADE_IN_lte': int``
- - ``'deltaLast_TRADE_IN_gte': int``
- - ``'deltaLast_USED_lte': int``
- - ``'deltaLast_USED_gte': int``
- - ``'deltaLast_USED_ACCEPTABLE_SHIPPING_lte': int``
- - ``'deltaLast_USED_ACCEPTABLE_SHIPPING_gte': int``
- - ``'deltaLast_USED_GOOD_SHIPPING_lte': int``
- - ``'deltaLast_USED_GOOD_SHIPPING_gte': int``
- - ``'deltaLast_USED_NEW_SHIPPING_lte': int``
- - ``'deltaLast_USED_NEW_SHIPPING_gte': int``
- - ``'deltaLast_USED_VERY_GOOD_SHIPPING_lte': int``
- - ``'deltaLast_USED_VERY_GOOD_SHIPPING_gte': int``
- - ``'deltaLast_WAREHOUSE_lte': int``
- - ``'deltaLast_WAREHOUSE_gte': int``
- - ``'deltaPercent1_AMAZON_lte': int``
- - ``'deltaPercent1_AMAZON_gte': int``
- - ``'deltaPercent1_BUY_BOX_SHIPPING_lte': int``
- - ``'deltaPercent1_BUY_BOX_SHIPPING_gte': int``
- - ``'deltaPercent1_COLLECTIBLE_lte': int``
- - ``'deltaPercent1_COLLECTIBLE_gte': int``
- - ``'deltaPercent1_COUNT_COLLECTIBLE_lte': int``
- - ``'deltaPercent1_COUNT_COLLECTIBLE_gte': int``
- - ``'deltaPercent1_COUNT_NEW_lte': int``
- - ``'deltaPercent1_COUNT_NEW_gte': int``
- - ``'deltaPercent1_COUNT_REFURBISHED_lte': int``
- - ``'deltaPercent1_COUNT_REFURBISHED_gte': int``
- - ``'deltaPercent1_COUNT_REVIEWS_lte': int``
- - ``'deltaPercent1_COUNT_REVIEWS_gte': int``
- - ``'deltaPercent1_COUNT_USED_lte': int``
- - ``'deltaPercent1_COUNT_USED_gte': int``
- - ``'deltaPercent1_EBAY_NEW_SHIPPING_lte': int``
- - ``'deltaPercent1_EBAY_NEW_SHIPPING_gte': int``
- - ``'deltaPercent1_EBAY_USED_SHIPPING_lte': int``
- - ``'deltaPercent1_EBAY_USED_SHIPPING_gte': int``
- - ``'deltaPercent1_LIGHTNING_DEAL_lte': int``
- - ``'deltaPercent1_LIGHTNING_DEAL_gte': int``
- - ``'deltaPercent1_LISTPRICE_lte': int``
- - ``'deltaPercent1_LISTPRICE_gte': int``
- - ``'deltaPercent1_NEW_lte': int``
- - ``'deltaPercent1_NEW_gte': int``
- - ``'deltaPercent1_NEW_FBA_lte': int``
- - ``'deltaPercent1_NEW_FBA_gte': int``
- - ``'deltaPercent1_NEW_FBM_SHIPPING_lte': int``
- - ``'deltaPercent1_NEW_FBM_SHIPPING_gte': int``
- - ``'deltaPercent1_RATING_lte': int``
- - ``'deltaPercent1_RATING_gte': int``
- - ``'deltaPercent1_REFURBISHED_lte': int``
- - ``'deltaPercent1_REFURBISHED_gte': int``
- - ``'deltaPercent1_REFURBISHED_SHIPPING_lte': int``
- - ``'deltaPercent1_REFURBISHED_SHIPPING_gte': int``
- - ``'deltaPercent1_RENT_lte': int``
- - ``'deltaPercent1_RENT_gte': int``
- - ``'deltaPercent1_SALES_lte': int``
- - ``'deltaPercent1_SALES_gte': int``
- - ``'deltaPercent1_TRADE_IN_lte': int``
- - ``'deltaPercent1_TRADE_IN_gte': int``
- - ``'deltaPercent1_USED_lte': int``
- - ``'deltaPercent1_USED_gte': int``
- - ``'deltaPercent1_USED_ACCEPTABLE_SHIPPING_lte': int``
- - ``'deltaPercent1_USED_ACCEPTABLE_SHIPPING_gte': int``
- - ``'deltaPercent1_USED_GOOD_SHIPPING_lte': int``
- - ``'deltaPercent1_USED_GOOD_SHIPPING_gte': int``
- - ``'deltaPercent1_USED_NEW_SHIPPING_lte': int``
- - ``'deltaPercent1_USED_NEW_SHIPPING_gte': int``
- - ``'deltaPercent1_USED_VERY_GOOD_SHIPPING_lte': int``
- - ``'deltaPercent1_USED_VERY_GOOD_SHIPPING_gte': int``
- - ``'deltaPercent1_WAREHOUSE_lte': int``
- - ``'deltaPercent1_WAREHOUSE_gte': int``
- - ``'deltaPercent30_AMAZON_lte': int``
- - ``'deltaPercent30_AMAZON_gte': int``
- - ``'deltaPercent30_BUY_BOX_SHIPPING_lte': int``
- - ``'deltaPercent30_BUY_BOX_SHIPPING_gte': int``
- - ``'deltaPercent30_COLLECTIBLE_lte': int``
- - ``'deltaPercent30_COLLECTIBLE_gte': int``
- - ``'deltaPercent30_COUNT_COLLECTIBLE_lte': int``
- - ``'deltaPercent30_COUNT_COLLECTIBLE_gte': int``
- - ``'deltaPercent30_COUNT_NEW_lte': int``
- - ``'deltaPercent30_COUNT_NEW_gte': int``
- - ``'deltaPercent30_COUNT_REFURBISHED_lte': int``
- - ``'deltaPercent30_COUNT_REFURBISHED_gte': int``
- - ``'deltaPercent30_COUNT_REVIEWS_lte': int``
- - ``'deltaPercent30_COUNT_REVIEWS_gte': int``
- - ``'deltaPercent30_COUNT_USED_lte': int``
- - ``'deltaPercent30_COUNT_USED_gte': int``
- - ``'deltaPercent30_EBAY_NEW_SHIPPING_lte': int``
- - ``'deltaPercent30_EBAY_NEW_SHIPPING_gte': int``
- - ``'deltaPercent30_EBAY_USED_SHIPPING_lte': int``
- - ``'deltaPercent30_EBAY_USED_SHIPPING_gte': int``
- - ``'deltaPercent30_LIGHTNING_DEAL_lte': int``
- - ``'deltaPercent30_LIGHTNING_DEAL_gte': int``
- - ``'deltaPercent30_LISTPRICE_lte': int``
- - ``'deltaPercent30_LISTPRICE_gte': int``
- - ``'deltaPercent30_NEW_lte': int``
- - ``'deltaPercent30_NEW_gte': int``
- - ``'deltaPercent30_NEW_FBA_lte': int``
- - ``'deltaPercent30_NEW_FBA_gte': int``
- - ``'deltaPercent30_NEW_FBM_SHIPPING_lte': int``
- - ``'deltaPercent30_NEW_FBM_SHIPPING_gte': int``
- - ``'deltaPercent30_RATING_lte': int``
- - ``'deltaPercent30_RATING_gte': int``
- - ``'deltaPercent30_REFURBISHED_lte': int``
- - ``'deltaPercent30_REFURBISHED_gte': int``
- - ``'deltaPercent30_REFURBISHED_SHIPPING_lte': int``
- - ``'deltaPercent30_REFURBISHED_SHIPPING_gte': int``
- - ``'deltaPercent30_RENT_lte': int``
- - ``'deltaPercent30_RENT_gte': int``
- - ``'deltaPercent30_SALES_lte': int``
- - ``'deltaPercent30_SALES_gte': int``
- - ``'deltaPercent30_TRADE_IN_lte': int``
- - ``'deltaPercent30_TRADE_IN_gte': int``
- - ``'deltaPercent30_USED_lte': int``
- - ``'deltaPercent30_USED_gte': int``
- - ``'deltaPercent30_USED_ACCEPTABLE_SHIPPING_lte': int``
- - ``'deltaPercent30_USED_ACCEPTABLE_SHIPPING_gte': int``
- - ``'deltaPercent30_USED_GOOD_SHIPPING_lte': int``
- - ``'deltaPercent30_USED_GOOD_SHIPPING_gte': int``
- - ``'deltaPercent30_USED_NEW_SHIPPING_lte': int``
- - ``'deltaPercent30_USED_NEW_SHIPPING_gte': int``
- - ``'deltaPercent30_USED_VERY_GOOD_SHIPPING_lte': int``
- - ``'deltaPercent30_USED_VERY_GOOD_SHIPPING_gte': int``
- - ``'deltaPercent30_WAREHOUSE_lte': int``
- - ``'deltaPercent30_WAREHOUSE_gte': int``
- - ``'deltaPercent7_AMAZON_lte': int``
- - ``'deltaPercent7_AMAZON_gte': int``
- - ``'deltaPercent7_BUY_BOX_SHIPPING_lte': int``
- - ``'deltaPercent7_BUY_BOX_SHIPPING_gte': int``
- - ``'deltaPercent7_COLLECTIBLE_lte': int``
- - ``'deltaPercent7_COLLECTIBLE_gte': int``
- - ``'deltaPercent7_COUNT_COLLECTIBLE_lte': int``
- - ``'deltaPercent7_COUNT_COLLECTIBLE_gte': int``
- - ``'deltaPercent7_COUNT_NEW_lte': int``
- - ``'deltaPercent7_COUNT_NEW_gte': int``
- - ``'deltaPercent7_COUNT_REFURBISHED_lte': int``
- - ``'deltaPercent7_COUNT_REFURBISHED_gte': int``
- - ``'deltaPercent7_COUNT_REVIEWS_lte': int``
- - ``'deltaPercent7_COUNT_REVIEWS_gte': int``
- - ``'deltaPercent7_COUNT_USED_lte': int``
- - ``'deltaPercent7_COUNT_USED_gte': int``
- - ``'deltaPercent7_EBAY_NEW_SHIPPING_lte': int``
- - ``'deltaPercent7_EBAY_NEW_SHIPPING_gte': int``
- - ``'deltaPercent7_EBAY_USED_SHIPPING_lte': int``
- - ``'deltaPercent7_EBAY_USED_SHIPPING_gte': int``
- - ``'deltaPercent7_LIGHTNING_DEAL_lte': int``
- - ``'deltaPercent7_LIGHTNING_DEAL_gte': int``
- - ``'deltaPercent7_LISTPRICE_lte': int``
- - ``'deltaPercent7_LISTPRICE_gte': int``
- - ``'deltaPercent7_NEW_lte': int``
- - ``'deltaPercent7_NEW_gte': int``
- - ``'deltaPercent7_NEW_FBA_lte': int``
- - ``'deltaPercent7_NEW_FBA_gte': int``
- - ``'deltaPercent7_NEW_FBM_SHIPPING_lte': int``
- - ``'deltaPercent7_NEW_FBM_SHIPPING_gte': int``
- - ``'deltaPercent7_RATING_lte': int``
- - ``'deltaPercent7_RATING_gte': int``
- - ``'deltaPercent7_REFURBISHED_lte': int``
- - ``'deltaPercent7_REFURBISHED_gte': int``
- - ``'deltaPercent7_REFURBISHED_SHIPPING_lte': int``
- - ``'deltaPercent7_REFURBISHED_SHIPPING_gte': int``
- - ``'deltaPercent7_RENT_lte': int``
- - ``'deltaPercent7_RENT_gte': int``
- - ``'deltaPercent7_SALES_lte': int``
- - ``'deltaPercent7_SALES_gte': int``
- - ``'deltaPercent7_TRADE_IN_lte': int``
- - ``'deltaPercent7_TRADE_IN_gte': int``
- - ``'deltaPercent7_USED_lte': int``
- - ``'deltaPercent7_USED_gte': int``
- - ``'deltaPercent7_USED_ACCEPTABLE_SHIPPING_lte': int``
- - ``'deltaPercent7_USED_ACCEPTABLE_SHIPPING_gte': int``
- - ``'deltaPercent7_USED_GOOD_SHIPPING_lte': int``
- - ``'deltaPercent7_USED_GOOD_SHIPPING_gte': int``
- - ``'deltaPercent7_USED_NEW_SHIPPING_lte': int``
- - ``'deltaPercent7_USED_NEW_SHIPPING_gte': int``
- - ``'deltaPercent7_USED_VERY_GOOD_SHIPPING_lte': int``
- - ``'deltaPercent7_USED_VERY_GOOD_SHIPPING_gte': int``
- - ``'deltaPercent7_WAREHOUSE_lte': int``
- - ``'deltaPercent7_WAREHOUSE_gte': int``
- - ``'deltaPercent90_AMAZON_lte': int``
- - ``'deltaPercent90_AMAZON_gte': int``
- - ``'deltaPercent90_BUY_BOX_SHIPPING_lte': int``
- - ``'deltaPercent90_BUY_BOX_SHIPPING_gte': int``
- - ``'deltaPercent90_COLLECTIBLE_lte': int``
- - ``'deltaPercent90_COLLECTIBLE_gte': int``
- - ``'deltaPercent90_COUNT_COLLECTIBLE_lte': int``
- - ``'deltaPercent90_COUNT_COLLECTIBLE_gte': int``
- - ``'deltaPercent90_COUNT_NEW_lte': int``
- - ``'deltaPercent90_COUNT_NEW_gte': int``
- - ``'deltaPercent90_COUNT_REFURBISHED_lte': int``
- - ``'deltaPercent90_COUNT_REFURBISHED_gte': int``
- - ``'deltaPercent90_COUNT_REVIEWS_lte': int``
- - ``'deltaPercent90_COUNT_REVIEWS_gte': int``
- - ``'deltaPercent90_COUNT_USED_lte': int``
- - ``'deltaPercent90_COUNT_USED_gte': int``
- - ``'deltaPercent90_EBAY_NEW_SHIPPING_lte': int``
- - ``'deltaPercent90_EBAY_NEW_SHIPPING_gte': int``
- - ``'deltaPercent90_EBAY_USED_SHIPPING_lte': int``
- - ``'deltaPercent90_EBAY_USED_SHIPPING_gte': int``
- - ``'deltaPercent90_LIGHTNING_DEAL_lte': int``
- - ``'deltaPercent90_LIGHTNING_DEAL_gte': int``
- - ``'deltaPercent90_LISTPRICE_lte': int``
- - ``'deltaPercent90_LISTPRICE_gte': int``
- - ``'deltaPercent90_NEW_lte': int``
- - ``'deltaPercent90_NEW_gte': int``
- - ``'deltaPercent90_NEW_FBA_lte': int``
- - ``'deltaPercent90_NEW_FBA_gte': int``
- - ``'deltaPercent90_NEW_FBM_SHIPPING_lte': int``
- - ``'deltaPercent90_NEW_FBM_SHIPPING_gte': int``
- - ``'deltaPercent90_RATING_lte': int``
- - ``'deltaPercent90_RATING_gte': int``
- - ``'deltaPercent90_REFURBISHED_lte': int``
- - ``'deltaPercent90_REFURBISHED_gte': int``
- - ``'deltaPercent90_REFURBISHED_SHIPPING_lte': int``
- - ``'deltaPercent90_REFURBISHED_SHIPPING_gte': int``
- - ``'deltaPercent90_RENT_lte': int``
- - ``'deltaPercent90_RENT_gte': int``
- - ``'deltaPercent90_SALES_lte': int``
- - ``'deltaPercent90_SALES_gte': int``
- - ``'deltaPercent90_TRADE_IN_lte': int``
- - ``'deltaPercent90_TRADE_IN_gte': int``
- - ``'deltaPercent90_USED_lte': int``
- - ``'deltaPercent90_USED_gte': int``
- - ``'deltaPercent90_USED_ACCEPTABLE_SHIPPING_lte': int``
- - ``'deltaPercent90_USED_ACCEPTABLE_SHIPPING_gte': int``
- - ``'deltaPercent90_USED_GOOD_SHIPPING_lte': int``
- - ``'deltaPercent90_USED_GOOD_SHIPPING_gte': int``
- - ``'deltaPercent90_USED_NEW_SHIPPING_lte': int``
- - ``'deltaPercent90_USED_NEW_SHIPPING_gte': int``
- - ``'deltaPercent90_USED_VERY_GOOD_SHIPPING_lte': int``
- - ``'deltaPercent90_USED_VERY_GOOD_SHIPPING_gte': int``
- - ``'deltaPercent90_WAREHOUSE_lte': int``
- - ``'deltaPercent90_WAREHOUSE_gte': int``
- - ``'department': str``
- - ``'edition': str``
- - ``'fbaFees_lte': int``
- - ``'fbaFees_gte': int``
- - ``'format': str``
- - ``'genre': str``
- - ``'hasParentASIN': bool``
- - ``'hasReviews': bool``
- - ``'hazardousMaterialType_lte': int``
- - ``'hazardousMaterialType_gte': int``
- - ``'isAdultProduct': bool``
- - ``'isEligibleForSuperSaverShipping': bool``
- - ``'isEligibleForTradeIn': bool``
- - ``'isHighestOffer': bool``
- - ``'isHighest_AMAZON': bool``
- - ``'isHighest_BUY_BOX_SHIPPING': bool``
- - ``'isHighest_COLLECTIBLE': bool``
- - ``'isHighest_COUNT_COLLECTIBLE': bool``
- - ``'isHighest_COUNT_NEW': bool``
- - ``'isHighest_COUNT_REFURBISHED': bool``
- - ``'isHighest_COUNT_REVIEWS': bool``
- - ``'isHighest_COUNT_USED': bool``
- - ``'isHighest_EBAY_NEW_SHIPPING': bool``
- - ``'isHighest_EBAY_USED_SHIPPING': bool``
- - ``'isHighest_LIGHTNING_DEAL': bool``
- - ``'isHighest_LISTPRICE': bool``
- - ``'isHighest_NEW': bool``
- - ``'isHighest_NEW_FBA': bool``
- - ``'isHighest_NEW_FBM_SHIPPING': bool``
- - ``'isHighest_RATING': bool``
- - ``'isHighest_REFURBISHED': bool``
- - ``'isHighest_REFURBISHED_SHIPPING': bool``
- - ``'isHighest_RENT': bool``
- - ``'isHighest_SALES': bool``
- - ``'isHighest_TRADE_IN': bool``
- - ``'isHighest_USED': bool``
- - ``'isHighest_USED_ACCEPTABLE_SHIPPING': bool``
- - ``'isHighest_USED_GOOD_SHIPPING': bool``
- - ``'isHighest_USED_NEW_SHIPPING': bool``
- - ``'isHighest_USED_VERY_GOOD_SHIPPING': bool``
- - ``'isHighest_WAREHOUSE': bool``
- - ``'isLowestOffer': bool``
- - ``'isLowest_AMAZON': bool``
- - ``'isLowest_BUY_BOX_SHIPPING': bool``
- - ``'isLowest_COLLECTIBLE': bool``
- - ``'isLowest_COUNT_COLLECTIBLE': bool``
- - ``'isLowest_COUNT_NEW': bool``
- - ``'isLowest_COUNT_REFURBISHED': bool``
- - ``'isLowest_COUNT_REVIEWS': bool``
- - ``'isLowest_COUNT_USED': bool``
- - ``'isLowest_EBAY_NEW_SHIPPING': bool``
- - ``'isLowest_EBAY_USED_SHIPPING': bool``
- - ``'isLowest_LIGHTNING_DEAL': bool``
- - ``'isLowest_LISTPRICE': bool``
- - ``'isLowest_NEW': bool``
- - ``'isLowest_NEW_FBA': bool``
- - ``'isLowest_NEW_FBM_SHIPPING': bool``
- - ``'isLowest_RATING': bool``
- - ``'isLowest_REFURBISHED': bool``
- - ``'isLowest_REFURBISHED_SHIPPING': bool``
- - ``'isLowest_RENT': bool``
- - ``'isLowest_SALES': bool``
- - ``'isLowest_TRADE_IN': bool``
- - ``'isLowest_USED': bool``
- - ``'isLowest_USED_ACCEPTABLE_SHIPPING': bool``
- - ``'isLowest_USED_GOOD_SHIPPING': bool``
- - ``'isLowest_USED_NEW_SHIPPING': bool``
- - ``'isLowest_USED_VERY_GOOD_SHIPPING': bool``
- - ``'isLowest_WAREHOUSE': bool``
- - ``'isPrimeExclusive': bool``
- - ``'isSNS': bool``
- - ``'label': str``
- - ``'languages': str``
- - ``'lastOffersUpdate_lte': int``
- - ``'lastOffersUpdate_gte': int``
- - ``'lastPriceChange_lte': int``
- - ``'lastPriceChange_gte': int``
- - ``'lastRatingUpdate_lte': int``
- - ``'lastRatingUpdate_gte': int``
- - ``'lastUpdate_lte': int``
- - ``'lastUpdate_gte': int``
- - ``'lightningEnd_lte': int``
- - ``'lightningEnd_gte': int``
- - ``'lightningStart_lte': int``
- - ``'lightningStart_gte': int``
- - ``'listedSince_lte': int``
- - ``'listedSince_gte': int``
- - ``'manufacturer': str``
- - ``'model': str``
- - ``'newPriceIsMAP': bool``
- - ``'nextUpdate_lte': int``
- - ``'nextUpdate_gte': int``
- - ``'numberOfItems_lte': int``
- - ``'numberOfItems_gte': int``
- - ``'numberOfPages_lte': int``
- - ``'numberOfPages_gte': int``
- - ``'numberOfTrackings_lte': int``
- - ``'numberOfTrackings_gte': int``
- - ``'offerCountFBA_lte': int``
- - ``'offerCountFBA_gte': int``
- - ``'offerCountFBM_lte': int``
- - ``'offerCountFBM_gte': int``
- - ``'outOfStockPercentageInInterval_lte': int``
- - ``'outOfStockPercentageInInterval_gte': int``
- - ``'packageDimension_lte': int``
- - ``'packageDimension_gte': int``
- - ``'packageHeight_lte': int``
- - ``'packageHeight_gte': int``
- - ``'packageLength_lte': int``
- - ``'packageLength_gte': int``
- - ``'packageQuantity_lte': int``
- - ``'packageQuantity_gte': int``
- - ``'packageWeight_lte': int``
- - ``'packageWeight_gte': int``
- - ``'packageWidth_lte': int``
- - ``'packageWidth_gte': int``
- - ``'partNumber': str``
- - ``'platform': str``
- - ``'productGroup': str``
- - ``'productType': int``
- - ``'promotions': int``
- - ``'publicationDate_lte': int``
- - ``'publicationDate_gte': int``
- - ``'publisher': str``
- - ``'releaseDate_lte': int``
- - ``'releaseDate_gte': int``
- - ``'rootCategory': int``
- - ``'sellerIds': str``
- - ``'sellerIdsLowestFBA': str``
- - ``'sellerIdsLowestFBM': str``
- - ``'size': str``
- - ``'salesRankDrops180_lte': int``
- - ``'salesRankDrops180_gte': int``
- - ``'salesRankDrops90_lte': int``
- - ``'salesRankDrops90_gte': int``
- - ``'salesRankDrops30_lte': int``
- - ``'salesRankDrops30_gte': int``
- - ``'sort': list``
- - ``'stockAmazon_lte': int``
- - ``'stockAmazon_gte': int``
- - ``'stockBuyBox_lte': int``
- - ``'stockBuyBox_gte': int``
- - ``'studio': str``
- - ``'title': str``
- - ``'title_flag': str``
- - ``'trackingSince_lte': int``
- - ``'trackingSince_gte': int``
- - ``'type': str``
- - ``'mpn': str``
- - ``'outOfStockPercentage90_lte': int``
- - ``'outOfStockPercentage90_gte': int``
- - ``'categories_include': int``
- - ``'categories_exclude': int``
-
- domain : str, default: 'US'
- One of the following Amazon domains: RESERVED, US, GB, DE,
- FR, JP, CA, CN, IT, ES, IN, MX.
-
- wait : bool, default: True
- Wait available token before doing effective query.
-
- Returns
- -------
- list
- List of ASINs matching the product parameters.
-
- 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"]``
-
- Examples
- --------
- Query for all of Jim Butcher's books using the synchronous
- ``keepa.Keepa`` class. Sort by current sales
-
- >>> import keepa
- >>> api = keepa.Keepa('')
- >>> product_parms = {
- ... 'author': 'jim butcher',
- ... 'sort': ``["current_SALES", "asc"]``,
- }
- >>> asins = api.product_finder(product_parms)
- >>> asins
- ['B000HRMAR2',
- '0578799790',
- 'B07PW1SVHM',
- ...
- 'B003MXM744',
- '0133235750',
- 'B01MXXLJPZ']
-
- Query for all of Jim Butcher's books using the asynchronous
- ``keepa.AsyncKeepa`` class.
-
- >>> import asyncio
- >>> import keepa
- >>> product_parms = {'author': 'jim butcher'}
- >>> async def main():
- ... key = ''
- ... api = await keepa.AsyncKeepa().create(key)
- ... return await api.product_finder(product_parms)
- >>> asins = asyncio.run(main())
- >>> asins
- ['B000HRMAR2',
- '0578799790',
- 'B07PW1SVHM',
- ...
- 'B003MXM744',
- '0133235750',
- 'B01MXXLJPZ']
-
- """
- # verify valid keys
- for key in product_parms:
- if key not in PRODUCT_REQUEST_KEYS:
- raise ValueError(f'Invalid key "{key}"')
-
- # verify json type
- key_type = PRODUCT_REQUEST_KEYS[key]
- product_parms[key] = key_type(product_parms[key])
-
- payload = {
- "key": self.accesskey,
- "domain": DCODES.index(domain),
- "selection": json.dumps(product_parms),
- }
-
- response = self._request("query", payload, wait=wait)
- return response["asinList"]
-
- def deals(self, deal_parms, domain="US", wait=True) -> dict:
- """Query the Keepa API for product deals.
-
- You can find products that recently changed and match your
- search criteria. A single request will return a maximum of
- 150 deals. Try out the deals page to first get accustomed to
- the options:
- https://keepa.com/#!deals
-
- For more details please visit:
- https://keepa.com/#!discuss/t/browsing-deals/338
-
- Parameters
- ----------
- deal_parms : dict
- Dictionary containing one or more of the following keys:
-
- - ``"page"``: int
- - ``"domainId"``: int
- - ``"excludeCategories"``: list
- - ``"includeCategories"``: list
- - ``"priceTypes"``: list
- - ``"deltaRange"``: list
- - ``"deltaPercentRange"``: list
- - ``"deltaLastRange"``: list
- - ``"salesRankRange"``: list
- - ``"currentRange"``: list
- - ``"minRating"``: int
- - ``"isLowest"``: bool
- - ``"isLowestOffer"``: bool
- - ``"isOutOfStock"``: bool
- - ``"titleSearch"``: String
- - ``"isRangeEnabled"``: bool
- - ``"isFilterEnabled"``: bool
- - ``"hasReviews"``: bool
- - ``"filterErotic"``: bool
- - ``"sortType"``: int
- - ``"dateRange"``: int
-
- domain : str, optional
- One of the following Amazon domains: RESERVED, US, GB, DE,
- FR, JP, CA, CN, IT, ES, IN, MX Defaults to US.
-
- wait : bool, optional
- Wait available token before doing effective query, Defaults to ``True``.
-
- Returns
- -------
- dict
- Dictionary containing the deals including the following keys:
-
- * ``'dr'`` - Ordered array of all deal objects matching your query.
- * ``'categoryIds'`` - Contains all root categoryIds of the matched
- deal products.
- * ``'categoryNames'`` - Contains all root category names of the
- matched deal products.
- * ``'categoryCount'`` - Contains how many deal products in the
- respective root category are found.
-
- Examples
- --------
- Return deals from category 16310101 using the synchronous
- ``keepa.Keepa`` class
-
- >>> import keepa
- >>> key = ''
- >>> api = keepa.Keepa(key)
- >>> deal_parms = {"page": 0,
- ... "domainId": 1,
- ... "excludeCategories": [1064954, 11091801],
- ... "includeCategories": [16310101]}
- >>> deals = api.deals(deal_parms)
-
- Get the title of the first deal.
-
- >>> deals['dr'][0]['title']
- 'Orange Cream Rooibos, Tea Bags - Vanilla, Orange | Caffeine-Free,
- Antioxidant-rich, Hot & Iced | The Spice Hut, First Sip Of Tea'
-
- Conduct the same query with the asynchronous ``keepa.AsyncKeepa``
- class.
-
- >>> import asyncio
- >>> import keepa
- >>> deal_parms = {"page": 0,
- ... "domainId": 1,
- ... "excludeCategories": [1064954, 11091801],
- ... "includeCategories": [16310101]}
- >>> async def main():
- ... key = ''
- ... api = await keepa.AsyncKeepa().create(key)
- ... categories = await api.search_for_categories("movies")
- ... return await api.deals(deal_parms)
- >>> asins = asyncio.run(main())
- >>> asins
- ['B0BF3P5XZS',
- 'B08JQN5VDT',
- 'B09SP8JPPK',
- '0999296345',
- 'B07HPG684T',
- '1984825577',
- ...
-
- """
- # verify valid keys
- for key in deal_parms:
- if key not in DEAL_REQUEST_KEYS:
- raise ValueError('Invalid key "{key}"')
-
- # verify json type
- key_type = DEAL_REQUEST_KEYS[key]
- deal_parms[key] = key_type(deal_parms[key])
-
- deal_parms.setdefault("priceTypes", 0)
-
- payload = {
- "key": self.accesskey,
- "domain": DCODES.index(domain),
- "selection": json.dumps(deal_parms),
- }
-
- return self._request("deal", payload, wait=wait)["deals"]
-
- def _request(self, request_type, payload, wait=True, raw_response=False):
- """Query keepa api server.
-
- Parses raw response from keepa into a json format. Handles
- errors and waits for available tokens if allowed.
- """
- if wait:
- self.wait_for_tokens()
-
- while True:
- raw = requests.get(
- f"https://api.keepa.com/{request_type}/?",
- payload,
- timeout=self._timeout,
- )
- status_code = str(raw.status_code)
- if status_code != "200":
- if status_code in SCODES:
- if status_code == "429" and wait:
- print("Response from server: %s" % SCODES[status_code])
- self.wait_for_tokens()
- continue
- else:
- raise RuntimeError(SCODES[status_code])
- else:
- raise RuntimeError(f"REQUEST_FAILED: {status_code}")
- break
-
- response = raw.json()
-
- if "tokensConsumed" in response:
- log.debug("%d tokens consumed", response["tokensConsumed"])
-
- if "error" in response:
- if response["error"]:
- raise Exception(response["error"]["message"])
-
- # always update tokens
- self.tokens_left = response["tokensLeft"]
-
- if raw_response:
- return raw
- return response
-
-
-class AsyncKeepa:
- r"""Class to support an asynchronous Python interface to keepa server.
-
- Initializes API with access key. Access key can be obtained by
- signing up for a reoccurring or one time plan at:
- https://keepa.com/#!api
-
- Parameters
- ----------
- accesskey : str
- 64 character access key string.
-
- timeout : float, optional
- Default timeout when issuing any request. This is not a time
- limit on the entire response download; rather, an exception is
- raised if the server has not issued a response for timeout
- seconds. Setting this to 0 disables the timeout, but will
- cause any request to hang indefiantly should keepa.com be down
-
- Examples
- --------
- Query for all of Jim Butcher's books using the asynchronous
- ``keepa.AsyncKeepa`` class.
-
- >>> import asyncio
- >>> import keepa
- >>> product_parms = {'author': 'jim butcher'}
- >>> async def main():
- ... key = ''
- ... api = await keepa.AsyncKeepa().create(key)
- ... return await api.product_finder(product_parms)
- >>> asins = asyncio.run(main())
- >>> asins
- ['B000HRMAR2',
- '0578799790',
- 'B07PW1SVHM',
- ...
- 'B003MXM744',
- '0133235750',
- 'B01MXXLJPZ']
-
- Query for product with ASIN ``'B0088PUEPK'`` using the asynchronous
- keepa interface.
-
- >>> import asyncio
- >>> import keepa
- >>> async def main():
- ... key = ''
- ... api = await keepa.AsyncKeepa().create(key)
- ... return await api.query('B0088PUEPK')
- >>> response = asyncio.run(main())
- >>> response[0]['title']
- 'Western Digital 1TB WD Blue PC Internal Hard Drive HDD - 7200 RPM,
- SATA 6 Gb/s, 64 MB Cache, 3.5" - WD10EZEX'
-
- """
-
- @classmethod
- async def create(cls, accesskey, timeout=10):
- """Create the async object."""
- self = AsyncKeepa()
- self.accesskey = accesskey
- self.status = None
- self.tokens_left = 0
- self._timeout = timeout
-
- # Store user's available tokens
- log.info("Connecting to keepa using key ending in %s", accesskey[-6:])
- await self.update_status()
- log.info("%d tokens remain", self.tokens_left)
- return self
-
- @property
- def time_to_refill(self):
- """Return the time to refill in seconds."""
- # Get current timestamp in milliseconds from UNIX epoch
- now = int(time.time() * 1000)
- timeatrefile = self.status["timestamp"] + self.status["refillIn"]
-
- # wait plus one second fudge factor
- timetorefil = timeatrefile - now + 1000
- if timetorefil < 0:
- timetorefil = 0
-
- # Account for negative tokens left
- if self.tokens_left < 0:
- timetorefil += (abs(self.tokens_left) / self.status["refillRate"]) * 60000
-
- # Return value in seconds
- return timetorefil / 1000.0
-
- async def update_status(self):
- """Update available tokens."""
- self.status = await self._request("token", {"key": self.accesskey}, wait=False)
-
- async def wait_for_tokens(self):
- """Check if there are any remaining tokens and waits if none are available."""
- await self.update_status()
-
- # Wait if no tokens available
- if self.tokens_left <= 0:
- tdelay = self.time_to_refill
- log.warning("Waiting %.0f seconds for additional tokens" % tdelay)
- await asyncio.sleep(tdelay)
- await self.update_status()
-
- @is_documented_by(Keepa.query)
- async def query(
- self,
- items,
- stats=None,
- domain="US",
- history=True,
- offers=None,
- update=None,
- to_datetime=True,
- rating=False,
- out_of_stock_as_nan=True,
- stock=False,
- product_code_is_asin=True,
- progress_bar=True,
- buybox=False,
- wait=True,
- days=None,
- only_live_offers=None,
- raw=False,
- ):
- """Documented in Keepa.query."""
- if raw:
- raise ValueError("Raw response is only available in the non-async class")
-
- # Format items into numpy array
- try:
- items = format_items(items)
- except BaseException:
- raise Exception("Invalid product codes input")
- assert len(items), "No valid product codes"
-
- nitems = len(items)
- if nitems == 1:
- log.debug("Executing single product query")
- else:
- log.debug("Executing %d item product query", nitems)
-
- # check offer input
- if offers:
- if not isinstance(offers, int):
- raise TypeError('Parameter "offers" must be an interger')
-
- if offers > 100 or offers < 20:
- raise ValueError('Parameter "offers" must be between 20 and 100')
-
- # Report time to completion
- tcomplete = (
- float(nitems - self.tokens_left) / self.status["refillRate"]
- - (60000 - self.status["refillIn"]) / 60000.0
- )
- if tcomplete < 0.0:
- tcomplete = 0.5
- log.debug(
- "Estimated time to complete %d request(s) is %.2f minutes",
- nitems,
- tcomplete,
- )
- log.debug(
- "\twith a refill rate of %d token(s) per minute", self.status["refillRate"]
- )
-
- # product list
- products = []
-
- pbar = None
- if progress_bar:
- pbar = tqdm(total=nitems)
-
- # Number of requests is dependent on the number of items and
- # request limit. Use available tokens first
- idx = 0 # or number complete
- while idx < nitems:
- nrequest = nitems - idx
-
- # cap request
- if nrequest > REQUEST_LIMIT:
- nrequest = REQUEST_LIMIT
-
- # request from keepa and increment current position
- item_request = items[idx : idx + nrequest] # noqa: E203
- response = await self._product_query(
- item_request,
- product_code_is_asin,
- stats=stats,
- domain=domain,
- stock=stock,
- offers=offers,
- update=update,
- history=history,
- rating=rating,
- to_datetime=to_datetime,
- out_of_stock_as_nan=out_of_stock_as_nan,
- buybox=buybox,
- wait=wait,
- days=days,
- only_live_offers=only_live_offers,
- )
- idx += nrequest
- products.extend(response["products"])
-
- if pbar is not None:
- pbar.update(nrequest)
-
- return products
-
- @is_documented_by(Keepa._product_query)
- async def _product_query(self, items, product_code_is_asin=True, **kwargs):
- """Documented in Keepa._product_query."""
- # ASINs convert to comma joined string
- assert len(items) <= 100
-
- if product_code_is_asin:
- kwargs["asin"] = ",".join(items)
- else:
- kwargs["code"] = ",".join(items)
-
- kwargs["key"] = self.accesskey
- kwargs["domain"] = DCODES.index(kwargs["domain"])
-
- # Convert bool values to 0 and 1.
- kwargs["stock"] = int(kwargs["stock"])
- kwargs["history"] = int(kwargs["history"])
- kwargs["rating"] = int(kwargs["rating"])
- kwargs["buybox"] = int(kwargs["buybox"])
-
- if kwargs["update"] is None:
- del kwargs["update"]
- else:
- kwargs["update"] = int(kwargs["update"])
-
- if kwargs["offers"] is None:
- del kwargs["offers"]
- else:
- kwargs["offers"] = int(kwargs["offers"])
-
- if kwargs["only_live_offers"] is None:
- del kwargs["only_live_offers"]
- else:
- kwargs["only-live-offers"] = int(kwargs.pop("only_live_offers"))
- # Keepa's param actually doesn't use snake_case.
- # I believe using snake case throughout the Keepa interface is better.
-
- if kwargs["days"] is None:
- del kwargs["days"]
- else:
- assert kwargs["days"] > 0
-
- if kwargs["stats"] is None:
- del kwargs["stats"]
-
- out_of_stock_as_nan = kwargs.pop("out_of_stock_as_nan", True)
- to_datetime = kwargs.pop("to_datetime", True)
-
- # Query and replace csv with parsed data if history enabled
- wait = kwargs.get("wait")
- kwargs.pop("wait", None)
- response = await self._request("product", kwargs, wait=wait)
- if kwargs["history"]:
- for product in response["products"]:
- if product["csv"]: # if data exists
- product["data"] = parse_csv(
- product["csv"], to_datetime, out_of_stock_as_nan
- )
-
- if kwargs.get("stats", None):
- for product in response["products"]:
- stats = product.get("stats", None)
- if stats:
- product["stats_parsed"] = _parse_stats(stats, to_datetime)
-
- return response
-
- @is_documented_by(Keepa.best_sellers_query)
- async def best_sellers_query(
- self, category, rank_avg_range=0, domain="US", wait=True
- ):
- """Documented by Keepa.best_sellers_query."""
- assert domain in DCODES, "Invalid domain code"
-
- payload = {
- "key": self.accesskey,
- "domain": DCODES.index(domain),
- "category": category,
- "range": rank_avg_range,
- }
-
- response = await self._request("bestsellers", payload, wait=wait)
- if "bestSellersList" in response:
- return response["bestSellersList"]["asinList"]
- else: # pragma: no cover
- log.info("Best sellers search results not yet available")
-
- @is_documented_by(Keepa.search_for_categories)
- async def search_for_categories(self, searchterm, domain="US", wait=True):
- """Documented by Keepa.search_for_categories."""
- assert domain in DCODES, "Invalid domain code"
-
- payload = {
- "key": self.accesskey,
- "domain": DCODES.index(domain),
- "type": "category",
- "term": searchterm,
- }
-
- response = await self._request("search", payload, wait=wait)
- if response["categories"] == {}: # pragma no cover
- raise Exception(
- "Categories search results not yet available "
- + "or no search terms found."
- )
- else:
- return response["categories"]
-
- @is_documented_by(Keepa.category_lookup)
- async def category_lookup(
- self, category_id, domain="US", include_parents=0, wait=True
- ):
- """Documented by Keepa.category_lookup."""
- assert domain in DCODES, "Invalid domain code"
-
- payload = {
- "key": self.accesskey,
- "domain": DCODES.index(domain),
- "category": category_id,
- "parents": include_parents,
- }
-
- response = await self._request("category", payload, wait=wait)
- if response["categories"] == {}: # pragma no cover
- raise Exception(
- "Category lookup results not yet available or no" + "match found."
- )
- else:
- return response["categories"]
-
- @is_documented_by(Keepa.seller_query)
- async def seller_query(
- self,
- seller_id,
- domain="US",
- to_datetime=True,
- storefront=False,
- update=None,
- wait=True,
- ):
- """Documented by Keepa.sellerer_query."""
- if isinstance(seller_id, list):
- if len(seller_id) > 100:
- err_str = "seller_id can contain at maximum 100 sellers"
- raise RuntimeError(err_str)
- seller = ",".join(seller_id)
- else:
- seller = seller_id
-
- payload = {
- "key": self.accesskey,
- "domain": DCODES.index(domain),
- "seller": seller,
- }
-
- if storefront:
- payload["storefront"] = int(storefront)
- if update:
- payload["update"] = update
-
- response = await self._request("seller", payload, wait=wait)
- return _parse_seller(response["sellers"], to_datetime)
-
- @is_documented_by(Keepa.product_finder)
- async def product_finder(self, product_parms, domain="US", wait=True):
- """Documented by Keepa.product_finder."""
- # verify valid keys
- for key in product_parms:
- if key not in PRODUCT_REQUEST_KEYS:
- raise RuntimeError('Invalid key "%s"' % key)
-
- # verify json type
- key_type = PRODUCT_REQUEST_KEYS[key]
- product_parms[key] = key_type(product_parms[key])
-
- payload = {
- "key": self.accesskey,
- "domain": DCODES.index(domain),
- "selection": json.dumps(product_parms),
- }
-
- response = await self._request("query", payload, wait=wait)
- return response["asinList"]
-
- @is_documented_by(Keepa.deals)
- async def deals(self, deal_parms, domain="US", wait=True):
- """Documented in Keepa.deals."""
- # verify valid keys
- for key in deal_parms:
- if key not in DEAL_REQUEST_KEYS:
- raise ValueError('Invalid key "{key}"')
-
- # verify json type
- key_type = DEAL_REQUEST_KEYS[key]
- deal_parms[key] = key_type(deal_parms[key])
-
- deal_parms.setdefault("priceTypes", 0)
-
- payload = {
- "key": self.accesskey,
- "domain": DCODES.index(domain),
- "selection": json.dumps(deal_parms),
- }
-
- deals = await self._request("deal", payload, wait=wait)
- return deals["deals"]
-
- async def _request(self, request_type, payload, wait=True):
- """Documented in Keepa._request."""
- while True:
- async with aiohttp.ClientSession() as session:
- async with session.get(
- f"https://api.keepa.com/{request_type}/?",
- params=payload,
- timeout=self._timeout,
- ) as raw:
- status_code = str(raw.status)
- if status_code != "200":
- if status_code in SCODES:
- if status_code == "429" and wait:
- await self.wait_for_tokens()
- continue
- else:
- raise Exception(SCODES[status_code])
- else:
- raise Exception("REQUEST_FAILED")
-
- response = await raw.json()
-
- if "error" in response:
- if response["error"]:
- raise Exception(response["error"]["message"])
-
- # always update tokens
- self.tokens_left = response["tokensLeft"]
- return response
- break
-
-
-def convert_offer_history(csv, to_datetime=True):
- """Convert an offer history to human readable values.
-
- Parameters
- ----------
- csv : list
- Offer list csv obtained from ``['offerCSV']``
-
- to_datetime : bool, optional
- Modifies ``numpy`` minutes to ``datetime.datetime`` values.
- Default ``True``.
-
- Returns
- -------
- times : numpy.ndarray
- List of time values for an offer history.
-
- prices : numpy.ndarray
- Price (including shipping) of an offer for each time at an
- index of times.
-
- """
- # convert these values to numpy arrays
- times = csv[::3]
- values = np.array(csv[1::3])
- values += np.array(csv[2::3]) # add in shipping
-
- # convert to dollars and datetimes
- times = keepa_minutes_to_time(times, to_datetime)
- prices = values / 100.0
- return times, prices
-
-
-def keepa_minutes_to_time(minutes, to_datetime=True):
- """Accept an array or list of minutes and converts it to a numpy datetime array.
-
- Assumes that keepa time is from keepa minutes from ordinal.
- """
- # Convert to timedelta64 and shift
- dt = np.array(minutes, dtype="timedelta64[m]")
- dt = dt + KEEPA_ST_ORDINAL # shift from ordinal
-
- # Convert to datetime if requested
- if to_datetime:
- return dt.astype(datetime.datetime)
- return dt
-
-
-def run_and_get(coro):
- """Attempt to run an async request."""
- try:
- loop = asyncio.get_event_loop()
- except RuntimeError:
- loop = asyncio.new_event_loop()
- task = loop.create_task(coro)
- loop.run_until_complete(task)
- return task.result()
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..c1d5d21
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,67 @@
+[build-system]
+build-backend = "flit_core.buildapi"
+requires = ["flit_core >=3,<4"]
+
+[mypy]
+plugins = "pydantic.mypy"
+
+[project]
+authors = [
+ {name = "Alex Kaszynski", email = "akascap@gmail.com"}
+]
+classifiers = [
+ "Development Status :: 5 - Production/Stable",
+ "Intended Audience :: End Users/Desktop",
+ "Topic :: Database :: Front-Ends",
+ "License :: OSI Approved :: Apache Software License",
+ "Programming Language :: Python :: 3.10",
+ "Programming Language :: Python :: 3.11",
+ "Programming Language :: Python :: 3.12",
+ "Programming Language :: Python :: 3.13",
+ "Programming Language :: Python :: 3.14"
+]
+dependencies = [
+ "numpy >=1.9.3",
+ "requests >=2.2",
+ "tqdm",
+ "aiohttp",
+ "pandas <= 3.0",
+ "pydantic"
+]
+description = "Interfaces with keepa.com's API."
+keywords = ["keepa"]
+name = "keepa"
+readme = "README.rst"
+requires-python = ">=3.10"
+version = "1.5.dev0"
+
+[project.optional-dependencies]
+doc = [
+ "sphinx==7.3.7",
+ "pydata-sphinx-theme==0.15.4",
+ "numpydoc==1.7.0"
+]
+test = [
+ "matplotlib",
+ "pandas",
+ "pytest-asyncio",
+ "pytest-cov",
+ "pytest",
+ "pytest-rerunfailures"
+]
+
+[project.urls]
+Documentation = "https://keepaapi.readthedocs.io/en/latest/"
+Source = "https://github.com/akaszynski/keepa"
+
+[tool.pytest.ini_options]
+addopts = "--cov=keepa --cov-fail-under=85"
+asyncio_default_fixture_loop_scope = "function"
+testpaths = 'tests'
+
+[tool.ruff]
+line-length = 100
+
+[tool.ruff.lint]
+ignore = []
+select = ["E", "F", "W", "I001"] # pyflakes, pycodestyle, isort
diff --git a/pytest.ini b/pytest.ini
deleted file mode 100644
index 7f864b6..0000000
--- a/pytest.ini
+++ /dev/null
@@ -1,7 +0,0 @@
-[pytest]
-junit_family=legacy
-filterwarnings =
- # bogus numpy ABI warning (see numpy/#432)
- ignore:.*numpy.dtype size changed.*:RuntimeWarning
- ignore:.*numpy.ufunc size changed.*:RuntimeWarning
-addopts = --cov=keepa --cov-report html --cov-fail-under=85
diff --git a/requirements_docs.txt b/requirements_docs.txt
index e09e003..b7f2c16 100644
--- a/requirements_docs.txt
+++ b/requirements_docs.txt
@@ -1,2 +1,2 @@
-sphinx==6.1.2
-pydata-sphinx-theme==0.12.0
+pydata-sphinx-theme==0.15.4
+sphinx==7.3.7
diff --git a/requirements_test.txt b/requirements_test.txt
deleted file mode 100644
index 7b77702..0000000
--- a/requirements_test.txt
+++ /dev/null
@@ -1,5 +0,0 @@
-matplotlib==3.7.1
-pandas
-pytest-asyncio==0.21.0
-pytest-cov==4.0.0
-pytest==7.3.1
diff --git a/setup.py b/setup.py
deleted file mode 100644
index 4e67663..0000000
--- a/setup.py
+++ /dev/null
@@ -1,42 +0,0 @@
-"""Setup for keepaapi."""
-from io import open as io_open
-import os
-
-from setuptools import setup
-
-package_name = "keepa"
-
-# Get version from ./_version.py
-__version__ = None
-version_file = os.path.join(os.path.dirname(__file__), package_name, "_version.py")
-
-with io_open(version_file, mode="r") as fd:
- exec(fd.read())
-
-filepath = os.path.dirname(__file__)
-readme_file = os.path.join(filepath, "README.rst")
-
-setup(
- name=package_name,
- packages=[package_name],
- version=__version__,
- description="Interfaces with keepa.com",
- long_description=open(readme_file).read(),
- author="Alex Kaszynski",
- author_email="akascap@gmail.com",
- license="Apache Software License",
- classifiers=[
- "Development Status :: 5 - Production/Stable",
- "Intended Audience :: End Users/Desktop",
- "Topic :: Database :: Front-Ends",
- "License :: OSI Approved :: Apache Software License",
- "Programming Language :: Python :: 3.7",
- "Programming Language :: Python :: 3.8",
- "Programming Language :: Python :: 3.9",
- "Programming Language :: Python :: 3.10",
- "Programming Language :: Python :: 3.11",
- ],
- url="https://github.com/akaszynski/keepa",
- keywords="keepa",
- install_requires=["numpy>=1.9.3", "requests>=2.2", "tqdm", "aiohttp", "pandas"],
-)
diff --git a/src/keepa/__init__.py b/src/keepa/__init__.py
new file mode 100644
index 0000000..29b79cd
--- /dev/null
+++ b/src/keepa/__init__.py
@@ -0,0 +1,46 @@
+"""Keepa module."""
+
+from importlib.metadata import PackageNotFoundError, version
+
+# single source versioning from the installed package (stored in pyproject.toml)
+try:
+ __version__ = version("keepa")
+except PackageNotFoundError:
+ __version__ = "unknown"
+
+from keepa.data_models import ProductParams
+from keepa.interface import (
+ DCODES,
+ KEEPA_ST_ORDINAL,
+ SCODES,
+ AsyncKeepa,
+ Domain,
+ Keepa,
+ convert_offer_history,
+ csv_indices,
+ format_items,
+ keepa_minutes_to_time,
+ parse_csv,
+ process_used_buybox,
+ run_and_get,
+)
+from keepa.plotting import plot_product
+
+__all__ = [
+ "AsyncKeepa",
+ "DCODES",
+ "Domain",
+ "KEEPA_ST_ORDINAL",
+ "Keepa",
+ "ProductParams",
+ "SCODES",
+ "__version__",
+ "convert_offer_history",
+ "csv_indices",
+ "format_items",
+ "keepa_minutes_to_time",
+ "parse_csv",
+ "plot_product",
+ "process_used_buybox",
+ "run_and_get",
+]
diff --git a/src/keepa/data_models.py b/src/keepa/data_models.py
new file mode 100644
index 0000000..6166b84
--- /dev/null
+++ b/src/keepa/data_models.py
@@ -0,0 +1,1132 @@
+"""Contains the data models for keepa requests."""
+
+from typing import Optional, Union
+
+from pydantic import BaseModel
+
+
+class ProductParams(BaseModel):
+ """Product request parameters.
+
+ See:
+ https://github.com/keepacom/api_backend/blob/6f2048e1b8551875324445113e30041bbe37a147/src/main/java/com/keepa/api/backend/structs/ProductFinderRequest.java
+
+ Examples
+ --------
+ Use attributes:
+
+ >>> import keepa
+ >>> product_params = keepa.ProductParams()
+ >>> product_params.author = "J. R. R. Tolkien"
+
+ Use keywords:
+
+ >>> product_params = keepa.ProductParams(author="J. R. R. Tolkien")
+
+ Use within :func:`keepa.Keepa.product_finder`:
+
+ >>> import keepa
+ >>> api = keepa.Keepa("")
+ >>> product_params = keepa.ProductParams(author="J. R. R. Tolkien")
+ >>> asins = api.product_finder(product_parms, n_products=100)
+
+ """
+
+ author: Optional[Union[list[str], str]] = None
+ availabilityAmazon: Optional[int] = None
+ avg180_AMAZON_lte: Optional[int] = None
+ avg180_AMAZON_gte: Optional[int] = None
+ avg180_BUY_BOX_SHIPPING_lte: Optional[int] = None
+ avg180_BUY_BOX_SHIPPING_gte: Optional[int] = None
+ avg180_BUY_BOX_USED_SHIPPING_lte: Optional[int] = None
+ avg180_BUY_BOX_USED_SHIPPING_gte: Optional[int] = None
+ avg180_COLLECTIBLE_lte: Optional[int] = None
+ avg180_COLLECTIBLE_gte: Optional[int] = None
+ avg180_COUNT_COLLECTIBLE_lte: Optional[int] = None
+ avg180_COUNT_COLLECTIBLE_gte: Optional[int] = None
+ avg180_COUNT_NEW_lte: Optional[int] = None
+ avg180_COUNT_NEW_gte: Optional[int] = None
+ avg180_COUNT_REFURBISHED_lte: Optional[int] = None
+ avg180_COUNT_REFURBISHED_gte: Optional[int] = None
+ avg180_COUNT_REVIEWS_lte: Optional[int] = None
+ avg180_COUNT_REVIEWS_gte: Optional[int] = None
+ avg180_COUNT_USED_lte: Optional[int] = None
+ avg180_COUNT_USED_gte: Optional[int] = None
+ avg180_EBAY_NEW_SHIPPING_lte: Optional[int] = None
+ avg180_EBAY_NEW_SHIPPING_gte: Optional[int] = None
+ avg180_EBAY_USED_SHIPPING_lte: Optional[int] = None
+ avg180_EBAY_USED_SHIPPING_gte: Optional[int] = None
+ avg180_LIGHTNING_DEAL_lte: Optional[int] = None
+ avg180_LIGHTNING_DEAL_gte: Optional[int] = None
+ avg180_LISTPRICE_lte: Optional[int] = None
+ avg180_LISTPRICE_gte: Optional[int] = None
+ avg180_NEW_lte: Optional[int] = None
+ avg180_NEW_gte: Optional[int] = None
+ avg180_NEW_FBA_lte: Optional[int] = None
+ avg180_NEW_FBA_gte: Optional[int] = None
+ avg180_NEW_FBM_SHIPPING_lte: Optional[int] = None
+ avg180_NEW_FBM_SHIPPING_gte: Optional[int] = None
+ avg180_PRIME_EXCL_lte: Optional[int] = None
+ avg180_PRIME_EXCL_gte: Optional[int] = None
+ avg180_RATING_lte: Optional[int] = None
+ avg180_RATING_gte: Optional[int] = None
+ avg180_REFURBISHED_lte: Optional[int] = None
+ avg180_REFURBISHED_gte: Optional[int] = None
+ avg180_REFURBISHED_SHIPPING_lte: Optional[int] = None
+ avg180_REFURBISHED_SHIPPING_gte: Optional[int] = None
+ avg180_RENT_lte: Optional[int] = None
+ avg180_RENT_gte: Optional[int] = None
+ avg180_SALES_lte: Optional[int] = None
+ avg180_SALES_gte: Optional[int] = None
+ avg180_TRADE_IN_lte: Optional[int] = None
+ avg180_TRADE_IN_gte: Optional[int] = None
+ avg180_USED_lte: Optional[int] = None
+ avg180_USED_gte: Optional[int] = None
+ avg180_USED_ACCEPTABLE_SHIPPING_lte: Optional[int] = None
+ avg180_USED_ACCEPTABLE_SHIPPING_gte: Optional[int] = None
+ avg180_USED_GOOD_SHIPPING_lte: Optional[int] = None
+ avg180_USED_GOOD_SHIPPING_gte: Optional[int] = None
+ avg180_USED_NEW_SHIPPING_lte: Optional[int] = None
+ avg180_USED_NEW_SHIPPING_gte: Optional[int] = None
+ avg180_USED_VERY_GOOD_SHIPPING_lte: Optional[int] = None
+ avg180_USED_VERY_GOOD_SHIPPING_gte: Optional[int] = None
+ avg180_WAREHOUSE_lte: Optional[int] = None
+ avg180_WAREHOUSE_gte: Optional[int] = None
+ avg1_AMAZON_lte: Optional[int] = None
+ avg1_AMAZON_gte: Optional[int] = None
+ avg1_BUY_BOX_SHIPPING_lte: Optional[int] = None
+ avg1_BUY_BOX_SHIPPING_gte: Optional[int] = None
+ avg1_BUY_BOX_USED_SHIPPING_lte: Optional[int] = None
+ avg1_BUY_BOX_USED_SHIPPING_gte: Optional[int] = None
+ avg1_COLLECTIBLE_lte: Optional[int] = None
+ avg1_COLLECTIBLE_gte: Optional[int] = None
+ avg1_COUNT_COLLECTIBLE_lte: Optional[int] = None
+ avg1_COUNT_COLLECTIBLE_gte: Optional[int] = None
+ avg1_COUNT_NEW_lte: Optional[int] = None
+ avg1_COUNT_NEW_gte: Optional[int] = None
+ avg1_COUNT_REFURBISHED_lte: Optional[int] = None
+ avg1_COUNT_REFURBISHED_gte: Optional[int] = None
+ avg1_COUNT_REVIEWS_lte: Optional[int] = None
+ avg1_COUNT_REVIEWS_gte: Optional[int] = None
+ avg1_COUNT_USED_lte: Optional[int] = None
+ avg1_COUNT_USED_gte: Optional[int] = None
+ avg1_EBAY_NEW_SHIPPING_lte: Optional[int] = None
+ avg1_EBAY_NEW_SHIPPING_gte: Optional[int] = None
+ avg1_EBAY_USED_SHIPPING_lte: Optional[int] = None
+ avg1_EBAY_USED_SHIPPING_gte: Optional[int] = None
+ avg1_LIGHTNING_DEAL_lte: Optional[int] = None
+ avg1_LIGHTNING_DEAL_gte: Optional[int] = None
+ avg1_LISTPRICE_lte: Optional[int] = None
+ avg1_LISTPRICE_gte: Optional[int] = None
+ avg1_NEW_lte: Optional[int] = None
+ avg1_NEW_gte: Optional[int] = None
+ avg1_NEW_FBA_lte: Optional[int] = None
+ avg1_NEW_FBA_gte: Optional[int] = None
+ avg1_NEW_FBM_SHIPPING_lte: Optional[int] = None
+ avg1_NEW_FBM_SHIPPING_gte: Optional[int] = None
+ avg1_PRIME_EXCL_lte: Optional[int] = None
+ avg1_PRIME_EXCL_gte: Optional[int] = None
+ avg1_RATING_lte: Optional[int] = None
+ avg1_RATING_gte: Optional[int] = None
+ avg1_REFURBISHED_lte: Optional[int] = None
+ avg1_REFURBISHED_gte: Optional[int] = None
+ avg1_REFURBISHED_SHIPPING_lte: Optional[int] = None
+ avg1_REFURBISHED_SHIPPING_gte: Optional[int] = None
+ avg1_RENT_lte: Optional[int] = None
+ avg1_RENT_gte: Optional[int] = None
+ avg1_SALES_lte: Optional[int] = None
+ avg1_SALES_gte: Optional[int] = None
+ avg1_TRADE_IN_lte: Optional[int] = None
+ avg1_TRADE_IN_gte: Optional[int] = None
+ avg1_USED_lte: Optional[int] = None
+ avg1_USED_gte: Optional[int] = None
+ avg1_USED_ACCEPTABLE_SHIPPING_lte: Optional[int] = None
+ avg1_USED_ACCEPTABLE_SHIPPING_gte: Optional[int] = None
+ avg1_USED_GOOD_SHIPPING_lte: Optional[int] = None
+ avg1_USED_GOOD_SHIPPING_gte: Optional[int] = None
+ avg1_USED_NEW_SHIPPING_lte: Optional[int] = None
+ avg1_USED_NEW_SHIPPING_gte: Optional[int] = None
+ avg1_USED_VERY_GOOD_SHIPPING_lte: Optional[int] = None
+ avg1_USED_VERY_GOOD_SHIPPING_gte: Optional[int] = None
+ avg1_WAREHOUSE_lte: Optional[int] = None
+ avg1_WAREHOUSE_gte: Optional[int] = None
+ avg30_AMAZON_lte: Optional[int] = None
+ avg30_AMAZON_gte: Optional[int] = None
+ avg30_BUY_BOX_SHIPPING_lte: Optional[int] = None
+ avg30_BUY_BOX_SHIPPING_gte: Optional[int] = None
+ avg30_BUY_BOX_USED_SHIPPING_lte: Optional[int] = None
+ avg30_BUY_BOX_USED_SHIPPING_gte: Optional[int] = None
+ avg30_COLLECTIBLE_lte: Optional[int] = None
+ avg30_COLLECTIBLE_gte: Optional[int] = None
+ avg30_COUNT_COLLECTIBLE_lte: Optional[int] = None
+ avg30_COUNT_COLLECTIBLE_gte: Optional[int] = None
+ avg30_COUNT_NEW_lte: Optional[int] = None
+ avg30_COUNT_NEW_gte: Optional[int] = None
+ avg30_COUNT_REFURBISHED_lte: Optional[int] = None
+ avg30_COUNT_REFURBISHED_gte: Optional[int] = None
+ avg30_COUNT_REVIEWS_lte: Optional[int] = None
+ avg30_COUNT_REVIEWS_gte: Optional[int] = None
+ avg30_COUNT_USED_lte: Optional[int] = None
+ avg30_COUNT_USED_gte: Optional[int] = None
+ avg30_EBAY_NEW_SHIPPING_lte: Optional[int] = None
+ avg30_EBAY_NEW_SHIPPING_gte: Optional[int] = None
+ avg30_EBAY_USED_SHIPPING_lte: Optional[int] = None
+ avg30_EBAY_USED_SHIPPING_gte: Optional[int] = None
+ avg30_LIGHTNING_DEAL_lte: Optional[int] = None
+ avg30_LIGHTNING_DEAL_gte: Optional[int] = None
+ avg30_LISTPRICE_lte: Optional[int] = None
+ avg30_LISTPRICE_gte: Optional[int] = None
+ avg30_NEW_lte: Optional[int] = None
+ avg30_NEW_gte: Optional[int] = None
+ avg30_NEW_FBA_lte: Optional[int] = None
+ avg30_NEW_FBA_gte: Optional[int] = None
+ avg30_NEW_FBM_SHIPPING_lte: Optional[int] = None
+ avg30_NEW_FBM_SHIPPING_gte: Optional[int] = None
+ avg30_PRIME_EXCL_lte: Optional[int] = None
+ avg30_PRIME_EXCL_gte: Optional[int] = None
+ avg30_RATING_lte: Optional[int] = None
+ avg30_RATING_gte: Optional[int] = None
+ avg30_REFURBISHED_lte: Optional[int] = None
+ avg30_REFURBISHED_gte: Optional[int] = None
+ avg30_REFURBISHED_SHIPPING_lte: Optional[int] = None
+ avg30_REFURBISHED_SHIPPING_gte: Optional[int] = None
+ avg30_RENT_lte: Optional[int] = None
+ avg30_RENT_gte: Optional[int] = None
+ avg30_SALES_lte: Optional[int] = None
+ avg30_SALES_gte: Optional[int] = None
+ avg30_TRADE_IN_lte: Optional[int] = None
+ avg30_TRADE_IN_gte: Optional[int] = None
+ avg30_USED_lte: Optional[int] = None
+ avg30_USED_gte: Optional[int] = None
+ avg30_USED_ACCEPTABLE_SHIPPING_lte: Optional[int] = None
+ avg30_USED_ACCEPTABLE_SHIPPING_gte: Optional[int] = None
+ avg30_USED_GOOD_SHIPPING_lte: Optional[int] = None
+ avg30_USED_GOOD_SHIPPING_gte: Optional[int] = None
+ avg30_USED_NEW_SHIPPING_lte: Optional[int] = None
+ avg30_USED_NEW_SHIPPING_gte: Optional[int] = None
+ avg30_USED_VERY_GOOD_SHIPPING_lte: Optional[int] = None
+ avg30_USED_VERY_GOOD_SHIPPING_gte: Optional[int] = None
+ avg30_WAREHOUSE_lte: Optional[int] = None
+ avg30_WAREHOUSE_gte: Optional[int] = None
+ avg7_AMAZON_lte: Optional[int] = None
+ avg7_AMAZON_gte: Optional[int] = None
+ avg7_BUY_BOX_SHIPPING_lte: Optional[int] = None
+ avg7_BUY_BOX_SHIPPING_gte: Optional[int] = None
+ avg7_BUY_BOX_USED_SHIPPING_lte: Optional[int] = None
+ avg7_BUY_BOX_USED_SHIPPING_gte: Optional[int] = None
+ avg7_COLLECTIBLE_lte: Optional[int] = None
+ avg7_COLLECTIBLE_gte: Optional[int] = None
+ avg7_COUNT_COLLECTIBLE_lte: Optional[int] = None
+ avg7_COUNT_COLLECTIBLE_gte: Optional[int] = None
+ avg7_COUNT_NEW_lte: Optional[int] = None
+ avg7_COUNT_NEW_gte: Optional[int] = None
+ avg7_COUNT_REFURBISHED_lte: Optional[int] = None
+ avg7_COUNT_REFURBISHED_gte: Optional[int] = None
+ avg7_COUNT_REVIEWS_lte: Optional[int] = None
+ avg7_COUNT_REVIEWS_gte: Optional[int] = None
+ avg7_COUNT_USED_lte: Optional[int] = None
+ avg7_COUNT_USED_gte: Optional[int] = None
+ avg7_EBAY_NEW_SHIPPING_lte: Optional[int] = None
+ avg7_EBAY_NEW_SHIPPING_gte: Optional[int] = None
+ avg7_EBAY_USED_SHIPPING_lte: Optional[int] = None
+ avg7_EBAY_USED_SHIPPING_gte: Optional[int] = None
+ avg7_LIGHTNING_DEAL_lte: Optional[int] = None
+ avg7_LIGHTNING_DEAL_gte: Optional[int] = None
+ avg7_LISTPRICE_lte: Optional[int] = None
+ avg7_LISTPRICE_gte: Optional[int] = None
+ avg7_NEW_lte: Optional[int] = None
+ avg7_NEW_gte: Optional[int] = None
+ avg7_NEW_FBA_lte: Optional[int] = None
+ avg7_NEW_FBA_gte: Optional[int] = None
+ avg7_NEW_FBM_SHIPPING_lte: Optional[int] = None
+ avg7_NEW_FBM_SHIPPING_gte: Optional[int] = None
+ avg7_PRIME_EXCL_lte: Optional[int] = None
+ avg7_PRIME_EXCL_gte: Optional[int] = None
+ avg7_RATING_lte: Optional[int] = None
+ avg7_RATING_gte: Optional[int] = None
+ avg7_REFURBISHED_lte: Optional[int] = None
+ avg7_REFURBISHED_gte: Optional[int] = None
+ avg7_REFURBISHED_SHIPPING_lte: Optional[int] = None
+ avg7_REFURBISHED_SHIPPING_gte: Optional[int] = None
+ avg7_RENT_lte: Optional[int] = None
+ avg7_RENT_gte: Optional[int] = None
+ avg7_SALES_lte: Optional[int] = None
+ avg7_SALES_gte: Optional[int] = None
+ avg7_TRADE_IN_lte: Optional[int] = None
+ avg7_TRADE_IN_gte: Optional[int] = None
+ avg7_USED_lte: Optional[int] = None
+ avg7_USED_gte: Optional[int] = None
+ avg7_USED_ACCEPTABLE_SHIPPING_lte: Optional[int] = None
+ avg7_USED_ACCEPTABLE_SHIPPING_gte: Optional[int] = None
+ avg7_USED_GOOD_SHIPPING_lte: Optional[int] = None
+ avg7_USED_GOOD_SHIPPING_gte: Optional[int] = None
+ avg7_USED_NEW_SHIPPING_lte: Optional[int] = None
+ avg7_USED_NEW_SHIPPING_gte: Optional[int] = None
+ avg7_USED_VERY_GOOD_SHIPPING_lte: Optional[int] = None
+ avg7_USED_VERY_GOOD_SHIPPING_gte: Optional[int] = None
+ avg7_WAREHOUSE_lte: Optional[int] = None
+ avg7_WAREHOUSE_gte: Optional[int] = None
+ avg90_AMAZON_lte: Optional[int] = None
+ avg90_AMAZON_gte: Optional[int] = None
+ avg90_BUY_BOX_SHIPPING_lte: Optional[int] = None
+ avg90_BUY_BOX_SHIPPING_gte: Optional[int] = None
+ avg90_BUY_BOX_USED_SHIPPING_lte: Optional[int] = None
+ avg90_BUY_BOX_USED_SHIPPING_gte: Optional[int] = None
+ avg90_COLLECTIBLE_lte: Optional[int] = None
+ avg90_COLLECTIBLE_gte: Optional[int] = None
+ avg90_COUNT_COLLECTIBLE_lte: Optional[int] = None
+ avg90_COUNT_COLLECTIBLE_gte: Optional[int] = None
+ avg90_COUNT_NEW_lte: Optional[int] = None
+ avg90_COUNT_NEW_gte: Optional[int] = None
+ avg90_COUNT_REFURBISHED_lte: Optional[int] = None
+ avg90_COUNT_REFURBISHED_gte: Optional[int] = None
+ avg90_COUNT_REVIEWS_lte: Optional[int] = None
+ avg90_COUNT_REVIEWS_gte: Optional[int] = None
+ avg90_COUNT_USED_lte: Optional[int] = None
+ avg90_COUNT_USED_gte: Optional[int] = None
+ avg90_EBAY_NEW_SHIPPING_lte: Optional[int] = None
+ avg90_EBAY_NEW_SHIPPING_gte: Optional[int] = None
+ avg90_EBAY_USED_SHIPPING_lte: Optional[int] = None
+ avg90_EBAY_USED_SHIPPING_gte: Optional[int] = None
+ avg90_LIGHTNING_DEAL_lte: Optional[int] = None
+ avg90_LIGHTNING_DEAL_gte: Optional[int] = None
+ avg90_LISTPRICE_lte: Optional[int] = None
+ avg90_LISTPRICE_gte: Optional[int] = None
+ avg90_NEW_lte: Optional[int] = None
+ avg90_NEW_gte: Optional[int] = None
+ avg90_NEW_FBA_lte: Optional[int] = None
+ avg90_NEW_FBA_gte: Optional[int] = None
+ avg90_NEW_FBM_SHIPPING_lte: Optional[int] = None
+ avg90_NEW_FBM_SHIPPING_gte: Optional[int] = None
+ avg90_PRIME_EXCL_lte: Optional[int] = None
+ avg90_PRIME_EXCL_gte: Optional[int] = None
+ avg90_RATING_lte: Optional[int] = None
+ avg90_RATING_gte: Optional[int] = None
+ avg90_REFURBISHED_lte: Optional[int] = None
+ avg90_REFURBISHED_gte: Optional[int] = None
+ avg90_REFURBISHED_SHIPPING_lte: Optional[int] = None
+ avg90_REFURBISHED_SHIPPING_gte: Optional[int] = None
+ avg90_RENT_lte: Optional[int] = None
+ avg90_RENT_gte: Optional[int] = None
+ avg90_SALES_lte: Optional[int] = None
+ avg90_SALES_gte: Optional[int] = None
+ avg90_TRADE_IN_lte: Optional[int] = None
+ avg90_TRADE_IN_gte: Optional[int] = None
+ avg90_USED_lte: Optional[int] = None
+ avg90_USED_gte: Optional[int] = None
+ avg90_USED_ACCEPTABLE_SHIPPING_lte: Optional[int] = None
+ avg90_USED_ACCEPTABLE_SHIPPING_gte: Optional[int] = None
+ avg90_USED_GOOD_SHIPPING_lte: Optional[int] = None
+ avg90_USED_GOOD_SHIPPING_gte: Optional[int] = None
+ avg90_USED_NEW_SHIPPING_lte: Optional[int] = None
+ avg90_USED_NEW_SHIPPING_gte: Optional[int] = None
+ avg90_USED_VERY_GOOD_SHIPPING_lte: Optional[int] = None
+ avg90_USED_VERY_GOOD_SHIPPING_gte: Optional[int] = None
+ avg90_WAREHOUSE_lte: Optional[int] = None
+ avg90_WAREHOUSE_gte: Optional[int] = None
+ backInStock_AMAZON: Optional[bool] = None
+ backInStock_BUY_BOX_SHIPPING: Optional[bool] = None
+ backInStock_BUY_BOX_USED_SHIPPING: Optional[bool] = None
+ backInStock_COLLECTIBLE: Optional[bool] = None
+ backInStock_COUNT_COLLECTIBLE: Optional[bool] = None
+ backInStock_COUNT_NEW: Optional[bool] = None
+ backInStock_COUNT_REFURBISHED: Optional[bool] = None
+ backInStock_COUNT_REVIEWS: Optional[bool] = None
+ backInStock_COUNT_USED: Optional[bool] = None
+ backInStock_EBAY_NEW_SHIPPING: Optional[bool] = None
+ backInStock_EBAY_USED_SHIPPING: Optional[bool] = None
+ backInStock_LIGHTNING_DEAL: Optional[bool] = None
+ backInStock_LISTPRICE: Optional[bool] = None
+ backInStock_NEW: Optional[bool] = None
+ backInStock_NEW_FBA: Optional[bool] = None
+ backInStock_NEW_FBM_SHIPPING: Optional[bool] = None
+ backInStock_PRIME_EXCL: Optional[bool] = None
+ backInStock_RATING: Optional[bool] = None
+ backInStock_REFURBISHED: Optional[bool] = None
+ backInStock_REFURBISHED_SHIPPING: Optional[bool] = None
+ backInStock_RENT: Optional[bool] = None
+ backInStock_SALES: Optional[bool] = None
+ backInStock_TRADE_IN: Optional[bool] = None
+ backInStock_USED: Optional[bool] = None
+ backInStock_USED_ACCEPTABLE_SHIPPING: Optional[bool] = None
+ backInStock_USED_GOOD_SHIPPING: Optional[bool] = None
+ backInStock_USED_NEW_SHIPPING: Optional[bool] = None
+ backInStock_USED_VERY_GOOD_SHIPPING: Optional[bool] = None
+ backInStock_WAREHOUSE: Optional[bool] = None
+ binding: Optional[Union[list[str], str]] = None
+ brand: Optional[Union[list[str], str]] = None
+ buyBoxIsAmazon: Optional[bool] = None
+ buyBoxIsFBA: Optional[bool] = None
+ buyBoxIsUnqualified: Optional[bool] = None
+ buyBoxSellerId: Optional[Union[list[str], str]] = None
+ buyBoxUsedCondition_lte: Optional[int] = None
+ buyBoxUsedCondition_gte: Optional[int] = None
+ buyBoxUsedIsFBA: Optional[bool] = None
+ buyBoxUsedSellerId: Optional[str] = None
+ categories_include: Optional[Union[list[int], int]] = None
+ categories_exclude: Optional[Union[list[int], int]] = None
+ color: Optional[Union[list[str], str]] = None
+ couponOneTimeAbsolute_lte: Optional[int] = None
+ couponOneTimeAbsolute_gte: Optional[int] = None
+ couponOneTimePercent_lte: Optional[int] = None
+ couponOneTimePercent_gte: Optional[int] = None
+ couponSNSPercent_lte: Optional[int] = None
+ couponSNSPercent_gte: Optional[int] = None
+ current_AMAZON_lte: Optional[int] = None
+ current_AMAZON_gte: Optional[int] = None
+ current_BUY_BOX_SHIPPING_lte: Optional[int] = None
+ current_BUY_BOX_SHIPPING_gte: Optional[int] = None
+ current_BUY_BOX_USED_SHIPPING_lte: Optional[int] = None
+ current_BUY_BOX_USED_SHIPPING_gte: Optional[int] = None
+ current_COLLECTIBLE_lte: Optional[int] = None
+ current_COLLECTIBLE_gte: Optional[int] = None
+ current_COUNT_COLLECTIBLE_lte: Optional[int] = None
+ current_COUNT_COLLECTIBLE_gte: Optional[int] = None
+ current_COUNT_NEW_lte: Optional[int] = None
+ current_COUNT_NEW_gte: Optional[int] = None
+ current_COUNT_REFURBISHED_lte: Optional[int] = None
+ current_COUNT_REFURBISHED_gte: Optional[int] = None
+ current_COUNT_REVIEWS_lte: Optional[int] = None
+ current_COUNT_REVIEWS_gte: Optional[int] = None
+ current_COUNT_USED_lte: Optional[int] = None
+ current_COUNT_USED_gte: Optional[int] = None
+ current_EBAY_NEW_SHIPPING_lte: Optional[int] = None
+ current_EBAY_NEW_SHIPPING_gte: Optional[int] = None
+ current_EBAY_USED_SHIPPING_lte: Optional[int] = None
+ current_EBAY_USED_SHIPPING_gte: Optional[int] = None
+ current_LIGHTNING_DEAL_lte: Optional[int] = None
+ current_LIGHTNING_DEAL_gte: Optional[int] = None
+ current_LISTPRICE_lte: Optional[int] = None
+ current_LISTPRICE_gte: Optional[int] = None
+ current_NEW_lte: Optional[int] = None
+ current_NEW_gte: Optional[int] = None
+ current_NEW_FBA_lte: Optional[int] = None
+ current_NEW_FBA_gte: Optional[int] = None
+ current_NEW_FBM_SHIPPING_lte: Optional[int] = None
+ current_NEW_FBM_SHIPPING_gte: Optional[int] = None
+ current_PRIME_EXCL_lte: Optional[int] = None
+ current_PRIME_EXCL_gte: Optional[int] = None
+ current_RATING_lte: Optional[int] = None
+ current_RATING_gte: Optional[int] = None
+ current_REFURBISHED_lte: Optional[int] = None
+ current_REFURBISHED_gte: Optional[int] = None
+ current_REFURBISHED_SHIPPING_lte: Optional[int] = None
+ current_REFURBISHED_SHIPPING_gte: Optional[int] = None
+ current_RENT_lte: Optional[int] = None
+ current_RENT_gte: Optional[int] = None
+ current_SALES_lte: Optional[int] = None
+ current_SALES_gte: Optional[int] = None
+ current_TRADE_IN_lte: Optional[int] = None
+ current_TRADE_IN_gte: Optional[int] = None
+ current_USED_lte: Optional[int] = None
+ current_USED_gte: Optional[int] = None
+ current_USED_ACCEPTABLE_SHIPPING_lte: Optional[int] = None
+ current_USED_ACCEPTABLE_SHIPPING_gte: Optional[int] = None
+ current_USED_GOOD_SHIPPING_lte: Optional[int] = None
+ current_USED_GOOD_SHIPPING_gte: Optional[int] = None
+ current_USED_NEW_SHIPPING_lte: Optional[int] = None
+ current_USED_NEW_SHIPPING_gte: Optional[int] = None
+ current_USED_VERY_GOOD_SHIPPING_lte: Optional[int] = None
+ current_USED_VERY_GOOD_SHIPPING_gte: Optional[int] = None
+ current_WAREHOUSE_lte: Optional[int] = None
+ current_WAREHOUSE_gte: Optional[int] = None
+ delta1_AMAZON_lte: Optional[int] = None
+ delta1_AMAZON_gte: Optional[int] = None
+ delta1_BUY_BOX_SHIPPING_lte: Optional[int] = None
+ delta1_BUY_BOX_SHIPPING_gte: Optional[int] = None
+ delta1_BUY_BOX_USED_SHIPPING_lte: Optional[int] = None
+ delta1_BUY_BOX_USED_SHIPPING_gte: Optional[int] = None
+ delta1_COLLECTIBLE_lte: Optional[int] = None
+ delta1_COLLECTIBLE_gte: Optional[int] = None
+ delta1_COUNT_COLLECTIBLE_lte: Optional[int] = None
+ delta1_COUNT_COLLECTIBLE_gte: Optional[int] = None
+ delta1_COUNT_NEW_lte: Optional[int] = None
+ delta1_COUNT_NEW_gte: Optional[int] = None
+ delta1_COUNT_REFURBISHED_lte: Optional[int] = None
+ delta1_COUNT_REFURBISHED_gte: Optional[int] = None
+ delta1_COUNT_REVIEWS_lte: Optional[int] = None
+ delta1_COUNT_REVIEWS_gte: Optional[int] = None
+ delta1_COUNT_USED_lte: Optional[int] = None
+ delta1_COUNT_USED_gte: Optional[int] = None
+ delta1_EBAY_NEW_SHIPPING_lte: Optional[int] = None
+ delta1_EBAY_NEW_SHIPPING_gte: Optional[int] = None
+ delta1_EBAY_USED_SHIPPING_lte: Optional[int] = None
+ delta1_EBAY_USED_SHIPPING_gte: Optional[int] = None
+ delta1_LIGHTNING_DEAL_lte: Optional[int] = None
+ delta1_LIGHTNING_DEAL_gte: Optional[int] = None
+ delta1_LISTPRICE_lte: Optional[int] = None
+ delta1_LISTPRICE_gte: Optional[int] = None
+ delta1_NEW_lte: Optional[int] = None
+ delta1_NEW_gte: Optional[int] = None
+ delta1_NEW_FBA_lte: Optional[int] = None
+ delta1_NEW_FBA_gte: Optional[int] = None
+ delta1_NEW_FBM_SHIPPING_lte: Optional[int] = None
+ delta1_NEW_FBM_SHIPPING_gte: Optional[int] = None
+ delta1_PRIME_EXCL_lte: Optional[int] = None
+ delta1_PRIME_EXCL_gte: Optional[int] = None
+ delta1_RATING_lte: Optional[int] = None
+ delta1_RATING_gte: Optional[int] = None
+ delta1_REFURBISHED_lte: Optional[int] = None
+ delta1_REFURBISHED_gte: Optional[int] = None
+ delta1_REFURBISHED_SHIPPING_lte: Optional[int] = None
+ delta1_REFURBISHED_SHIPPING_gte: Optional[int] = None
+ delta1_RENT_lte: Optional[int] = None
+ delta1_RENT_gte: Optional[int] = None
+ delta1_SALES_lte: Optional[int] = None
+ delta1_SALES_gte: Optional[int] = None
+ delta1_TRADE_IN_lte: Optional[int] = None
+ delta1_TRADE_IN_gte: Optional[int] = None
+ delta1_USED_lte: Optional[int] = None
+ delta1_USED_gte: Optional[int] = None
+ delta1_USED_ACCEPTABLE_SHIPPING_lte: Optional[int] = None
+ delta1_USED_ACCEPTABLE_SHIPPING_gte: Optional[int] = None
+ delta1_USED_GOOD_SHIPPING_lte: Optional[int] = None
+ delta1_USED_GOOD_SHIPPING_gte: Optional[int] = None
+ delta1_USED_NEW_SHIPPING_lte: Optional[int] = None
+ delta1_USED_NEW_SHIPPING_gte: Optional[int] = None
+ delta1_USED_VERY_GOOD_SHIPPING_lte: Optional[int] = None
+ delta1_USED_VERY_GOOD_SHIPPING_gte: Optional[int] = None
+ delta1_WAREHOUSE_lte: Optional[int] = None
+ delta1_WAREHOUSE_gte: Optional[int] = None
+ delta30_AMAZON_lte: Optional[int] = None
+ delta30_AMAZON_gte: Optional[int] = None
+ delta30_BUY_BOX_SHIPPING_lte: Optional[int] = None
+ delta30_BUY_BOX_SHIPPING_gte: Optional[int] = None
+ delta30_BUY_BOX_USED_SHIPPING_lte: Optional[int] = None
+ delta30_BUY_BOX_USED_SHIPPING_gte: Optional[int] = None
+ delta30_COLLECTIBLE_lte: Optional[int] = None
+ delta30_COLLECTIBLE_gte: Optional[int] = None
+ delta30_COUNT_COLLECTIBLE_lte: Optional[int] = None
+ delta30_COUNT_COLLECTIBLE_gte: Optional[int] = None
+ delta30_COUNT_NEW_lte: Optional[int] = None
+ delta30_COUNT_NEW_gte: Optional[int] = None
+ delta30_COUNT_REFURBISHED_lte: Optional[int] = None
+ delta30_COUNT_REFURBISHED_gte: Optional[int] = None
+ delta30_COUNT_REVIEWS_lte: Optional[int] = None
+ delta30_COUNT_REVIEWS_gte: Optional[int] = None
+ delta30_COUNT_USED_lte: Optional[int] = None
+ delta30_COUNT_USED_gte: Optional[int] = None
+ delta30_EBAY_NEW_SHIPPING_lte: Optional[int] = None
+ delta30_EBAY_NEW_SHIPPING_gte: Optional[int] = None
+ delta30_EBAY_USED_SHIPPING_lte: Optional[int] = None
+ delta30_EBAY_USED_SHIPPING_gte: Optional[int] = None
+ delta30_LIGHTNING_DEAL_lte: Optional[int] = None
+ delta30_LIGHTNING_DEAL_gte: Optional[int] = None
+ delta30_LISTPRICE_lte: Optional[int] = None
+ delta30_LISTPRICE_gte: Optional[int] = None
+ delta30_NEW_lte: Optional[int] = None
+ delta30_NEW_gte: Optional[int] = None
+ delta30_NEW_FBA_lte: Optional[int] = None
+ delta30_NEW_FBA_gte: Optional[int] = None
+ delta30_NEW_FBM_SHIPPING_lte: Optional[int] = None
+ delta30_NEW_FBM_SHIPPING_gte: Optional[int] = None
+ delta30_PRIME_EXCL_lte: Optional[int] = None
+ delta30_PRIME_EXCL_gte: Optional[int] = None
+ delta30_RATING_lte: Optional[int] = None
+ delta30_RATING_gte: Optional[int] = None
+ delta30_REFURBISHED_lte: Optional[int] = None
+ delta30_REFURBISHED_gte: Optional[int] = None
+ delta30_REFURBISHED_SHIPPING_lte: Optional[int] = None
+ delta30_REFURBISHED_SHIPPING_gte: Optional[int] = None
+ delta30_RENT_lte: Optional[int] = None
+ delta30_RENT_gte: Optional[int] = None
+ delta30_SALES_lte: Optional[int] = None
+ delta30_SALES_gte: Optional[int] = None
+ delta30_TRADE_IN_lte: Optional[int] = None
+ delta30_TRADE_IN_gte: Optional[int] = None
+ delta30_USED_lte: Optional[int] = None
+ delta30_USED_gte: Optional[int] = None
+ delta30_USED_ACCEPTABLE_SHIPPING_lte: Optional[int] = None
+ delta30_USED_ACCEPTABLE_SHIPPING_gte: Optional[int] = None
+ delta30_USED_GOOD_SHIPPING_lte: Optional[int] = None
+ delta30_USED_GOOD_SHIPPING_gte: Optional[int] = None
+ delta30_USED_NEW_SHIPPING_lte: Optional[int] = None
+ delta30_USED_NEW_SHIPPING_gte: Optional[int] = None
+ delta30_USED_VERY_GOOD_SHIPPING_lte: Optional[int] = None
+ delta30_USED_VERY_GOOD_SHIPPING_gte: Optional[int] = None
+ delta30_WAREHOUSE_lte: Optional[int] = None
+ delta30_WAREHOUSE_gte: Optional[int] = None
+ delta7_AMAZON_lte: Optional[int] = None
+ delta7_AMAZON_gte: Optional[int] = None
+ delta7_BUY_BOX_SHIPPING_lte: Optional[int] = None
+ delta7_BUY_BOX_SHIPPING_gte: Optional[int] = None
+ delta7_BUY_BOX_USED_SHIPPING_lte: Optional[int] = None
+ delta7_BUY_BOX_USED_SHIPPING_gte: Optional[int] = None
+ delta7_COLLECTIBLE_lte: Optional[int] = None
+ delta7_COLLECTIBLE_gte: Optional[int] = None
+ delta7_COUNT_COLLECTIBLE_lte: Optional[int] = None
+ delta7_COUNT_COLLECTIBLE_gte: Optional[int] = None
+ delta7_COUNT_NEW_lte: Optional[int] = None
+ delta7_COUNT_NEW_gte: Optional[int] = None
+ delta7_COUNT_REFURBISHED_lte: Optional[int] = None
+ delta7_COUNT_REFURBISHED_gte: Optional[int] = None
+ delta7_COUNT_REVIEWS_lte: Optional[int] = None
+ delta7_COUNT_REVIEWS_gte: Optional[int] = None
+ delta7_COUNT_USED_lte: Optional[int] = None
+ delta7_COUNT_USED_gte: Optional[int] = None
+ delta7_EBAY_NEW_SHIPPING_lte: Optional[int] = None
+ delta7_EBAY_NEW_SHIPPING_gte: Optional[int] = None
+ delta7_EBAY_USED_SHIPPING_lte: Optional[int] = None
+ delta7_EBAY_USED_SHIPPING_gte: Optional[int] = None
+ delta7_LIGHTNING_DEAL_lte: Optional[int] = None
+ delta7_LIGHTNING_DEAL_gte: Optional[int] = None
+ delta7_LISTPRICE_lte: Optional[int] = None
+ delta7_LISTPRICE_gte: Optional[int] = None
+ delta7_NEW_lte: Optional[int] = None
+ delta7_NEW_gte: Optional[int] = None
+ delta7_NEW_FBA_lte: Optional[int] = None
+ delta7_NEW_FBA_gte: Optional[int] = None
+ delta7_NEW_FBM_SHIPPING_lte: Optional[int] = None
+ delta7_NEW_FBM_SHIPPING_gte: Optional[int] = None
+ delta7_PRIME_EXCL_lte: Optional[int] = None
+ delta7_PRIME_EXCL_gte: Optional[int] = None
+ delta7_RATING_lte: Optional[int] = None
+ delta7_RATING_gte: Optional[int] = None
+ delta7_REFURBISHED_lte: Optional[int] = None
+ delta7_REFURBISHED_gte: Optional[int] = None
+ delta7_REFURBISHED_SHIPPING_lte: Optional[int] = None
+ delta7_REFURBISHED_SHIPPING_gte: Optional[int] = None
+ delta7_RENT_lte: Optional[int] = None
+ delta7_RENT_gte: Optional[int] = None
+ delta7_SALES_lte: Optional[int] = None
+ delta7_SALES_gte: Optional[int] = None
+ delta7_TRADE_IN_lte: Optional[int] = None
+ delta7_TRADE_IN_gte: Optional[int] = None
+ delta7_USED_lte: Optional[int] = None
+ delta7_USED_gte: Optional[int] = None
+ delta7_USED_ACCEPTABLE_SHIPPING_lte: Optional[int] = None
+ delta7_USED_ACCEPTABLE_SHIPPING_gte: Optional[int] = None
+ delta7_USED_GOOD_SHIPPING_lte: Optional[int] = None
+ delta7_USED_GOOD_SHIPPING_gte: Optional[int] = None
+ delta7_USED_NEW_SHIPPING_lte: Optional[int] = None
+ delta7_USED_NEW_SHIPPING_gte: Optional[int] = None
+ delta7_USED_VERY_GOOD_SHIPPING_lte: Optional[int] = None
+ delta7_USED_VERY_GOOD_SHIPPING_gte: Optional[int] = None
+ delta7_WAREHOUSE_lte: Optional[int] = None
+ delta7_WAREHOUSE_gte: Optional[int] = None
+ delta90_AMAZON_lte: Optional[int] = None
+ delta90_AMAZON_gte: Optional[int] = None
+ delta90_BUY_BOX_SHIPPING_lte: Optional[int] = None
+ delta90_BUY_BOX_SHIPPING_gte: Optional[int] = None
+ delta90_BUY_BOX_USED_SHIPPING_lte: Optional[int] = None
+ delta90_BUY_BOX_USED_SHIPPING_gte: Optional[int] = None
+ delta90_COLLECTIBLE_lte: Optional[int] = None
+ delta90_COLLECTIBLE_gte: Optional[int] = None
+ delta90_COUNT_COLLECTIBLE_lte: Optional[int] = None
+ delta90_COUNT_COLLECTIBLE_gte: Optional[int] = None
+ delta90_COUNT_NEW_lte: Optional[int] = None
+ delta90_COUNT_NEW_gte: Optional[int] = None
+ delta90_COUNT_REFURBISHED_lte: Optional[int] = None
+ delta90_COUNT_REFURBISHED_gte: Optional[int] = None
+ delta90_COUNT_REVIEWS_lte: Optional[int] = None
+ delta90_COUNT_REVIEWS_gte: Optional[int] = None
+ delta90_COUNT_USED_lte: Optional[int] = None
+ delta90_COUNT_USED_gte: Optional[int] = None
+ delta90_EBAY_NEW_SHIPPING_lte: Optional[int] = None
+ delta90_EBAY_NEW_SHIPPING_gte: Optional[int] = None
+ delta90_EBAY_USED_SHIPPING_lte: Optional[int] = None
+ delta90_EBAY_USED_SHIPPING_gte: Optional[int] = None
+ delta90_LIGHTNING_DEAL_lte: Optional[int] = None
+ delta90_LIGHTNING_DEAL_gte: Optional[int] = None
+ delta90_LISTPRICE_lte: Optional[int] = None
+ delta90_LISTPRICE_gte: Optional[int] = None
+ delta90_NEW_lte: Optional[int] = None
+ delta90_NEW_gte: Optional[int] = None
+ delta90_NEW_FBA_lte: Optional[int] = None
+ delta90_NEW_FBA_gte: Optional[int] = None
+ delta90_NEW_FBM_SHIPPING_lte: Optional[int] = None
+ delta90_NEW_FBM_SHIPPING_gte: Optional[int] = None
+ delta90_PRIME_EXCL_lte: Optional[int] = None
+ delta90_PRIME_EXCL_gte: Optional[int] = None
+ delta90_RATING_lte: Optional[int] = None
+ delta90_RATING_gte: Optional[int] = None
+ delta90_REFURBISHED_lte: Optional[int] = None
+ delta90_REFURBISHED_gte: Optional[int] = None
+ delta90_REFURBISHED_SHIPPING_lte: Optional[int] = None
+ delta90_REFURBISHED_SHIPPING_gte: Optional[int] = None
+ delta90_RENT_lte: Optional[int] = None
+ delta90_RENT_gte: Optional[int] = None
+ delta90_SALES_lte: Optional[int] = None
+ delta90_SALES_gte: Optional[int] = None
+ delta90_TRADE_IN_lte: Optional[int] = None
+ delta90_TRADE_IN_gte: Optional[int] = None
+ delta90_USED_lte: Optional[int] = None
+ delta90_USED_gte: Optional[int] = None
+ delta90_USED_ACCEPTABLE_SHIPPING_lte: Optional[int] = None
+ delta90_USED_ACCEPTABLE_SHIPPING_gte: Optional[int] = None
+ delta90_USED_GOOD_SHIPPING_lte: Optional[int] = None
+ delta90_USED_GOOD_SHIPPING_gte: Optional[int] = None
+ delta90_USED_NEW_SHIPPING_lte: Optional[int] = None
+ delta90_USED_NEW_SHIPPING_gte: Optional[int] = None
+ delta90_USED_VERY_GOOD_SHIPPING_lte: Optional[int] = None
+ delta90_USED_VERY_GOOD_SHIPPING_gte: Optional[int] = None
+ delta90_WAREHOUSE_lte: Optional[int] = None
+ delta90_WAREHOUSE_gte: Optional[int] = None
+ deltaLast_AMAZON_lte: Optional[int] = None
+ deltaLast_AMAZON_gte: Optional[int] = None
+ deltaLast_BUY_BOX_SHIPPING_lte: Optional[int] = None
+ deltaLast_BUY_BOX_SHIPPING_gte: Optional[int] = None
+ deltaLast_BUY_BOX_USED_SHIPPING_lte: Optional[int] = None
+ deltaLast_BUY_BOX_USED_SHIPPING_gte: Optional[int] = None
+ deltaLast_COLLECTIBLE_lte: Optional[int] = None
+ deltaLast_COLLECTIBLE_gte: Optional[int] = None
+ deltaLast_COUNT_COLLECTIBLE_lte: Optional[int] = None
+ deltaLast_COUNT_COLLECTIBLE_gte: Optional[int] = None
+ deltaLast_COUNT_NEW_lte: Optional[int] = None
+ deltaLast_COUNT_NEW_gte: Optional[int] = None
+ deltaLast_COUNT_REFURBISHED_lte: Optional[int] = None
+ deltaLast_COUNT_REFURBISHED_gte: Optional[int] = None
+ deltaLast_COUNT_REVIEWS_lte: Optional[int] = None
+ deltaLast_COUNT_REVIEWS_gte: Optional[int] = None
+ deltaLast_COUNT_USED_lte: Optional[int] = None
+ deltaLast_COUNT_USED_gte: Optional[int] = None
+ deltaLast_EBAY_NEW_SHIPPING_lte: Optional[int] = None
+ deltaLast_EBAY_NEW_SHIPPING_gte: Optional[int] = None
+ deltaLast_EBAY_USED_SHIPPING_lte: Optional[int] = None
+ deltaLast_EBAY_USED_SHIPPING_gte: Optional[int] = None
+ deltaLast_LIGHTNING_DEAL_lte: Optional[int] = None
+ deltaLast_LIGHTNING_DEAL_gte: Optional[int] = None
+ deltaLast_LISTPRICE_lte: Optional[int] = None
+ deltaLast_LISTPRICE_gte: Optional[int] = None
+ deltaLast_NEW_lte: Optional[int] = None
+ deltaLast_NEW_gte: Optional[int] = None
+ deltaLast_NEW_FBA_lte: Optional[int] = None
+ deltaLast_NEW_FBA_gte: Optional[int] = None
+ deltaLast_NEW_FBM_SHIPPING_lte: Optional[int] = None
+ deltaLast_NEW_FBM_SHIPPING_gte: Optional[int] = None
+ deltaLast_PRIME_EXCL_lte: Optional[int] = None
+ deltaLast_PRIME_EXCL_gte: Optional[int] = None
+ deltaLast_RATING_lte: Optional[int] = None
+ deltaLast_RATING_gte: Optional[int] = None
+ deltaLast_REFURBISHED_lte: Optional[int] = None
+ deltaLast_REFURBISHED_gte: Optional[int] = None
+ deltaLast_REFURBISHED_SHIPPING_lte: Optional[int] = None
+ deltaLast_REFURBISHED_SHIPPING_gte: Optional[int] = None
+ deltaLast_RENT_lte: Optional[int] = None
+ deltaLast_RENT_gte: Optional[int] = None
+ deltaLast_SALES_lte: Optional[int] = None
+ deltaLast_SALES_gte: Optional[int] = None
+ deltaLast_TRADE_IN_lte: Optional[int] = None
+ deltaLast_TRADE_IN_gte: Optional[int] = None
+ deltaLast_USED_lte: Optional[int] = None
+ deltaLast_USED_gte: Optional[int] = None
+ deltaLast_USED_ACCEPTABLE_SHIPPING_lte: Optional[int] = None
+ deltaLast_USED_ACCEPTABLE_SHIPPING_gte: Optional[int] = None
+ deltaLast_USED_GOOD_SHIPPING_lte: Optional[int] = None
+ deltaLast_USED_GOOD_SHIPPING_gte: Optional[int] = None
+ deltaLast_USED_NEW_SHIPPING_lte: Optional[int] = None
+ deltaLast_USED_NEW_SHIPPING_gte: Optional[int] = None
+ deltaLast_USED_VERY_GOOD_SHIPPING_lte: Optional[int] = None
+ deltaLast_USED_VERY_GOOD_SHIPPING_gte: Optional[int] = None
+ deltaLast_WAREHOUSE_lte: Optional[int] = None
+ deltaLast_WAREHOUSE_gte: Optional[int] = None
+ deltaPercent1_AMAZON_lte: Optional[int] = None
+ deltaPercent1_AMAZON_gte: Optional[int] = None
+ deltaPercent1_BUY_BOX_SHIPPING_lte: Optional[int] = None
+ deltaPercent1_BUY_BOX_SHIPPING_gte: Optional[int] = None
+ deltaPercent1_BUY_BOX_USED_SHIPPING_lte: Optional[int] = None
+ deltaPercent1_BUY_BOX_USED_SHIPPING_gte: Optional[int] = None
+ deltaPercent1_COLLECTIBLE_lte: Optional[int] = None
+ deltaPercent1_COLLECTIBLE_gte: Optional[int] = None
+ deltaPercent1_COUNT_COLLECTIBLE_lte: Optional[int] = None
+ deltaPercent1_COUNT_COLLECTIBLE_gte: Optional[int] = None
+ deltaPercent1_COUNT_NEW_lte: Optional[int] = None
+ deltaPercent1_COUNT_NEW_gte: Optional[int] = None
+ deltaPercent1_COUNT_REFURBISHED_lte: Optional[int] = None
+ deltaPercent1_COUNT_REFURBISHED_gte: Optional[int] = None
+ deltaPercent1_COUNT_REVIEWS_lte: Optional[int] = None
+ deltaPercent1_COUNT_REVIEWS_gte: Optional[int] = None
+ deltaPercent1_COUNT_USED_lte: Optional[int] = None
+ deltaPercent1_COUNT_USED_gte: Optional[int] = None
+ deltaPercent1_EBAY_NEW_SHIPPING_lte: Optional[int] = None
+ deltaPercent1_EBAY_NEW_SHIPPING_gte: Optional[int] = None
+ deltaPercent1_EBAY_USED_SHIPPING_lte: Optional[int] = None
+ deltaPercent1_EBAY_USED_SHIPPING_gte: Optional[int] = None
+ deltaPercent1_LIGHTNING_DEAL_lte: Optional[int] = None
+ deltaPercent1_LIGHTNING_DEAL_gte: Optional[int] = None
+ deltaPercent1_LISTPRICE_lte: Optional[int] = None
+ deltaPercent1_LISTPRICE_gte: Optional[int] = None
+ deltaPercent1_NEW_lte: Optional[int] = None
+ deltaPercent1_NEW_gte: Optional[int] = None
+ deltaPercent1_NEW_FBA_lte: Optional[int] = None
+ deltaPercent1_NEW_FBA_gte: Optional[int] = None
+ deltaPercent1_NEW_FBM_SHIPPING_lte: Optional[int] = None
+ deltaPercent1_NEW_FBM_SHIPPING_gte: Optional[int] = None
+ deltaPercent1_PRIME_EXCL_lte: Optional[int] = None
+ deltaPercent1_PRIME_EXCL_gte: Optional[int] = None
+ deltaPercent1_RATING_lte: Optional[int] = None
+ deltaPercent1_RATING_gte: Optional[int] = None
+ deltaPercent1_REFURBISHED_lte: Optional[int] = None
+ deltaPercent1_REFURBISHED_gte: Optional[int] = None
+ deltaPercent1_REFURBISHED_SHIPPING_lte: Optional[int] = None
+ deltaPercent1_REFURBISHED_SHIPPING_gte: Optional[int] = None
+ deltaPercent1_RENT_lte: Optional[int] = None
+ deltaPercent1_RENT_gte: Optional[int] = None
+ deltaPercent1_SALES_lte: Optional[int] = None
+ deltaPercent1_SALES_gte: Optional[int] = None
+ deltaPercent1_TRADE_IN_lte: Optional[int] = None
+ deltaPercent1_TRADE_IN_gte: Optional[int] = None
+ deltaPercent1_USED_lte: Optional[int] = None
+ deltaPercent1_USED_gte: Optional[int] = None
+ deltaPercent1_USED_ACCEPTABLE_SHIPPING_lte: Optional[int] = None
+ deltaPercent1_USED_ACCEPTABLE_SHIPPING_gte: Optional[int] = None
+ deltaPercent1_USED_GOOD_SHIPPING_lte: Optional[int] = None
+ deltaPercent1_USED_GOOD_SHIPPING_gte: Optional[int] = None
+ deltaPercent1_USED_NEW_SHIPPING_lte: Optional[int] = None
+ deltaPercent1_USED_NEW_SHIPPING_gte: Optional[int] = None
+ deltaPercent1_USED_VERY_GOOD_SHIPPING_lte: Optional[int] = None
+ deltaPercent1_USED_VERY_GOOD_SHIPPING_gte: Optional[int] = None
+ deltaPercent1_WAREHOUSE_lte: Optional[int] = None
+ deltaPercent1_WAREHOUSE_gte: Optional[int] = None
+ deltaPercent30_AMAZON_lte: Optional[int] = None
+ deltaPercent30_AMAZON_gte: Optional[int] = None
+ deltaPercent30_BUY_BOX_SHIPPING_lte: Optional[int] = None
+ deltaPercent30_BUY_BOX_SHIPPING_gte: Optional[int] = None
+ deltaPercent30_BUY_BOX_USED_SHIPPING_lte: Optional[int] = None
+ deltaPercent30_BUY_BOX_USED_SHIPPING_gte: Optional[int] = None
+ deltaPercent30_COLLECTIBLE_lte: Optional[int] = None
+ deltaPercent30_COLLECTIBLE_gte: Optional[int] = None
+ deltaPercent30_COUNT_COLLECTIBLE_lte: Optional[int] = None
+ deltaPercent30_COUNT_COLLECTIBLE_gte: Optional[int] = None
+ deltaPercent30_COUNT_NEW_lte: Optional[int] = None
+ deltaPercent30_COUNT_NEW_gte: Optional[int] = None
+ deltaPercent30_COUNT_REFURBISHED_lte: Optional[int] = None
+ deltaPercent30_COUNT_REFURBISHED_gte: Optional[int] = None
+ deltaPercent30_COUNT_REVIEWS_lte: Optional[int] = None
+ deltaPercent30_COUNT_REVIEWS_gte: Optional[int] = None
+ deltaPercent30_COUNT_USED_lte: Optional[int] = None
+ deltaPercent30_COUNT_USED_gte: Optional[int] = None
+ deltaPercent30_EBAY_NEW_SHIPPING_lte: Optional[int] = None
+ deltaPercent30_EBAY_NEW_SHIPPING_gte: Optional[int] = None
+ deltaPercent30_EBAY_USED_SHIPPING_lte: Optional[int] = None
+ deltaPercent30_EBAY_USED_SHIPPING_gte: Optional[int] = None
+ deltaPercent30_LIGHTNING_DEAL_lte: Optional[int] = None
+ deltaPercent30_LIGHTNING_DEAL_gte: Optional[int] = None
+ deltaPercent30_LISTPRICE_lte: Optional[int] = None
+ deltaPercent30_LISTPRICE_gte: Optional[int] = None
+ deltaPercent30_NEW_lte: Optional[int] = None
+ deltaPercent30_NEW_gte: Optional[int] = None
+ deltaPercent30_NEW_FBA_lte: Optional[int] = None
+ deltaPercent30_NEW_FBA_gte: Optional[int] = None
+ deltaPercent30_NEW_FBM_SHIPPING_lte: Optional[int] = None
+ deltaPercent30_NEW_FBM_SHIPPING_gte: Optional[int] = None
+ deltaPercent30_PRIME_EXCL_lte: Optional[int] = None
+ deltaPercent30_PRIME_EXCL_gte: Optional[int] = None
+ deltaPercent30_RATING_lte: Optional[int] = None
+ deltaPercent30_RATING_gte: Optional[int] = None
+ deltaPercent30_REFURBISHED_lte: Optional[int] = None
+ deltaPercent30_REFURBISHED_gte: Optional[int] = None
+ deltaPercent30_REFURBISHED_SHIPPING_lte: Optional[int] = None
+ deltaPercent30_REFURBISHED_SHIPPING_gte: Optional[int] = None
+ deltaPercent30_RENT_lte: Optional[int] = None
+ deltaPercent30_RENT_gte: Optional[int] = None
+ deltaPercent30_SALES_lte: Optional[int] = None
+ deltaPercent30_SALES_gte: Optional[int] = None
+ deltaPercent30_TRADE_IN_lte: Optional[int] = None
+ deltaPercent30_TRADE_IN_gte: Optional[int] = None
+ deltaPercent30_USED_lte: Optional[int] = None
+ deltaPercent30_USED_gte: Optional[int] = None
+ deltaPercent30_USED_ACCEPTABLE_SHIPPING_lte: Optional[int] = None
+ deltaPercent30_USED_ACCEPTABLE_SHIPPING_gte: Optional[int] = None
+ deltaPercent30_USED_GOOD_SHIPPING_lte: Optional[int] = None
+ deltaPercent30_USED_GOOD_SHIPPING_gte: Optional[int] = None
+ deltaPercent30_USED_NEW_SHIPPING_lte: Optional[int] = None
+ deltaPercent30_USED_NEW_SHIPPING_gte: Optional[int] = None
+ deltaPercent30_USED_VERY_GOOD_SHIPPING_lte: Optional[int] = None
+ deltaPercent30_USED_VERY_GOOD_SHIPPING_gte: Optional[int] = None
+ deltaPercent30_WAREHOUSE_lte: Optional[int] = None
+ deltaPercent30_WAREHOUSE_gte: Optional[int] = None
+ deltaPercent7_AMAZON_lte: Optional[int] = None
+ deltaPercent7_AMAZON_gte: Optional[int] = None
+ deltaPercent7_BUY_BOX_SHIPPING_lte: Optional[int] = None
+ deltaPercent7_BUY_BOX_SHIPPING_gte: Optional[int] = None
+ deltaPercent7_BUY_BOX_USED_SHIPPING_lte: Optional[int] = None
+ deltaPercent7_BUY_BOX_USED_SHIPPING_gte: Optional[int] = None
+ deltaPercent7_COLLECTIBLE_lte: Optional[int] = None
+ deltaPercent7_COLLECTIBLE_gte: Optional[int] = None
+ deltaPercent7_COUNT_COLLECTIBLE_lte: Optional[int] = None
+ deltaPercent7_COUNT_COLLECTIBLE_gte: Optional[int] = None
+ deltaPercent7_COUNT_NEW_lte: Optional[int] = None
+ deltaPercent7_COUNT_NEW_gte: Optional[int] = None
+ deltaPercent7_COUNT_REFURBISHED_lte: Optional[int] = None
+ deltaPercent7_COUNT_REFURBISHED_gte: Optional[int] = None
+ deltaPercent7_COUNT_REVIEWS_lte: Optional[int] = None
+ deltaPercent7_COUNT_REVIEWS_gte: Optional[int] = None
+ deltaPercent7_COUNT_USED_lte: Optional[int] = None
+ deltaPercent7_COUNT_USED_gte: Optional[int] = None
+ deltaPercent7_EBAY_NEW_SHIPPING_lte: Optional[int] = None
+ deltaPercent7_EBAY_NEW_SHIPPING_gte: Optional[int] = None
+ deltaPercent7_EBAY_USED_SHIPPING_lte: Optional[int] = None
+ deltaPercent7_EBAY_USED_SHIPPING_gte: Optional[int] = None
+ deltaPercent7_LIGHTNING_DEAL_lte: Optional[int] = None
+ deltaPercent7_LIGHTNING_DEAL_gte: Optional[int] = None
+ deltaPercent7_LISTPRICE_lte: Optional[int] = None
+ deltaPercent7_LISTPRICE_gte: Optional[int] = None
+ deltaPercent7_NEW_lte: Optional[int] = None
+ deltaPercent7_NEW_gte: Optional[int] = None
+ deltaPercent7_NEW_FBA_lte: Optional[int] = None
+ deltaPercent7_NEW_FBA_gte: Optional[int] = None
+ deltaPercent7_NEW_FBM_SHIPPING_lte: Optional[int] = None
+ deltaPercent7_NEW_FBM_SHIPPING_gte: Optional[int] = None
+ deltaPercent7_PRIME_EXCL_lte: Optional[int] = None
+ deltaPercent7_PRIME_EXCL_gte: Optional[int] = None
+ deltaPercent7_RATING_lte: Optional[int] = None
+ deltaPercent7_RATING_gte: Optional[int] = None
+ deltaPercent7_REFURBISHED_lte: Optional[int] = None
+ deltaPercent7_REFURBISHED_gte: Optional[int] = None
+ deltaPercent7_REFURBISHED_SHIPPING_lte: Optional[int] = None
+ deltaPercent7_REFURBISHED_SHIPPING_gte: Optional[int] = None
+ deltaPercent7_RENT_lte: Optional[int] = None
+ deltaPercent7_RENT_gte: Optional[int] = None
+ deltaPercent7_SALES_lte: Optional[int] = None
+ deltaPercent7_SALES_gte: Optional[int] = None
+ deltaPercent7_TRADE_IN_lte: Optional[int] = None
+ deltaPercent7_TRADE_IN_gte: Optional[int] = None
+ deltaPercent7_USED_lte: Optional[int] = None
+ deltaPercent7_USED_gte: Optional[int] = None
+ deltaPercent7_USED_ACCEPTABLE_SHIPPING_lte: Optional[int] = None
+ deltaPercent7_USED_ACCEPTABLE_SHIPPING_gte: Optional[int] = None
+ deltaPercent7_USED_GOOD_SHIPPING_lte: Optional[int] = None
+ deltaPercent7_USED_GOOD_SHIPPING_gte: Optional[int] = None
+ deltaPercent7_USED_NEW_SHIPPING_lte: Optional[int] = None
+ deltaPercent7_USED_NEW_SHIPPING_gte: Optional[int] = None
+ deltaPercent7_USED_VERY_GOOD_SHIPPING_lte: Optional[int] = None
+ deltaPercent7_USED_VERY_GOOD_SHIPPING_gte: Optional[int] = None
+ deltaPercent7_WAREHOUSE_lte: Optional[int] = None
+ deltaPercent7_WAREHOUSE_gte: Optional[int] = None
+ deltaPercent90_AMAZON_lte: Optional[int] = None
+ deltaPercent90_AMAZON_gte: Optional[int] = None
+ deltaPercent90_BUY_BOX_SHIPPING_lte: Optional[int] = None
+ deltaPercent90_BUY_BOX_SHIPPING_gte: Optional[int] = None
+ deltaPercent90_BUY_BOX_USED_SHIPPING_lte: Optional[int] = None
+ deltaPercent90_BUY_BOX_USED_SHIPPING_gte: Optional[int] = None
+ deltaPercent90_COLLECTIBLE_lte: Optional[int] = None
+ deltaPercent90_COLLECTIBLE_gte: Optional[int] = None
+ deltaPercent90_COUNT_COLLECTIBLE_lte: Optional[int] = None
+ deltaPercent90_COUNT_COLLECTIBLE_gte: Optional[int] = None
+ deltaPercent90_COUNT_NEW_lte: Optional[int] = None
+ deltaPercent90_COUNT_NEW_gte: Optional[int] = None
+ deltaPercent90_COUNT_REFURBISHED_lte: Optional[int] = None
+ deltaPercent90_COUNT_REFURBISHED_gte: Optional[int] = None
+ deltaPercent90_COUNT_REVIEWS_lte: Optional[int] = None
+ deltaPercent90_COUNT_REVIEWS_gte: Optional[int] = None
+ deltaPercent90_COUNT_USED_lte: Optional[int] = None
+ deltaPercent90_COUNT_USED_gte: Optional[int] = None
+ deltaPercent90_EBAY_NEW_SHIPPING_lte: Optional[int] = None
+ deltaPercent90_EBAY_NEW_SHIPPING_gte: Optional[int] = None
+ deltaPercent90_EBAY_USED_SHIPPING_lte: Optional[int] = None
+ deltaPercent90_EBAY_USED_SHIPPING_gte: Optional[int] = None
+ deltaPercent90_LIGHTNING_DEAL_lte: Optional[int] = None
+ deltaPercent90_LIGHTNING_DEAL_gte: Optional[int] = None
+ deltaPercent90_LISTPRICE_lte: Optional[int] = None
+ deltaPercent90_LISTPRICE_gte: Optional[int] = None
+ deltaPercent90_NEW_lte: Optional[int] = None
+ deltaPercent90_NEW_gte: Optional[int] = None
+ deltaPercent90_NEW_FBA_lte: Optional[int] = None
+ deltaPercent90_NEW_FBA_gte: Optional[int] = None
+ deltaPercent90_NEW_FBM_SHIPPING_lte: Optional[int] = None
+ deltaPercent90_NEW_FBM_SHIPPING_gte: Optional[int] = None
+ deltaPercent90_PRIME_EXCL_lte: Optional[int] = None
+ deltaPercent90_PRIME_EXCL_gte: Optional[int] = None
+ deltaPercent90_RATING_lte: Optional[int] = None
+ deltaPercent90_RATING_gte: Optional[int] = None
+ deltaPercent90_REFURBISHED_lte: Optional[int] = None
+ deltaPercent90_REFURBISHED_gte: Optional[int] = None
+ deltaPercent90_REFURBISHED_SHIPPING_lte: Optional[int] = None
+ deltaPercent90_REFURBISHED_SHIPPING_gte: Optional[int] = None
+ deltaPercent90_RENT_lte: Optional[int] = None
+ deltaPercent90_RENT_gte: Optional[int] = None
+ deltaPercent90_SALES_lte: Optional[int] = None
+ deltaPercent90_SALES_gte: Optional[int] = None
+ deltaPercent90_TRADE_IN_lte: Optional[int] = None
+ deltaPercent90_TRADE_IN_gte: Optional[int] = None
+ deltaPercent90_USED_lte: Optional[int] = None
+ deltaPercent90_USED_gte: Optional[int] = None
+ deltaPercent90_USED_ACCEPTABLE_SHIPPING_lte: Optional[int] = None
+ deltaPercent90_USED_ACCEPTABLE_SHIPPING_gte: Optional[int] = None
+ deltaPercent90_USED_GOOD_SHIPPING_lte: Optional[int] = None
+ deltaPercent90_USED_GOOD_SHIPPING_gte: Optional[int] = None
+ deltaPercent90_USED_NEW_SHIPPING_lte: Optional[int] = None
+ deltaPercent90_USED_NEW_SHIPPING_gte: Optional[int] = None
+ deltaPercent90_USED_VERY_GOOD_SHIPPING_lte: Optional[int] = None
+ deltaPercent90_USED_VERY_GOOD_SHIPPING_gte: Optional[int] = None
+ deltaPercent90_WAREHOUSE_lte: Optional[int] = None
+ deltaPercent90_WAREHOUSE_gte: Optional[int] = None
+ edition: Optional[Union[list[str], str]] = None
+ fbaFees_lte: Optional[int] = None
+ fbaFees_gte: Optional[int] = None
+ format: Optional[Union[list[str], str]] = None
+ genre: Optional[Union[list[str], str]] = None
+ hasParentASIN: Optional[bool] = None
+ hasReviews: Optional[bool] = None
+ isAdultProduct: Optional[bool] = None
+ isEligibleForSuperSaverShipping: Optional[bool] = None
+ isEligibleForTradeIn: Optional[bool] = None
+ isHighestOffer: Optional[bool] = None
+ isLowestOffer: Optional[bool] = None
+ isLowest_AMAZON: Optional[bool] = None
+ isLowest_BUY_BOX_SHIPPING: Optional[bool] = None
+ isLowest_BUY_BOX_USED_SHIPPING: Optional[bool] = None
+ isLowest_COLLECTIBLE: Optional[bool] = None
+ isLowest_COUNT_COLLECTIBLE: Optional[bool] = None
+ isLowest_COUNT_NEW: Optional[bool] = None
+ isLowest_COUNT_REFURBISHED: Optional[bool] = None
+ isLowest_COUNT_REVIEWS: Optional[bool] = None
+ isLowest_COUNT_USED: Optional[bool] = None
+ isLowest_EBAY_NEW_SHIPPING: Optional[bool] = None
+ isLowest_EBAY_USED_SHIPPING: Optional[bool] = None
+ isLowest_LIGHTNING_DEAL: Optional[bool] = None
+ isLowest_LISTPRICE: Optional[bool] = None
+ isLowest_NEW: Optional[bool] = None
+ isLowest_NEW_FBA: Optional[bool] = None
+ isLowest_NEW_FBM_SHIPPING: Optional[bool] = None
+ isLowest_PRIME_EXCL: Optional[bool] = None
+ isLowest_RATING: Optional[bool] = None
+ isLowest_REFURBISHED: Optional[bool] = None
+ isLowest_REFURBISHED_SHIPPING: Optional[bool] = None
+ isLowest_RENT: Optional[bool] = None
+ isLowest_SALES: Optional[bool] = None
+ isLowest_TRADE_IN: Optional[bool] = None
+ isLowest_USED: Optional[bool] = None
+ isLowest_USED_ACCEPTABLE_SHIPPING: Optional[bool] = None
+ isLowest_USED_GOOD_SHIPPING: Optional[bool] = None
+ isLowest_USED_NEW_SHIPPING: Optional[bool] = None
+ isLowest_USED_VERY_GOOD_SHIPPING: Optional[bool] = None
+ isLowest_WAREHOUSE: Optional[bool] = None
+ isPrimeExclusive: Optional[bool] = None
+ isSNS: Optional[bool] = None
+ itemDimension_lte: Optional[int] = None
+ itemDimension_gte: Optional[int] = None
+ itemHeight_lte: Optional[int] = None
+ itemHeight_gte: Optional[int] = None
+ itemLength_lte: Optional[int] = None
+ itemLength_gte: Optional[int] = None
+ itemWeight_lte: Optional[int] = None
+ itemWeight_gte: Optional[int] = None
+ itemWidth_lte: Optional[int] = None
+ itemWidth_gte: Optional[int] = None
+ label: Optional[Union[list[str], str]] = None
+ languages: Optional[Union[list[str], str]] = None
+ lastOffersUpdate_lte: Optional[int] = None
+ lastOffersUpdate_gte: Optional[int] = None
+ lastPriceChange_lte: Optional[int] = None
+ lastPriceChange_gte: Optional[int] = None
+ lastRatingUpdate_lte: Optional[int] = None
+ lastRatingUpdate_gte: Optional[int] = None
+ lastUpdate_lte: Optional[int] = None
+ lastUpdate_gte: Optional[int] = None
+ lightningEnd_lte: Optional[int] = None
+ lightningEnd_gte: Optional[int] = None
+ lightningStart_lte: Optional[int] = None
+ lightningStart_gte: Optional[int] = None
+ listedSince_lte: Optional[int] = None
+ listedSince_gte: Optional[int] = None
+ manufacturer: Optional[Union[list[str], str]] = None
+ model: Optional[Union[list[str], str]] = None
+ newPriceIsMAP: Optional[bool] = None
+ nextUpdate_lte: Optional[int] = None
+ nextUpdate_gte: Optional[int] = None
+ numberOfItems_lte: Optional[int] = None
+ numberOfItems_gte: Optional[int] = None
+ numberOfPages_lte: Optional[int] = None
+ numberOfPages_gte: Optional[int] = None
+ numberOfTrackings_lte: Optional[int] = None
+ numberOfTrackings_gte: Optional[int] = None
+ offerCountFBA_lte: Optional[int] = None
+ offerCountFBA_gte: Optional[int] = None
+ offerCountFBM_lte: Optional[int] = None
+ offerCountFBM_gte: Optional[int] = None
+ outOfStockPercentage90_BB_lte: Optional[int] = None
+ outOfStockPercentage90_BB_gte: Optional[int] = None
+ outOfStockPercentage90_BB_USED_lte: Optional[int] = None
+ outOfStockPercentage90_BB_USED_gte: Optional[int] = None
+ outOfStockPercentage90_NEW_lte: Optional[int] = None
+ outOfStockPercentage90_NEW_gte: Optional[int] = None
+ outOfStockPercentage90_USED_lte: Optional[int] = None
+ outOfStockPercentage90_USED_gte: Optional[int] = None
+ outOfStockPercentageInInterval_lte: Optional[int] = None
+ outOfStockPercentageInInterval_gte: Optional[int] = None
+ packageDimension_lte: Optional[int] = None
+ packageDimension_gte: Optional[int] = None
+ packageHeight_lte: Optional[int] = None
+ packageHeight_gte: Optional[int] = None
+ packageLength_lte: Optional[int] = None
+ packageLength_gte: Optional[int] = None
+ packageQuantity_lte: Optional[int] = None
+ packageQuantity_gte: Optional[int] = None
+ packageWeight_lte: Optional[int] = None
+ packageWeight_gte: Optional[int] = None
+ packageWidth_lte: Optional[int] = None
+ packageWidth_gte: Optional[int] = None
+ partNumber: Optional[Union[list[str], str]] = None
+ platform: Optional[Union[list[str], str]] = None
+ productGroup: Optional[Union[list[str], str]] = None
+ productType: Optional[int] = None
+ publicationDate_lte: Optional[int] = None
+ publicationDate_gte: Optional[int] = None
+ publisher: Optional[Union[list[str], str]] = None
+ releaseDate_lte: Optional[int] = None
+ releaseDate_gte: Optional[int] = None
+ rootCategory: Optional[int] = None
+ salesRankDrops180_lte: Optional[int] = None
+ salesRankDrops180_gte: Optional[int] = None
+ salesRankDrops30_lte: Optional[int] = None
+ salesRankDrops30_gte: Optional[int] = None
+ salesRankDrops365_lte: Optional[int] = None
+ salesRankDrops365_gte: Optional[int] = None
+ salesRankDrops90_lte: Optional[int] = None
+ salesRankDrops90_gte: Optional[int] = None
+ salesRankReference: Optional[int] = None
+ salesRankTopPct_lte: Optional[int] = None
+ salesRankTopPct_gte: Optional[int] = None
+ sellerIds: Optional[Union[list[str], str]] = None
+ sellerIdsLowestFBA: Optional[Union[list[str], str]] = None
+ sellerIdsLowestFBM: Optional[Union[list[str], str]] = None
+ size: Optional[Union[list[str], str]] = None
+ studio: Optional[Union[list[str], str]] = None
+ title: Optional[str] = None
+ title_flag: Optional[str] = None
+ totalOfferCount_lte: Optional[int] = None
+ totalOfferCount_gte: Optional[int] = None
+ trackingSince_lte: Optional[int] = None
+ trackingSince_gte: Optional[int] = None
+ monthlySold_lte: Optional[int] = None
+ monthlySold_gte: Optional[int] = None
+ buyBoxIsPreorder: Optional[bool] = None
+ buyBoxIsBackorder: Optional[bool] = None
+ buyBoxIsPrimeExclusive: Optional[bool] = None
+ type: Optional[Union[list[str], str]] = None
+ warehouseCondition: Optional[int] = None
+ singleVariation: Optional[bool] = None
+ outOfStockPercentage90_lte: Optional[int] = None
+ outOfStockPercentage90_gte: Optional[int] = None
+ variationCount_lte: Optional[int] = None
+ variationCount_gte: Optional[int] = None
+ imageCount_lte: Optional[int] = None
+ imageCount_gte: Optional[int] = None
+ buyBoxStatsAmazon30_lte: Optional[int] = None
+ buyBoxStatsAmazon30_gte: Optional[int] = None
+ buyBoxStatsAmazon90_lte: Optional[int] = None
+ buyBoxStatsAmazon90_gte: Optional[int] = None
+ buyBoxStatsAmazon180_lte: Optional[int] = None
+ buyBoxStatsAmazon180_gte: Optional[int] = None
+ buyBoxStatsAmazon365_lte: Optional[int] = None
+ buyBoxStatsAmazon365_gte: Optional[int] = None
+ buyBoxStatsTopSeller30_lte: Optional[int] = None
+ buyBoxStatsTopSeller30_gte: Optional[int] = None
+ buyBoxStatsTopSeller90_lte: Optional[int] = None
+ buyBoxStatsTopSeller90_gte: Optional[int] = None
+ buyBoxStatsTopSeller180_lte: Optional[int] = None
+ buyBoxStatsTopSeller180_gte: Optional[int] = None
+ buyBoxStatsTopSeller365_lte: Optional[int] = None
+ buyBoxStatsTopSeller365_gte: Optional[int] = None
+ buyBoxStatsSellerCount30_lte: Optional[int] = None
+ buyBoxStatsSellerCount30_gte: Optional[int] = None
+ buyBoxStatsSellerCount90_lte: Optional[int] = None
+ buyBoxStatsSellerCount90_gte: Optional[int] = None
+ buyBoxStatsSellerCount180_lte: Optional[int] = None
+ buyBoxStatsSellerCount180_gte: Optional[int] = None
+ buyBoxStatsSellerCount365_lte: Optional[int] = None
+ buyBoxStatsSellerCount365_gte: Optional[int] = None
+ isHazMat: Optional[bool] = None
+ perPage: Optional[int] = None
diff --git a/src/keepa/interface.py b/src/keepa/interface.py
new file mode 100644
index 0000000..2e9b54d
--- /dev/null
+++ b/src/keepa/interface.py
@@ -0,0 +1,2487 @@
+"""Interface module to download Amazon product and history data from keepa.com."""
+
+import asyncio
+import datetime
+import json
+import logging
+import time
+from collections.abc import Sequence
+from enum import Enum
+from pathlib import Path
+from typing import Any, Literal
+
+import aiohttp
+import numpy as np
+import pandas as pd
+import requests
+from tqdm import tqdm
+
+from keepa.data_models import ProductParams
+from keepa.query_keys import DEAL_REQUEST_KEYS
+
+
+def is_documented_by(original):
+ """Avoid copying the documentation."""
+
+ def wrapper(target):
+ target.__doc__ = original.__doc__
+ return target
+
+ return wrapper
+
+
+log = logging.getLogger(__name__)
+
+# hardcoded ordinal time from
+KEEPA_ST_ORDINAL = np.datetime64("2011-01-01")
+
+# Request limit
+REQUEST_LIMIT = 100
+
+# Status code dictionary/key
+SCODES = {
+ "400": "REQUEST_REJECTED",
+ "402": "PAYMENT_REQUIRED",
+ "405": "METHOD_NOT_ALLOWED",
+ "429": "NOT_ENOUGH_TOKEN",
+}
+
+# domain codes
+# Valid values: [ 1: com | 2: co.uk | 3: de | 4: fr | 5:
+# co.jp | 6: ca | 7: cn | 8: it | 9: es | 10: in | 11: com.mx | 12: com.br ]
+DCODES = ["RESERVED", "US", "GB", "DE", "FR", "JP", "CA", "CN", "IT", "ES", "IN", "MX", "BR"]
+# developer note: appears like CN (China) has changed to RESERVED2
+
+# csv indices. used when parsing csv and stats fields.
+# https://github.com/keepacom/api_backend
+# see api_backend/src/main/java/com/keepa/api/backend/structs/Product.java
+# [index in csv, key name, isfloat(is price or rating)]
+csv_indices: list[tuple[int, str, bool]] = [
+ (0, "AMAZON", True),
+ (1, "NEW", True),
+ (2, "USED", True),
+ (3, "SALES", False),
+ (4, "LISTPRICE", True),
+ (5, "COLLECTIBLE", True),
+ (6, "REFURBISHED", True),
+ (7, "NEW_FBM_SHIPPING", True),
+ (8, "LIGHTNING_DEAL", True),
+ (9, "WAREHOUSE", True),
+ (10, "NEW_FBA", True),
+ (11, "COUNT_NEW", False),
+ (12, "COUNT_USED", False),
+ (13, "COUNT_REFURBISHED", False),
+ (14, "CollectableOffers", False),
+ (15, "EXTRA_INFO_UPDATES", False),
+ (16, "RATING", True),
+ (17, "COUNT_REVIEWS", False),
+ (18, "BUY_BOX_SHIPPING", True),
+ (19, "USED_NEW_SHIPPING", True),
+ (20, "USED_VERY_GOOD_SHIPPING", True),
+ (21, "USED_GOOD_SHIPPING", True),
+ (22, "USED_ACCEPTABLE_SHIPPING", True),
+ (23, "COLLECTIBLE_NEW_SHIPPING", True),
+ (24, "COLLECTIBLE_VERY_GOOD_SHIPPING", True),
+ (25, "COLLECTIBLE_GOOD_SHIPPING", True),
+ (26, "COLLECTIBLE_ACCEPTABLE_SHIPPING", True),
+ (27, "REFURBISHED_SHIPPING", True),
+ (28, "EBAY_NEW_SHIPPING", True),
+ (29, "EBAY_USED_SHIPPING", True),
+ (30, "TRADE_IN", True),
+ (31, "RENT", False),
+]
+
+_SELLER_TIME_DATA_KEYS = ["trackedSince", "lastUpdate"]
+
+
+def _normalize_value(v: int, isfloat: bool, key: str) -> float | None:
+ """Normalize a single value based on its type and key context."""
+ if v < 0:
+ return None
+ if isfloat:
+ v = float(v) / 100
+ if key == "RATING":
+ v *= 10
+ return v
+
+
+def _is_stat_value_skippable(key: str, value: Any) -> bool:
+ """Determine if the stat value is skippable."""
+ if key in {
+ "buyBoxSellerId",
+ "sellerIdsLowestFBA",
+ "sellerIdsLowestFBM",
+ "buyBoxShippingCountry",
+ "buyBoxAvailabilityMessage",
+ }:
+ return True
+
+ # -1 or -2 --> not exist
+ if isinstance(value, int) and value < 0:
+ return True
+
+ return False
+
+
+def _parse_stat_value_list(
+ value_list: list, to_datetime: bool
+) -> dict[str, float | tuple[Any, float]]:
+ """Parse a list of stat values into a structured dict."""
+ convert_time = any(isinstance(v, list) for v in value_list if v is not None)
+ result = {}
+
+ for ind, key, isfloat in csv_indices:
+ item = value_list[ind] if ind < len(value_list) else None
+ if item is None:
+ continue
+
+ if convert_time:
+ ts, val = item
+ val = _normalize_value(val, isfloat, key)
+ if val is not None:
+ ts = keepa_minutes_to_time([ts], to_datetime)[0]
+ result[key] = (ts, val)
+ else:
+ val = _normalize_value(item, isfloat, key)
+ if val is not None:
+ result[key] = val
+
+ return result
+
+
+def _parse_stats(stats: dict[str, None, int, list[int]], to_datetime: bool):
+ """Parse numeric stats object.
+
+ There is no need to parse strings or list of strings. Keepa stats object
+ response documentation:
+ https://keepa.com/#!discuss/t/statistics-object/1308
+ """
+ stats_parsed = {}
+
+ for stat_key, stat_value in stats.items():
+ if _is_stat_value_skippable(stat_key, stat_value):
+ continue
+
+ if stat_value is not None:
+ if stat_key == "lastOffersUpdate":
+ stats_parsed[stat_key] = keepa_minutes_to_time([stat_value], to_datetime)[0]
+ elif isinstance(stat_value, list) and len(stat_value) > 0:
+ stat_value_dict = _parse_stat_value_list(stat_value, to_datetime)
+ if stat_value_dict:
+ stats_parsed[stat_key] = stat_value_dict
+ else:
+ stats_parsed[stat_key] = stat_value
+
+ return stats_parsed
+
+
+def _parse_seller(seller_raw_response, to_datetime):
+ sellers = list(seller_raw_response.values())
+ for seller in sellers:
+
+ def convert_time_data(key):
+ date_val = seller.get(key, None)
+ if date_val is not None:
+ return (key, keepa_minutes_to_time([date_val], to_datetime)[0])
+ else:
+ return None
+
+ seller.update(
+ filter(lambda p: p is not None, map(convert_time_data, _SELLER_TIME_DATA_KEYS))
+ )
+
+ return dict(map(lambda seller: (seller["sellerId"], seller), sellers))
+
+
+def parse_csv(csv, to_datetime: bool = True, out_of_stock_as_nan: bool = True) -> dict[str, Any]:
+ """
+ Parse csv list from keepa into a python dictionary.
+
+ Parameters
+ ----------
+ csv : list
+ csv list from keepa
+ to_datetime : bool, default: True
+ Modifies numpy minutes to datetime.datetime values.
+ Default True.
+ out_of_stock_as_nan : bool, optional
+ When True, prices are NAN when price category is out of stock.
+ When False, prices are -0.01
+ Default True
+
+ Returns
+ -------
+ product_data : dict
+ Dictionary containing the following fields with timestamps:
+
+ AMAZON: Amazon price history
+
+ NEW: Marketplace/3rd party New price history - Amazon is
+ considered to be part of the marketplace as well, so if
+ Amazon has the overall lowest new (!) price, the
+ marketplace new price in the corresponding time interval
+ will be identical to the Amazon price (except if there is
+ only one marketplace offer). Shipping and Handling costs
+ not included!
+
+ USED: Marketplace/3rd party Used price history
+
+ SALES: Sales Rank history. Not every product has a Sales Rank.
+
+ LISTPRICE: List Price history
+
+ 5 COLLECTIBLE: Collectible Price history
+
+ 6 REFURBISHED: Refurbished Price history
+
+ 7 NEW_FBM_SHIPPING: 3rd party (not including Amazon) New price
+ history including shipping costs, only fulfilled by
+ merchant (FBM).
+
+ 8 LIGHTNING_DEAL: 3rd party (not including Amazon) New price
+ history including shipping costs, only fulfilled by
+ merchant (FBM).
+
+ 9 WAREHOUSE: Amazon Warehouse Deals price history. Mostly of
+ used condition, rarely new.
+
+ 10 NEW_FBA: Price history of the lowest 3rd party (not
+ including Amazon/Warehouse) New offer that is fulfilled
+ by Amazon
+
+ 11 COUNT_NEW: New offer count history
+
+ 12 COUNT_USED: Used offer count history
+
+ 13 COUNT_REFURBISHED: Refurbished offer count history
+
+ 14 COUNT_COLLECTIBLE: Collectible offer count history
+
+ 16 RATING: The product's rating history. A rating is an
+ integer from 0 to 50 (e.g. 45 = 4.5 stars)
+
+ 17 COUNT_REVIEWS: The product's review count history.
+
+ 18 BUY_BOX_SHIPPING: The price history of the buy box. If no
+ offer qualified for the buy box the price has the value
+ -1. Including shipping costs. The ``buybox`` parameter
+ must be True for this field to be in the data.
+
+ 19 USED_NEW_SHIPPING: "Used - Like New" price history
+ including shipping costs.
+
+ 20 USED_VERY_GOOD_SHIPPING: "Used - Very Good" price history
+ including shipping costs.
+
+ 21 USED_GOOD_SHIPPING: "Used - Good" price history including
+ shipping costs.
+
+ 22 USED_ACCEPTABLE_SHIPPING: "Used - Acceptable" price history
+ including shipping costs.
+
+ 23 COLLECTIBLE_NEW_SHIPPING: "Collectible - Like New" price
+ history including shipping costs.
+
+ 24 COLLECTIBLE_VERY_GOOD_SHIPPING: "Collectible - Very Good"
+ price history including shipping costs.
+
+ 25 COLLECTIBLE_GOOD_SHIPPING: "Collectible - Good" price
+ history including shipping costs.
+
+ 26 COLLECTIBLE_ACCEPTABLE_SHIPPING: "Collectible - Acceptable"
+ price history including shipping costs.
+
+ 27 REFURBISHED_SHIPPING: Refurbished price history including
+ shipping costs.
+
+ 30 TRADE_IN: The trade in price history. Amazon trade-in is
+ not available for every locale.
+
+ 31 RENT: Rental price history. Requires use of the rental
+ and offers parameter. Amazon Rental is only available
+ for Amazon US.
+
+ Notes
+ -----
+ Negative prices
+
+ """
+ product_data = {}
+
+ for ind, key, isfloat in csv_indices:
+ if csv[ind]: # Check if entry it exists
+ if "SHIPPING" in key: # shipping price is included
+ # Data goes [time0, value0, shipping0, time1, value1,
+ # shipping1, ...]
+ times = csv[ind][::3]
+ values = np.array(csv[ind][1::3])
+ values += np.array(csv[ind][2::3])
+ else:
+ # Data goes [time0, value0, time1, value1, ...]
+ times = csv[ind][::2]
+ values = np.array(csv[ind][1::2])
+
+ # Convert to float price if applicable
+ if isfloat:
+ nan_mask = values < 0
+ values = values.astype(float) / 100
+ if out_of_stock_as_nan:
+ values[nan_mask] = np.nan
+
+ if key == "RATING":
+ values *= 10
+
+ timeval = keepa_minutes_to_time(times, to_datetime)
+
+ product_data["%s_time" % key] = timeval
+ product_data[key] = values
+
+ # combine time and value into a data frame using time as index
+ product_data[f"df_{key}"] = pd.DataFrame({"value": values}, index=timeval)
+
+ return product_data
+
+
+def format_items(items):
+ """Check if the input items are valid and formats them."""
+ if isinstance(items, list) or isinstance(items, np.ndarray):
+ return np.unique(items)
+ elif isinstance(items, str):
+ return np.asarray([items])
+
+
+class Domain(Enum):
+ """Enumeration for Amazon domain regions.
+
+ Examples
+ --------
+ >>> import keepa
+ >>> keepa.Domain.US
+
+
+ """
+
+ RESERVED = "RESERVED"
+ US = "US"
+ GB = "GB"
+ DE = "DE"
+ FR = "FR"
+ JP = "JP"
+ CA = "CA"
+ RESERVED2 = "RESERVED2"
+ IT = "IT"
+ ES = "ES"
+ IN = "IN"
+ MX = "MX"
+ BR = "BR"
+
+
+def _domain_to_dcode(domain: str | Domain) -> int:
+ """Convert a domain to a domain code."""
+ if isinstance(domain, Domain):
+ domain_str = domain.value
+ else:
+ domain_str = domain
+
+ if domain_str not in DCODES:
+ raise ValueError(f"Invalid domain code {domain}. Should be one of the following:\n{DCODES}")
+ return DCODES.index(domain_str)
+
+
+class Keepa:
+ r"""
+ Synchronous Python interface to keepa data backend.
+
+ Initializes API with access key. Access key can be obtained by signing up
+ for a reoccurring or one time plan. To obtain a key, sign up for one at
+ `Keepa Data `_
+
+ Parameters
+ ----------
+ accesskey : str
+ 64 character access key string.
+ timeout : float, default: 10.0
+ Default timeout when issuing any request. This is not a time limit on
+ the entire response download; rather, an exception is raised if the
+ server has not issued a response for timeout seconds. Setting this to
+ 0.0 disables the timeout, but will cause any request to hang
+ indefiantly should keepa.com be down
+ logging_level: string, default: "DEBUG"
+ Logging level to use. Default is "DEBUG". Other options are "INFO",
+ "WARNING", "ERROR", and "CRITICAL".
+
+ Examples
+ --------
+ Create the api object.
+
+ >>> import keepa
+ >>> key = ""
+ >>> api = keepa.Keepa(key)
+
+ Request data from two ASINs.
+
+ >>> products = api.query(["0439064872", "1426208081"])
+
+ Print item details.
+
+ >>> print("Item 1")
+ >>> print("\t ASIN: {:s}".format(products[0]["asin"]))
+ >>> print("\t Title: {:s}".format(products[0]["title"]))
+ Item 1
+ ASIN: 0439064872
+ Title: Harry Potter and the Chamber of Secrets (2)
+
+ Print item price.
+
+ >>> usedprice = products[0]["data"]["USED"]
+ >>> usedtimes = products[0]["data"]["USED_time"]
+ >>> print("\t Used price: ${:.2f}".format(usedprice[-1]))
+ >>> print("\t as of: {:s}".format(str(usedtimes[-1])))
+ Used price: $0.52
+ as of: 2023-01-03 04:46:00
+
+ """
+
+ def __init__(self, accesskey: str, timeout: float = 10.0, logging_level: str = "DEBUG"):
+ """Initialize server connection."""
+ self.accesskey = accesskey
+ self.tokens_left = 0
+ self._timeout = timeout
+
+ # Set up logging
+ levels = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
+ if logging_level not in levels:
+ raise TypeError("logging_level must be one of: " + ", ".join(levels))
+ log.setLevel(logging_level)
+
+ # Don't check available tokens on init
+ log.info("Using key ending in %s", accesskey[-6:])
+ self.status = {"tokensLeft": None, "refillIn": None, "refillRate": None, "timestamp": None}
+
+ @property
+ def time_to_refill(self) -> float:
+ """
+ Return the time to refill in seconds.
+
+ Examples
+ --------
+ Return the time to refill. If you have tokens available, this time
+ should be 0.0 seconds.
+
+ >>> import keepa
+ >>> key = ""
+ >>> api = keepa.Keepa(key)
+ >>> api.time_to_refill
+ 0.0
+
+ """
+ # Get current timestamp in milliseconds from UNIX epoch
+ now = int(time.time() * 1000)
+ timeatrefile = self.status["timestamp"] + self.status["refillIn"]
+
+ # wait plus one second fudge factor
+ timetorefil = timeatrefile - now + 1000
+ if timetorefil < 0:
+ timetorefil = 0
+
+ # Account for negative tokens left
+ if self.tokens_left < 0:
+ timetorefil += (abs(self.tokens_left) / self.status["refillRate"]) * 60000
+
+ # Return value in seconds
+ return timetorefil / 1000.0
+
+ def update_status(self) -> dict[str, Any]:
+ """Update available tokens."""
+ status = self._request("token", {"key": self.accesskey}, wait=False)
+ self.status = status
+ return status
+
+ def wait_for_tokens(self) -> None:
+ """Check if there are any remaining tokens and waits if none are available."""
+ self.update_status()
+
+ # Wait if no tokens available
+ if self.tokens_left <= 0:
+ tdelay = self.time_to_refill
+ log.warning("Waiting %.0f seconds for additional tokens" % tdelay)
+ time.sleep(tdelay)
+ self.update_status()
+
+ def download_graph_image(
+ self,
+ asin: str,
+ filename: str | Path,
+ domain: str | Domain = "US",
+ wait: bool = True,
+ **graph_kwargs: dict[str, Any],
+ ) -> None:
+ """
+ Download the graph image of an ASIN from keepa.
+
+ See `Graph Image API
+ `_ for more
+ details.
+
+ Parameters
+ ----------
+ asin : str
+ The ASIN of the product.
+ filename : str | pathlib.Path
+ Path to save the png to.
+ domain : str | keepa.Domain, default: 'US'
+ A valid Amazon domain. See :class:`keepa.Domain`.
+ wait : bool, default: True
+ Wait for available tokens before querying the keepa backend.
+ **graph_kwargs : dict[str, Any], optional
+ Optional graph keyword arguments. See `Graph Image API
+ `_ for more
+ details.
+
+ Notes
+ -----
+ Graph images are cached for 90 minutes on a per-user basis. The cache
+ invalidates if any parameter changes. Submitting the exact same request
+ within this time frame will not consume any tokens.
+
+ Examples
+ --------
+ Download a keepa graph image showing the current Amazon price, new
+ price, and the sales rank of a product with ASIN ``"B09YNQCQKR"``.
+
+ >>> from keepa import Keepa
+ >>> api = Keepa("")
+ >>> api.download_graph_image(
+ ... asin="B09YNQCQKR",
+ ... filename="product_graph.png",
+ ... amazon=1,
+ ... new=1,
+ ... salesrank=1,
+ ... )
+
+ Show Amazon price, new and used graphs, buy box and FBA, for last 365
+ days, with custom width/height and custom colors. See
+ `_ for more
+ details.
+
+ api.download_graph_image(
+ asin="B09YNQCQKR",
+ filename="product_graph_365.png",
+ domain="US",
+ amazon=1,
+ new=1,
+ used=1,
+ bb=1,
+ fba=1,
+ range=365,
+ width=800,
+ height=400,
+ cBackground="ffffff",
+ cAmazon="FFA500",
+ cNew="8888dd",
+ cUsed="444444",
+ cBB="ff00b4",
+ cFBA="ff5722"
+ )
+
+ """
+ payload = {"asin": asin, "key": self.accesskey, "domain": _domain_to_dcode(domain)}
+ payload.update(graph_kwargs)
+
+ resp = self._request("graphimage", payload, wait=wait, is_json=False)
+
+ first_chunk = True
+ filename = Path(filename)
+ with open(filename, "wb") as f:
+ for chunk in resp.iter_content(8192):
+ if first_chunk:
+ if not chunk.startswith(b"\x89PNG\r\n\x1a\n"):
+ raise ValueError(
+ "Response from api.keepa.com/graphimage is not a valid PNG image"
+ )
+ first_chunk = False
+ f.write(chunk)
+
+ def query(
+ self,
+ items: str | Sequence[str],
+ stats: int | None = None,
+ domain: str | Domain = "US",
+ history: bool = True,
+ offers: int | None = None,
+ update: int | None = None,
+ to_datetime: bool = True,
+ rating: bool = False,
+ out_of_stock_as_nan: bool = True,
+ stock: bool = False,
+ product_code_is_asin: bool = True,
+ progress_bar: bool = True,
+ buybox: bool = False,
+ wait: bool = True,
+ days: int | None = None,
+ only_live_offers: bool | None = None,
+ raw: bool = False,
+ videos: bool = False,
+ aplus: bool = False,
+ extra_params: dict[str, Any] = {},
+ ) -> list[dict[str, Any]]:
+ """
+ Perform a product query of a list, array, or single ASIN.
+
+ Returns a list of product data with one entry for each product.
+
+ Parameters
+ ----------
+ items : str, Sequence[str]
+ A list, array, or single asin, UPC, EAN, or ISBN-13 identifying a
+ product. ASINs should be 10 characters and match a product on
+ Amazon. Items not matching Amazon product or duplicate Items will
+ return no data. When using non-ASIN items, set
+ ``product_code_is_asin`` to ``False``.
+
+ stats : int or date, optional
+ No extra token cost. If specified the product object will
+ have a stats field with quick access to current prices,
+ min/max prices and the weighted mean values. If the offers
+ parameter was used it will also provide stock counts and
+ buy box information.
+
+ You can provide the stats parameter in two forms:
+
+ Last x days (positive integer value): calculates the stats
+ of the last x days, where x is the value of the stats
+ parameter. Interval: You can provide a date range for the
+ stats calculation. You can specify the range via two
+ timestamps (unix epoch time milliseconds) or two date
+ strings (ISO8601, with or without time in UTC).
+
+ domain : str | keepa.Domain, default: 'US'
+ A valid Amazon domain. See :class:`keepa.Domain`.
+
+ history : bool, optional
+ When set to True includes the price, sales, and offer
+ history of a product. Set to False to reduce request time
+ if data is not required. Default True
+
+ offers : int, optional
+ Adds available offers to product data. Default 0. Must be between
+ 20 and 100. Enabling this also enables the ``"buyBoxUsedHistory"``.
+
+ update : int, optional
+ If data is older than the input integer, keepa will update their
+ database and return live data. If set to 0 (live data), request may
+ cost an additional token. Default (``None``) will not update.
+
+ to_datetime : bool, default: True
+ When ``True`` casts the time values of the product data
+ (e.g. ``"AMAZON_TIME"``) to ``datetime.datetime``. For example
+ ``datetime.datetime(2025, 10, 24, 10, 40)``. When ``False``, the
+ values are represented as ``numpy`` ``"`_
+ and not yet supported in this function. For example,
+ `extra_params={'rental': 1}`.
+
+ Returns
+ -------
+ list
+ List of products when ``raw=False``. Each product within the list
+ is a dictionary. The keys of each item may vary, so see the keys
+ within each product for further details.
+
+ Each product should contain at a minimum a "data" key containing a
+ formatted dictionary. For the available fields see the notes
+ section.
+
+ When ``raw=True``, a list of unparsed responses are
+ returned as :class:`requests.models.Response`.
+
+ See: https://keepa.com/#!discuss/t/product-object/116
+
+ Notes
+ -----
+ The following are some of the fields a product dictionary. For a full
+ list and description, please see:
+ `product-object `_
+
+ AMAZON
+ Amazon price history
+
+ NEW
+ Marketplace/3rd party New price history - Amazon is
+ considered to be part of the marketplace as well, so if
+ Amazon has the overall lowest new (!) price, the
+ marketplace new price in the corresponding time interval
+ will be identical to the Amazon price (except if there is
+ only one marketplace offer). Shipping and Handling costs
+ not included!
+
+ USED
+ Marketplace/3rd party Used price history
+
+ SALES
+ Sales Rank history. Not every product has a Sales Rank.
+
+ LISTPRICE
+ List Price history
+
+ COLLECTIBLE
+ Collectible Price history
+
+ REFURBISHED
+ Refurbished Price history
+
+ NEW_FBM_SHIPPING
+ 3rd party (not including Amazon) New price history
+ including shipping costs, only fulfilled by merchant
+ (FBM).
+
+ LIGHTNING_DEAL
+ 3rd party (not including Amazon) New price history
+ including shipping costs, only fulfilled by merchant
+ (FBM).
+
+ WAREHOUSE
+ Amazon Warehouse Deals price history. Mostly of used
+ condition, rarely new.
+
+ NEW_FBA
+ Price history of the lowest 3rd party (not including
+ Amazon/Warehouse) New offer that is fulfilled by Amazon
+
+ COUNT_NEW
+ New offer count history
+
+ COUNT_USED
+ Used offer count history
+
+ COUNT_REFURBISHED
+ Refurbished offer count history
+
+ COUNT_COLLECTIBLE
+ Collectible offer count history
+
+ RATING
+ The product's rating history. A rating is an integer from
+ 0 to 50 (e.g. 45 = 4.5 stars)
+
+ COUNT_REVIEWS
+ The product's review count history.
+
+ BUY_BOX_SHIPPING
+ The price history of the buy box. If no offer qualified
+ for the buy box the price has the value -1. Including
+ shipping costs.
+
+ USED_NEW_SHIPPING
+ "Used - Like New" price history including shipping costs.
+
+ USED_VERY_GOOD_SHIPPING
+ "Used - Very Good" price history including shipping costs.
+
+ USED_GOOD_SHIPPING
+ "Used - Good" price history including shipping costs.
+
+ USED_ACCEPTABLE_SHIPPING
+ "Used - Acceptable" price history including shipping costs.
+
+ COLLECTIBLE_NEW_SHIPPING
+ "Collectible - Like New" price history including shipping
+ costs.
+
+ COLLECTIBLE_VERY_GOOD_SHIPPING
+ "Collectible - Very Good" price history including shipping
+ costs.
+
+ COLLECTIBLE_GOOD_SHIPPING
+ "Collectible - Good" price history including shipping
+ costs.
+
+ COLLECTIBLE_ACCEPTABLE_SHIPPING
+ "Collectible - Acceptable" price history including
+ shipping costs.
+
+ REFURBISHED_SHIPPING
+ Refurbished price history including shipping costs.
+
+ TRADE_IN
+ The trade in price history. Amazon trade-in is not
+ available for every locale.
+
+ BUY_BOX_SHIPPING
+ The price history of the buy box. If no offer qualified
+ for the buy box the price has the value -1. Including
+ shipping costs. The ``buybox`` parameter must be True for
+ this field to be in the data.
+
+ Examples
+ --------
+ Query for product with ASIN ``'B0088PUEPK'`` using the synchronous
+ keepa interface.
+
+ >>> import keepa
+ >>> key = ""
+ >>> api = keepa.Keepa(key)
+ >>> response = api.query("B0088PUEPK")
+ >>> response[0]["title"]
+ 'Western Digital 1TB WD Blue PC Internal Hard Drive HDD - 7200 RPM,
+ SATA 6 Gb/s, 64 MB Cache, 3.5" - WD10EZEX'
+
+ Query for product with ASIN ``'B0088PUEPK'`` using the asynchronous
+ keepa interface.
+
+ >>> import asyncio
+ >>> import keepa
+ >>> async def main():
+ ... key = ""
+ ... api = await keepa.AsyncKeepa().create(key)
+ ... return await api.query("B0088PUEPK")
+ ...
+ >>> response = asyncio.run(main())
+ >>> response[0]["title"]
+ 'Western Digital 1TB WD Blue PC Internal Hard Drive HDD - 7200 RPM,
+ SATA 6 Gb/s, 64 MB Cache, 3.5" - WD10EZEX'
+
+ Load in product offers and convert the buy box data into a
+ ``pandas.DataFrame``.
+
+ >>> import keepa
+ >>> key = ""
+ >>> api = keepa.Keepa(key)
+ >>> response = api.query("B0088PUEPK", offers=20)
+ >>> product = response[0]
+ >>> buybox_info = product["buyBoxUsedHistory"]
+ >>> df = keepa.process_used_buybox(buybox_info)
+ datetime user_id condition isFBA
+ 0 2022-11-02 16:46:00 A1QUAC68EAM09F Used - Like New True
+ 1 2022-11-13 10:36:00 A18WXU4I7YR6UA Used - Very Good False
+ 2 2022-11-15 23:50:00 AYUGEV9WZ4X5O Used - Like New False
+ 3 2022-11-17 06:16:00 A18WXU4I7YR6UA Used - Very Good False
+ 4 2022-11-17 10:56:00 AYUGEV9WZ4X5O Used - Like New False
+ .. ... ... ... ...
+ 115 2023-10-23 10:00:00 AYUGEV9WZ4X5O Used - Like New False
+ 116 2023-10-25 21:14:00 A1U9HDFCZO1A84 Used - Like New False
+ 117 2023-10-26 04:08:00 AYUGEV9WZ4X5O Used - Like New False
+ 118 2023-10-27 08:14:00 A1U9HDFCZO1A84 Used - Like New False
+ 119 2023-10-27 12:34:00 AYUGEV9WZ4X5O Used - Like New False
+
+ Query a video with the "videos" metadata.
+
+ >>> response = api.query("B00UFMKSDW", history=False, videos=True)
+ >>> product = response[0]
+ >>> "videos" in product
+ True
+
+
+ """
+ # Format items into numpy array
+ try:
+ items = format_items(items)
+ except BaseException:
+ raise ValueError("Invalid product codes input")
+ if not len(items):
+ raise ValueError("No valid product codes")
+
+ nitems = len(items)
+ if nitems == 1:
+ log.debug("Executing single product query")
+ else:
+ log.debug("Executing %d item product query", nitems)
+
+ # check offer input
+ if offers:
+ if not isinstance(offers, int):
+ raise TypeError('Parameter "offers" must be an interger')
+
+ if offers > 100 or offers < 20:
+ raise ValueError('Parameter "offers" must be between 20 and 100')
+
+ # Report time to completion
+ if self.status["refillRate"] is not None:
+ tcomplete = (
+ float(nitems - self.tokens_left) / self.status["refillRate"]
+ - (60000 - self.status["refillIn"]) / 60000.0
+ )
+ if tcomplete < 0.0:
+ tcomplete = 0.5
+ log.debug(
+ "Estimated time to complete %d request(s) is %.2f minutes",
+ nitems,
+ tcomplete,
+ )
+ log.debug("\twith a refill rate of %d token(s) per minute", self.status["refillRate"])
+
+ # product list
+ products = []
+
+ pbar = None
+ if progress_bar:
+ pbar = tqdm(total=nitems)
+
+ # Number of requests is dependent on the number of items and
+ # request limit. Use available tokens first
+ idx = 0 # or number complete
+ while idx < nitems:
+ nrequest = nitems - idx
+
+ # cap request
+ if nrequest > REQUEST_LIMIT:
+ nrequest = REQUEST_LIMIT
+
+ # request from keepa and increment current position
+ item_request = items[idx : idx + nrequest] # noqa: E203
+ response = self._product_query(
+ item_request,
+ product_code_is_asin,
+ stats=stats,
+ domain=domain,
+ stock=stock,
+ offers=offers,
+ update=update,
+ history=history,
+ rating=rating,
+ to_datetime=to_datetime,
+ out_of_stock_as_nan=out_of_stock_as_nan,
+ buybox=buybox,
+ wait=wait,
+ days=days,
+ only_live_offers=only_live_offers,
+ raw=raw,
+ videos=videos,
+ aplus=aplus,
+ **extra_params,
+ )
+ idx += nrequest
+ if raw:
+ products.append(response)
+ else:
+ products.extend(response["products"])
+
+ if pbar is not None:
+ pbar.update(nrequest)
+
+ return products
+
+ def _product_query(self, items, product_code_is_asin=True, **kwargs):
+ """Send query to keepa server and returns parsed JSON result.
+
+ Parameters
+ ----------
+ items : np.ndarray
+ Array of asins. If UPC, EAN, or ISBN-13, as_asin must be
+ False. Must be between 1 and 100 ASINs
+
+ as_asin : bool, optional
+ Interpret product codes as ASINs only.
+
+ stats : int or date format
+ Set the stats time for get sales rank inside this range
+
+ domain : str | keepa.Domain, default: 'US'
+ A valid Amazon domain. See :class:`keepa.Domain`.
+
+ offers : bool, optional
+ Adds product offers to product data.
+
+ update : int, optional
+ If data is older than the input integer, keepa will update
+ their database and return live data. If set to 0 (live
+ data), then request may cost an additional token.
+
+ history : bool, optional
+ When set to True includes the price, sales, and offer
+ history of a product. Set to False to reduce request time
+ if data is not required.
+
+ Returns
+ -------
+ products : list
+ List of products. Length equal to number of successful
+ ASINs.
+
+ refillIn : float
+ Time in milliseconds to the next refill of tokens.
+
+ refilRate : float
+ Number of tokens refilled per minute
+
+ timestamp : float
+
+ tokensLeft : int
+ Remaining tokens
+
+ tz : int
+ Timezone. 0 is UTC
+
+ """
+ # ASINs convert to comma joined string
+ assert len(items) <= 100
+
+ if product_code_is_asin:
+ kwargs["asin"] = ",".join(items)
+ else:
+ kwargs["code"] = ",".join(items)
+
+ kwargs["key"] = self.accesskey
+ kwargs["domain"] = _domain_to_dcode(kwargs["domain"])
+
+ # Convert bool values to 0 and 1.
+ kwargs["stock"] = int(kwargs["stock"])
+ kwargs["history"] = int(kwargs["history"])
+ kwargs["rating"] = int(kwargs["rating"])
+ kwargs["buybox"] = int(kwargs["buybox"])
+ kwargs["videos"] = int(kwargs["videos"])
+ kwargs["aplus"] = int(kwargs["aplus"])
+
+ if kwargs["update"] is None:
+ del kwargs["update"]
+ else:
+ kwargs["update"] = int(kwargs["update"])
+
+ if kwargs["offers"] is None:
+ del kwargs["offers"]
+ else:
+ kwargs["offers"] = int(kwargs["offers"])
+
+ if kwargs["only_live_offers"] is None:
+ del kwargs["only_live_offers"]
+ else:
+ # Keepa's param actually doesn't use snake_case.
+ kwargs["only-live-offers"] = int(kwargs.pop("only_live_offers"))
+
+ if kwargs["days"] is None:
+ del kwargs["days"]
+ else:
+ assert kwargs["days"] > 0
+
+ if kwargs["stats"] is None:
+ del kwargs["stats"]
+
+ out_of_stock_as_nan = kwargs.pop("out_of_stock_as_nan", True)
+ to_datetime = kwargs.pop("to_datetime", True)
+
+ # Query and replace csv with parsed data if history enabled
+ wait = kwargs.get("wait")
+ kwargs.pop("wait", None)
+ raw_response = kwargs.pop("raw", False)
+ response = self._request("product", kwargs, wait=wait, raw_response=raw_response)
+
+ if kwargs["history"] and not raw_response:
+ if "products" not in response:
+ raise RuntimeError("No products in response. Possibly invalid ASINs")
+
+ for product in response["products"]:
+ if product["csv"]: # if data exists
+ product["data"] = parse_csv(product["csv"], to_datetime, out_of_stock_as_nan)
+
+ if kwargs.get("stats", None) and not raw_response:
+ for product in response["products"]:
+ stats = product.get("stats", None)
+ if stats:
+ product["stats_parsed"] = _parse_stats(stats, to_datetime)
+
+ return response
+
+ def best_sellers_query(
+ self,
+ category: str,
+ rank_avg_range: Literal[0, 30, 90, 180] = 0,
+ variations: bool = False,
+ sublist: bool = False,
+ domain: str | Domain = "US",
+ wait: bool = True,
+ ):
+ """
+ Retrieve an ASIN list of the most popular products.
+
+ This is based on sales in a specific category or product group. See
+ "search_for_categories" for information on how to get a category.
+
+ Root category lists (e.g. "Home & Kitchen") or product group lists
+ contain up to 100,000 ASINs.
+
+ Sub-category lists (e.g. "Home Entertainment Furniture") contain up to
+ 3,000 ASINs. As we only have access to the product's primary sales rank
+ and not the ones of all categories it is listed in, the sub-category
+ lists are created by us based on the product's primary sales rank and
+ do not reflect the actual ordering on Amazon.
+
+ Lists are ordered, starting with the best selling product.
+
+ Lists are updated daily. If a product does not have an accessible
+ sales rank it will not be included in the lists. This in particular
+ affects many products in the Clothing and Sports & Outdoors categories.
+
+ We can not correctly identify the sales rank reference category in all
+ cases, so some products may be misplaced.
+
+ See the keepa documentation at `Request Best Sellers
+ `_ for additional
+ details.
+
+ Parameters
+ ----------
+ category : str
+ The category node id of the category you want to request
+ the best sellers list for. You can find category node ids
+ via the category search :meth:`Keepa.search_for_categories`.
+ rank_avg_range : int, default: 0
+ Optionally specify to retrieve a best seller list based on a sales
+ rank average instead of the current sales rank. Valid values:
+
+ * 0: Use current rank
+ * 30: 30-day average
+ * 90: 90-day average
+ * 180: 180-day average
+ variations : bool, default: False
+ Restrict list entries to a single variation for items with multiple
+ variations. The variation returned will be the one with the highest
+ monthly units sold (if that data point is available). When
+ ``False`` (default), do not include variations. When ``True``,
+ return all variations.
+
+ By default we return one variation per parent. If the variations
+ share the same sales rank, the representative is the variation with
+ the highest monthly units sold. If monthly sold data is missing or
+ tied, the representative falls back to randomly picked one.
+ sublist : bool, default: False
+ By default (``False``), the best seller list for sub-categories is created
+ based on the product’s primary sales rank, if available. To request
+ a best seller list based on the sub-category sales rank
+ (classification rank), set this parameter to ``True``. Note that
+ not all products have a primary sales rank or a sub-category sales
+ rank and not all sub-category levels have sales ranks.
+ domain : str | keepa.Domain, default: 'US'
+ A valid Amazon domain. See :class:`keepa.Domain`.
+ wait : bool, default: True
+ Wait for available tokens before querying the keepa backend.
+
+ Returns
+ -------
+ list
+ List of best seller ASINs
+
+ Examples
+ --------
+ Query for the best sellers among the ``"movies"`` category.
+
+ >>> import keepa
+ >>> key = ""
+ >>> api = keepa.Keepa(key)
+ >>> categories = api.search_for_categories("movies")
+ >>> category = list(categories.items())[0][0]
+ >>> asins = api.best_sellers_query(category)
+ >>> asins
+ ['B0BF3P5XZS',
+ 'B08JQN5VDT',
+ 'B09SP8JPPK',
+ '0999296345',
+ 'B07HPG684T',
+ '1984825577',
+ ...
+
+ Query for the best sellers among the ``"movies"`` category using the
+ asynchronous keepa interface.
+
+ >>> import asyncio
+ >>> import keepa
+ >>> async def main():
+ ... key = ""
+ ... api = await keepa.AsyncKeepa().create(key)
+ ... categories = await api.search_for_categories("movies")
+ ... category = list(categories.items())[0][0]
+ ... return await api.best_sellers_query(category)
+ ...
+ >>> asins = asyncio.run(main())
+ >>> asins
+ ['B0BF3P5XZS',
+ 'B08JQN5VDT',
+ 'B09SP8JPPK',
+ '0999296345',
+ 'B07HPG684T',
+ '1984825577',
+ ...
+
+ """
+ payload = {
+ "key": self.accesskey,
+ "domain": _domain_to_dcode(domain),
+ "variations": int(variations),
+ "sublist": int(sublist),
+ "category": category,
+ "range": rank_avg_range,
+ }
+
+ response = self._request("bestsellers", payload, wait=wait)
+ if "bestSellersList" not in response:
+ raise RuntimeError(f"Best sellers search results for {category} not yet available")
+
+ return response["bestSellersList"]["asinList"]
+
+ def search_for_categories(
+ self, searchterm: str, domain: str | Domain = "US", wait: bool = True
+ ) -> list:
+ """
+ Search for categories from Amazon.
+
+ Parameters
+ ----------
+ searchterm : str
+ Input search term.
+ domain : str | keepa.Domain, default: 'US'
+ A valid Amazon domain. See :class:`keepa.Domain`.
+ wait : bool, default: True
+ Wait for available tokens before querying the keepa backend.
+
+ Returns
+ -------
+ dict[str, Any]
+ The response contains a categories dictionary with all matching
+ categories.
+
+ Examples
+ --------
+ Print all categories from science.
+
+ >>> import keepa
+ >>> key = ""
+ >>> api = keepa.Keepa(key)
+ >>> categories = api.search_for_categories("science")
+ >>> for cat_id in categories:
+ ... print(cat_id, categories[cat_id]["name"])
+ ...
+ 9091159011 Behavioral Sciences
+ 8407535011 Fantasy, Horror & Science Fiction
+ 8407519011 Sciences & Technology
+ 12805 Science & Religion
+ 13445 Astrophysics & Space Science
+ 12038 Science Fiction & Fantasy
+ 3207 Science, Nature & How It Works
+ 144 Science Fiction & Fantasy
+
+ """
+ payload = {
+ "key": self.accesskey,
+ "domain": _domain_to_dcode(domain),
+ "type": "category",
+ "term": searchterm,
+ }
+
+ response = self._request("search", payload, wait=wait)
+ if response["categories"] == {}: # pragma no cover
+ raise RuntimeError(
+ "Categories search results not yet available or no search terms found."
+ )
+ return response["categories"]
+
+ def category_lookup(
+ self,
+ category_id: int,
+ domain: str | Domain = "US",
+ include_parents=False,
+ wait: bool = True,
+ ) -> dict[str, Any]:
+ """
+ Return root categories given a categoryId.
+
+ Parameters
+ ----------
+ category_id : int
+ ID for specific category or 0 to return a list of root categories.
+ domain : str | keepa.Domain, default: 'US'
+ A valid Amazon domain. See :class:`keepa.Domain`.
+ include_parents : bool, default: False
+ Include parents.
+ wait : bool, default: True
+ Wait for available tokens before querying the keepa backend.
+
+ Returns
+ -------
+ dict[str, Any]
+ Output format is the same as :meth:`Keepa.`search_for_categories`.
+
+ Examples
+ --------
+ Use 0 to return all root categories.
+
+ >>> import keepa
+ >>> key = ""
+ >>> api = keepa.Keepa(key)
+ >>> categories = api.category_lookup(0)
+
+ Output the first category.
+
+ >>> list(categories.values())[0]
+ {'domainId': 1,
+ 'catId': 133140011,
+ 'name': 'Kindle Store',
+ 'children': [133141011,
+ 133143011,
+ 6766606011,
+ 7529231011,
+ 118656435011,
+ 2268072011,
+ 119757513011,
+ 358606011,
+ 3000677011,
+ 1293747011],
+ 'parent': 0,
+ 'highestRank': 6984155,
+ 'productCount': 6417325,
+ 'contextFreeName': 'Kindle Store',
+ 'lowestRank': 1,
+ 'matched': True}
+
+ """
+ payload = {
+ "key": self.accesskey,
+ "domain": _domain_to_dcode(domain),
+ "category": category_id,
+ "parents": int(include_parents),
+ }
+
+ response = self._request("category", payload, wait=wait)
+ if response["categories"] == {}: # pragma no cover
+ raise Exception("Category lookup results not yet available or no match found.")
+ return response["categories"]
+
+ def seller_query(
+ self,
+ seller_id: str | list[str],
+ domain: str | Domain = "US",
+ to_datetime: bool = True,
+ storefront: bool = False,
+ update: int | None = None,
+ wait: bool = True,
+ ):
+ """
+ Receive seller information for a given seller id or ids.
+
+ If a seller is not found no tokens will be consumed.
+
+ Token cost: 1 per requested seller
+
+ Parameters
+ ----------
+ seller_id : str or list[str]
+ The seller id of the merchant you want to request. For batch
+ requests, you may submit a list of 100 seller_ids. The seller id
+ can also be found on Amazon on seller profile pages in the seller
+ parameter of the URL as well as in the offers results from a
+ product query.
+ domain : str | keepa.Domain, default: 'US'
+ A valid Amazon domain. See :class:`keepa.Domain`.
+ to_datetime : bool, default: True
+ When ``True`` casts the time values to ``datetime.datetime``. For
+ example ``datetime.datetime(2025, 10, 24, 10, 40)``. When
+ ``False``, the values are represented as ``numpy`` ``">> import keepa
+ >>> key = ""
+ >>> api = keepa.Keepa(key)
+ >>> seller_info = api.seller_query("A2L77EE7U53NWQ", "US")
+ >>> seller_info["A2L77EE7U53NWQ"]["sellerName"]
+ 'Amazon Warehouse'
+
+ Notes
+ -----
+ Seller data is not available for Amazon China.
+
+ """
+ if isinstance(seller_id, list):
+ if len(seller_id) > 100:
+ err_str = "seller_id can contain at maximum 100 sellers"
+ raise RuntimeError(err_str)
+ seller = ",".join(seller_id)
+ else:
+ seller = seller_id
+
+ payload = {
+ "key": self.accesskey,
+ "domain": _domain_to_dcode(domain),
+ "seller": seller,
+ }
+
+ if storefront:
+ payload["storefront"] = int(storefront)
+ if update is not False:
+ payload["update"] = update
+
+ response = self._request("seller", payload, wait=wait)
+ return _parse_seller(response["sellers"], to_datetime)
+
+ def product_finder(
+ self,
+ product_parms: dict[str, Any] | ProductParams,
+ domain: str | Domain = "US",
+ wait: bool = True,
+ n_products: int = 50,
+ ) -> list[str]:
+ """
+ Query the keepa product database to find products matching criteria.
+
+ Almost all product fields can be searched for and sorted.
+
+ Parameters
+ ----------
+ product_parms : dict, ProductParams
+ Dictionary or :class:`keepa.ProductParams`.
+ domain : str | keepa.Domain, default: 'US'
+ A valid Amazon domain. See :class:`keepa.Domain`.
+ wait : bool, default: True
+ Wait for available tokens before querying the keepa backend.
+ n_products : int, default: 50
+ Maximum number of matching products returned by keepa. This can be
+ overridden by the 'perPage' key in ``product_parms``.
+
+ Returns
+ -------
+ list[str]
+ List of ASINs matching the product parameters.
+
+ 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"]``
+
+ Examples
+ --------
+ Query for the first 100 of Jim Butcher's books using the synchronous
+ ``keepa.Keepa`` class. Sort by current sales.
+
+ >>> import keepa
+ >>> api = keepa.Keepa("")
+ >>> product_parms = {
+ ... "author": "jim butcher",
+ ... "sort": ["current_SALES", "asc"],
+ ... }
+ >>> asins = api.product_finder(product_parms, n_products=100)
+ >>> asins
+ ['B000HRMAR2',
+ '0578799790',
+ 'B07PW1SVHM',
+ ...
+ 'B003MXM744',
+ '0133235750',
+ 'B01MXXLJPZ']
+
+ Alternatively, use the :class:`keepa.ProductParams`:
+
+ >>> product_parms = keepa.ProductParams(
+ ... author="jim butcher",
+ ... sort=["current_SALES", "asc"],
+ ... )
+ >>> asins = api.product_finder(product_parms, n_products=100)
+
+ Query for all of Jim Butcher's books using the asynchronous
+ ``keepa.AsyncKeepa`` class.
+
+ >>> import asyncio
+ >>> import keepa
+ >>> product_parms = {"author": "jim butcher"}
+ >>> async def main():
+ ... key = ""
+ ... api = await keepa.AsyncKeepa().create(key)
+ ... return await api.product_finder(product_parms)
+ ...
+ >>> asins = asyncio.run(main())
+ >>> asins
+ ['B000HRMAR2',
+ '0578799790',
+ 'B07PW1SVHM',
+ ...
+ 'B003MXM744',
+ '0133235750',
+ 'B01MXXLJPZ']
+
+ """
+ if isinstance(product_parms, dict):
+ product_parms_valid = ProductParams(**product_parms)
+ else:
+ product_parms_valid = product_parms
+ product_parms_dict = product_parms_valid.model_dump(exclude_none=True)
+ product_parms_dict.setdefault("perPage", n_products)
+ payload = {
+ "key": self.accesskey,
+ "domain": _domain_to_dcode(domain),
+ "selection": json.dumps(product_parms_dict),
+ }
+
+ response = self._request("query", payload, wait=wait)
+ return response["asinList"]
+
+ def deals(
+ self, deal_parms: dict[str, Any], domain: str | Domain = "US", wait: bool = True
+ ) -> dict[str, Any]:
+ """Query the Keepa API for product deals.
+
+ You can find products that recently changed and match your
+ search criteria. A single request will return a maximum of
+ 150 deals. Try out the deals page to first get accustomed to
+ the options:
+ https://keepa.com/#!deals
+
+ For more details please visit:
+ https://keepa.com/#!discuss/t/browsing-deals/338
+
+ Parameters
+ ----------
+ deal_parms : dict
+ Dictionary containing one or more of the following keys:
+
+ - ``"page"``: int
+ - ``"domainId"``: int
+ - ``"excludeCategories"``: list
+ - ``"includeCategories"``: list
+ - ``"priceTypes"``: list
+ - ``"deltaRange"``: list
+ - ``"deltaPercentRange"``: list
+ - ``"deltaLastRange"``: list
+ - ``"salesRankRange"``: list
+ - ``"currentRange"``: list
+ - ``"minRating"``: int
+ - ``"isLowest"``: bool
+ - ``"isLowestOffer"``: bool
+ - ``"isOutOfStock"``: bool
+ - ``"titleSearch"``: String
+ - ``"isRangeEnabled"``: bool
+ - ``"isFilterEnabled"``: bool
+ - ``"hasReviews"``: bool
+ - ``"filterErotic"``: bool
+ - ``"sortType"``: int
+ - ``"dateRange"``: int
+ domain : str | keepa.Domain, default: 'US'
+ A valid Amazon domain. See :class:`keepa.Domain`.
+ wait : bool, default: True
+ Wait for available tokens before querying the keepa backend.
+
+ Returns
+ -------
+ dict
+ Dictionary containing the deals including the following keys:
+
+ * ``'dr'`` - Ordered array of all deal objects matching your query.
+ * ``'categoryIds'`` - Contains all root categoryIds of the matched
+ deal products.
+ * ``'categoryNames'`` - Contains all root category names of the
+ matched deal products.
+ * ``'categoryCount'`` - Contains how many deal products in the
+ respective root category are found.
+
+ Examples
+ --------
+ Return deals from category 16310101 using the synchronous
+ ``keepa.Keepa`` class
+
+ >>> import keepa
+ >>> key = ""
+ >>> api = keepa.Keepa(key)
+ >>> deal_parms = {
+ ... "page": 0,
+ ... "domainId": 1,
+ ... "excludeCategories": [1064954, 11091801],
+ ... "includeCategories": [16310101],
+ ... }
+ >>> deals = api.deals(deal_parms)
+
+ Get the title of the first deal.
+
+ >>> deals["dr"][0]["title"]
+ 'Orange Cream Rooibos, Tea Bags - Vanilla, Orange | Caffeine-Free,
+ Antioxidant-rich, Hot & Iced | The Spice Hut, First Sip Of Tea'
+
+ Conduct the same query with the asynchronous ``keepa.AsyncKeepa``
+ class.
+
+ >>> import asyncio
+ >>> import keepa
+ >>> deal_parms = {
+ ... "page": 0,
+ ... "domainId": 1,
+ ... "excludeCategories": [1064954, 11091801],
+ ... "includeCategories": [16310101],
+ ... }
+ >>> async def main():
+ ... key = ""
+ ... api = await keepa.AsyncKeepa().create(key)
+ ... categories = await api.search_for_categories("movies")
+ ... return await api.deals(deal_parms)
+ ...
+ >>> asins = asyncio.run(main())
+ >>> asins
+ ['B0BF3P5XZS',
+ 'B08JQN5VDT',
+ 'B09SP8JPPK',
+ '0999296345',
+ 'B07HPG684T',
+ '1984825577',
+ ...
+
+ """
+ # verify valid keys
+ for key in deal_parms:
+ if key not in DEAL_REQUEST_KEYS:
+ raise ValueError(f'Invalid key "{key}"')
+
+ # verify json type
+ key_type = DEAL_REQUEST_KEYS[key]
+ deal_parms[key] = key_type(deal_parms[key])
+
+ deal_parms.setdefault("priceTypes", 0)
+
+ payload = {
+ "key": self.accesskey,
+ "domain": _domain_to_dcode(domain),
+ "selection": json.dumps(deal_parms),
+ }
+
+ return self._request("deal", payload, wait=wait)["deals"]
+
+ def _request(
+ self,
+ request_type: str,
+ payload: dict[str, Any],
+ wait: bool = True,
+ raw_response: bool = False,
+ is_json: bool = True,
+ ):
+ """
+ Query keepa api server.
+
+ Parses raw response from keepa into a json format. Handles errors and
+ waits for available tokens if allowed.
+ """
+ while True:
+ raw = requests.get(
+ f"https://api.keepa.com/{request_type}/?",
+ payload,
+ timeout=self._timeout,
+ )
+ status_code = str(raw.status_code)
+
+ if is_json:
+ try:
+ response = raw.json()
+ except Exception:
+ raise RuntimeError(f"Invalid JSON from Keepa API (status {status_code})")
+ else:
+ return raw
+
+ # user status is always returned
+ if "tokensLeft" in response:
+ self.tokens_left = response["tokensLeft"]
+ self.status["tokensLeft"] = self.tokens_left
+ log.info("%d tokens remain", self.tokens_left)
+ for key in ["refillIn", "refillRate", "timestamp"]:
+ if key in response:
+ self.status[key] = response[key]
+
+ if status_code == "200":
+ if raw_response:
+ return raw
+ return response
+
+ if status_code == "429" and wait:
+ tdelay = self.time_to_refill
+ log.warning("Waiting %.0f seconds for additional tokens", tdelay)
+ time.sleep(tdelay)
+ continue
+
+ # otherwise, it's an error code
+ if status_code in SCODES:
+ raise RuntimeError(SCODES[status_code])
+ raise RuntimeError(f"REQUEST_FAILED. Status code: {status_code}")
+
+
+class AsyncKeepa:
+ r"""
+ Asynchronous Python interface to keepa backend.
+
+ Initializes API with access key. Access key can be obtained by signing up
+ for a reoccurring or one time plan. To obtain a key, sign up for one at
+ `Keepa Data `_
+
+ Parameters
+ ----------
+ accesskey : str
+ 64 character access key string.
+ timeout : float, default: 10.0
+ Default timeout when issuing any request. This is not a time
+ limit on the entire response download; rather, an exception is
+ raised if the server has not issued a response for timeout
+ seconds. Setting this to 0.0 disables the timeout, but will
+ cause any request to hang indefiantly should keepa.com be down
+
+ Examples
+ --------
+ Query for all of Jim Butcher's books using the asynchronous
+ ``keepa.AsyncKeepa`` class.
+
+ >>> import asyncio
+ >>> import keepa
+ >>> product_parms = {"author": "jim butcher"}
+ >>> async def main():
+ ... key = ""
+ ... api = await keepa.AsyncKeepa().create(key)
+ ... return await api.product_finder(product_parms)
+ ...
+ >>> asins = asyncio.run(main())
+ >>> asins
+ ['B000HRMAR2',
+ '0578799790',
+ 'B07PW1SVHM',
+ ...
+ 'B003MXM744',
+ '0133235750',
+ 'B01MXXLJPZ']
+
+ Query for product with ASIN ``'B0088PUEPK'`` using the asynchronous
+ keepa interface.
+
+ >>> import asyncio
+ >>> import keepa
+ >>> async def main():
+ ... key = ""
+ ... api = await keepa.AsyncKeepa().create(key)
+ ... return await api.query("B0088PUEPK")
+ ...
+ >>> response = asyncio.run(main())
+ >>> response[0]["title"]
+ 'Western Digital 1TB WD Blue PC Internal Hard Drive HDD - 7200 RPM,
+ SATA 6 Gb/s, 64 MB Cache, 3.5" - WD10EZEX'
+
+ """
+
+ @classmethod
+ async def create(cls, accesskey: str, timeout: float = 10.0):
+ """Create the async object."""
+ self = AsyncKeepa()
+ self.accesskey = accesskey
+ self.tokens_left = 0
+ self._timeout = timeout
+
+ # don't update the user status on init
+ self.status = {"tokensLeft": None, "refillIn": None, "refillRate": None, "timestamp": None}
+ return self
+
+ @property
+ def time_to_refill(self) -> float:
+ """Return the time to refill in seconds."""
+ # Get current timestamp in milliseconds from UNIX epoch
+ now = int(time.time() * 1000)
+ timeatrefile = self.status["timestamp"] + self.status["refillIn"]
+
+ # wait plus one second fudge factor
+ timetorefil = timeatrefile - now + 1000
+ if timetorefil < 0:
+ timetorefil = 0
+
+ # Account for negative tokens left
+ if self.tokens_left < 0:
+ timetorefil += (abs(self.tokens_left) / self.status["refillRate"]) * 60000
+
+ # Return value in seconds
+ return timetorefil / 1000.0
+
+ async def update_status(self) -> None:
+ """Update available tokens."""
+ self.status = await self._request("token", {"key": self.accesskey}, wait=False)
+
+ async def wait_for_tokens(self) -> None:
+ """Check if there are any remaining tokens and waits if none are available."""
+ await self.update_status()
+
+ # Wait if no tokens available
+ if self.tokens_left <= 0:
+ tdelay = self.time_to_refill
+ log.warning("Waiting %.0f seconds for additional tokens", tdelay)
+ await asyncio.sleep(tdelay)
+ await self.update_status()
+
+ @is_documented_by(Keepa.query)
+ async def query(
+ self,
+ items: str | Sequence[str],
+ stats: int | None = None,
+ domain: str = "US",
+ history: bool = True,
+ offers: int | None = None,
+ update: int | None = None,
+ to_datetime: bool = True,
+ rating: bool = False,
+ out_of_stock_as_nan: bool = True,
+ stock: bool = False,
+ product_code_is_asin: bool = True,
+ progress_bar: bool = True,
+ buybox: bool = False,
+ wait: bool = True,
+ days: int | None = None,
+ only_live_offers: bool | None = None,
+ raw: bool = False,
+ videos: bool = False,
+ aplus: bool = False,
+ extra_params: dict[str, Any] = {},
+ ):
+ """Documented in Keepa.query."""
+ if raw:
+ raise ValueError("Raw response is only available in the non-async class")
+
+ # Format items into numpy array
+ try:
+ items = format_items(items)
+ except BaseException:
+ raise Exception("Invalid product codes input")
+ assert len(items), "No valid product codes"
+
+ nitems = len(items)
+ if nitems == 1:
+ log.debug("Executing single product query")
+ else:
+ log.debug("Executing %d item product query", nitems)
+
+ # check offer input
+ if offers:
+ if not isinstance(offers, int):
+ raise TypeError('Parameter "offers" must be an interger')
+
+ if offers > 100 or offers < 20:
+ raise ValueError('Parameter "offers" must be between 20 and 100')
+
+ # Report time to completion
+ if self.status["refillRate"] is not None:
+ tcomplete = (
+ float(nitems - self.tokens_left) / self.status["refillRate"]
+ - (60000 - self.status["refillIn"]) / 60000.0
+ )
+ if tcomplete < 0.0:
+ tcomplete = 0.5
+ log.debug(
+ "Estimated time to complete %d request(s) is %.2f minutes",
+ nitems,
+ tcomplete,
+ )
+ log.debug("\twith a refill rate of %d token(s) per minute", self.status["refillRate"])
+
+ # product list
+ products = []
+
+ pbar = None
+ if progress_bar:
+ pbar = tqdm(total=nitems)
+
+ # Number of requests is dependent on the number of items and
+ # request limit. Use available tokens first
+ idx = 0 # or number complete
+ while idx < nitems:
+ nrequest = nitems - idx
+
+ # cap request
+ if nrequest > REQUEST_LIMIT:
+ nrequest = REQUEST_LIMIT
+
+ # request from keepa and increment current position
+ item_request = items[idx : idx + nrequest] # noqa: E203
+ response = await self._product_query(
+ item_request,
+ product_code_is_asin,
+ stats=stats,
+ domain=domain,
+ stock=stock,
+ offers=offers,
+ update=update,
+ history=history,
+ rating=rating,
+ to_datetime=to_datetime,
+ out_of_stock_as_nan=out_of_stock_as_nan,
+ buybox=buybox,
+ wait=wait,
+ days=days,
+ only_live_offers=only_live_offers,
+ videos=videos,
+ aplus=aplus,
+ **extra_params,
+ )
+ idx += nrequest
+ products.extend(response["products"])
+
+ if pbar is not None:
+ pbar.update(nrequest)
+
+ return products
+
+ @is_documented_by(Keepa._product_query)
+ async def _product_query(self, items, product_code_is_asin=True, **kwargs):
+ """Documented in Keepa._product_query."""
+ # ASINs convert to comma joined string
+ assert len(items) <= 100
+
+ if product_code_is_asin:
+ kwargs["asin"] = ",".join(items)
+ else:
+ kwargs["code"] = ",".join(items)
+
+ kwargs["key"] = self.accesskey
+ kwargs["domain"] = _domain_to_dcode(kwargs["domain"])
+
+ # Convert bool values to 0 and 1.
+ kwargs["stock"] = int(kwargs["stock"])
+ kwargs["history"] = int(kwargs["history"])
+ kwargs["rating"] = int(kwargs["rating"])
+ kwargs["buybox"] = int(kwargs["buybox"])
+
+ if kwargs["update"] is None:
+ del kwargs["update"]
+ else:
+ kwargs["update"] = int(kwargs["update"])
+
+ if kwargs["offers"] is None:
+ del kwargs["offers"]
+ else:
+ kwargs["offers"] = int(kwargs["offers"])
+
+ if kwargs["only_live_offers"] is None:
+ del kwargs["only_live_offers"]
+ else:
+ kwargs["only-live-offers"] = int(kwargs.pop("only_live_offers"))
+ # Keepa's param actually doesn't use snake_case.
+ # Keeping with snake case for consistency
+
+ if kwargs["days"] is None:
+ del kwargs["days"]
+ else:
+ assert kwargs["days"] > 0
+
+ if kwargs["stats"] is None:
+ del kwargs["stats"]
+
+ # videos and aplus must be ints
+ kwargs["videos"] = int(kwargs["videos"])
+ kwargs["aplus"] = int(kwargs["aplus"])
+
+ out_of_stock_as_nan = kwargs.pop("out_of_stock_as_nan", True)
+ to_datetime = kwargs.pop("to_datetime", True)
+
+ # Query and replace csv with parsed data if history enabled
+ wait = kwargs.get("wait")
+ kwargs.pop("wait", None)
+
+ raw_response = kwargs.pop("raw", False)
+ response = await self._request("product", kwargs, wait=wait, raw_response=raw_response)
+ if kwargs["history"]:
+ if "products" not in response:
+ raise RuntimeError("No products in response. Possibly invalid ASINs")
+
+ for product in response["products"]:
+ if product["csv"]: # if data exists
+ product["data"] = parse_csv(product["csv"], to_datetime, out_of_stock_as_nan)
+
+ if kwargs.get("stats", None):
+ for product in response["products"]:
+ stats = product.get("stats", None)
+ if stats:
+ product["stats_parsed"] = _parse_stats(stats, to_datetime)
+
+ return response
+
+ @is_documented_by(Keepa.best_sellers_query)
+ async def best_sellers_query(
+ self,
+ category: str,
+ rank_avg_range: Literal[0, 30, 90, 180] = 0,
+ variations: bool = False,
+ sublist: bool = False,
+ domain: str | Domain = "US",
+ wait: bool = True,
+ ):
+ """Documented by Keepa.best_sellers_query."""
+ payload = {
+ "key": self.accesskey,
+ "domain": _domain_to_dcode(domain),
+ "variations": int(variations),
+ "sublist": int(sublist),
+ "category": category,
+ "range": rank_avg_range,
+ }
+
+ response = await self._request("bestsellers", payload, wait=wait)
+ if "bestSellersList" not in response:
+ raise RuntimeError(f"Best sellers search results for {category} not yet available")
+ return response["bestSellersList"]["asinList"]
+
+ @is_documented_by(Keepa.search_for_categories)
+ async def search_for_categories(
+ self, searchterm, domain: str | Domain = "US", wait: bool = True
+ ):
+ """Documented by Keepa.search_for_categories."""
+ payload = {
+ "key": self.accesskey,
+ "domain": _domain_to_dcode(domain),
+ "type": "category",
+ "term": searchterm,
+ }
+
+ response = await self._request("search", payload, wait=wait)
+ if response["categories"] == {}: # pragma no cover
+ raise Exception(
+ "Categories search results not yet available " + "or no search terms found."
+ )
+ else:
+ return response["categories"]
+
+ @is_documented_by(Keepa.category_lookup)
+ async def category_lookup(
+ self, category_id, domain: str | Domain = "US", include_parents=0, wait: bool = True
+ ):
+ """Documented by Keepa.category_lookup."""
+ payload = {
+ "key": self.accesskey,
+ "domain": _domain_to_dcode(domain),
+ "category": category_id,
+ "parents": include_parents,
+ }
+
+ response = await self._request("category", payload, wait=wait)
+ if response["categories"] == {}: # pragma no cover
+ raise Exception("Category lookup results not yet available or no" + "match found.")
+ else:
+ return response["categories"]
+
+ @is_documented_by(Keepa.seller_query)
+ async def seller_query(
+ self,
+ seller_id,
+ domain: str | Domain = "US",
+ to_datetime=True,
+ storefront=False,
+ update=None,
+ wait: bool = True,
+ ):
+ """Documented by Keepa.sellerer_query."""
+ if isinstance(seller_id, list):
+ if len(seller_id) > 100:
+ err_str = "seller_id can contain at maximum 100 sellers"
+ raise RuntimeError(err_str)
+ seller = ",".join(seller_id)
+ else:
+ seller = seller_id
+
+ payload = {
+ "key": self.accesskey,
+ "domain": _domain_to_dcode(domain),
+ "seller": seller,
+ }
+
+ if storefront:
+ payload["storefront"] = int(storefront)
+ if update:
+ payload["update"] = update
+
+ response = await self._request("seller", payload, wait=wait)
+ return _parse_seller(response["sellers"], to_datetime)
+
+ @is_documented_by(Keepa.product_finder)
+ async def product_finder(
+ self,
+ product_parms: dict[str, Any] | ProductParams,
+ domain: str | Domain = "US",
+ wait: bool = True,
+ n_products: int = 50,
+ ) -> list[str]:
+ """Documented by Keepa.product_finder."""
+ if isinstance(product_parms, dict):
+ product_parms_valid = ProductParams(**product_parms)
+ else:
+ product_parms_valid = product_parms
+ product_parms_dict = product_parms_valid.model_dump(exclude_none=True)
+ product_parms_dict.setdefault("perPage", n_products)
+ payload = {
+ "key": self.accesskey,
+ "domain": _domain_to_dcode(domain),
+ "selection": json.dumps(product_parms_dict),
+ }
+
+ response = await self._request("query", payload, wait=wait)
+ return response["asinList"]
+
+ @is_documented_by(Keepa.deals)
+ async def deals(self, deal_parms, domain: str | Domain = "US", wait: bool = True):
+ """Documented in Keepa.deals."""
+ # verify valid keys
+ for key in deal_parms:
+ if key not in DEAL_REQUEST_KEYS:
+ raise ValueError(f'Invalid key "{key}"')
+
+ # verify json type
+ key_type = DEAL_REQUEST_KEYS[key]
+ deal_parms[key] = key_type(deal_parms[key])
+
+ deal_parms.setdefault("priceTypes", 0)
+
+ payload = {
+ "key": self.accesskey,
+ "domain": _domain_to_dcode(domain),
+ "selection": json.dumps(deal_parms),
+ }
+
+ deals = await self._request("deal", payload, wait=wait)
+ return deals["deals"]
+
+ @is_documented_by(Keepa.download_graph_image)
+ async def download_graph_image(
+ self,
+ asin: str,
+ filename: str | Path,
+ domain: str | Domain = "US",
+ wait: bool = True,
+ **graph_kwargs: dict[str, Any],
+ ) -> None:
+ """Documented in Keepa.download_graph_image."""
+ payload = {"asin": asin, "key": self.accesskey, "domain": _domain_to_dcode(domain)}
+ payload.update(graph_kwargs)
+
+ async with aiohttp.ClientSession() as session:
+ async with session.get(
+ "https://api.keepa.com/graphimage",
+ params=payload,
+ timeout=self._timeout,
+ ) as resp:
+ first_chunk = True
+ filename = Path(filename)
+ with open(filename, "wb") as f:
+ async for chunk in resp.content.iter_chunked(8192):
+ if first_chunk:
+ if not chunk.startswith(b"\x89PNG\r\n\x1a\n"):
+ raise ValueError(
+ "Response from api.keepa.com/graphimage is not a valid "
+ "PNG image"
+ )
+ first_chunk = False
+ f.write(chunk)
+
+ async def _request(
+ self,
+ request_type: str,
+ payload: dict[str, Any],
+ wait: bool = True,
+ raw_response: bool = False,
+ is_json: bool = True,
+ ):
+ """Documented in Keepa._request."""
+ while True:
+ async with aiohttp.ClientSession() as session:
+ async with session.get(
+ f"https://api.keepa.com/{request_type}/?",
+ params=payload,
+ timeout=self._timeout,
+ ) as raw:
+ status_code = str(raw.status)
+
+ if not is_json:
+ return raw
+
+ try:
+ response = await raw.json()
+ except Exception:
+ raise RuntimeError(f"Invalid JSON from Keepa API (status {status_code})")
+
+ # user status is always returned
+ if "tokensLeft" in response:
+ self.tokens_left = response["tokensLeft"]
+ self.status["tokensLeft"] = self.tokens_left
+ log.info("%d tokens remain", self.tokens_left)
+ for key in ["refillIn", "refillRate", "timestamp"]:
+ if key in response:
+ self.status[key] = response[key]
+
+ if status_code == "200":
+ if raw_response:
+ return raw
+ return response
+
+ if status_code == "429" and wait:
+ tdelay = self.time_to_refill
+ log.warning("Waiting %.0f seconds for additional tokens", tdelay)
+ time.sleep(tdelay)
+ continue
+
+ # otherwise, it's an error code
+ if status_code in SCODES:
+ raise RuntimeError(SCODES[status_code])
+ raise RuntimeError(f"REQUEST_FAILED. Status code: {status_code}")
+
+
+def convert_offer_history(csv, to_datetime=True):
+ """Convert an offer history to human readable values.
+
+ Parameters
+ ----------
+ csv : list
+ Offer list csv obtained from ``['offerCSV']``
+
+ to_datetime : bool, optional
+ Modifies ``numpy`` minutes to ``datetime.datetime`` values.
+ Default ``True``.
+
+ Returns
+ -------
+ times : numpy.ndarray
+ List of time values for an offer history.
+
+ prices : numpy.ndarray
+ Price (including shipping) of an offer for each time at an
+ index of times.
+
+ """
+ # convert these values to numpy arrays
+ times = csv[::3]
+ values = np.array(csv[1::3])
+ values += np.array(csv[2::3]) # add in shipping
+
+ # convert to dollars and datetimes
+ times = keepa_minutes_to_time(times, to_datetime)
+ prices = values / 100.0
+ return times, prices
+
+
+def _str_to_bool(string: str) -> bool:
+ if string:
+ return bool(int(string))
+ return False
+
+
+def process_used_buybox(buybox_info: list[str]) -> pd.DataFrame:
+ """
+ Process used buybox information to create a Pandas DataFrame.
+
+ Parameters
+ ----------
+ buybox_info : list of str
+ A list containing information about used buybox in a specific order:
+ [Keepa time minutes, seller id, condition, isFBA, ...]
+
+ Returns
+ -------
+ pd.DataFrame
+ A DataFrame containing four columns:
+ - 'datetime': Datetime objects converted from Keepa time minutes.
+ - 'user_id': String representing the seller ID.
+ - 'condition': String representing the condition of the product.
+ - 'isFBA': Boolean indicating whether the offer is Fulfilled by Amazon.
+
+ Notes
+ -----
+ The `condition` is mapped from its code to a descriptive string.
+ The `isFBA` field is converted to a boolean.
+
+ Examples
+ --------
+ Load in product offers and convert the buy box data into a
+ ``pandas.DataFrame``.
+
+ >>> import keepa
+ >>> key = ""
+ >>> api = keepa.Keepa(key)
+ >>> response = api.query("B0088PUEPK", offers=20)
+ >>> product = response[0]
+ >>> buybox_info = product["buyBoxUsedHistory"]
+ >>> df = keepa.process_used_buybox(buybox_info)
+ datetime user_id condition isFBA
+ 0 2022-11-02 16:46:00 A1QUAC68EAM09F Used - Like New True
+ 1 2022-11-13 10:36:00 A18WXU4I7YR6UA Used - Very Good False
+ 2 2022-11-15 23:50:00 AYUGEV9WZ4X5O Used - Like New False
+ 3 2022-11-17 06:16:00 A18WXU4I7YR6UA Used - Very Good False
+ 4 2022-11-17 10:56:00 AYUGEV9WZ4X5O Used - Like New False
+ .. ... ... ... ...
+ 115 2023-10-23 10:00:00 AYUGEV9WZ4X5O Used - Like New False
+ 116 2023-10-25 21:14:00 A1U9HDFCZO1A84 Used - Like New False
+ 117 2023-10-26 04:08:00 AYUGEV9WZ4X5O Used - Like New False
+ 118 2023-10-27 08:14:00 A1U9HDFCZO1A84 Used - Like New False
+ 119 2023-10-27 12:34:00 AYUGEV9WZ4X5O Used - Like New False
+
+ """
+ datetime_arr = []
+ user_id_arr = []
+ condition_map = {
+ "": "Unknown",
+ "2": "Used - Like New",
+ "3": "Used - Very Good",
+ "4": "Used - Good",
+ "5": "Used - Acceptable",
+ }
+ condition_arr = []
+ isFBA_arr = []
+
+ for i in range(0, len(buybox_info), 4):
+ keepa_time = int(buybox_info[i])
+ datetime_arr.append(keepa_minutes_to_time([keepa_time])[0])
+ user_id_arr.append(buybox_info[i + 1])
+ condition_arr.append(condition_map[buybox_info[i + 2]])
+ isFBA_arr.append(_str_to_bool(buybox_info[i + 3]))
+
+ df = pd.DataFrame(
+ {
+ "datetime": datetime_arr,
+ "user_id": user_id_arr,
+ "condition": condition_arr,
+ "isFBA": isFBA_arr,
+ }
+ )
+
+ return df
+
+
+def keepa_minutes_to_time(minutes, to_datetime=True):
+ """Accept an array or list of minutes and converts it to a numpy datetime array.
+
+ Assumes that keepa time is from keepa minutes from ordinal.
+ """
+ # Convert to timedelta64 and shift
+ dt = np.array(minutes, dtype="timedelta64[m]")
+ dt = dt + KEEPA_ST_ORDINAL # shift from ordinal
+
+ # Convert to datetime if requested
+ if to_datetime:
+ return dt.astype(datetime.datetime)
+ return dt
+
+
+def run_and_get(coro):
+ """Attempt to run an async request."""
+ try:
+ loop = asyncio.get_event_loop()
+ except RuntimeError:
+ loop = asyncio.new_event_loop()
+ task = loop.create_task(coro)
+ loop.run_until_complete(task)
+ return task.result()
diff --git a/keepa/plotting.py b/src/keepa/plotting.py
similarity index 98%
rename from keepa/plotting.py
rename to src/keepa/plotting.py
index 5944606..a0e8c92 100644
--- a/keepa/plotting.py
+++ b/src/keepa/plotting.py
@@ -1,4 +1,5 @@
"""Plotting module product data returned from keepa interface module."""
+
import numpy as np
from keepa.interface import keepa_minutes_to_time, parse_csv
@@ -71,9 +72,7 @@ def plot_product(
elif "SALES" in key and "time" not in key:
if product["data"][key].size > 1:
x = np.append(product["data"][key + "_time"], lstupdate)
- y = np.append(product["data"][key], product["data"][key][-1]).astype(
- float
- )
+ y = np.append(product["data"][key], product["data"][key][-1]).astype(float)
replace_invalid(y)
if np.all(np.isnan(y)):
diff --git a/src/keepa/py.typed b/src/keepa/py.typed
new file mode 100644
index 0000000..5fcb852
--- /dev/null
+++ b/src/keepa/py.typed
@@ -0,0 +1 @@
+partial
\ No newline at end of file
diff --git a/keepa/query_keys.py b/src/keepa/query_keys.py
similarity index 100%
rename from keepa/query_keys.py
rename to src/keepa/query_keys.py
diff --git a/tests/test_async_interface.py b/tests/test_async_interface.py
index 0ff7717..8bfea3c 100644
--- a/tests/test_async_interface.py
+++ b/tests/test_async_interface.py
@@ -1,3 +1,8 @@
+"""
+Test the asynchronous interface to the keepa backend.
+"""
+
+from pathlib import Path
import datetime
import os
import warnings
@@ -26,8 +31,10 @@
TESTINGKEY = os.environ["KEEPAKEY"]
WEAKTESTINGKEY = os.environ["WEAKKEEPAKEY"]
-# The Great Gatsby: The Original 1925 Edition (F. Scott Fitzgerald Classics)
-PRODUCT_ASIN = "B09X6JCFF5"
+# Dead Man's Hand (The Unorthodox Chronicles)
+# just need an active product with a buybox
+PRODUCT_ASIN = "0593440412"
+HARD_DRIVE_PRODUCT_ASIN = "B0088PUEPK"
# ASINs of a bunch of chairs
# categories = API.search_for_categories('chairs')
@@ -84,8 +91,6 @@
@pytest_asyncio.fixture()
async def api():
keepa_api = await keepa.AsyncKeepa.create(TESTINGKEY)
- assert keepa_api.tokens_left
- assert keepa_api.time_to_refill >= 0
yield keepa_api
@@ -109,6 +114,14 @@ async def test_product_finder_categories(api):
assert products
+@pytest.mark.asyncio
+async def test_extra_params(api):
+ # simply ensure that extra parameters are passed. Since this is a duplicate
+ # parameter, it's expected to fail.
+ with pytest.raises(TypeError):
+ await api.query("B0DJHC1PL8", extra_params={"rating": 1})
+
+
@pytest.mark.asyncio
async def test_product_finder_query(api):
product_parms = {
@@ -120,12 +133,22 @@ async def test_product_finder_query(api):
asins = await api.product_finder(product_parms)
assert asins
+ # using ProductParams
+ product_parms = keepa.ProductParams(
+ author="jim butcher",
+ page=1,
+ perPage=50,
+ categories_exclude=["1055398"],
+ )
+ asins = api.product_finder(product_parms)
+ assert asins
+
# def test_throttling(api):
# api = keepa.Keepa(WEAKTESTINGKEY)
# keepa.interface.REQLIM = 20
-# # exaust tokens
+# # exhaust tokens
# while api.tokens_left > 0:
# api.query(PRODUCT_ASINS[:5])
@@ -155,8 +178,8 @@ async def test_productquery_nohistory(api):
@pytest.mark.asyncio
async def test_not_an_asin(api):
- with pytest.raises(Exception):
- asins = ["0000000000", "000000000x"]
+ with pytest.raises(RuntimeError, match="invalid ASINs"):
+ asins = ["XXXXXXXXXX"]
await api.query(asins)
@@ -167,6 +190,7 @@ async def test_isbn13(api):
@pytest.mark.asyncio
+@pytest.mark.xfail # will fail if not run in a while due to timeout
async def test_buybox(api):
request = await api.query(PRODUCT_ASIN, history=True, buybox=True)
product = request[0]
@@ -181,7 +205,7 @@ async def test_productquery_update(api):
# should be live data
now = datetime.datetime.now()
delta = now - product["data"]["USED_time"][-1]
- assert delta.days <= 35
+ assert delta.days <= 60
# check for empty arrays
history = product["data"]
@@ -199,10 +223,11 @@ async def test_productquery_update(api):
assert "stats" in product
# no offers requested by default
- assert product["offers"] is None
+ assert "offers" not in product or product["offers"] is None
@pytest.mark.asyncio
+@pytest.mark.flaky(reruns=3)
async def test_productquery_offers(api):
request = await api.query(PRODUCT_ASIN, offers=20)
product = request[0]
@@ -233,7 +258,7 @@ async def test_productquery_offers_multiple(api):
asins = np.unique([product["asin"] for product in products])
assert len(asins) == len(PRODUCT_ASINS)
- assert np.in1d(asins, PRODUCT_ASINS).all()
+ assert np.isin(asins, PRODUCT_ASINS).all()
@pytest.mark.asyncio
@@ -258,6 +283,15 @@ async def test_bestsellers(api):
assert len(asins) == valid_asins.size
+@pytest.mark.asyncio
+@pytest.mark.xfail # will fail if not run in a while due to timeout
+async def test_buybox_used(api):
+ # history must be true to get used buybox
+ request = await api.query(HARD_DRIVE_PRODUCT_ASIN, history=True, offers=20)
+ df = keepa.process_used_buybox(request[0]["buyBoxUsedHistory"])
+ assert isinstance(df, pd.DataFrame)
+
+
@pytest.mark.asyncio
async def test_categories(api):
categories = await api.search_for_categories("chairs")
@@ -283,7 +317,7 @@ async def test_invalid_category(api):
async def test_stock(api):
request = await api.query(PRODUCT_ASIN, history=False, stock=True, offers=20)
- # all live offers must have stock
+ # all live offers should have stock
product = request[0]
assert product["offersSuccessful"]
live = product["liveOffersOrder"]
@@ -291,16 +325,32 @@ async def test_stock(api):
for offer in product["offers"]:
if offer["offerId"] in live:
if "stockCSV" in offer:
- assert offer["stockCSV"][-1]
+ if not offer["stockCSV"][-1]:
+ warnings.warn(f"No live offers for {PRODUCT_ASIN}")
else:
warnings.warn(f"No live offers for {PRODUCT_ASIN}")
@pytest.mark.asyncio
-async def test_keepatime(api):
- keepa_st_ordinal = datetime.datetime(2011, 1, 1)
- assert keepa_st_ordinal == keepa.keepa_minutes_to_time(0)
- assert keepa.keepa_minutes_to_time(0, to_datetime=False)
+async def test_to_datetime_parm(api):
+ request = await api.query(PRODUCT_ASIN, to_datetime=True)
+ product = request[0]
+ times = product["data"]["AMAZON_time"]
+ assert isinstance(times[0], datetime.datetime)
+
+ request = await api.query(PRODUCT_ASIN, to_datetime=False)
+ product = request[0]
+ times = product["data"]["AMAZON_time"]
+ assert times[0].dtype == " None:
+ filename = tmp_path / "out.png"
+ await api.download_graph_image(PRODUCT_ASIN, filename)
+
+ data = filename.read_bytes()
+ assert data.startswith(b"\x89PNG\r\n\x1a\n")
@pytest.mark.asyncio
diff --git a/tests/test_interface.py b/tests/test_interface.py
index 34f5abe..df67867 100644
--- a/tests/test_interface.py
+++ b/tests/test_interface.py
@@ -1,3 +1,8 @@
+"""
+Test the synchronous interface to the keepa backend.
+"""
+
+from pathlib import Path
import datetime
from itertools import chain
import os
@@ -10,9 +15,9 @@
import keepa
from keepa import keepa_minutes_to_time
+from keepa import Keepa
# reduce the request limit for testing
-
keepa.interface.REQLIM = 2
path = os.path.dirname(os.path.realpath(__file__))
@@ -29,9 +34,11 @@
TESTINGKEY = os.environ["KEEPAKEY"]
WEAKTESTINGKEY = os.environ["WEAKKEEPAKEY"]
-# The Great Gatsby: The Original 1925 Edition (F. Scott Fitzgerald Classics)
-PRODUCT_ASIN = "B09X6JCFF5"
-
+# Dead Man's Hand (The Unorthodox Chronicles)
+# just need an active product with a buybox
+PRODUCT_ASIN = "0593440412"
+HARD_DRIVE_PRODUCT_ASIN = "B0088PUEPK"
+VIDEO_ASIN = "B0060CU5DE"
# ASINs of a bunch of chairs generated with
# categories = API.search_for_categories('chairs')
@@ -87,14 +94,11 @@
# open connection to keepa
@pytest.fixture(scope="module")
-def api():
- keepa_api = keepa.Keepa(TESTINGKEY)
- assert keepa_api.tokens_left
- assert keepa_api.time_to_refill >= 0
- return keepa_api
+def api() -> Keepa:
+ return Keepa(TESTINGKEY)
-def test_deals(api):
+def test_deals(api: Keepa) -> None:
deal_parms = {
"page": 0,
"domainId": 1,
@@ -114,32 +118,42 @@ def test_invalidkey():
def test_deadkey():
with pytest.raises(Exception):
# this key returns "payment required"
- deadkey = "8ueigrvvnsp5too0atlb5f11veinerkud" "47p686ekr7vgr9qtj1t1tle15fffkkm"
+ deadkey = "8ueigrvvnsp5too0atlb5f11veinerkud47p686ekr7vgr9qtj1t1tle15fffkkm"
keepa.Api(deadkey)
+def test_extra_params(api: keepa.Keepa) -> None:
+ # simply ensure that extra parameters are passed. Since this is a duplicate
+ # parameter, it's expected to fail.
+ with pytest.raises(TypeError):
+ api.query("B0DJHC1PL8", extra_params={"rating": 1})
+
+
def test_product_finder_categories(api):
product_parms = {"categories_include": ["1055398"]}
products = api.product_finder(product_parms)
assert products
-def test_product_finder_query(api):
+def test_product_finder_query(api: keepa.Keepa) -> None:
+ """Test product finder and ensure perPage overrides n_products."""
+ per_page_n_products = 50
product_parms = {
"author": "jim butcher",
"page": 1,
- "perPage": 50,
+ "perPage": per_page_n_products,
"categories_exclude": ["1055398"],
}
- asins = api.product_finder(product_parms)
+ asins = api.product_finder(product_parms, n_products=100)
assert asins
+ assert len(asins) == per_page_n_products
# def test_throttling(api):
# api = keepa.Keepa(WEAKTESTINGKEY)
# keepa.interface.REQLIM = 20
-# # exaust tokens
+# # exhaust tokens
# while api.tokens_left > 0:
# api.query(PRODUCT_ASINS[:5])
@@ -168,8 +182,8 @@ def test_productquery_nohistory(api):
def test_not_an_asin(api):
- with pytest.raises(Exception):
- asins = ["0000000000", "000000000x"]
+ with pytest.raises(RuntimeError, match="invalid ASINs"):
+ asins = ["XXXXXXXXXX"]
api.query(asins)
@@ -178,7 +192,7 @@ def test_isbn13(api):
api.query(isbn13, product_code_is_asin=False, history=False)
-def test_buybox(api):
+def test_buybox(api: keepa.Keepa) -> None:
request = api.query(PRODUCT_ASIN, history=True, buybox=True)
product = request[0]
assert "BUY_BOX_SHIPPING" in product["data"]
@@ -191,7 +205,7 @@ def test_productquery_update(api):
# should be live data
now = datetime.datetime.now()
delta = now - product["data"]["USED_time"][-1]
- assert delta.days <= 35
+ assert delta.days <= 60
# check for empty arrays
history = product["data"]
@@ -209,7 +223,7 @@ def test_productquery_update(api):
assert "stats" in product
# no offers requested by default
- assert product["offers"] is None
+ assert "offers" not in product or product["offers"] is None
def test_productquery_offers(api):
@@ -233,9 +247,7 @@ def test_productquery_offers(api):
def test_productquery_only_live_offers(api):
"""Tests that no historical offer data was returned from response if only_live_offers param was specified."""
max_offers = 20
- request = api.query(
- PRODUCT_ASIN, offers=max_offers, only_live_offers=True, history=False
- )
+ request = api.query(PRODUCT_ASIN, offers=max_offers, only_live_offers=True, history=False)
# there may not be any offers
product_offers = request[0]["offers"]
@@ -261,9 +273,7 @@ def test_productquery_days(api, max_days: int = 5):
def convert(minutes):
"""Convert keepaminutes to time."""
- times = set(
- keepa_minutes_to_time(keepa_minute).date() for keepa_minute in minutes
- )
+ times = {keepa_minutes_to_time(keepa_minute).date() for keepa_minute in minutes}
return list(times)
# Converting each field's list of keepa minutes into flat list of unique days.
@@ -272,9 +282,7 @@ def convert(minutes):
buy_box_seller_id_history = convert(product["buyBoxSellerIdHistory"][0::2])
offers_csv = list(convert(offer["offerCSV"][0::3]) for offer in product["offers"])
df_dates = list(
- list(df.axes[0])
- for df_name, df in product["data"].items()
- if "df_" in df_name and any(df)
+ list(df.axes[0]) for df_name, df in product["data"].items() if "df_" in df_name and any(df)
)
df_dates = list(
list(datetime.date(year=ts.year, month=ts.month, day=ts.day) for ts in stamps)
@@ -313,18 +321,20 @@ def test_productquery_offers_multiple(api):
asins = np.unique([product["asin"] for product in products])
assert len(asins) == len(PRODUCT_ASINS)
- assert np.in1d(asins, PRODUCT_ASINS).all()
+ assert np.isin(asins, PRODUCT_ASINS).all()
-def test_domain(api):
- request = api.query(PRODUCT_ASIN, history=False, domain="DE")
+def test_domain(api: Keepa) -> None:
+ """A domain different than the default."""
+ asin = "3492704794"
+ request = api.query(asin, history=False, domain=keepa.Domain.DE)
product = request[0]
- assert product["asin"] == PRODUCT_ASIN
+ assert product["asin"] == asin
def test_invalid_domain(api):
with pytest.raises(ValueError):
- api.query(PRODUCT_ASIN, history=False, domain="XX")
+ api.query(PRODUCT_ASIN, domain="XX")
def test_bestsellers(api):
@@ -335,6 +345,13 @@ def test_bestsellers(api):
assert len(asins) == valid_asins.size
+@pytest.mark.xfail # will fail if not run in a while due to timeout
+def test_buybox_used(api):
+ request = api.query(HARD_DRIVE_PRODUCT_ASIN, history=True, offers=20)
+ df = keepa.process_used_buybox(request[0]["buyBoxUsedHistory"])
+ assert isinstance(df, pd.DataFrame)
+
+
def test_categories(api):
categories = api.search_for_categories("chairs")
catids = list(categories.keys())
@@ -364,7 +381,8 @@ def test_stock(api):
for offer in product["offers"]:
if offer["offerId"] in live:
if "stockCSV" in offer:
- assert offer["stockCSV"][-1]
+ if not offer["stockCSV"][-1]:
+ warnings.warn(f"No live offers for {PRODUCT_ASIN}")
else:
warnings.warn(f"No live offers for {PRODUCT_ASIN}")
@@ -375,7 +393,43 @@ def test_keepatime(api):
assert keepa.keepa_minutes_to_time(0, to_datetime=False)
-def test_plotting(api):
+def test_to_datetime_parm(api: Keepa) -> None:
+ product = api.query(PRODUCT_ASIN, to_datetime=True)[0]
+ times = product["data"]["AMAZON_time"]
+ assert isinstance(times[0], datetime.datetime)
+
+ product = api.query(PRODUCT_ASIN, to_datetime=False)[0]
+ times = product["data"]["AMAZON_time"]
+ assert times[0].dtype == " None:
+ filename = tmp_path / "out.png"
+ api.download_graph_image(
+ asin=PRODUCT_ASIN,
+ filename=filename,
+ domain="US",
+ amazon=1,
+ new=1,
+ used=1,
+ bb=1,
+ fba=1,
+ range=365,
+ width=800,
+ height=400,
+ cBackground="ffffff",
+ cAmazon="FFA500",
+ cNew="8888dd",
+ cUsed="444444",
+ cBB="ff00b4",
+ cFBA="ff5722",
+ )
+
+ data = filename.read_bytes()
+ assert data.startswith(b"\x89PNG\r\n\x1a\n")
+
+
+def test_plotting(api: Keepa) -> None:
request = api.query(PRODUCT_ASIN, history=True)
product = request[0]
keepa.plot_product(product, show=False)
@@ -408,3 +462,21 @@ def test_seller_query_long_list(api):
seller_id = ["A2L77EE7U53NWQ"] * 200
with pytest.raises(RuntimeError):
api.seller_query(seller_id)
+
+
+def test_video_query(api: keepa.Keepa) -> None:
+ """Test if the videos query parameter works."""
+ response = api.query("B00UFMKSDW", history=False, videos=False)
+ product = response[0]
+ assert "videos" not in product
+
+ response = api.query("B00UFMKSDW", history=False, videos=True)
+ product = response[0]
+ assert "videos" in product
+
+
+def test_aplus(api: keepa.Keepa) -> None:
+ product_nominal = api.query("B0DDDD8WD6", history=False, aplus=False)[0]
+ assert "aPlus" not in product_nominal
+ product_aplus = api.query("B0DDDD8WD6", history=False, aplus=True)[0]
+ assert "aPlus" in product_aplus