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