diff --git a/.github/workflows/testing-and-deployment.yml b/.github/workflows/testing-and-deployment.yml index f9151db..422dc9e 100644 --- a/.github/workflows/testing-and-deployment.yml +++ b/.github/workflows/testing-and-deployment.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.10', '3.11', '3.12', '3.13', '3.14'] + python-version: ['3.10', '3.14'] env: KEEPAKEY: ${{ secrets.KEEPAKEY }} diff --git a/.gitignore b/.gitignore index 6416e03..656291f 100644 --- a/.gitignore +++ b/.gitignore @@ -29,10 +29,11 @@ tests/htmlcov/ .coverage .hypothesis .venv +docs/source/api/ # key storage tests/key tests/weak_key # pycharm IDE -.idea \ No newline at end of file +.idea diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c424380..17cce95 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,7 +20,7 @@ repos: rev: v2.4.2 hooks: - id: codespell - args: [-S ./docs/\*] + args: [-S ./docs/\*, --ignore-words-list, thirdparty] - repo: https://github.com/pycqa/pydocstyle rev: 6.3.0 hooks: diff --git a/README.rst b/README.rst index aedfc25..6f70cc8 100644 --- a/README.rst +++ b/README.rst @@ -30,14 +30,16 @@ This library is compatible with Python >= 3.10 and requires: - ``numpy`` - ``aiohttp`` -- ``matplotlib`` +- ``pandas`` +- ``pydantic >= 2`` +- ``requests`` - ``tqdm`` 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 `_. +Interfacing with ``keepa`` requires an access key and a monthly subscription +from `Keepa Pricing `_. Installation ------------ @@ -69,6 +71,21 @@ Brief Example # Plot result (requires matplotlib) keepa.plot_product(products[0]) +Typed responses are available with ``typed=True`` for users who prefer +Pydantic models while keeping the default dictionary output unchanged. + +.. code:: python + + products = api.query('B0088PUEPK', typed=True) + product = products[0] + print(product.asin) + print(product.title) + product_dict = product.model_dump(exclude_none=True, by_alias=True) + +See the `typed response documentation +`_ Typed Responses page for +supported methods, complete return shapes, serialization, and async usage. + .. figure:: https://github.com/akaszynski/keepa/raw/main/docs/source/images/Product_Price_Plot.png :width: 500pt @@ -80,10 +97,10 @@ Brief Example Product Offers Plot -Brief Example using async +Brief Example Using Async ------------------------- -Here's an example of obtaining a product and plotting its price and -offer history using the ``keepa.AsyncKeepa`` class: +Here's an example of finding product ASINs using the +``keepa.AsyncKeepa`` class: .. code:: python @@ -92,7 +109,7 @@ offer history using the ``keepa.AsyncKeepa`` class: >>> product_parms = {'author': 'jim butcher'} >>> async def main(): ... key = '' - ... api = await keepa.AsyncKeepa().create(key) + ... api = await keepa.AsyncKeepa.create(key) ... return await api.product_finder(product_parms) >>> asins = asyncio.run(main()) >>> asins @@ -113,7 +130,7 @@ keepa interface. >>> import keepa >>> async def main(): ... key = '' - ... api = await keepa.AsyncKeepa().create(key) + ... api = await keepa.AsyncKeepa.create(key) ... return await api.query('B0088PUEPK') >>> response = asyncio.run(main()) >>> response[0]['title'] @@ -141,20 +158,19 @@ Single ASIN query # See help(api.query) for available options when querying the API -You can use keepa witch async / await too +The asynchronous client uses the same query interface: .. code:: python + import asyncio import keepa - accesskey = 'XXXXXXXXXXXXXXXX' # enter real access key here - api = await keepa.AsyncKeepa.create(accesskey) - - -Single ASIN query (async) -.. code:: python + async def main(): + accesskey = 'XXXXXXXXXXXXXXXX' # enter real access key here + api = await keepa.AsyncKeepa.create(accesskey) + return await api.query('059035342X') - products = await api.query('059035342X') + products = asyncio.run(main()) Multiple ASIN query from List @@ -168,10 +184,14 @@ Multiple ASIN query from numpy array .. code:: python + import numpy as np + asins = np.asarray(['0022841350', '0022841369', '0022841369', '0022841369']) products = api.query(asins) -Products is a list of product data with one entry per successful result from the Keepa server. Each entry is a dictionary containing the same product data available from `Amazon `_. +``products`` is a list with one entry per successful result from the Keepa +server. By default, each entry is a dictionary containing the available +Amazon product data. .. code:: python @@ -182,20 +202,25 @@ Products is a list of product data with one entry per successful result from the print('ASIN is ' + products[0]['asin']) print('Title is ' + products[0]['title']) -The raw data is contained within each product result. Raw data is stored as a dictionary with each key paired with its associated time history. + # Index batch results by ASIN when random access is more convenient + products_by_asin = {product['asin']: product for product in products} + +When Keepa has history for a product, ``data`` contains arrays paired with +corresponding ``*_time`` arrays. Individual history types may be absent. .. code:: python # Access new price history and associated time data - newprice = products[0]['data']['NEW'] - newpricetime = products[0]['data']['NEW_time'] + history = products[0].get('data', {}) + newprice = history.get('NEW', []) + newpricetime = history.get('NEW_time', []) # Can be plotted with matplotlib using: import matplotlib.pyplot as plt plt.step(newpricetime, newprice, where='pre') # Keys can be listed by - print(products[0]['data'].keys()) + print(history.keys()) The product history can also be plotted from the module if ``matplotlib`` is installed @@ -237,12 +262,13 @@ You can obtain the offers history for an ASIN (or multiple ASINs) using the ``of plt.step(offer_times[i], offer_prices[i]) plt.show() -If you plan to do a lot of simulatneous query, you might want to speedup query using -``wait=False`` arguments. +By default, the client waits for Keepa tokens when necessary. Use ``wait=False`` +only when your application manages token availability itself; it does not make +the API response faster and may produce a token error. .. code:: python - products = await api.query('059035342X', wait=False) + products = api.query('059035342X', wait=False) Buy Box Statistics @@ -297,8 +323,8 @@ Finally, `create a pull request`_ from your fork and I'll be sure to review it. Credits ------- -This Python module, written by Alex Kaszynski and several contribitors, is -based on Java code written by Marius Johann, CEO Keepa. Java source is can be +This Python module, written by Alex Kaszynski and several contributors, is +based on Java code written by Marius Johann, CEO of Keepa. Java source can be found at `keepacom/api_backend `_. diff --git a/docs/source/_static/custom.css b/docs/source/_static/custom.css new file mode 100644 index 0000000..32fd10f --- /dev/null +++ b/docs/source/_static/custom.css @@ -0,0 +1,31 @@ +.plot-gallery { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 1.5rem; + margin: 1.5rem 0 2rem; +} + +.plot-gallery figure { + margin: 0; +} + +.plot-gallery img { + width: 100%; + height: auto; +} + +html[data-theme="dark"] .navbar-brand img { + filter: brightness(0) invert(0.9); +} + +@media (max-width: 767px) { + .plot-gallery { + grid-template-columns: 1fr; + } + + .bd-footer .bd-footer__inner { + align-items: flex-start; + flex-direction: column; + gap: 0.5rem; + } +} diff --git a/docs/source/_templates/autosummary/enum.rst b/docs/source/_templates/autosummary/enum.rst new file mode 100644 index 0000000..de237f0 --- /dev/null +++ b/docs/source/_templates/autosummary/enum.rst @@ -0,0 +1,18 @@ +{{ fullname | escape | underline }} + +.. currentmodule:: {{ module }} + +.. autoclass:: {{ objname }} + +{% if attributes %} + .. rubric:: Members + + .. autosummary:: +{% for item in attributes %} + ~{{ objname }}.{{ item }} +{% endfor %} + +{% for item in attributes %} + .. autoattribute:: {{ objname }}.{{ item }} +{% endfor %} +{% endif %} diff --git a/docs/source/_templates/autosummary/pydantic_model.rst b/docs/source/_templates/autosummary/pydantic_model.rst new file mode 100644 index 0000000..589294a --- /dev/null +++ b/docs/source/_templates/autosummary/pydantic_model.rst @@ -0,0 +1,7 @@ +{{ fullname | escape | underline }} + +.. currentmodule:: {{ module }} + +.. autopydantic_model:: {{ objname }} + :members: + :undoc-members: diff --git a/docs/source/api_methods.rst b/docs/source/api_methods.rst index d500ed2..f58d36b 100644 --- a/docs/source/api_methods.rst +++ b/docs/source/api_methods.rst @@ -1,23 +1,16 @@ .. _ref_api_methods: -keepa.Api Methods ------------------ -These are the core ``keepa`` classes. +API Reference +------------- +Use :doc:`sync_client` for regular Python applications and +:doc:`async_client` when requests must integrate with an asyncio event loop. +Both clients expose the same Keepa endpoints. Shared client types are listed +in :doc:`api_types`, and generated backend request and response models are +introduced in :doc:`backend_models`. -.. autoclass:: keepa.Keepa - :members: +.. toctree:: + :maxdepth: 1 -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 + sync_client + async_client + api_types diff --git a/docs/source/api_types.rst b/docs/source/api_types.rst new file mode 100644 index 0000000..8dc6a74 --- /dev/null +++ b/docs/source/api_types.rst @@ -0,0 +1,38 @@ +Client and Backend Types +------------------------ +These hand-written types validate domains and product-finder parameters. + +.. autosummary:: + :toctree: api/types + + keepa.Domain + +.. autosummary:: + :toctree: api/types + :template: pydantic_model + + keepa.ProductParams + +Generated Request Models +~~~~~~~~~~~~~~~~~~~~~~~~ +The backend schema also defines request-shaped structs. They are exported from +``keepa.backend_models`` and documented here for schema tooling and advanced +users. High-level client methods still accept the existing Python arguments by +default for backwards compatibility. + +* :class:`keepa.models.backend.DealRequest` +* :class:`keepa.models.backend.ProductFinderRequest` +* :class:`keepa.models.backend.Request` +* :class:`keepa.models.backend.TrackingRequest` + +Common Response Models +~~~~~~~~~~~~~~~~~~~~~~ +The full generated model reference is available in +:doc:`backend_model_reference`. These are the response models returned directly +by high-level client methods with ``typed=True``. + +* :class:`keepa.models.backend.BestSellers` +* :class:`keepa.models.backend.Category` +* :class:`keepa.models.backend.DealResponse` +* :class:`keepa.models.backend.Product` +* :class:`keepa.models.backend.Seller` diff --git a/docs/source/async_client.rst b/docs/source/async_client.rst new file mode 100644 index 0000000..11ab3be --- /dev/null +++ b/docs/source/async_client.rst @@ -0,0 +1,45 @@ +Asynchronous Client +------------------- +Create ``AsyncKeepa`` with its asynchronous classmethod, then await endpoint +calls. + +.. code:: python + + import keepa + + api = await keepa.AsyncKeepa.create("") + products = await api.query("B0088PUEPK") + +Client +~~~~~~ + +.. autosummary:: + :toctree: api/async + + keepa.AsyncKeepa + +Methods +~~~~~~~ + +.. autosummary:: + :toctree: api/async + + keepa.AsyncKeepa.best_sellers_query + keepa.AsyncKeepa.category_lookup + keepa.AsyncKeepa.create + keepa.AsyncKeepa.deals + keepa.AsyncKeepa.download_graph_image + keepa.AsyncKeepa.product_finder + keepa.AsyncKeepa.query + keepa.AsyncKeepa.search_for_categories + keepa.AsyncKeepa.seller_query + keepa.AsyncKeepa.update_status + keepa.AsyncKeepa.wait_for_tokens + +Attributes +~~~~~~~~~~ + +.. autosummary:: + :toctree: api/async + + keepa.AsyncKeepa.time_to_refill diff --git a/docs/source/backend_model_reference.rst b/docs/source/backend_model_reference.rst new file mode 100644 index 0000000..c37b60d --- /dev/null +++ b/docs/source/backend_model_reference.rst @@ -0,0 +1,83 @@ +.. Generated by utilities/generate-backend-models.py; do not edit by hand. + +Backend Model API +================= +Every model and enum is generated from the pinned Keepa backend schema. + +Models +------ + +.. autosummary:: + :toctree: api/backend/models + :template: pydantic_model + + keepa.models.backend.KeepaBackendModel + keepa.models.backend.APlus + keepa.models.backend.APlusModule + keepa.models.backend.BestSellers + keepa.models.backend.BuyBoxStatsObject + keepa.models.backend.Category + keepa.models.backend.CategoryTreeEntry + keepa.models.backend.Competitors + keepa.models.backend.Deal + keepa.models.backend.DealDetails + keepa.models.backend.DealRequest + keepa.models.backend.DealResponse + keepa.models.backend.FBAFeesObject + keepa.models.backend.FeedbackObject + keepa.models.backend.Format + keepa.models.backend.HazardousMaterial + keepa.models.backend.Image + keepa.models.backend.LightningDeal + keepa.models.backend.MerchantBrandStatistics + keepa.models.backend.MerchantCategoryStatistics + keepa.models.backend.Notification + keepa.models.backend.Offer + keepa.models.backend.OfferDuplicate + keepa.models.backend.Product + keepa.models.backend.ProductFinderRequest + keepa.models.backend.PromotionObject + keepa.models.backend.Request + keepa.models.backend.RequestError + keepa.models.backend.Response + keepa.models.backend.ReviewObject + keepa.models.backend.Seller + keepa.models.backend.Stats + keepa.models.backend.Tracking + keepa.models.backend.TrackingNotifyIf + keepa.models.backend.TrackingRequest + keepa.models.backend.TrackingThresholdValue + keepa.models.backend.UnitCountObject + keepa.models.backend.VariationAttributeObject + keepa.models.backend.VariationObject + keepa.models.backend.Video + +Enums +----- + +.. autosummary:: + :toctree: api/backend/enums + :template: enum + + keepa.models.backend.AmazonLocale + keepa.models.backend.AvailabilityType + keepa.models.backend.CsvType + keepa.models.backend.DealInterval + keepa.models.backend.DealState + keepa.models.backend.MerchantCsvType + keepa.models.backend.NotificationType + keepa.models.backend.NotifyIfType + keepa.models.backend.OfferCondition + keepa.models.backend.ProductType + keepa.models.backend.PromotionType + keepa.models.backend.ResponseStatus + keepa.models.backend.TrackingNotificationCause + keepa.models.backend.VideoCreatorType + +Schema Metadata +--------------- + +.. autosummary:: + :toctree: api/backend/data + + keepa.models.backend.BACKEND_COMMIT diff --git a/docs/source/backend_models.rst b/docs/source/backend_models.rst new file mode 100644 index 0000000..a473fd6 --- /dev/null +++ b/docs/source/backend_models.rst @@ -0,0 +1,165 @@ +.. _ref_backend_models: + +Typed Responses +--------------- +By default, ``keepa`` returns dictionaries for backwards compatibility. +Set ``typed=True`` on supported methods to get permissive Pydantic models +generated from Keepa's backend Java schema. Typed responses are planned to +become the default in a future release. The option changes only the response +representation; request parameters and token costs are unchanged. + +.. code:: python + + import keepa + + api = keepa.Keepa("") + products = api.query("B0088PUEPK", typed=True) + product = products[0] + + print(product.asin) + print(product.title) + +Return Shapes +~~~~~~~~~~~~~ +The sync and async clients expose the same typed response shapes. + +=================================== ========================================== +Method Return value with ``typed=True`` +=================================== ========================================== +``query`` ``list[``:class:`keepa.models.backend.Product`\ ``]`` +``search_for_categories`` ``dict[str,`` :class:`keepa.models.backend.Category`\ ``]`` +``category_lookup`` ``dict[str,`` :class:`keepa.models.backend.Category`\ ``]`` +``best_sellers_query`` :class:`keepa.models.backend.BestSellers` +``seller_query`` ``dict[str,`` :class:`keepa.models.backend.Seller`\ ``]`` +``deals`` :class:`keepa.models.backend.DealResponse` +=================================== ========================================== + +Fields are optional because Keepa omits fields based on the endpoint, +request parameters, product type, and data availability. Check optional +containers before indexing them. + +.. code:: python + + if product.offers: + first_offer = product.offers[0] + if first_offer is not None: + print(first_offer.offerId) + +Nested backend objects are models as well, so editors and type checkers can +follow the response structure. Pydantic validates known fields and reports the +complete field path when a value does not match the backend schema. Unknown +fields are retained to remain forward-compatible with backend additions. + +Serialization +~~~~~~~~~~~~~ +Use ``model_dump()`` for a regular Python dictionary. Pass ``exclude_none=True`` +to omit unavailable backend fields and ``by_alias=True`` to preserve exact +backend field names for aliased Python identifiers. + +.. code:: python + + product_dict = product.model_dump(exclude_none=True, by_alias=True) + +Product History and Stats +~~~~~~~~~~~~~~~~~~~~~~~~~ +Product queries still perform the library's normal history and statistics +parsing in typed mode. When Keepa returns history for ``history=True``, parsed +NumPy arrays are available through ``product.data``. When requested statistics +are available, ``product.stats_parsed`` contains their parsed values. These +attributes are omitted when their source data is unavailable. They are +client-generated conveniences rather than backend schema fields, so they are +retained as extra model attributes. + +.. code:: python + + products = api.query("B0088PUEPK", history=True, stats=90, typed=True) + product = products[0] + history = getattr(product, "data", {}) + new_prices = history.get("NEW") + + stats = getattr(product, "stats_parsed", {}) + current_amazon_price = stats.get("current", {}).get("AMAZON") + +Model Discovery +~~~~~~~~~~~~~~~ + +Every request and response structure from the pinned backend schema is +available as a model, including structures that are not returned directly by +the high-level client. Use Pydantic's schema API to inspect a complete output +shape programmatically. + +.. code:: python + + product_schema = keepa.backend_models.Product.model_json_schema() + print(product_schema["properties"]["offers"]) + +The generated models are available from ``keepa.backend_models`` and +``keepa.models.backend``. + +.. code:: python + + from keepa import backend_models + + assert isinstance(product, backend_models.Product) + print(backend_models.BACKEND_COMMIT) + +``backend_models.__all__`` lists every generated model and enum at the pinned +commit. This is useful for schema tooling that needs to inspect the entire +backend surface rather than one response type. + +.. code:: python + + available_models = backend_models.__all__ + +See :doc:`backend_model_reference` for an individual API page for every +generated model and enum. + +.. toctree:: + :hidden: + + backend_model_reference + +Other Endpoints +~~~~~~~~~~~~~~~ + +.. code:: python + + categories = api.category_lookup(0, typed=True) + first_category = next(iter(categories.values())) + print(first_category.name) + + best_sellers = api.best_sellers_query("402283011", typed=True) + print(best_sellers.asinList[:10]) + + sellers = api.seller_query("A2L77EE7U53NWQ", typed=True) + print(sellers["A2L77EE7U53NWQ"].sellerName) + + deals = api.deals({"page": 0, "domainId": 1}, typed=True) + deal_asins = [deal.asin for deal in deals.dr or [] if deal is not None] + +The asynchronous client uses the same ``typed=True`` option. + +.. code:: python + + import asyncio + + async def main(): + api = await keepa.AsyncKeepa.create("") + products = await api.query("B0088PUEPK", typed=True) + return products[0].title + + title = asyncio.run(main()) + +Typed seller models mirror the raw backend schema. The default dictionary +seller response can convert selected Keepa time fields to Python datetime +values with ``to_datetime=True``; typed sellers leave those fields as the +backend integer values. + +On the synchronous client, ``raw=True`` returns HTTP responses and cannot be +combined with ``typed=True``. The asynchronous client does not support raw +HTTP responses. + +The models are regenerated from the pinned backend commit recorded in +``backend_models.BACKEND_COMMIT``. If Keepa adds fields before this library is +updated, typed models still preserve those fields because they allow extra +backend data. diff --git a/docs/source/category_queries.rst b/docs/source/category_queries.rst new file mode 100644 index 0000000..afe408e --- /dev/null +++ b/docs/source/category_queries.rst @@ -0,0 +1,54 @@ +Category and Best-Seller Queries +================================ +Use category search for matching names, category lookup for exact IDs and +root categories, and best-seller queries for ranked ASIN lists. + +Search Categories +----------------- + +.. code-block:: python + + categories = api.search_for_categories("chairs") + for category_id, category in list(categories.items())[:5]: + print(category_id, category["name"]) + +Search results are dictionaries keyed by category ID. Pass ``typed=True`` to +receive ``Category`` model values instead. + +Root and Parent Categories +-------------------------- +Look up category ID ``0`` to retrieve root categories. Set +``include_parents=True`` when an exact category lookup should include its +parent chain. + +.. code-block:: python + + roots = api.category_lookup(0) + category = api.category_lookup(402283011, include_parents=True) + +Best Sellers +------------ +``best_sellers_query`` returns an ordered list of ASINs for a category by +default. Pass ``typed=True`` to receive the full ``BestSellers`` backend model, +including metadata such as ``domainId``, ``categoryId``, and ``lastUpdate`` +when Keepa includes it. + +.. code-block:: python + + asins = api.best_sellers_query("402283011") + print(asins[:10]) + + best_sellers = api.best_sellers_query("402283011", typed=True) + print(best_sellers.asinList[:10]) + +The ``rank_avg_range`` option accepts ``0``, ``30``, ``90``, or ``180``. +Use ``variations=True`` to retain variations and ``sublist=True`` to request +sub-category-rank ordering. + +Async Usage +----------- + +.. code-block:: python + + categories = await async_api.search_for_categories("chairs", typed=True) + best_sellers = await async_api.best_sellers_query("402283011", typed=True) diff --git a/docs/source/conf.py b/docs/source/conf.py index 59303f9..aa46264 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -2,8 +2,18 @@ # import pydata_sphinx_theme # noqa from datetime import datetime +import importlib +import inspect +from pathlib import Path -from keepa import __version__ +try: + import tomllib +except ModuleNotFoundError: # pragma: no cover - Python 3.10 docs builds + import tomli as tomllib + +PROJECT_ROOT = Path(__file__).resolve().parents[2] +with (PROJECT_ROOT / "pyproject.toml").open("rb") as project_file: + project_metadata = tomllib.load(project_file)["project"] # If your documentation needs a minimal Sphinx version, state it here. # @@ -14,17 +24,68 @@ # ones. extensions = [ "sphinx.ext.autodoc", + "sphinx.ext.autosummary", "numpydoc", "sphinx.ext.intersphinx", + "sphinx.ext.linkcode", + "sphinx_copybutton", + "sphinxcontrib.autodoc_pydantic", ] +numpydoc_show_class_members = False +autosummary_generate = True + +autodoc_pydantic_model_hide_paramlist = True +autodoc_pydantic_model_show_config_summary = False +autodoc_pydantic_model_show_json = False +autodoc_pydantic_model_show_validator_summary = False +autodoc_pydantic_model_summary_list_order = "bysource" intersphinx_mapping = { "python": ( - "https://docs.python.org/3.11", - (None, "../intersphinx/python-objects.inv"), + "https://docs.python.org/3", + None, ), } +nitpicky = True +nitpick_ignore = [ + ("py:class", "requests.models.Response"), + ("py:obj", "keepa.AsyncKeepa.accesskey"), + ("py:obj", "keepa.AsyncKeepa.status"), + ("py:obj", "keepa.AsyncKeepa.tokens_left"), + ("py:obj", "keepa.Keepa.accesskey"), + ("py:obj", "keepa.Keepa.status"), + ("py:obj", "keepa.Keepa.tokens_left"), +] +nitpick_ignore_regex = [ + ("py:obj", r"keepa\.Domain\..+"), + ("py:obj", r"keepa\.models\.product_params\.ProductParams\..+"), +] + + +def linkcode_resolve(domain: str, info: dict[str, str]) -> str | None: + """Resolve documented Python objects to their GitHub source lines.""" + if domain != "py" or not info.get("module") or not info.get("fullname"): + return None + + try: + obj = importlib.import_module(info["module"]) + for part in info["fullname"].split("."): + obj = getattr(obj, part) + obj = inspect.unwrap(obj) + source_file = Path(inspect.getsourcefile(obj) or "").resolve() + source, start_line = inspect.getsourcelines(obj) + relative_file = source_file.relative_to(PROJECT_ROOT) + except (AttributeError, ImportError, OSError, TypeError, ValueError): + return None + + end_line = start_line + len(source) - 1 + return ( + "https://github.com/akaszynski/keepa/blob/main/" + f"{relative_file.as_posix()}#L{start_line}-L{end_line}" + ) + + # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] @@ -39,7 +100,7 @@ # General information about the project. project = "keepa" -copyright = f"2018-{datetime.now().year}, Alex Kaszynski" +copyright = f"2018 to {datetime.now().year}, Alex Kaszynski" author = "Alex Kaszynski" # The version info for the project you're documenting, acts as replacement for @@ -48,7 +109,8 @@ # # The full version, including alpha/beta/rc tags. # The short X.Y version. -release = version = __version__ +release = project_metadata["version"] +version = ".".join(release.split(".")[:2]) # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. @@ -81,8 +143,8 @@ html_context = { "github_user": "akaszynski", "github_repo": "keepa", - "github_version": "master", - "doc_path": "doc", + "github_version": "main", + "doc_path": "docs/source", } html_show_sourcelink = False @@ -109,6 +171,7 @@ # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ["_static"] +html_css_files = ["custom.css"] htmlhelp_basename = "keepadoc" @@ -154,7 +217,7 @@ "keepa Documentation", author, "keepa", - "One line description of project.", + project_metadata["description"], "Miscellaneous", ), ] diff --git a/docs/source/deal_queries.rst b/docs/source/deal_queries.rst new file mode 100644 index 0000000..f97949d --- /dev/null +++ b/docs/source/deal_queries.rst @@ -0,0 +1,42 @@ +Deal Queries +============ +``deals`` returns recently changed products that match deal filters. Dictionary +requests and responses remain the default, while generated Pydantic models are +available for users who want structured request construction and typed output. + +Basic Deal Search +----------------- + +.. code-block:: python + + parameters = { + "page": 0, + "domainId": 1, + "includeCategories": [16310101], + "priceTypes": [0], + } + deals = api.deals(parameters) + first_asin = deals["dr"][0]["asin"] + +Typed Request and Response +-------------------------- +Use :class:`keepa.models.backend.DealRequest` to construct the request and +``typed=True`` to receive a :class:`keepa.models.backend.DealResponse`. + +.. code-block:: python + + request = keepa.backend_models.DealRequest( + page=0, + domainId=1, + includeCategories=[16310101], + priceTypes=[0], + ) + response = api.deals(request, typed=True) + asins = [deal.asin for deal in response.dr or [] if deal is not None] + +Async Usage +----------- + +.. code-block:: python + + response = await async_api.deals(request, typed=True) diff --git a/docs/source/index.rst b/docs/source/index.rst index fa6ae7c..d6fd033 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -1,19 +1,157 @@ -keepa Python API Documentation -****************************** +Keepa Python Client +******************* -.. toctree:: - :maxdepth: 2 - :caption: Contents: +``keepa`` is a synchronous and asynchronous Python client for Keepa's Amazon +product, price-history, offer, seller, category, best-seller, product-finder, +and deals API. It is built for analysis workflows: fetch product metadata, +turn Keepa's compact history arrays into Python objects, inspect offers, and +move into pandas, NumPy, or Matplotlib without writing request plumbing. - product_query - api_methods +Get an API key through `Keepa Data Access `_. +Install +------- -.. include:: ../../README.rst +.. code-block:: console + pip install keepa -Indices and tables +Query Product Data ================== -* :ref:`genindex` -* :ref:`modindex` -* :ref:`search` +Start with a product query. Dictionary responses remain the default for +backwards compatibility. + +.. code-block:: python + + import keepa + + api = keepa.Keepa("") + products = api.query("B0088PUEPK", history=True, stats=90) + product = products[0] + + print(product["title"]) + print(product["stats_parsed"]["current"].get("AMAZON")) + +Use ``typed=True`` for generated Pydantic models with editor-friendly +attribute access. Typed responses are planned to become the default in a +future release. + +.. code-block:: python + + typed_product = api.query("B0088PUEPK", typed=True)[0] + print(typed_product.title) + print(typed_product.asin) + +What Comes Back +=============== +The high-level methods keep request handling small while returning data at the +right level of detail. + +=================================== ========================================== +Method Return value +=================================== ========================================== +``query`` Product dictionaries or + :class:`keepa.models.backend.Product` + models with ``typed=True``. +``deals`` Deal dictionaries or + :class:`keepa.models.backend.DealResponse` + with ``typed=True``. +``best_sellers_query`` Ranked ASIN lists, or + :class:`keepa.models.backend.BestSellers` + with ``typed=True``. +``category_lookup`` Categories keyed by category ID. +``search_for_categories`` Matching categories keyed by category ID. +``seller_query`` Seller dictionaries or + :class:`keepa.models.backend.Seller` + models with ``typed=True``. +``product_finder`` ASINs matching more than a thousand + product filters. +=================================== ========================================== + +Every generated backend model keeps unknown fields, so new backend fields are +preserved even before the Python library is regenerated. + +Product Histories +================= + +Price and offer histories are returned as timestamped arrays ready for +analysis with NumPy, pandas, or Matplotlib. + +.. code-block:: python + + history = product.get("data", {}) + new_prices = history.get("NEW") + new_times = history.get("NEW_time") + + stats = product.get("stats_parsed", {}) + current_sales_rank = stats.get("current", {}).get("SALES") + +.. container:: plot-gallery + + .. figure:: images/Product_Price_Plot.png + :alt: Amazon product price history with multiple offer conditions + + Product price history + + .. figure:: images/Product_Offer_Plot.png + :alt: Amazon marketplace offer history over time + + Marketplace offer history + + .. figure:: images/Offer_History.png + :alt: Active marketplace offer histories + + Active offer histories + +Explore the Catalog +=================== +Use product finder when you need discovery instead of direct ASIN lookup. +Use category and best-seller queries when category rank matters. Use deals +when you want products that recently changed and match deal filters. + +.. code-block:: python + + params = keepa.ProductParams( + author="jim butcher", + current_SALES_lte=50_000, + sort=["current_SALES", "asc"], + perPage=100, + ) + asins = api.product_finder(params) + products = api.query(asins[:10], history=False, typed=True) + +.. code-block:: python + + categories = api.search_for_categories("office chairs", typed=True) + category_id = next(iter(categories)) + best_sellers = api.best_sellers_query(category_id, typed=True) + print(best_sellers.asinList[:10]) + +Sync or Async +============= +The synchronous and asynchronous clients expose the same endpoint surface and +typed return shapes. + +.. code-block:: python + + async_api = await keepa.AsyncKeepa.create("") + products = await async_api.query(["B0088PUEPK", "B000HRMAR2"], typed=True) + +Next Steps +========== + +* :doc:`product_query` explains product history, named statistics, offers, + categories, and product finder queries. +* :doc:`api_methods` links the synchronous, asynchronous, and shared-type API + references. +* :doc:`backend_models` documents typed return shapes, serialization, and + generated model discovery. + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + :hidden: + + product_query + api_methods + backend_models diff --git a/docs/source/offer_queries.rst b/docs/source/offer_queries.rst new file mode 100644 index 0000000..9def21e --- /dev/null +++ b/docs/source/offer_queries.rst @@ -0,0 +1,53 @@ +Offer Queries +============= +Set ``offers`` between 20 and 100 to include marketplace offers. Offer queries +consume more tokens than metadata-only product queries. + +.. code-block:: python + + product = api.query("1454857935", offers=20)[0] + offers = product.get("offers", []) + +Offer History +------------- +Each offer can contain an ``offerCSV`` history. Convert it into timestamp and +price arrays with ``convert_offer_history``. + +.. code-block:: python + + offer = offers[0] + times, prices = keepa.convert_offer_history(offer["offerCSV"]) + + for timestamp, price in list(zip(times, prices))[:10]: + print(timestamp, price) + +Active Offers +------------- +``liveOffersOrder`` contains indices into ``offers`` for currently active +offers. Not every historical offer is active, and the field can be absent. + +.. code-block:: python + + active_offers = [ + offers[index] + for index in product.get("liveOffersOrder", []) + if index < len(offers) + ] + + for offer in active_offers: + times, prices = keepa.convert_offer_history(offer["offerCSV"]) + +.. figure:: images/Offer_History.png + :alt: Active marketplace offer histories + :width: 700px + + Active offer price histories + +Typed Offers +------------ +With ``typed=True``, ``product.offers`` contains ``Offer`` models and +``product.liveOffersOrder`` contains the active indices. Both are optional. + +See Keepa's `product request documentation +`_ for offer-specific +token costs and backend behavior. diff --git a/docs/source/product_data.rst b/docs/source/product_data.rst new file mode 100644 index 0000000..82e0feb --- /dev/null +++ b/docs/source/product_data.rst @@ -0,0 +1,69 @@ +Product Data +============ +``query`` accepts one product code or a sequence. It always returns a list, +including for a single product. + +Single Product +-------------- + +.. code-block:: python + + products = api.query("059035342X", history=False) + product = products[0] + print(product["asin"], product.get("title")) + +ASIN and ISBN-10 values are accepted by default. Set +``product_code_is_asin=False`` for UPC, EAN, or ISBN-13 values. + +.. code-block:: python + + products = api.query("978-0786222728", product_code_is_asin=False) + +Batch Queries +------------- +Pass a list, tuple, NumPy array, or other sequence of codes. Results contain +one entry per successful product, so iterate over the returned list rather +than indexing it with an ASIN. + +.. code-block:: python + + asins = ["0022841350", "0022841369"] + products = api.query(asins, history=False) + + for product in products: + print(product["asin"], product.get("title")) + + products_by_asin = {product["asin"]: product for product in products} + +Response Modes +-------------- +Dictionary responses remain the default. ``typed=True`` returns generated +Pydantic ``Product`` models without changing request behavior. + +.. code-block:: python + + product = api.query("B0088PUEPK", history=False, typed=True)[0] + print(product.asin, product.title) + +The synchronous client also supports ``raw=True`` for unparsed HTTP responses. +``raw=True`` and ``typed=True`` cannot be combined. The asynchronous client +does not expose raw HTTP responses. + +Common Options +-------------- + +``history`` + Include and parse product history. Disable it for metadata-only requests. +``stats`` + Include summary statistics for the requested number of days. +``offers`` + Include marketplace offers. Valid values are 20 through 100 and consume + additional tokens. +``update`` + Request fresher data from Keepa. This can increase token cost. +``days`` + Limit returned history to the most recent number of days. +``videos`` / ``aplus`` + Include video or A+ content metadata. + +See :meth:`keepa.Keepa.query` for the complete parameter reference. diff --git a/docs/source/product_finder.rst b/docs/source/product_finder.rst new file mode 100644 index 0000000..ce557f2 --- /dev/null +++ b/docs/source/product_finder.rst @@ -0,0 +1,81 @@ +Product Finder +============== +``product_finder`` searches Keepa's product database and returns ASINs. It does +not return full product objects; pass the ASINs to ``query`` when product data +is needed. + +Basic Search +------------ + +.. code-block:: python + + parameters = { + "author": "jim butcher", + "sort": ["current_SALES", "asc"], + "perPage": 100, + } + asins = api.product_finder(parameters) + +Validated Parameters +-------------------- +:class:`keepa.ProductParams` validates backend field names before a request +consumes tokens. Unknown names raise a validation error. + +.. code-block:: python + + parameters = keepa.ProductParams( + author="jim butcher", + sort=["current_SALES", "asc"], + perPage=100, + ) + asins = api.product_finder(parameters) + +The generated backend :class:`keepa.models.backend.ProductFinderRequest` model +is accepted too. Use it when you want a request object that mirrors the pinned +backend schema exactly. + +.. code-block:: python + + request = keepa.backend_models.ProductFinderRequest( + author=["jim butcher"], + current_SALES_lte=50000, + ) + asins = api.product_finder(request) + +The backend exposes more than a thousand filters. Inspect them through +``ProductParams.model_fields``, +``ProductParams.model_json_schema()``, or +``keepa.backend_models.ProductFinderRequest.model_json_schema()``. + +Result Limits and Pages +----------------------- +``n_products`` sets ``perPage`` only when the supplied parameters do not +already define it. An explicit ``perPage`` therefore takes precedence. + +.. code-block:: python + + first_page = keepa.ProductParams( + categories_include=["2619533011"], + current_SALES_lte=50000, + page=0, + perPage=200, + ) + asins = api.product_finder(first_page) + +Increment ``page`` for additional backend pages. Keepa limits deep pagination; +use selective filters and a stable sort for large result sets. + +Fetch Product Data +------------------ + +.. code-block:: python + + asins = api.product_finder(parameters) + products = api.query(asins, history=False) + +Async Usage +----------- + +.. code-block:: python + + asins = await async_api.product_finder(parameters) diff --git a/docs/source/product_history.rst b/docs/source/product_history.rst new file mode 100644 index 0000000..c4ff420 --- /dev/null +++ b/docs/source/product_history.rst @@ -0,0 +1,89 @@ +Product History and Statistics +============================== +With ``history=True`` (the default), Keepa history is parsed into NumPy arrays +under ``product["data"]``. Each available value array has a matching +``*_time`` array. + +History Availability +-------------------- +Products do not necessarily contain every history type. Use ``get`` when +availability is not guaranteed. + +.. code-block:: python + + product = api.query("059035342X")[0] + history = product.get("data", {}) + new_prices = history.get("NEW", []) + new_times = history.get("NEW_time", []) + + for timestamp, price in list(zip(new_times, new_prices))[:10]: + print(timestamp, price) + +Common History Keys +------------------- + +========================== ================================================= +Key Meaning +========================== ================================================= +``AMAZON`` Amazon price +``NEW`` / ``USED`` Marketplace new or used price +``SALES`` Sales rank +``LISTPRICE`` List price +``NEW_FBA`` Lowest new Fulfilled by Amazon price +``NEW_FBM_SHIPPING`` Lowest merchant-fulfilled new price with shipping +``BUY_BOX_SHIPPING`` Buy box price with shipping +``COUNT_NEW`` New offer count +``RATING`` Rating history +``COUNT_REVIEWS`` Review count history +========================== ================================================= + +Plotting +-------- +History values are discontinuous and are best represented as step plots. + +.. code-block:: python + + import matplotlib.pyplot as plt + + if "NEW" in history: + plt.step(history["NEW_time"], history["NEW"], where="pre") + plt.xlabel("Date") + plt.ylabel("New price") + plt.show() + +The convenience plotter renders the available product histories directly: + +.. code-block:: python + + keepa.plot_product(product) + +.. figure:: images/Product_Price_Plot.png + :alt: Product price histories rendered as step plots + :width: 700px + + Amazon and marketplace price history + +Named Statistics +---------------- +Request statistics with ``stats``. Raw positional arrays remain under +``product["stats"]`` for compatibility; ``stats_parsed`` maps positions to +names such as ``AMAZON``, ``NEW``, and ``SALES``. + +.. code-block:: python + + product = api.query("059035342X", stats=90)[0] + stats = product.get("stats_parsed", {}) + + current_amazon_price = stats.get("current", {}).get("AMAZON") + minimum_new_price = stats.get("minInInterval", {}).get("NEW") + +Minimum and maximum entries are ``(timestamp, value)`` tuples when present. +Keepa's `statistics object documentation +`_ defines the interval +semantics. + +Typed Products +-------------- +Typed queries preserve the same parsed ``data`` and ``stats_parsed`` +convenience attributes when their source data is available. They are +client-generated extras rather than backend schema fields. diff --git a/docs/source/product_query.rst b/docs/source/product_query.rst index 4537a2d..957986d 100644 --- a/docs/source/product_query.rst +++ b/docs/source/product_query.rst @@ -1,337 +1,44 @@ Queries ======= -Interfacing with the ``keepa`` requires a valid access key. This -requires a monthly subscription from `Pricing -`_. Here's a brief description of the -subscription model from their website. +Keepa requests require a paid API key from `Keepa Data Access +`_ and consume tokens. The client waits for tokens +by default; use ``wait=False`` only when your application manages token +availability itself. -All plans are prepaid for 1 month with a subscription model. A -subscription can be canceled at any time. Multiple plans can be active -on the same account and an upgrade is possible at any time, a -downgrade once per month. The plans differentiate by the number of -tokens generated per minute. For example: With a single token you can -retrieve the complete data set for one product. Unused tokens expire -after one hour. You can find more information on how our plans work in -our documentation. +.. code-block:: python + import keepa -Connecting to Keepa -~~~~~~~~~~~~~~~~~~~ -Import interface and establish connection to server: + api = keepa.Keepa("") -.. code:: python +Choose a guide based on the result you need: - import keepa - accesskey = 'XXXXXXXXXXXXXXXX' # enter real access key here - api = keepa.Keepa(accesskey) +* :doc:`product_data` for product metadata and batch queries. +* :doc:`product_history` for price history, sales rank, and named statistics. +* :doc:`offer_queries` for marketplace offers and offer history. +* :doc:`deal_queries` for deal discovery and typed deal responses. +* :doc:`category_queries` for category lookup and best-seller lists. +* :doc:`product_finder` for filtered product discovery. +The synchronous and asynchronous clients accept the same endpoint parameters. +Async calls use ``await`` and return the same response shapes. -Product History Query -~~~~~~~~~~~~~~~~~~~~~ -The product data for a single ASIN can be queried using: +.. code-block:: python -.. code:: python + async_api = await keepa.AsyncKeepa.create("") + products = await async_api.query("B0088PUEPK") - products = api.query('059035342X') - product = products[0] +.. toctree:: + :maxdepth: 1 + :hidden: -where ``products`` is always a list of products, even with a single request. + product_data + product_history + offer_queries + deal_queries + category_queries + product_finder -You can query using ISBN-10 or ASIN like the above example by default, or by using UPC, - EAN, and ISBN-13 codes by setting ``product_code_is_asin`` to ``False``: - -.. code:: python - - products = api.query('978-0786222728', product_code_is_asin=False) - - -Multiple products can be queried using a list or ``numpy`` array: - -.. code:: python - - asins = ['0022841350', '0022841369', '0022841369', '0022841369'] - asins = np.asarray(['0022841350', '0022841369', '0022841369', '0022841369']) - products = api.query(asins) - product = products[0] - -The ``products`` variable is a list of product data with one entry per successful result from the Keepa server. Each entry is a dictionary containing the same product data available from `Amazon `_: - -.. code:: python - - # Available keys - print(products[0].keys()) - - # Print ASIN and title - print('ASIN is ' + products[0]['asin']) - print('Title is ' + products[0]['title']) - -When the parameter ``history`` is ``True`` (enabled by default), each -product contains a The raw data is contained within each product -result. Raw data is stored as a dictionary with each key paired with -its associated time history. - -.. code:: python - - # Access new price history and associated time data - newprice = product['data']['NEW'] - newpricetime = product['data']['NEW_time'] - - # print the first 10 prices - print('%20s %s' % ('Date', 'Price')) - for i in range(10): - print('%20s $%.2f' % (newpricetime[i], newprice[i])) - -.. code:: - - Date Price - 2014-07-31 05:00:00 $55.00 - 2014-08-02 11:00:00 $56.19 - 2014-08-04 02:00:00 $56.22 - 2014-08-04 06:00:00 $54.99 - 2014-08-08 01:00:00 $49.99 - 2014-08-08 16:00:00 $55.66 - 2014-08-10 02:00:00 $49.99 - 2014-08-10 07:00:00 $55.66 - 2014-08-10 18:00:00 $57.00 - 2014-08-10 20:00:00 $52.51 - -Each time a user makes a query to keepa as well as other points in -time, an entry is stored on their servers. This means that there will -sometimes be gaps in the history followed by closely spaced entries -like in this example data. - -The data dictionary contains keys for each type of history available -for the product. These keys include: - - 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. - - -Each data key has a corresponding ``_time`` key containing the time -values of each key. These can be plotted with: - -.. code:: python - - import matplotlib.pyplot as plt - key = 'TRADE_IN' - history = product['data'] - plt.step(history[key], history[key + '_time'], where='pre') - -Historical data should be plotted as a step plot since the data is -discontinuous. Values are unknown between each entry. - -The product history can also be plotted from the module if -``matplotlib`` is installed - -.. code:: python - - keepa.plot_product(product) - - -Offer Queries -~~~~~~~~~~~~~ -You can obtain the offers history for an ASIN (or multiple ASINs) using the ``offers`` parameter. See the documentation at `Request Products `_ for further details. Offer queries use more tokens than a normal request. Here's an example query - -.. code:: python - - asin = '1454857935' - products = api.query(asin, offers=20) - product = products[0] - offers = product['offers'] - - # each offer contains the price history of each offer - offer = offers[0] - csv = offer['offerCSV'] - - # convert these values to numpy arrays - times, prices = keepa.convert_offer_history(csv) - - # print the first 10 prices - print('%20s %s' % ('Date', 'Price')) - for i in range(10): - print('%20s $%.2f' % (times[i], prices[i])) - -.. code:: - - Date Price - 2017-01-17 11:22:00 $155.41 - 2017-04-07 10:40:00 $165.51 - 2017-06-30 18:56:00 $171.94 - 2017-09-13 03:30:00 $234.99 - 2017-09-16 12:16:00 $170.95 - 2018-01-30 08:44:00 $259.21 - 2018-02-01 08:40:00 $255.97 - 2018-02-02 08:36:00 $211.91 - 2018-02-03 08:32:00 $203.48 - 2018-02-04 08:40:00 $217.37 - -Not all offers are active and some are only historical. The following -example plots the history of active offers for a single Amazon product. - -.. code:: python - - # for a list of active offers, use - indices = product['liveOffersOrder'] - - # with this you can loop through active offers: - indices = product['liveOffersOrder'] - offer_times = [] - offer_prices = [] - for index in indices: - csv = offers[index]['offerCSV'] - times, prices = keepa.convert_offer_history(csv) - offer_times.append(times) - offer_prices.append(prices)p - - # you can aggregate these using np.hstack or plot at the history individually - import matplotlib.pyplot as plt - for i in range(len(offer_prices)): - plt.step(offer_times[i], offer_prices[i]) - - plt.xlabel('Date') - plt.ylabel('Offer Price') - plt.show() - - -.. figure:: ./images/Offer_History.png - :width: 350pt - - -Category Queries -~~~~~~~~~~~~~~~~ -You can retrieve an ASIN list of the most popular products based on -sales in a specific category or product group. Here's an example that -assumes you've already setup your api. - -.. code:: python - - # get category id numbers for chairs - if test_categories: - categories = api.search_for_categories('chairs') - - # print the first 5 catIds - catids = list(categories.keys()) - for catid in catids[:5]: - print(catid, categories[catid]['name']) - - # query the best sellers for "Arm Chairs" - bestsellers = api.best_sellers_query('402283011') - - print('\nBest Sellers:') - for bestseller in bestsellers: - print(bestseller) - -.. code:: - - 8728936011 Stools, Chairs & Seat Cushions - 16053799011 Mamagreen Outdoor Dining Chairs - 8297445011 Medical Chairs - 3290537011 kitchen chairs - 5769032011 Office Chairs - - Best Sellers: - B00HGE0MT2 - B006W6U006 - B006Z8RD60 - B006Z8S6UC - B009UVKXY8 - B009FXIVMC - B0077LGFTK - B0078NISRY - B00ESI56B8 - B00EOQ5W8G - - -Product Search -~~~~~~~~~~~~~~ -You can search for products using ``keepa`` using the ``product_finder`` method. There are many parameters you can search using. See ``help(api.product_finder)`` or check the description of the function at :ref:`ref_api_methods`. - -.. code:: python - - Query for all of Jim Butcher's books: - - import keepa - api = keepa.Keepa('ENTER_ACTUAL_KEY_HERE') - product_parms = {'author': 'jim butcher'} - products = api.product_finder(product_parms) +See :doc:`api_methods` for complete signatures and `Keepa Pricing +`_ for current token costs and subscription +options. diff --git a/docs/source/sync_client.rst b/docs/source/sync_client.rst new file mode 100644 index 0000000..abb1149 --- /dev/null +++ b/docs/source/sync_client.rst @@ -0,0 +1,37 @@ +Synchronous Client +------------------ +``Keepa`` is the synchronous client. Calls block until the API response is +available or the configured timeout is reached. + +Client +~~~~~~ + +.. autosummary:: + :toctree: api/sync + + keepa.Keepa + +Methods +~~~~~~~ + +.. autosummary:: + :toctree: api/sync + + keepa.Keepa.best_sellers_query + keepa.Keepa.category_lookup + keepa.Keepa.deals + keepa.Keepa.download_graph_image + keepa.Keepa.product_finder + keepa.Keepa.query + keepa.Keepa.search_for_categories + keepa.Keepa.seller_query + keepa.Keepa.update_status + keepa.Keepa.wait_for_tokens + +Attributes +~~~~~~~~~~ + +.. autosummary:: + :toctree: api/sync + + keepa.Keepa.time_to_refill diff --git a/pyproject.toml b/pyproject.toml index de4da78..0b2efaf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,7 +23,7 @@ dependencies = [ "tqdm", "aiohttp", "pandas <= 3.0", - "pydantic" + "pydantic >=2" ] description = "Interfaces with keepa.com's API." keywords = ["keepa"] @@ -34,9 +34,14 @@ version = "1.5.dev0" [project.optional-dependencies] doc = [ - "sphinx==9.1.0", - "pydata-sphinx-theme==0.16.1", - "numpydoc==1.10.0" + "sphinx==8.1.3; python_version < '3.11'", + "sphinx==8.2.3; python_version == '3.11'", + "sphinx==9.1.0; python_version >= '3.12'", + "pydata-sphinx-theme==0.19.0", + "autodoc-pydantic==2.2.0", + "sphinx-copybutton==0.5.2", + "numpydoc==1.10.0", + "tomli; python_version < '3.11'" ] test = [ "matplotlib", diff --git a/src/keepa/__init__.py b/src/keepa/__init__.py index cadf18d..5f748e4 100644 --- a/src/keepa/__init__.py +++ b/src/keepa/__init__.py @@ -11,6 +11,7 @@ from keepa.constants import DCODES, KEEPA_ST_ORDINAL, SCODES, csv_indices from keepa.keepa_async import AsyncKeepa from keepa.keepa_sync import Keepa +from keepa.models import backend as backend_models from keepa.models.domain import Domain from keepa.models.product_params import ProductParams from keepa.plotting import plot_product @@ -32,6 +33,7 @@ "ProductParams", "SCODES", "__version__", + "backend_models", "convert_offer_history", "csv_indices", "format_items", diff --git a/src/keepa/keepa_async.py b/src/keepa/keepa_async.py index 92dedce..b031cd3 100644 --- a/src/keepa/keepa_async.py +++ b/src/keepa/keepa_async.py @@ -13,6 +13,15 @@ from keepa.constants import SCODES from keepa.keepa_sync import Keepa +from keepa.models.backend import ( + BestSellers, + Category, + DealRequest, + DealResponse, + Product, + ProductFinderRequest, + Seller, +) from keepa.models.domain import Domain from keepa.models.product_params import ProductParams from keepa.models.status import Status @@ -38,7 +47,7 @@ class AsyncKeepa: 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 + for a recurring or one-time plan. To obtain a key, sign up for one at `Keepa Data `_. Parameters @@ -50,7 +59,7 @@ class AsyncKeepa: 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 + cause any request to hang indefinitely should keepa.com be down Examples -------- @@ -62,7 +71,7 @@ class AsyncKeepa: >>> product_parms = {"author": "jim butcher"} >>> async def main(): ... key = "" - ... api = await keepa.AsyncKeepa().create(key) + ... api = await keepa.AsyncKeepa.create(key) ... return await api.product_finder(product_parms) ... >>> asins = asyncio.run(main()) @@ -82,7 +91,7 @@ class AsyncKeepa: >>> import keepa >>> async def main(): ... key = "" - ... api = await keepa.AsyncKeepa().create(key) + ... api = await keepa.AsyncKeepa.create(key) ... return await api.query("B0088PUEPK") ... >>> response = asyncio.run(main()) @@ -99,7 +108,7 @@ class AsyncKeepa: @classmethod async def create(cls, accesskey: str, timeout: float = 10.0) -> "AsyncKeepa": - """Create the async object.""" + """Create an asynchronous Keepa client without making an API request.""" self = AsyncKeepa() self.accesskey = accesskey self.tokens_left = 0 @@ -148,7 +157,7 @@ async def query( self, items: str | Sequence[str], stats: int | None = None, - domain: str = "US", + domain: str | Domain = "US", history: bool = True, offers: int | None = None, update: int | None = None, @@ -165,8 +174,9 @@ async def query( raw: bool = False, videos: bool = False, aplus: bool = False, + typed: bool = False, extra_params: dict[str, Any] | None = None, - ): + ) -> list[dict[str, Any]] | list[Product]: """Documented in Keepa.query.""" if raw: raise ValueError("Raw response is only available in the non-async class") @@ -250,7 +260,10 @@ async def query( **extra_params, ) idx += nrequest - products.extend(response["products"]) + if typed: + products.extend(Product.model_validate(product) for product in response["products"]) + else: + products.extend(response["products"]) if pbar is not None: pbar.update(nrequest) @@ -340,7 +353,8 @@ async def best_sellers_query( sublist: bool = False, domain: str | Domain = "US", wait: bool = True, - ): + typed: bool = False, + ) -> list[str | None] | BestSellers: """Documented by Keepa.best_sellers_query.""" payload = { "key": self.accesskey, @@ -354,12 +368,19 @@ async def best_sellers_query( 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"] + best_sellers = response["bestSellersList"] + if typed: + return BestSellers.model_validate(best_sellers) + return best_sellers["asinList"] @is_documented_by(Keepa.search_for_categories) async def search_for_categories( - self, searchterm, domain: str | Domain = "US", wait: bool = True - ): + self, + searchterm: str, + domain: str | Domain = "US", + wait: bool = True, + typed: bool = False, + ) -> dict[str, dict[str, Any]] | dict[str, Category]: """Documented by Keepa.search_for_categories.""" payload = { "key": self.accesskey, @@ -374,40 +395,54 @@ async def search_for_categories( "Categories search results not yet available " + "or no search terms found." ) else: - return response["categories"] + categories = response["categories"] + if typed: + return { + cat_id: Category.model_validate(category) + for cat_id, category in categories.items() + } + return categories @is_documented_by(Keepa.category_lookup) async def category_lookup( self, - category_id, + category_id: int, domain: str | Domain = "US", - include_parents=0, + include_parents: bool = False, wait: bool = True, - ): + typed: bool = False, + ) -> dict[str, dict[str, Any]] | dict[str, Category]: """Documented by Keepa.category_lookup.""" payload = { "key": self.accesskey, "domain": _domain_to_dcode(domain), "category": category_id, - "parents": include_parents, + "parents": int(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"] + categories = response["categories"] + if typed: + return { + cat_id: Category.model_validate(category) + for cat_id, category in categories.items() + } + return categories @is_documented_by(Keepa.seller_query) async def seller_query( self, - seller_id, + seller_id: str | list[str], domain: str | Domain = "US", - to_datetime=True, - storefront=False, - update=None, + to_datetime: bool = True, + storefront: bool = False, + update: int | None = None, wait: bool = True, - ): + typed: bool = False, + ) -> dict[str, dict[str, Any]] | dict[str, Seller]: """Documented by Keepa.sellerer_query.""" if isinstance(seller_id, list): if len(seller_id) > 100: @@ -429,22 +464,30 @@ async def seller_query( payload["update"] = update response = await self._request("seller", payload, wait=wait) + if typed: + return { + seller_id: Seller.model_validate(seller) + for seller_id, seller in response["sellers"].items() + } return _parse_seller(response["sellers"], to_datetime) @is_documented_by(Keepa.product_finder) async def product_finder( self, - product_parms: dict[str, Any] | ProductParams, + product_parms: dict[str, Any] | ProductFinderRequest | ProductParams, domain: str | Domain = "US", wait: bool = True, n_products: int = 50, ) -> list[str]: """Documented by Keepa.product_finder.""" - if isinstance(product_parms, dict): + if isinstance(product_parms, ProductFinderRequest): + product_parms_dict = product_parms.model_dump(exclude_none=True) + elif isinstance(product_parms, dict): product_parms_valid = ProductParams(**product_parms) + product_parms_dict = product_parms_valid.model_dump(exclude_none=True) else: product_parms_valid = product_parms - product_parms_dict = product_parms_valid.model_dump(exclude_none=True) + product_parms_dict = product_parms_valid.model_dump(exclude_none=True) product_parms_dict.setdefault("perPage", n_products) payload = { "key": self.accesskey, @@ -456,16 +499,27 @@ async def product_finder( return response["asinList"] @is_documented_by(Keepa.deals) - async def deals(self, deal_parms, domain: str | Domain = "US", wait: bool = True): + async def deals( + self, + deal_parms: dict[str, Any] | DealRequest, + domain: str | Domain = "US", + wait: bool = True, + typed: bool = False, + ) -> dict[str, Any] | DealResponse: """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}"') + if isinstance(deal_parms, DealRequest): + deal_parms = deal_parms.model_dump(exclude_none=True) + else: + deal_parms = deal_parms.copy() + + # 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]) + # verify json type + key_type = DEAL_REQUEST_KEYS[key] + deal_parms[key] = key_type(deal_parms[key]) deal_parms.setdefault("priceTypes", 0) @@ -475,8 +529,10 @@ async def deals(self, deal_parms, domain: str | Domain = "US", wait: bool = True "selection": json.dumps(deal_parms), } - deals = await self._request("deal", payload, wait=wait) - return deals["deals"] + deals = (await self._request("deal", payload, wait=wait))["deals"] + if typed: + return DealResponse.model_validate(deals) + return deals @is_documented_by(Keepa.download_graph_image) async def download_graph_image( diff --git a/src/keepa/keepa_sync.py b/src/keepa/keepa_sync.py index 79e8505..da8236e 100644 --- a/src/keepa/keepa_sync.py +++ b/src/keepa/keepa_sync.py @@ -11,6 +11,15 @@ from tqdm import tqdm from keepa.constants import SCODES +from keepa.models.backend import ( + BestSellers, + Category, + DealRequest, + DealResponse, + Product, + ProductFinderRequest, + Seller, +) from keepa.models.domain import Domain from keepa.models.product_params import ProductParams from keepa.models.status import Status @@ -27,7 +36,7 @@ class Keepa: 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 + for a recurring or one-time plan. To obtain a key, sign up for one at `Keepa Data `_. Parameters @@ -39,7 +48,7 @@ class Keepa: 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 + indefinitely should keepa.com be down logging_level: str, default: "DEBUG" Logging level to use. Default is "DEBUG". Other options are "INFO", "WARNING", "ERROR", and "CRITICAL". @@ -226,28 +235,29 @@ def download_graph_image( Show Amazon price, new and used graphs, buy box and FBA, for last 365 days, with custom width/height and custom colors. See + `Graph Image API `_ 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" - ) + >>> 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 = { @@ -292,8 +302,9 @@ def query( raw: bool = False, videos: bool = False, aplus: bool = False, - extra_params: dict[str, Any] | None = {}, - ) -> list[dict[str, Any]]: + typed: bool = False, + extra_params: dict[str, Any] | None = None, + ) -> list[dict[str, Any]] | list[Product] | list[requests.Response]: """ Perform a product query of a list, array, or single ASIN. @@ -433,27 +444,39 @@ def query( available. If you need up-to-date data, you have to use the offers parameter. + typed : bool, default: False + When ``True``, return products as + :class:`keepa.models.backend.Product` Pydantic models instead of + dictionaries. Extra backend fields are preserved on the model. + Cannot be combined with ``raw=True``. + extra_params : dict[str, Any], optional Dictionary of parameters that are not specifically called out in - the api. For example, a new parameters might be added to + the API. For example, a new parameter might be added to `Request.java - `_ + `_ 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. + List of products when ``raw=False``. By default, each product 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. + With ``history=True``, products include a ``data`` attribute or + key containing parsed history arrays. For the available fields see + the notes section. When ``raw=True``, a list of unparsed responses are - returned as :class:`requests.models.Response`. + returned as ``requests.Response``. + + When ``typed=True``, each product is returned as a permissive + Pydantic model generated from the pinned Keepa backend schema. + Use attribute access for known fields and + ``model_dump(exclude_none=True, by_alias=True)`` to convert a + typed response back to backend-compatible field names. See: https://keepa.com/#!discuss/t/product-object/116 @@ -527,11 +550,6 @@ def query( 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. @@ -593,7 +611,7 @@ def query( >>> import keepa >>> async def main(): ... key = "" - ... api = await keepa.AsyncKeepa().create(key) + ... api = await keepa.AsyncKeepa.create(key) ... return await api.query("B0088PUEPK") ... >>> response = asyncio.run(main()) @@ -647,6 +665,9 @@ def query( else: log.debug("Executing %d item product query", nitems) + if raw and typed: + raise ValueError("typed=True cannot be used with raw=True") + if extra_params is None: extra_params = {} @@ -721,6 +742,8 @@ def query( idx += nrequest if raw: products.append(response) + elif typed: + products.extend(Product.model_validate(product) for product in response["products"]) else: products.extend(response["products"]) @@ -857,7 +880,8 @@ def best_sellers_query( sublist: bool = False, domain: str | Domain = "US", wait: bool = True, - ): + typed: bool = False, + ) -> list[str | None] | BestSellers: """ Retrieve an ASIN list of the most popular products. @@ -922,11 +946,17 @@ def best_sellers_query( A valid Amazon domain. See :class:`keepa.Domain`. wait : bool, default: True Wait for available tokens before querying the keepa backend. + typed : bool, default: False + When ``True``, return the full + :class:`keepa.models.backend.BestSellers` Pydantic model instead + of only its ASIN list. Returns ------- - list - List of best seller ASINs + list[str | None] | BestSellers + List of best seller ASINs by default and the full + :class:`keepa.models.backend.BestSellers` model when + ``typed=True``. Examples -------- @@ -954,7 +984,7 @@ def best_sellers_query( >>> import keepa >>> async def main(): ... key = "" - ... api = await keepa.AsyncKeepa().create(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) @@ -983,11 +1013,18 @@ def best_sellers_query( if "bestSellersList" not in response: raise RuntimeError(f"Best sellers search results for {category} not yet available") - return response["bestSellersList"]["asinList"] + best_sellers = response["bestSellersList"] + if typed: + return BestSellers.model_validate(best_sellers) + return best_sellers["asinList"] def search_for_categories( - self, searchterm: str, domain: str | Domain = "US", wait: bool = True - ) -> dict[str, Any]: + self, + searchterm: str, + domain: str | Domain = "US", + wait: bool = True, + typed: bool = False, + ) -> dict[str, dict[str, Any]] | dict[str, Category]: """ Search for categories from Amazon. @@ -999,12 +1036,17 @@ def search_for_categories( A valid Amazon domain. See :class:`keepa.Domain`. wait : bool, default: True Wait for available tokens before querying the keepa backend. + typed : bool, default: False + When ``True``, return category values as + :class:`keepa.models.backend.Category` Pydantic models instead of + dictionaries. Returns ------- - dict[str, Any] - The response contains a categories dictionary with all matching - categories. + dict[str, dict[str, Any]] | dict[str, Category] + Categories keyed by category ID. Values are dictionaries by + default and :class:`keepa.models.backend.Category` models when + ``typed=True``. Examples -------- @@ -1039,15 +1081,21 @@ def search_for_categories( raise RuntimeError( "Categories search results not yet available or no search terms found." ) - return response["categories"] + categories = response["categories"] + if typed: + return { + cat_id: Category.model_validate(category) for cat_id, category in categories.items() + } + return categories def category_lookup( self, category_id: int, domain: str | Domain = "US", - include_parents=False, + include_parents: bool = False, wait: bool = True, - ) -> dict[str, Any]: + typed: bool = False, + ) -> dict[str, dict[str, Any]] | dict[str, Category]: """ Return root categories given a categoryId. @@ -1061,11 +1109,16 @@ def category_lookup( Include parents. wait : bool, default: True Wait for available tokens before querying the keepa backend. + typed : bool, default: False + When ``True``, return category values as + :class:`keepa.models.backend.Category` Pydantic models instead of + dictionaries. Returns ------- - dict[str, Any] - Output format is the same as :meth:`Keepa.`search_for_categories`. + dict[str, dict[str, Any]] | dict[str, Category] + Output format is the same as + :meth:`Keepa.search_for_categories`. Examples -------- @@ -1110,7 +1163,12 @@ def category_lookup( 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"] + categories = response["categories"] + if typed: + return { + cat_id: Category.model_validate(category) for cat_id, category in categories.items() + } + return categories def seller_query( self, @@ -1120,7 +1178,8 @@ def seller_query( storefront: bool = False, update: int | None = None, wait: bool = True, - ): + typed: bool = False, + ) -> dict[str, dict[str, Any]] | dict[str, Seller]: """ Receive seller information for a given seller id or ids. @@ -1173,11 +1232,18 @@ def seller_query( will be updated. wait : bool, default: True Wait for available tokens before querying the keepa backend. + typed : bool, default: False + When ``True``, return sellers as + :class:`keepa.models.backend.Seller` Pydantic models instead of + dictionaries. Typed sellers use the raw backend time fields; + ``to_datetime`` only applies to the default dictionary response. Returns ------- - dict - Dictionary containing one entry per input ``seller_id``. + dict[str, dict[str, Any]] | dict[str, Seller] + One entry per input ``seller_id``. Values are dictionaries by + default and :class:`keepa.models.backend.Seller` models when + ``typed=True``. Examples -------- @@ -1215,11 +1281,16 @@ def seller_query( payload["update"] = update response = self._request("seller", payload, wait=wait) + if typed: + return { + seller_id: Seller.model_validate(seller) + for seller_id, seller in response["sellers"].items() + } return _parse_seller(response["sellers"], to_datetime) def product_finder( self, - product_parms: dict[str, Any] | ProductParams, + product_parms: dict[str, Any] | ProductFinderRequest | ProductParams, domain: str | Domain = "US", wait: bool = True, n_products: int = 50, @@ -1231,8 +1302,9 @@ def product_finder( Parameters ---------- - product_parms : dict, ProductParams - Dictionary or :class:`keepa.ProductParams`. + product_parms : dict, ProductParams, ProductFinderRequest + Dictionary, :class:`keepa.ProductParams`, or generated + :class:`keepa.models.backend.ProductFinderRequest`. domain : str | keepa.Domain, default: 'US' A valid Amazon domain. See :class:`keepa.Domain`. wait : bool, default: True @@ -1289,7 +1361,7 @@ def product_finder( >>> product_parms = {"author": "jim butcher"} >>> async def main(): ... key = "" - ... api = await keepa.AsyncKeepa().create(key) + ... api = await keepa.AsyncKeepa.create(key) ... return await api.product_finder(product_parms) ... >>> asins = asyncio.run(main()) @@ -1303,11 +1375,14 @@ def product_finder( 'B01MXXLJPZ'] """ - if isinstance(product_parms, dict): + if isinstance(product_parms, ProductFinderRequest): + product_parms_dict = product_parms.model_dump(exclude_none=True) + elif isinstance(product_parms, dict): product_parms_valid = ProductParams(**product_parms) + product_parms_dict = product_parms_valid.model_dump(exclude_none=True) else: product_parms_valid = product_parms - product_parms_dict = product_parms_valid.model_dump(exclude_none=True) + product_parms_dict = product_parms_valid.model_dump(exclude_none=True) product_parms_dict.setdefault("perPage", n_products) payload = { "key": self.accesskey, @@ -1319,8 +1394,12 @@ def product_finder( return response["asinList"] def deals( - self, deal_parms: dict[str, Any], domain: str | Domain = "US", wait: bool = True - ) -> dict[str, Any]: + self, + deal_parms: dict[str, Any] | DealRequest, + domain: str | Domain = "US", + wait: bool = True, + typed: bool = False, + ) -> dict[str, Any] | DealResponse: """Query the Keepa API for product deals. You can find products that recently changed and match your @@ -1334,8 +1413,9 @@ def deals( Parameters ---------- - deal_parms : dict - Dictionary containing one or more of the following keys: + deal_parms : dict, DealRequest + Dictionary or generated :class:`keepa.models.backend.DealRequest` + containing one or more of the following keys: - ``"page"``: int - ``"domainId"``: int @@ -1362,11 +1442,16 @@ def deals( A valid Amazon domain. See :class:`keepa.Domain`. wait : bool, default: True Wait for available tokens before querying the keepa backend. + typed : bool, default: False + When ``True``, return a :class:`keepa.models.backend.DealResponse` + Pydantic model instead of a dictionary. Returns ------- - dict - Dictionary containing the deals including the following keys: + dict[str, Any] | DealResponse + Dictionary containing the deals, or a + :class:`keepa.models.backend.DealResponse` when ``typed=True``. + The response includes the following fields: * ``'dr'`` - Ordered array of all deal objects matching your query. * ``'categoryIds'`` - Contains all root categoryIds of the matched @@ -1411,29 +1496,27 @@ def deals( ... } >>> async def main(): ... key = "" - ... api = await keepa.AsyncKeepa().create(key) - ... categories = await api.search_for_categories("movies") + ... api = await keepa.AsyncKeepa.create(key) ... return await api.deals(deal_parms) ... - >>> asins = asyncio.run(main()) - >>> asins - ['B0BF3P5XZS', - 'B08JQN5VDT', - 'B09SP8JPPK', - '0999296345', - 'B07HPG684T', - '1984825577', - ... + >>> deals = asyncio.run(main()) + >>> deals["dr"][0]["asin"] + 'B0BF3P5XZS' """ - # verify valid keys - for key in deal_parms: - if key not in DEAL_REQUEST_KEYS: - raise ValueError(f'Invalid key "{key}"') + if isinstance(deal_parms, DealRequest): + deal_parms = deal_parms.model_dump(exclude_none=True) + else: + deal_parms = deal_parms.copy() + + # 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]) + # verify json type + key_type = DEAL_REQUEST_KEYS[key] + deal_parms[key] = key_type(deal_parms[key]) deal_parms.setdefault("priceTypes", 0) @@ -1443,7 +1526,10 @@ def deals( "selection": json.dumps(deal_parms), } - return self._request("deal", payload, wait=wait)["deals"] + deals = self._request("deal", payload, wait=wait)["deals"] + if typed: + return DealResponse.model_validate(deals) + return deals def _request( self, diff --git a/src/keepa/models/backend.py b/src/keepa/models/backend.py new file mode 100644 index 0000000..cf2382c --- /dev/null +++ b/src/keepa/models/backend.py @@ -0,0 +1,6347 @@ +"""Pydantic models generated from Keepa backend Java structs.""" + +# Generated by utilities/generate-backend-models.py; do not edit by hand. + +from __future__ import annotations + +from enum import Enum + +from pydantic import BaseModel, ConfigDict, Field + +BACKEND_COMMIT = "6e524d13bc25bdbe49be24d59a4b28feb9f98e5d" + + +class KeepaBackendModel(BaseModel): + """Base model for backend response/request structures.""" + + model_config = ConfigDict(extra="allow", coerce_numbers_to_str=True) + + +class AmazonLocale(str, Enum): + """Backend enum generated from the Keepa Java schema.""" + + 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" + + +class AvailabilityType(str, Enum): + """Backend enum generated from the Keepa Java schema.""" + + NO_OFFER = "NO_OFFER" + NOW = "NOW" + PREORDERABLE = "PREORDERABLE" + UNKNOWN = "UNKNOWN" + BACKORDERABLE = "BACKORDERABLE" + DELAYED = "DELAYED" + + +class CsvType(str, Enum): + """Backend enum generated from the Keepa Java schema.""" + + AMAZON = "AMAZON" + NEW = "NEW" + USED = "USED" + SALES = "SALES" + LISTPRICE = "LISTPRICE" + COLLECTIBLE = "COLLECTIBLE" + REFURBISHED = "REFURBISHED" + NEW_FBM_SHIPPING = "NEW_FBM_SHIPPING" + LIGHTNING_DEAL = "LIGHTNING_DEAL" + WAREHOUSE = "WAREHOUSE" + NEW_FBA = "NEW_FBA" + COUNT_NEW = "COUNT_NEW" + COUNT_USED = "COUNT_USED" + COUNT_REFURBISHED = "COUNT_REFURBISHED" + COUNT_COLLECTIBLE = "COUNT_COLLECTIBLE" + EXTRA_INFO_UPDATES = "EXTRA_INFO_UPDATES" + RATING = "RATING" + COUNT_REVIEWS = "COUNT_REVIEWS" + BUY_BOX_SHIPPING = "BUY_BOX_SHIPPING" + USED_NEW_SHIPPING = "USED_NEW_SHIPPING" + USED_VERY_GOOD_SHIPPING = "USED_VERY_GOOD_SHIPPING" + USED_GOOD_SHIPPING = "USED_GOOD_SHIPPING" + USED_ACCEPTABLE_SHIPPING = "USED_ACCEPTABLE_SHIPPING" + COLLECTIBLE_NEW_SHIPPING = "COLLECTIBLE_NEW_SHIPPING" + COLLECTIBLE_VERY_GOOD_SHIPPING = "COLLECTIBLE_VERY_GOOD_SHIPPING" + COLLECTIBLE_GOOD_SHIPPING = "COLLECTIBLE_GOOD_SHIPPING" + COLLECTIBLE_ACCEPTABLE_SHIPPING = "COLLECTIBLE_ACCEPTABLE_SHIPPING" + REFURBISHED_SHIPPING = "REFURBISHED_SHIPPING" + EBAY_NEW_SHIPPING = "EBAY_NEW_SHIPPING" + EBAY_USED_SHIPPING = "EBAY_USED_SHIPPING" + TRADE_IN = "TRADE_IN" + RENT = "RENT" + BUY_BOX_USED_SHIPPING = "BUY_BOX_USED_SHIPPING" + PRIME_EXCL = "PRIME_EXCL" + COUNT_NEW_FBA = "COUNT_NEW_FBA" + COUNT_NEW_FBM = "COUNT_NEW_FBM" + + +class DealInterval(str, Enum): + """Backend enum generated from the Keepa Java schema.""" + + DAY = "DAY" + WEEK = "WEEK" + MONTH = "MONTH" + _90_DAYS = "_90_DAYS" + + +class DealState(str, Enum): + """Backend enum generated from the Keepa Java schema.""" + + AVAILABLE = "AVAILABLE" + UPCOMING = "UPCOMING" + WAITLIST = "WAITLIST" + SOLDOUT = "SOLDOUT" + WAITLISTFULL = "WAITLISTFULL" + EXPIRED = "EXPIRED" + SUPPRESSED = "SUPPRESSED" + + +class MerchantCsvType(str, Enum): + """Backend enum generated from the Keepa Java schema.""" + + RATING = "RATING" + RATING_COUNT = "RATING_COUNT" + + +class NotificationType(str, Enum): + """Backend enum generated from the Keepa Java schema.""" + + EMAIL = "EMAIL" + TWITTER = "TWITTER" + FACEBOOK_NOTIFICATION = "FACEBOOK_NOTIFICATION" + BROWSER = "BROWSER" + FACEBOOK_MESSENGER_BOT = "FACEBOOK_MESSENGER_BOT" + API = "API" + MOBILE_APP = "MOBILE_APP" + DUMMY = "DUMMY" + + +class NotifyIfType(str, Enum): + """Backend enum generated from the Keepa Java schema.""" + + OUT_OF_STOCK = "OUT_OF_STOCK" + BACK_IN_STOCK = "BACK_IN_STOCK" + + +class OfferCondition(str, Enum): + """Backend enum generated from the Keepa Java schema.""" + + UNKNOWN = "UNKNOWN" + NEW = "NEW" + USED_NEW = "USED_NEW" + USED_VERY_GOOD = "USED_VERY_GOOD" + USED_GOOD = "USED_GOOD" + USED_ACCEPTABLE = "USED_ACCEPTABLE" + REFURBISHED = "REFURBISHED" + COLLECTIBLE_NEW = "COLLECTIBLE_NEW" + COLLECTIBLE_VERY_GOOD = "COLLECTIBLE_VERY_GOOD" + COLLECTIBLE_GOOD = "COLLECTIBLE_GOOD" + COLLECTIBLE_ACCEPTABLE = "COLLECTIBLE_ACCEPTABLE" + + +class ProductType(str, Enum): + """Backend enum generated from the Keepa Java schema.""" + + STANDARD = "STANDARD" + DOWNLOADABLE = "DOWNLOADABLE" + EBOOK = "EBOOK" + UNACCESSIBLE = "UNACCESSIBLE" + INVALID = "INVALID" + VARIATION_PARENT = "VARIATION_PARENT" + + +class PromotionType(str, Enum): + """Backend enum generated from the Keepa Java schema.""" + + SNS = "SNS" + PrimeExclusive = "PrimeExclusive" + + +class ResponseStatus(str, Enum): + """Backend enum generated from the Keepa Java schema.""" + + PENDING = "PENDING" + OK = "OK" + FAIL = "FAIL" + NOT_ENOUGH_TOKEN = "NOT_ENOUGH_TOKEN" + REQUEST_REJECTED = "REQUEST_REJECTED" + NOT_FOUND = "NOT_FOUND" + PAYMENT_REQUIRED = "PAYMENT_REQUIRED" + METHOD_NOT_ALLOWED = "METHOD_NOT_ALLOWED" + INTERNAL_SERVER_ERROR = "INTERNAL_SERVER_ERROR" + + +class TrackingNotificationCause(str, Enum): + """Backend enum generated from the Keepa Java schema.""" + + EXPIRED = "EXPIRED" + DESIRED_PRICE = "DESIRED_PRICE" + PRICE_CHANGE = "PRICE_CHANGE" + PRICE_CHANGE_AFTER_DESIRED_PRICE = "PRICE_CHANGE_AFTER_DESIRED_PRICE" + OUT_STOCK = "OUT_STOCK" + IN_STOCK = "IN_STOCK" + DESIRED_PRICE_AGAIN = "DESIRED_PRICE_AGAIN" + + +class VideoCreatorType(str, Enum): + """Backend enum generated from the Keepa Java schema.""" + + Main = "Main" + Customer = "Customer" + Seller = "Seller" + Influencer = "Influencer" + Vendor = "Vendor" + ThirdParty = "ThirdParty" + Amazon = "Amazon" + Merchant = "Merchant" + Brand = "Brand" + + +class APlus(KeepaBackendModel): + """Backend ``APlus`` model generated from the Keepa Java schema.""" + + module: list[APlusModule | None] | None = None + fromManufacturer: bool | None = None + + +class APlusModule(KeepaBackendModel): + """Backend ``APlusModule`` model generated from the Keepa Java schema.""" + + text: list[str | None] | None = None + image: list[str | None] | None = None + video: list[str | None] | None = None + imageAltText: list[str | None] | None = None + asin: list[str | None] | None = None + + +class BestSellers(KeepaBackendModel): + """Backend ``BestSellers`` model generated from the Keepa Java schema.""" + + domainId: int | None = Field( + default=None, + description=("Integer value for the Amazon locale this category belongs to.\nAmazonLocale"), + ) + lastUpdate: int | None = Field( + default=None, + description=( + "States the last time we have updated the" + " list, in Keepa Time minutes.\n" + "\n" + "Use " + "KeepaTime.keepaMinuteToUnixInMillis(int)" + " (long) to get an uncompressed timestamp" + " (Unix epoch time)." + ), + ) + categoryId: int | None = Field( + default=None, + description=( + "The category node id used by Amazon. " + "Represents the identifier of the " + "category. Also part of the Product " + "object's categories and rootCategory " + "fields. Always a positive Long value or " + "0 if a product group was used." + ), + ) + asinList: list[str | None] | None = Field( + default=None, + description=( + "An ASIN list. The list starts with the best selling product (lowest sales rank)." + ), + ) + + +class BuyBoxStatsObject(KeepaBackendModel): + """Backend ``BuyBoxStatsObject`` model generated from the Keepa Java schema.""" + + percentageWon: float | None = Field( + default=None, + description=("an approximation of the percentage the seller won the buy box *"), + ) + avgPrice: int | None = Field( + default=None, + description=("avg. price of the Buy Box offer of this seller *"), + ) + avgNewOfferCount: int | None = Field( + default=None, + description=('avg. "New" offer count during the time the seller held the Buy Box *'), + ) + isFBA: bool | None = Field( + default=None, + description=("whether or not this offer is fulfilled by Amazon *"), + ) + lastSeen: int | None = Field( + default=None, + description=("last time the seller won the buy box *"), + ) + + +class Category(KeepaBackendModel): + """Backend ``Category`` model generated from the Keepa Java schema.""" + + domainId: int | None = Field( + default=None, + description=("Integer value for the Amazon locale this category belongs to.\nAmazonLocale"), + ) + catId: int | None = Field( + default=None, + description=( + "The category node id used by Amazon. " + "Represents the identifier of the " + "category. Also part of the Product " + "object's categories and rootCategory " + "fields. Always a positive Long value." + ), + ) + name: str | None = Field( + default=None, + description=("The name of the category."), + ) + contextFreeName: str | None = Field( + default=None, + description=("The context free category name."), + ) + websiteDisplayGroup: str | None = Field( + default=None, + description=("The websiteDisplayGroup - available for most root categories."), + ) + children: list[int | None] | None = Field( + default=None, + description=( + "List of all sub categories. null or [] " + "(empty array) if the category has no sub" + " categories." + ), + ) + parent: int | None = Field( + default=None, + description=( + "The parent category's Id. Always a " + "positive Long value. If it is 0 the " + "category is a root category and has no " + "parent category." + ), + ) + highestRank: int | None = Field( + default=None, + description=( + "The highest (root category) sales rank " + "we have observed of a product that is " + "listed in this category. Note: Estimate," + " as the value is from the Keepa product " + "database and not retrieved from Amazon." + ), + ) + lowestRank: int | None = Field( + default=None, + description=( + "The lowest (root category) sales rank we" + " have observed of a product that is " + "listed in this category. Note: Estimate," + " as the value is from the Keepa product " + "database and not retrieved from Amazon." + ), + ) + productCount: int | None = Field( + default=None, + description=( + "Number of products that are listed in " + "this category. Note: Estimate, as the " + "value is from the Keepa product database" + " and not retrieved from Amazon." + ), + ) + isBrowseNode: bool | None = Field( + default=None, + description=( + "Determines if this category functions as" + " a standard browse node, rather than " + "serving promotional purposes (for " + "example, 'Specialty Stores')." + ), + ) + avgBuyBox: int | None = Field( + default=None, + description=( + "Average current buy box price of all " + "products in this category.\n" + "Value is in the currency's smallest unit" + " (e.g., cents for USD/EUR). May be null." + ), + ) + avgBuyBox90: int | None = Field( + default=None, + description=( + "Average 90 day buy box price of all " + "products in this category.\n" + "Value is in the currency's smallest unit" + " (e.g., cents for USD/EUR). May be null." + ), + ) + avgBuyBox365: int | None = Field( + default=None, + description=( + "Average 365 day buy box price of all " + "products in this category.\n" + "Value is in the currency's smallest unit" + " (e.g., cents for USD/EUR). May be null." + ), + ) + avgBuyBoxDeviation: int | None = Field( + default=None, + description=( + "Average 30 day buy box deviation " + "(standard deviation) of all products in " + "this category.\n" + "Value is in the currency's smallest unit" + " (e.g., cents for USD/EUR). May be null." + ), + ) + avgReviewCount: int | None = Field( + default=None, + description=("Average number of reviews of all products in this category. May be null."), + ) + avgRating: int | None = Field( + default=None, + description=( + "Average rating of all products in this " + "category.\n" + "Value is multiplied by 10 (e.g., 45 " + "means 4.5 stars). May be null." + ), + ) + isFBAPercent: float | None = Field( + default=None, + description=( + "Percentage of products fulfilled by " + "Amazon (FBA) in this category. May be " + "null.\n" + "Represents the distribution of FBA vs. " + "third-party sellers." + ), + ) + soldByAmazonPercent: float | None = Field( + default=None, + description=( + "Percentage of products sold directly by Amazon in this category. May be null." + ), + ) + hasCouponPercent: float | None = Field( + default=None, + description=( + "Percentage of products that have an active coupon in this category. May be null." + ), + ) + avgOfferCountNew: float | None = Field( + default=None, + description=("Average number of new offers of all products in this category. May be null."), + ) + avgOfferCountUsed: float | None = Field( + default=None, + description=( + "Average number of used offers of all products in this category. May be null." + ), + ) + sellerCount: int | None = Field( + default=None, + description=( + "Number of distinct sellers with at least" + " one active offer in this category. May " + "be null." + ), + ) + brandCount: int | None = Field( + default=None, + description=("Number of distinct brands present in this category. May be null."), + ) + + +class CategoryTreeEntry(KeepaBackendModel): + """Backend ``CategoryTreeEntry`` model generated from the Keepa Java schema.""" + + catId: int | None = None + name: str | None = None + + +class Competitors(KeepaBackendModel): + """Backend ``Competitors`` model generated from the Keepa Java schema.""" + + sellerId: str | None = Field( + default=None, + description=("The sellerId of the competitor, in lowercase."), + ) + percent: int | None = Field( + default=None, + description=("The percentage of listings this competitor shares with the seller."), + ) + + +class Deal(KeepaBackendModel): + """Backend ``Deal`` model generated from the Keepa Java schema.""" + + asin: str | None = Field( + default=None, + description=("The ASIN of the product"), + ) + parentAsin: str | None = Field( + default=None, + description=("The parent ASIN of the product"), + ) + title: str | None = Field( + default=None, + description=("Title of the product. Caution: may contain HTML markup in rare cases."), + ) + delta: list[list[int | None] | None] | None = Field( + default=None, + description=( + "Contains the absolute difference between" + " the current value and the average value" + " of the respective date range interval.\n" + "The value 0 means it did not change or " + "could not be calculated. First dimension" + " uses the Date Range indexing, second " + "the Price Type indexing.\n" + "\n" + "First dimension uses Product.CsvType, " + "second dimension DealInterval" + ), + ) + deltaPercent: list[list[int | None] | None] | None = Field( + default=None, + description=( + "Same as delta, but given in percent " + "instead of absolute values.\n" + "\n" + "First dimension uses Product.CsvType, " + "second dimension DealInterval" + ), + ) + deltaLast: list[int | None] | None = Field( + default=None, + description=( + "Contains the absolute difference of the " + "current and the previous price / rank. " + "The value 0 means it did not change or " + "could not be calculated.\n" + "\n" + "Uses Product.CsvType indexing" + ), + ) + avg: list[list[int | None] | None] | None = Field( + default=None, + description=( + "Contains the weighted averages in the " + "respective date range and price type.\n" + "\n" + "Note: The day interval (index 0) is " + "actually the average of the last 48 " + "hours, not 24 hours. This is due to the " + "way our deals work.\n" + "\n" + "First dimension uses Product.CsvType, " + "second dimension DealInterval" + ), + ) + current: list[int | None] | None = Field( + default=None, + description=( + "Contains the prices / ranks of the " + "product of the time we last updated it. " + "Uses the Price Type indexing.\n" + "The price is an integer of the " + "respective Amazon locale's smallest " + "currency unit (e.g. euro cents or yen).\n" + "If no offer was available in the given " + "interval (e.g. out of stock) the price " + "has the value -1.\n" + "Shipping and Handling costs are not " + "included. Amazon is considered to be " + "part of the marketplace, so if\n" + "Amazon has the overall lowest new price," + " the marketplace new price in the " + "corresponding time interval will\n" + "be identical to the Amazon price (except" + " if there is only one marketplace " + "offer).\n" + "\n" + "Uses Product.CsvType indexing" + ), + ) + rootCat: int | None = Field( + default=None, + description=( + "Category node id Category.catId of the " + "product's root category. 0 or " + "9223372036854775807 if no root category " + "known." + ), + ) + creationDate: int | None = Field( + default=None, + description=( + "States the time this deal was found, in " + "Keepa Time minutes.\n" + "\n" + "Use " + "KeepaTime.keepaMinuteToUnixInMillis(int)" + " (long) to get an uncompressed timestamp" + " (Unix epoch time)." + ), + ) + image: list[int | None] | None = Field( + default=None, + description=( + "The name of the main product image of " + "the product. Make sure you own the " + "rights to use the image.\n" + "\n" + "Each entry represents the integer of a " + "US-ASCII (ISO646-US) coded character. " + "Easiest way to convert it to a String in" + " Javascript would be var imageName = " + 'String.fromCharCode.apply("", ' + "productObject.image);.\n" + "\n" + "Example: [54,49,107,51,76,97,121,55,74,8" + "5,76,46,106,112,103], which equals " + '"61k3Lay7JUL.jpg".\n' + "\n" + "Full Amazon image path: https://m.media-" + "amazon.com/images/I/image name" + ), + ) + categories: list[int | None] | None = Field( + default=None, + description=( + "Array of Amazon category node ids " + "Category.catId this product is listed " + "in. Can be empty.\n" + "\n" + "Example: [569604]" + ), + ) + lastUpdate: int | None = Field( + default=None, + description=( + "States the last time we have updated the" + " information for this deal, in Keepa " + "Time minutes.\n" + "\n" + "Use " + "KeepaTime.keepaMinuteToUnixInMillis(int)" + " (long) to get an uncompressed timestamp" + " (Unix epoch time)." + ), + ) + lightningEnd: int | None = Field( + default=None, + description=( + "States the time this lightning deal is " + "scheduled to end, in Keepa Time minutes." + " Only applicable to lightning deals. 0 " + "otherwise.\n" + "\n" + "Use " + "KeepaTime.keepaMinuteToUnixInMillis(int)" + " (long) to get an uncompressed timestamp" + " (Unix epoch time)." + ), + ) + minRating: int | None = Field( + default=None, + description=( + "Limit to products with a minimum rating " + "(A rating is an integer from 0 to 50 " + "(e.g. 45 = 4.5 stars)).\n" + "If -1 the filter is inactive.\n" + "Example: 20 (= min. rating of 2 stars)" + ), + ) + warehouseCondition: int | None = Field( + default=None, + description=( + "The OfferCondition condition of the " + "cheapest warehouse deal of this product." + " Integer value:\n" + "\n" + "0 - Unknown: We were unable to determine" + " the condition or this is not a " + "warehouse deal in our data\n" + "\n" + "2 - Used - Like New\n" + "\n" + "3 - Used - Very Good\n" + "\n" + "4 - Used - Good\n" + "\n" + "5 - Used - Acceptable" + ), + ) + warehouseConditionComment: str | None = Field( + default=None, + description=( + "The offer comment of the cheapest " + "warehouse deal of this product. null if " + "no warehouse deal found in our data." + ), + ) + currentSince: list[int | None] | None = Field( + default=None, + description=( + "The timestamp indicating the starting " + "point from which the current value has " + "been in effect, in Keepa Time minutes.\n" + "\n" + "Uses Product.CsvType indexing.\n" + "\n" + "Use " + "KeepaTime.keepaMinuteToUnixInMillis(int)" + " (long) to get an uncompressed timestamp" + " (Unix epoch time)." + ), + ) + + +class DealDetails(KeepaBackendModel): + """Backend ``DealDetails`` model generated from the Keepa Java schema.""" + + accessType: str | None = Field( + default=None, + description=( + "Who can access the deal. New access " + "types can be added at any time.\n" + "\n" + "Currently observed values include " + "(non-exhaustive):\n" + "\n" + "ALL\n" + "PRIMEEARLYACCESS\n" + "PRIME_EXCLUSIVE" + ), + ) + endTime: int | None = Field( + default=None, + description=( + "Deal end time in Keepa Time minutes.\n" + "\n" + "May be null and is not available for all" + " deal types.\n" + "Use " + "KeepaTime.keepaMinuteToUnixInMillis(int)" + " (long) to get an uncompressed timestamp" + " (Unix epoch time)." + ), + ) + startTime: int | None = Field( + default=None, + description=( + "Deal start time in Keepa Time minutes.\n" + "\n" + "May be null and is not available for all" + " deal types.\n" + "Use " + "KeepaTime.keepaMinuteToUnixInMillis(int)" + " (long) to get an uncompressed timestamp" + " (Unix epoch time)." + ), + ) + percentClaimed: int | None = Field( + default=None, + description=( + "Percentage claimed for Lightning Deals " + "(i.e., dealType == LIMITEDTIMEDEAL).\n" + "\n" + "Range: 0\u2013100. May be null or absent for " + "other deal types." + ), + ) + badge: str | None = Field( + default=None, + description=( + 'The badge text as shown on the product page.\n\nExample: "Early Prime Deal".' + ), + ) + dealType: str | None = Field( + default=None, + description=( + "The type/category of the deal. New deal " + "types can be added at any time.\n" + "\n" + "Currently observed values include " + "(non-exhaustive):\n" + "\n" + "PRIME_DAY\n" + "PRIMEDAYEARLY\n" + "EARLYACCESSWITH_PRIME\n" + "PRIME_EXCLUSIVE\n" + "SELLING_FAST\n" + "PRIMESELLINGFAST\n" + "LIMITEDTIMEDEAL\n" + "COUNTDOWNENDSIN\n" + "APP_ONLY\n" + "CLEARANCENORETURNS\n" + "SPECIALEVENTSALE\n" + "GENERICOFFERPROMO\n" + "UNKNOWN" + ), + ) + + +class DealRequest(KeepaBackendModel): + """Backend ``DealRequest`` model generated from the Keepa Java schema.""" + + page: int | None = Field( + default=None, + description=( + "Most deal queries have more than 150 " + "results (which is the maximum page " + "size). To browse all deals found by a " + "query (which is cached for 120 seconds) " + "you iterate the page parameter and keep " + "all other parameters identical. You " + "start with page 0 and stop when the " + "response contains less than 150 results." + ), + ) + domainId: int | None = Field( + default=None, + description=("The domainId of the products Amazon locale AmazonLocale"), + ) + priceTypes: list[int | None] | None = Field( + default=None, + description=( + "Determines the type of the deal. Even " + "though it is an integer array, it can " + "contain only one entry. Multiple types " + "per query are not yet supported.\n" + "Uses Product.CsvType coding. Only those " + "applicable with " + "Product.CsvType.isDealRelevant set to " + "true." + ), + ) + isLowest: bool | None = Field( + default=None, + description=( + "Include only products for which the " + "specified price type is at its lowest " + "value (since tracking began)." + ), + ) + isLowest90: bool | None = Field( + default=None, + description=( + "Include only products for which the " + "specified price type is at its lowest " + "value in the past 90 days." + ), + ) + isLowestOffer: bool | None = Field( + default=None, + description=( + "Include only products if the selected " + "price type is the lowest of all New " + "offers (applicable to Amazon and " + "Marketplace New)." + ), + ) + dateRange: int | None = Field( + default=None, + description=( + "Our deals are divided in different sets," + " determined by the time interval in " + "which the product changed. The shorter " + "the interval, the more recent the " + "change; which is good for big price " + "drops but bad for slow incremental drops" + " that accumulate over a longer period.\n" + "For most deals the shorter intervals can" + " be considered as subsets of the longer " + "intervals. To find more deals use the " + "longer intervals.\n" + "\n" + "Possible values:\n" + "\n" + "0 - day: the last 24 hours\n" + "1 - week: the last 24 * 7 hours\n" + "2 - month: the last 24 * 31 hours\n" + "3 - 90 days: the last 24 * 90 hours" + ), + ) + deltaRange: list[int | None] | None = Field( + default=None, + description=( + "Limit the range of the absolute " + "difference between the current value and" + " the one at the beginning of the chosen " + "dateRange interval.\n" + "\n" + "Example: [0,999] (= no minimum " + "difference, maximum difference of " + "$9.99)." + ), + ) + excludeCategories: list[int | None] | None = Field( + default=None, + description=( + "Used to exclude products that are listed" + " in these categories. If it is a sub " + "category the product must be directly " + "listed in this category. It will not " + "exclude products in child categories of " + "the specified ones, unless it is a root " + "category.\n" + "Array with category node ids14." + ), + ) + includeCategories: list[int | None] | None = Field( + default=None, + description=( + "Used to only include products that are " + "listed in these categories. If it is a " + "sub category the product must be " + "directly listed in this category. It " + "will not include products in child " + "categories of the specified ones, unless" + " it is a root category. Array with " + "category node ids14." + ), + ) + deltaPercentRange: list[int | None] | None = Field( + default=None, + description=( + "Same as deltaRange, but given in percent" + " instead of absolute values. Minimum " + "percent is 10, for Sales Rank it is 80.\n" + "\n" + "Example: [30,80] (= between 30% and " + "80%)." + ), + ) + currentRange: list[int | None] | None = Field( + default=None, + description=( + "Limit the range of the current value of " + "the price type.\n" + "\n" + "Example: [105,50000] (= minimum price " + "$1.05, maximum price $500, or a rank " + "between 105 and 50000)." + ), + ) + titleSearch: str | None = Field( + default=None, + description=( + "Select deals by a keywords based product" + " title search. The search is " + "case-insensitive and supports multiple " + "keywords which, if specified and " + "separated by a space, must all match.\n" + "\n" + "Examples:\n" + '"samsung galaxy" matches all products ' + "which have the character sequences " + '"samsung" AND "galaxy" within their ' + "title" + ), + ) + isBackInStock: bool | None = Field( + default=None, + description=( + "Only include products which were out of " + "stock within the last 24 hours and can " + "now be ordered." + ), + ) + isOutOfStock: bool | None = Field( + default=None, + description=( + "Only include products which were " + "available to order within the last 24 " + "hours and now out of stock." + ), + ) + isRangeEnabled: bool | None = Field( + default=None, + description=("Switch to enable the range options."), + ) + filterErotic: bool | None = Field( + default=None, + description=("Excludes all products that are listed as adult items."), + ) + sortType: int | None = Field( + default=None, + description=( + "Determines the sort order of the " + "retrieved deals. To invert the sort " + "order use negative values.\n" + "\n" + "Possible sort by values:\n" + "\n" + "1 - deal age. Newest deals first, not " + "invertible.\n" + "2 - absolute delta. (the difference " + "between the current value and the one at" + " the beginning of the chosen dateRange " + "interval). Sort order is highest delta " + "to lowest.\n" + "3 - sales rank. Sort order is lowest " + "rank o highest.\n" + "4 - percentage delta (the percentage " + "difference between the current value and" + " the one at the beginning of the chosen " + "dateRange interval). Sort order is " + "highest percent to lowest." + ), + ) + salesRankRange: list[int | None] | None = Field( + default=None, + description=( + "Limit the Sales Rank range of the " + "product. Identical to currentRange if " + "price type is set to Sales Rank. If you " + "want to keep the upper bound open, you " + "can specify -1 (which will translate to " + "the maximum signed integer value).\n" + "\n" + "Important note: Once this range option " + "is used all products with no Sales Rank " + "will be excluded. Set it to null to not " + "use it.\n" + "\n" + "Examples:\n" + "\n" + "[0,5000] (= only products which have a " + "sales rank between 0 and 5000)\n" + "\n" + "[5000,-1] (= higher than 5000)" + ), + ) + isFilterEnabled: bool | None = Field( + default=None, + description=("Switch to enable the filter options."), + ) + deltaLastRange: list[int | None] | None = Field( + default=None, + description=( + "Limit the range of the absolute " + "difference between the current value and" + " previous one.\n" + "\n" + "Example: [100,500] (= last change " + "between one $1 and $5)" + ), + ) + hasReviews: bool | None = Field( + default=None, + description=( + "If true excludes all products with no reviews. If false the filter is inactive." + ), + ) + isPrimeExclusive: bool | None = Field( + default=None, + description=("Include only products flagged as Prime Exclusive.\n\nExample: true"), + ) + mustHaveAmazonOffer: bool | None = Field( + default=None, + description=( + "Include only products that currently " + "have an offer sold and fulfilled by " + "Amazon.\n" + "\n" + "Example: true" + ), + ) + mustNotHaveAmazonOffer: bool | None = Field( + default=None, + description=( + "Include only products that currently " + "have no offer sold and fulfilled by " + "Amazon.\n" + "\n" + "Example: true" + ), + ) + minRating: int | None = Field( + default=None, + description=( + "Minimum product rating to include.\n" + "\n" + "Integer from 0 to 50 (e.g., 45 = 4.5 " + "stars)\n" + "Use -1 to disable the filter\n" + "\n" + "Example: 20 // \u2265 2.0 stars" + ), + ) + warehouseConditions: list[int | None] | None = Field( + default=None, + description=( + "Include only Amazon Warehouse deals that" + " match these condition codes.\n" + "\n" + "Use integer-coded conditions such as:\n" + "1 = New, 2 = Used - Like New, 3 = Used -" + " Very Good, 24 = Used - Good, 5 = Used -" + " Acceptable.\n" + "\n" + "Example: [1, 2]\n" + "\n" + "Note: API expects integers; choose a " + "numeric type that fits your model." + ), + ) + singleVariation: bool | None = Field( + default=None, + description=( + "If multiple variations match, return " + "only a single (random) variation.\n" + "\n" + "Example: true" + ), + ) + material: list[str | None] | None = Field( + default=None, + description=( + "Include only products made of the " + 'specified materials (e.g., "cotton").\n' + "\n" + 'Example: ["cotton", "polyester"]' + ), + ) + type: list[str | None] | None = Field( + default=None, + description=( + "Include only products matching the " + 'specified type (e.g., "shirt", "dress").' + "\n" + "\n" + 'Example: ["shirt"]' + ), + ) + manufacturer: list[str | None] | None = Field( + default=None, + description=('Include only products from the specified manufacturer.\n\nExample: ["Sony"]'), + ) + brand: list[str | None] | None = Field( + default=None, + description=( + 'Include only products from the specified brand.\n\nExample: ["Apple", "Samsung"]' + ), + ) + productGroup: list[str | None] | None = Field( + default=None, + description=( + "Include only products in the specified " + 'Amazon product group (e.g., "home", ' + '"book").\n' + "\n" + 'Example: ["book"]' + ), + ) + model: list[str | None] | None = Field( + default=None, + description=( + 'Include only products matching the specified model identifier.\n\nExample: ["A2179"]' + ), + ) + color: list[str | None] | None = Field( + default=None, + description=( + "Include only products matching the " + "specified color attribute.\n" + "\n" + 'Example: ["black", "navy"]' + ), + ) + size: list[str | None] | None = Field( + default=None, + description=( + "Include only products matching the " + 'specified size (e.g., "small", "one ' + 'size").\n' + "\n" + 'Example: ["M", "L"]' + ), + ) + unitType: list[str | None] | None = Field( + default=None, + description=( + "Include only products with the specified" + ' unit type (e.g., "count", "ounce").\n' + "\n" + 'Example: ["count"]' + ), + ) + scent: list[str | None] | None = Field( + default=None, + description=( + "Include only products with the specified" + ' scent (e.g., "lavender", "citrus").\n' + "\n" + 'Example: ["lavender"]' + ), + ) + itemForm: list[str | None] | None = Field( + default=None, + description=( + "Include only products matching the " + 'specified item form (e.g., "liquid", ' + '"sheet").\n' + "\n" + 'Example: ["liquid"]' + ), + ) + pattern: list[str | None] | None = Field( + default=None, + description=( + "Include only products matching the " + 'specified pattern (e.g., "striped", ' + '"solid").\n' + "\n" + 'Example: ["striped"]' + ), + ) + style: list[str | None] | None = Field( + default=None, + description=( + "Include only products matching the " + "specified style attribute (e.g., " + '"modern", "vintage").\n' + "\n" + 'Example: ["modern"]' + ), + ) + itemTypeKeyword: list[str | None] | None = Field( + default=None, + description=( + "Include only products matching the " + "specified item type keyword (custom " + "search term).\n" + "\n" + 'Example: ["books", "prints"]' + ), + ) + targetAudienceKeyword: list[str | None] | None = Field( + default=None, + description=( + "Include only products targeting the " + 'specified audience (e.g., "kids", ' + '"professional").\n' + "\n" + 'Example: ["kids"]' + ), + ) + edition: list[str | None] | None = Field( + default=None, + description=( + "Include only products matching the " + 'specified edition (e.g., "first ' + 'edition", "standard edition").\n' + "\n" + 'Example: ["first edition"]' + ), + ) + format: list[str | None] | None = Field( + default=None, + description=( + "Include only products in the specified " + 'format (e.g., "kindle ebook", "import", ' + '"dvd").\n' + "\n" + 'Example: ["paperback"]' + ), + ) + author: list[str | None] | None = Field( + default=None, + description=( + "Include only products by the specified " + "author (applicable to books, music, " + "etc.).\n" + "\n" + 'Example: ["Neil Gaiman"]' + ), + ) + binding: list[str | None] | None = Field( + default=None, + description=( + "Include only products with the specified" + ' binding type (e.g., "paperback").\n' + "\n" + 'Example: ["hardcover"]' + ), + ) + languages: list[str | None] | None = Field( + default=None, + description=( + "Include only products available in the " + "specified languages (use language " + "names).\n" + "\n" + 'Example: ["English", "German"]' + ), + ) + brandStoreName: list[str | None] | None = Field( + default=None, + description=( + "Include only products sold under the " + "specified Brand Store name on Amazon.\n" + "\n" + 'Example: ["Amazon Basics"]' + ), + ) + brandStoreUrlName: list[str | None] | None = Field( + default=None, + description=( + "Include only products sold under the " + "specified URL-friendly Brand Store " + "identifier.\n" + "\n" + 'Example: ["amazonbasics"]' + ), + ) + websiteDisplayGroup: list[str | None] | None = Field( + default=None, + description=( + "Include only products in the specified " + "website display group.\n" + "\n" + 'Example: ["fashiondisplayon_website"]' + ), + ) + websiteDisplayGroupName: list[str | None] | None = Field( + default=None, + description=( + "Include only products in the specified " + "website display group name " + "(user-friendly label).\n" + "\n" + 'Example: ["Fashion"]' + ), + ) + salesRankDisplayGroup: list[str | None] | None = Field( + default=None, + description=( + "Include only products belonging to the " + "specified sales rank display group\n" + '(e.g., "fashiondisplayon_website").\n' + "\n" + 'Example: ["fashiondisplayon_website"]' + ), + ) + + +class DealResponse(KeepaBackendModel): + """Backend ``DealResponse`` model generated from the Keepa Java schema.""" + + dr: list[Deal | None] | None = Field( + default=None, + description=("Ordered array of all deal objects matching your query."), + ) + drDateIndex: list[int | None] | None = Field( + default=None, + description=("Not yet used / placeholder"), + ) + categoryIds: list[int | None] | None = Field( + default=None, + description=("Contains all root categoryIds of the matched deal products."), + ) + categoryNames: list[str | None] | None = Field( + default=None, + description=("Contains all root category names of the matched deal products."), + ) + categoryCount: list[int | None] | None = Field( + default=None, + description=("Contains how many deal products in the respective root category are found."), + ) + + +class FBAFeesObject(KeepaBackendModel): + """Backend ``FBAFeesObject`` model generated from the Keepa Java schema.""" + + storageFee: int | None = None + storageFeeTax: int | None = None + pickAndPackFee: int | None = None + pickAndPackFeeTax: int | None = None + + +class FeedbackObject(KeepaBackendModel): + """Backend ``FeedbackObject`` model generated from the Keepa Java schema.""" + + rating: int | None = Field( + default=None, + description=("the feedback star rating - value range from 10 (1 star) to 50 (5 stars)"), + ) + date: int | None = Field( + default=None, + description=("timestamp of the feedback, in Keepa Time minutes"), + ) + feedback: str | None = Field( + default=None, + description=("the feedback text"), + ) + isStriked: bool | None = Field( + default=None, + description=("whether or not the feedback is striked"), + ) + + +class Format(KeepaBackendModel): + """Backend ``Format`` model generated from the Keepa Java schema.""" + + asin: str | None = Field( + default=None, + description=("The ASIN of the format."), + ) + format: str | None = Field( + default=None, + description=( + "The type of format, which can be one of " + "the following: Kindle, Paperback, " + "Hardcover, Audiobook, or Spiral-bound." + ), + ) + + +class HazardousMaterial(KeepaBackendModel): + """Backend ``HazardousMaterial`` model generated from the Keepa Java schema.""" + + aspect: str | None = None + value: str | None = None + + +class Image(KeepaBackendModel): + """Backend ``Image`` model generated from the Keepa Java schema.""" + + l_: str | None = Field( + default=None, + alias="l", + description=("The filename of the large image."), + ) + lH: int | None = Field( + default=None, + description=("The height of the large image in pixels."), + ) + lW: int | None = Field( + default=None, + description=("The width of the large image in pixels."), + ) + m: str | None = Field( + default=None, + description=("The filename of the medium image."), + ) + mH: int | None = Field( + default=None, + description=("The height of the medium image in pixels."), + ) + mW: int | None = Field( + default=None, + description=("The width of the medium image in pixels."), + ) + + +class LightningDeal(KeepaBackendModel): + """Backend ``LightningDeal`` model generated from the Keepa Java schema.""" + + domainId: int | None = Field( + default=None, + description=("The domainId of the products Amazon locale\n\nAmazonLocale"), + ) + lastUpdate: int | None = Field( + default=None, + description=( + "States the time of our last data " + "collection of this lightning deal, in " + "Keepa Time minutes.\n" + "\n" + "Use " + "KeepaTime.keepaMinuteToUnixInMillis(int)" + " (long) to get an uncompressed timestamp" + " (Unix epoch time)." + ), + ) + asin: str | None = Field( + default=None, + description=("The ASIN of the product"), + ) + title: str | None = Field( + default=None, + description=("Title of the product. Caution: may contain HTML markup in rare cases."), + ) + sellerName: str | None = Field( + default=None, + description=("The name of the seller offering this deal."), + ) + sellerId: str | None = Field( + default=None, + description=("The seller id of the merchant offering this deal."), + ) + dealId: str | None = Field( + default=None, + description=("A unique ID for this deal."), + ) + dealPrice: int | None = Field( + default=None, + description=( + "The discounted price of this deal. " + "Available once the deal has started. -1 " + "if the deal\u2019s state is upcoming. The " + "price is an integer of the respective " + "Amazon locale\u2019s smallest currency unit " + "(e.g. euro cents or yen)." + ), + ) + currentPrice: int | None = Field( + default=None, + description=( + "The regular price of this product. " + "Available once the deal has started. -1 " + "if the deal\u2019s state is upcoming. The " + "price is an integer of the respective " + "Amazon locale\u2019s smallest currency unit " + "(e.g. euro cents or yen)." + ), + ) + image: str | None = Field( + default=None, + description=("The name of the primary image of the product. null if not available."), + ) + isPrimeEligible: bool | None = Field( + default=None, + description=("Whether or not the deal is Prime eligible."), + ) + isFulfilledByAmazon: bool | None = Field( + default=None, + description=("Whether or not the deal is fulfilled by Amazon."), + ) + isMAP: bool | None = Field( + default=None, + description=("Whether or not the price is restricted by MAP (Minimum Advertised Price)."), + ) + rating: int | None = Field( + default=None, + description=( + "The rating of the product. A rating is an integer from 0 to 50 (e.g. 45 = 4.5 stars)." + ), + ) + totalReviews: int | None = Field( + default=None, + description=("The product\u2019s review count."), + ) + dealState: DealState | None = Field( + default=None, + description=("The state of the deal."), + ) + startTime: int | None = Field( + default=None, + description=( + "The start time of this lightning deal, " + "in Keepa Time minutes. Note that due to " + "the delay in our data collection the " + "deal price might not be available " + "immediately once the deal has started on" + " Amazon.\n" + "\n" + "Use " + "KeepaTime.keepaMinuteToUnixInMillis(int)" + " (long) to get an uncompressed timestamp" + " (Unix epoch time)." + ), + ) + endTime: int | None = Field( + default=None, + description=( + "The end time of this lightning deal, in " + "Keepa Time minutes.\n" + "\n" + "Use " + "KeepaTime.keepaMinuteToUnixInMillis(int)" + " (long) to get an uncompressed timestamp" + " (Unix epoch time)." + ), + ) + percentClaimed: int | None = Field( + default=None, + description=( + "The percentage claimed of the lightning " + "deal. Since lightning deals have limited" + " stock, this number may change fast on " + "Amazon, but due to the delay of our data" + " collection the provided value may be " + "outdated." + ), + ) + percentOff: int | None = Field( + default=None, + description=( + "The provided discount of this deal, " + "according to Amazon. May be in reference" + " to the list price, not the current " + "price." + ), + ) + variation: list[VariationAttributeObject | None] | None = Field( + default=None, + description=("The dimension attributes of this deal."), + ) + + +class MerchantBrandStatistics(KeepaBackendModel): + """Backend ``MerchantBrandStatistics`` model generated from the Keepa Java schema.""" + + brand: str | None = Field( + default=None, + description=("the brand (in all lower-case)"), + ) + productCount: int | None = Field( + default=None, + description=("the number of products this merchant sells with this brand"), + ) + avg30SalesRank: int | None = Field( + default=None, + description=("the 30 day average sales rank of these products"), + ) + productCountWithAmazonOffer: int | None = Field( + default=None, + description=("how many of these products have an Amazon offer"), + ) + + +class MerchantCategoryStatistics(KeepaBackendModel): + """Backend ``MerchantCategoryStatistics`` model generated from the Keepa Java schema.""" + + catId: int | None = Field( + default=None, + description=("the category id"), + ) + productCount: int | None = Field( + default=None, + description=("the number of products this merchant sells with this category"), + ) + avg30SalesRank: int | None = Field( + default=None, + description=("the 30 day average sales rank of these products"), + ) + productCountWithAmazonOffer: int | None = Field( + default=None, + description=("how many of these products have an Amazon offer"), + ) + + +class Notification(KeepaBackendModel): + """Backend ``Notification`` model generated from the Keepa Java schema.""" + + asin: str | None = Field( + default=None, + description=("The notified product ASIN"), + ) + title: str | None = Field( + default=None, + description=("Title of the product. Caution: may contain HTML markup in rare cases."), + ) + image: str | None = Field( + default=None, + description=( + "The main image name of the product. Full" + " Amazon image path:\n" + "\n" + "https://m.media-amazon.com/images/I/imag" + "e name" + ), + ) + createDate: int | None = Field( + default=None, + description=("Creation date of the notification in KeepaTime minutes"), + ) + domainId: int | None = Field( + default=None, + description=( + "The main Amazon locale of the tracking " + "which determines the currency used for " + "all prices of this notification.\n" + "\n" + "Integer value for the Amazon locale " + "AmazonLocale" + ), + ) + notificationDomainId: int | None = Field( + default=None, + description=( + "The Amazon locale which triggered the " + "notification.\n" + "\n" + "Integer value for the Amazon locale " + "AmazonLocale" + ), + ) + csvType: int | None = Field( + default=None, + description=("The Product.CsvType which triggered the notification."), + ) + trackingNotificationCause: int | None = Field( + default=None, + description=("The Tracking.TrackingNotificationCause of the notification."), + ) + currentPrices: list[int | None] | None = Field( + default=None, + description=( + "Contains the prices / values of the " + "product of the time this notification " + "was created.\n" + "\n" + "Uses Product.CsvType indexing.\n" + "\n" + "The price is an integer of the " + "respective Amazon locale's smallest " + "currency unit (e.g. euro cents or yen).\n" + "If no offer was available in the given " + "interval (e.g. out of stock) the price " + "has the value -1." + ), + ) + sentNotificationVia: list[bool | None] | None = Field( + default=None, + description=( + "States through which notification " + "channels (Tracking.NotificationType) " + "this notification was delivered." + ), + ) + metaData: str | None = Field( + default=None, + description=("The meta data of the tracking."), + ) + + +class Offer(KeepaBackendModel): + """Backend ``Offer`` model generated from the Keepa Java schema.""" + + offerId: int | None = Field( + default=None, + description=( + "Unique id of this offer (in the scope of" + " the product).\n" + "Not related to the offerIds used by " + "Amazon, as those are user specific and " + "only valid for a short time.\n" + "The offerId can be used to identify the " + "same offers throughout requests.\n" + "\n" + "Example: 4" + ), + ) + lastSeen: int | None = Field( + default=None, + description=( + "States the last time we have seen (and " + "updated) this offer, in Keepa Time " + "minutes.\n" + "\n" + "Example: 2700145" + ), + ) + sellerId: str | None = Field( + default=None, + description=( + "The seller id of the merchant.\n\nExample: A2L77EE7U53NWQ (Amazon.com Warehouse Deals)" + ), + ) + offerCSV: list[int | None] | None = Field( + default=None, + description=( + "Contains the current price and shipping " + "costs of the offer as well as, if " + "available, the offer's history.\n" + "It has the format Keepa time minutes, " + "price, shipping cost, [...].\n" + "\n" + "The price and shipping cost are integers" + " of the respective Amazon locale's " + "smallest currency unit (e.g. euro cents " + "or yen).\n" + "If we were unable to determine the price" + " or shipping cost they have the value " + "-2.\n" + "Free shipping has the shipping cost of " + "0.\n" + "If an offer is not shippable or has " + "unspecified shipping costs the shipping " + "cost will be -1.\n" + "To get the newest price and shipping " + "cost access the last two entries of the " + "array.\n" + "\n" + "Most recent price: " + "offerCSV[offerCSV.length - 2]\n" + "\n" + "Most recent shipping cost: " + "offerCSV[offerCSV.length - 1]" + ), + ) + condition: int | None = Field( + default=None, + description=( + "The OfferCondition condition of the " + "offered product. Integer value:\n" + "\n" + "0 - Unknown: We were unable to determine" + " the condition.\n" + "\n" + "1 - New\n" + "\n" + "2 - Used - Like New\n" + "\n" + "3 - Used - Very Good\n" + "\n" + "4 - Used - Good\n" + "\n" + "5 - Used - Acceptable\n" + "\n" + "6 - Refurbished\n" + "\n" + "7 - Collectible - Like New\n" + "\n" + "8 - Collectible - Very Good\n" + "\n" + "9 - Collectible - Good\n" + "\n" + "10 - Collectible - Acceptable\n" + "\n" + "Note: Open Box conditions will be coded " + "as Used conditions." + ), + ) + conditionComment: str | None = Field( + default=None, + description=( + "The describing text of the condition.\n" + "\n" + "Example: The item may come repackaged. " + "Small cosmetic imperfection on top, " + "[...]" + ), + ) + isPrime: bool | None = Field( + default=None, + description=( + "Whether this offer is available via " + "Prime shipping. Can be used as a FBA " + '("Fulfillment by Amazon") indicator as ' + "well." + ), + ) + isMAP: bool | None = Field( + default=None, + description=( + "If the price of this offer is hidden on " + 'Amazon due to a MAP ("minimum advertised' + ' price") restriction.\n' + "Even if so, the offer object will " + "contain the price and shipping costs." + ), + ) + isShippable: bool | None = Field( + default=None, + description=( + "Indicating whether the offer is " + "currently shippable.\n" + "If not this could mean for example that " + "it is temporarily out of stock or a " + "pre-order." + ), + ) + isPreorder: bool | None = Field( + default=None, + description=("Indicating whether the offer is a pre-order."), + ) + isWarehouseDeal: bool | None = Field( + default=None, + description=("Indicating whether the offer is a Warehouse Deal."), + ) + shipsFromChina: bool | None = Field( + default=None, + description=("Indicating whether the offer ships from China."), + ) + isAmazon: bool | None = Field( + default=None, + description=( + "True if the seller is Amazon (e.g. " + '"Amazon.com").\n' + "\n" + "Note: Amazon's Warehouse Deals seller " + "account or other accounts Amazon is " + "maintaining under a different name are " + "not considered to be Amazon." + ), + ) + isFBA: bool | None = Field( + default=None, + description=("Whether this offer is fulfilled by Amazon."), + ) + isPrimeExcl: bool | None = Field( + default=None, + description=( + "This offer has a discounted Prime " + "exclusive price. A Prime exclusive offer" + " can only be ordered if the buyer has an" + " active Prime subscription." + ), + ) + primeExclCSV: list[int | None] | None = Field( + default=None, + description=( + "Contains the Prime exclusive price " + "history of this offer, if available. A " + "Prime exclusive offer can only be " + "ordered if the buyer has an active Prime" + " subscription.\n" + "It has the format Keepa time minutes, " + "price, [...].\n" + "\n" + "Most recent Prime exclusive price: " + "primeExclCSV[primeExclCSV.length - 1]" + ), + ) + stockCSV: list[int | None] | None = Field( + default=None, + description=( + "Contains the available stock of this " + "offer as well as, if available, the " + "stock's history.\n" + "It has the format Keepa time minutes, " + "stock, [...].\n" + "\n" + "Most recent stock: " + "stockCSV[stockCSV.length - 1]" + ), + ) + minOrderQty: int | None = Field( + default=None, + description=("Minimum order quantity. 0 if unknown."), + ) + coupon: int | None = Field( + default=None, + description=( + "Contains one-time coupon details of this" + " offer. Undefined if none is available.\n" + "Positive integer for an absolute " + "discount or negative for a percentage " + "discount.\n" + "Example:\n" + "500 - Coupon with a $5 discount.\n" + "-15 - Coupon with a 15% discount." + ), + ) + couponHistory: list[int | None] | None = Field( + default=None, + description=( + "Contains the coupon history of this " + "offer, if available.\n" + "It has the format Keepa time minutes, " + "coupon, [...]." + ), + ) + offerDuplicates: list[OfferDuplicate | None] | None = Field( + default=None, + description=( + "An array that lists identical offers we " + "detected for the same seller, condition," + " and shipping type that were excluded " + "from the main offers list because they " + "were not the cheapest." + ), + ) + + +class OfferDuplicate(KeepaBackendModel): + """Backend ``OfferDuplicate`` model generated from the Keepa Java schema.""" + + price: int | None = None + conditionComment: str | None = None + shipping: int | None = None + + +class Product(KeepaBackendModel): + """Backend ``Product`` model generated from the Keepa Java schema.""" + + asin: str | None = Field( + default=None, + description=("The ASIN of the product"), + ) + domainId: int | None = Field( + default=None, + description=("The domainId of the products Amazon locale\n\nAmazonLocale"), + ) + parentAsin: str | None = Field( + default=None, + description=( + "The ASIN of the parent product (if the product has variations, otherwise null)" + ), + ) + parentAsinHistory: list[str | None] | None = Field( + default=None, + description=( + "The history of the parentAsin field. " + "This array follows the format: [Keepa " + "time in minutes, previous parent ASIN, " + "...].\n" + "\n" + "The included timestamp indicates when " + "the previously associated parent ASIN " + "ceased to be valid.\n" + "\n" + "For the current parent ASIN, use the " + "parentAsin field.\n" + "\n" + "To convert a Keepa minute timestamp into" + " an uncompressed, Unix epoch time " + "timestamp, use the " + "KeepaTime.keepaMinuteToUnixInMillis(int)" + " method.\n" + "\n" + "null if the parentAsin field never " + "changed." + ), + ) + variationCSV: str | None = Field( + default=None, + description=( + "Comma separated list of variation ASINs " + "of the product (if the product is a " + "parent ASIN, otherwise null)\n" + "@deprecated use the field variations " + "instead" + ), + ) + upcList: list[str | None] | None = Field( + default=None, + description=( + "A list of UPC assigned to this product. " + "The first index is the primary UPC. null" + " if not available." + ), + ) + eanList: list[str | None] | None = Field( + default=None, + description=( + "A list of EAN assigned to this product. " + "The first index is the primary EAN. null" + " if not available." + ), + ) + gtinList: list[str | None] | None = Field( + default=None, + description=( + "A list of GTIN assigned to this product." + " The first index is the primary GTIN. " + "null if not available." + ), + ) + bundleItems: list[str | None] | None = Field( + default=None, + description=("A list of virtual bundle ASINs. Only available for virtual product bundles."), + ) + historicalVariations: list[str | None] | None = Field( + default=None, + description=( + "A list of historical or out of stock " + "variations. Requires the " + "historical-variations parameter." + ), + ) + imagesCSV: str | None = Field( + default=None, + description=( + "Comma separated list of image names of " + "the product. Full Amazon image path:\n" + "\n" + "https://m.media-amazon.com/images/I/imag" + "e name\n" + "@deprecated use the field images instead" + ), + ) + images: list[Image | None] | None = Field( + default=None, + description=( + "Provides metadata for images associated " + "with the product.\n" + "\n" + "This field is an array of Image objects," + " each of which may contain metadata for " + "up to two resolutions\n" + "(large and medium) of the same image, if" + " available. These images are typically " + "used in the product image carousel\n" + "on platforms such as Amazon. This field " + "is null if no image data is available.\n" + "\n" + "Each Image object includes:\n" + "\n" + "l (String): Filename of the large image." + "\n" + "lH (Short): Height of the large image, " + "in pixels.\n" + "lW (Short): Width of the large image, in" + " pixels.\n" + "m (String): Filename of the medium " + "image.\n" + "mH (Short): Height of the medium image, " + "in pixels.\n" + "mW (Short): Width of the medium image, " + "in pixels.\n" + "\n" + "Example:\n" + '"images": [{\n' + '"l": "81bRlmLRyPL.jpg",\n' + '"lH": 1500,\n' + '"lW": 1500,\n' + '"m": "g1dlEmb2mq3.jpg",\n' + '"mH": 500,\n' + '"mW": 500\n' + "]\n" + "}\n" + "\n" + "Full Amazon image path format: " + "https://m.media-amazon.com/images/I/" + ), + ) + categories: list[int | None] | None = Field( + default=None, + description=("Array of category node ids"), + ) + rootCategory: int | None = Field( + default=None, + description=( + "Category node id of the products' root category. 0 if no root category known" + ), + ) + manufacturer: str | None = Field( + default=None, + description=("Name of the manufacturer"), + ) + title: str | None = Field( + default=None, + description=("Title of the product. Caution: may contain HTML markup in rare cases."), + ) + trackingSince: int | None = Field( + default=None, + description=( + "States the time we have started tracking" + " this product, in Keepa Time minutes.\n" + "\n" + "Use " + "KeepaTime.keepaMinuteToUnixInMillis(int)" + " (long) to get an uncompressed timestamp" + " (Unix epoch time)." + ), + ) + listedSince: int | None = Field( + default=None, + description=( + "States the time the item was first " + "listed on Amazon, in Keepa Time minutes." + "\n" + "\n" + "It is updated in conjunction with the " + "offers request, but always accessible.\n" + "\n" + "This timestamp is only available for " + "some products. If not available the " + "field has the value 0.\n" + "Use " + "KeepaTime.keepaMinuteToUnixInMillis(int)" + " (long) to get an uncompressed timestamp" + " (Unix epoch time)." + ), + ) + brand: str | None = Field( + default=None, + description=("An item's brand. null if not available."), + ) + productGroup: str | None = Field( + default=None, + description=("The item's productGroup. null if not available."), + ) + partNumber: str | None = Field( + default=None, + description=("The item's partNumber. null if not available."), + ) + model: str | None = Field( + default=None, + description=("The item's model. null if not available."), + ) + color: str | None = Field( + default=None, + description=("The item's color. null if not available."), + ) + size: str | None = Field( + default=None, + description=("The item's size. null if not available."), + ) + edition: str | None = Field( + default=None, + description=("The item's edition. null if not available."), + ) + format: str | None = Field( + default=None, + description=("The item's format. null if not available."), + ) + author: str | None = Field( + default=None, + description=( + "The item\u2019s author. null if not " + "available.\n" + "\n" + "@deprecated use the field contributors " + "instead" + ), + ) + binding: str | None = Field( + default=None, + description=( + "The item\u2019s binding. null if not " + "available. If the item is not a book it " + "is usually the product category instead." + ), + ) + categoryTree: list[CategoryTreeEntry | None] | None = Field( + default=None, + description=( + "Represents the category tree as an ordered array of CategoryTreeEntry objects." + ), + ) + numberOfItems: int | None = Field( + default=None, + description=("The number of items of this product. -1 if not available."), + ) + numberOfPages: int | None = Field( + default=None, + description=("The number of pages of this product. -1 if not available."), + ) + publicationDate: int | None = Field( + default=None, + description=( + "The item\u2019s publication date in one of " + "the following three formats:\n" + "\n" + "YYYY or YYYYMM or YYYYMMDD (Y= year, M =" + " month, D = day)\n" + "\n" + "-1 if not available.\n" + "\n" + "Examples:\n" + "\n" + "1978 = the year 1978\n" + "\n" + "200301 = January 2003\n" + "\n" + "20150409 = April 9th, 2015" + ), + ) + releaseDate: int | None = Field( + default=None, + description=( + "The item\u2019s release date in one of the " + "following three formats:\n" + "\n" + "YYYY or YYYYMM or YYYYMMDD (Y= year, M =" + " month, D = day)\n" + "\n" + "-1 if not available.\n" + "\n" + "Examples:\n" + "\n" + "1978 = the year 1978\n" + "\n" + "200301 = January 2003\n" + "\n" + "20150409 = April 9th, 2015" + ), + ) + languages: list[list[str | None] | None] | None = Field( + default=None, + description=( + "An item can have one or more languages. " + "Each language entry has a name and a " + "type.\n" + "Some also have an audio format. null if " + "not available.\n" + "\n" + "Examples:\n" + "\n" + "[ [ \u201cEnglish\u201d, \u201cPublished\u201d ], [ " + "\u201cEnglish\u201d, \u201cOriginal Language\u201d ] ]\n" + "\n" + "With audio format:\n" + "\n" + "[ [ \u201cEnglisch\u201d, \u201cOriginalsprache\u201d, " + "\u201cDTS-HD 2.0\u201d ], [ \u201cDeutsch\u201d, null, " + "\u201cDTS-HD 2.0\u201d ] ]" + ), + ) + contributors: list[list[str | None] | None] | None = Field( + default=None, + description=( + "The contributors of the item. A " + "contributor can be an author, actor, " + "director, etc. Each contributor entry " + "has a name and a role type.\n" + "\n" + "Example:\n" + "\n" + "[ [ \u201cVin Diesel\u201d, \u201cactor\u201d ] ]" + ), + ) + features: list[str | None] | None = Field( + default=None, + description=( + "A list of the product features / bullet " + "points. null if not available.\n" + "\n" + "An entry can contain HTML markup in rare" + " cases. We currently limit each entry to" + " a maximum of 1000 characters\n" + "\n" + "(if the feature is longer it will be cut" + " off). This limitation may change in the" + " future without prior notice." + ), + ) + description: str | None = Field( + default=None, + description=( + "A description of the product. null if " + "not available. Most description contain " + "HTML markup.\n" + "\n" + "We limit the product description to a " + "maximum of 2000 characters (if the " + "description is\n" + "\n" + "longer it will be cut off). This " + "limitation may change in the future " + "without prior notice." + ), + ) + packageHeight: int | None = Field( + default=None, + description=("The package's height in millimeter. 0 or -1 if not available."), + ) + packageLength: int | None = Field( + default=None, + description=("The package's length in millimeter. 0 or -1 if not available."), + ) + packageWidth: int | None = Field( + default=None, + description=("The package's width in millimeter. 0 or -1 if not available."), + ) + packageWeight: int | None = Field( + default=None, + description=("The package's weight in gram. 0 or -1 if not available."), + ) + packageQuantity: int | None = Field( + default=None, + description=("Quantity of items in a package. 0 or -1 if not available."), + ) + itemHeight: int | None = Field( + default=None, + description=("The item's height in millimeter. 0 or -1 if not available."), + ) + itemLength: int | None = Field( + default=None, + description=("The item's length in millimeter. 0 or -1 if not available."), + ) + itemWidth: int | None = Field( + default=None, + description=("The item's width in millimeter. 0 or -1 if not available."), + ) + itemWeight: int | None = Field( + default=None, + description=("The item's weight in gram. 0 or -1 if not available."), + ) + ebayListingIds: list[int | None] | None = Field( + default=None, + description=( + "Contains the lowest priced matching eBay" + " listing Ids.\n" + "Always contains two entries, the first " + "one is the listing id of the lowest " + "priced listing in new condition,\n" + "the second in used condition. null or 0 " + "if not available.\n" + "\n" + "Example: [ 273344490183, 0 ]" + ), + ) + isAdultProduct: bool | None = Field( + default=None, + description=("Indicates if the item is considered to be for adults only."), + ) + isEligibleForTradeIn: bool | None = Field( + default=None, + description=("Whether or not the product is eligible for trade-in."), + ) + referralFeePercent: int | None = Field( + default=None, + description=("@deprecated use the field referralFeePercentage instead"), + ) + variableClosingFee: int | None = Field( + default=None, + description=( + "The variable closing fee. Fees are " + "integers of the respective Amazon " + "locale\u2019s smallest currency unit (e.g. " + "euro cents or yen). null if not " + "available.\n" + "Example: 81" + ), + ) + urlSlug: str | None = Field( + default=None, + description=( + "The product listing URL slug.\nExample: Ring-Video-Doorbell-Satin-Nickel-2020-Release" + ), + ) + ingredients: str | None = Field( + default=None, + description=( + "The ingredient list of the product. null" + " if not available.\n" + "Example: Purified Carbonated Water, " + "Natural Flavors" + ), + ) + isHaul: bool | None = Field( + default=None, + description=( + "True if this product is an Amazon Haul product. null otherwise.\nExample: true" + ), + ) + referralFeePercentage: float | None = Field( + default=None, + description=( + "The referral fee percent is determined " + "by either the current price or, in the " + "absence of a current offer, the previous" + " one. If neither of these prices is " + "available for reference, the fee percent" + " is calculated based on a standard sales" + " price of 100.00. *null* if not " + "available.\n" + "Example: 12" + ), + ) + lastSoldUpdate: int | None = Field( + default=None, + description=( + "States the last time we have updated the" + " monthlySold field, in Keepa Time " + "minutes. null if the monthlySold has no " + "value.\n" + "Use " + "KeepaTime.keepaMinuteToUnixInMillis(int)" + " (long) to get an uncompressed timestamp" + " (Unix epoch time)." + ), + ) + monthlySold: int | None = Field( + default=None, + description=( + "How often this product was bought in the" + " past month. This field represents the " + "bought past month metric found on Amazon" + " search result pages. It is not an " + "estimate. null if it has no value. Most " + "ASINs do not have this value set. The " + "value is variation specific.\n" + "Example: 1000 - the ASIN was bought at " + "least 1000 times in the past month." + ), + ) + monthlySoldHistory: list[int | None] | None = Field( + default=None, + description=( + "Contains historical values of the monthlySold field. null if it has no value." + ), + ) + isEligibleForSuperSaverShipping: bool | None = Field( + default=None, + description=( + "Whether or not the product is eligible for super saver shipping by Amazon (not FBA)." + ), + ) + lastUpdate: int | None = Field( + default=None, + description=( + "States the last time we have updated the" + " information for this product, in Keepa " + "Time minutes.\n" + "\n" + "Use " + "KeepaTime.keepaMinuteToUnixInMillis(int)" + " (long) to get an uncompressed timestamp" + " (Unix epoch time)." + ), + ) + lastPriceChange: int | None = Field( + default=None, + description=( + "States the last time we have registered " + "a price change (any price kind), in " + "Keepa Time minutes.\n" + "\n" + "Use " + "KeepaTime.keepaMinuteToUnixInMillis(int)" + " to get an uncompressed timestamp (Unix " + "epoch time)." + ), + ) + lastEbayUpdate: int | None = Field( + default=None, + description=( + "States the last time we have updated the" + " eBay prices for this product, in Keepa " + "Time minutes.\n" + "\n" + "If no matching products were found the " + "integer is negative.\n" + "\n" + "Use " + "KeepaTime.keepaMinuteToUnixInMillis(int)" + " (long) to get an uncompressed timestamp" + " (Unix epoch time)." + ), + ) + lastStockUpdate: int | None = Field( + default=None, + description=( + "The most recent update of the stock data" + " for this product\u2019s offers, in Keepa " + "Time minutes.\n" + "\n" + "Has the value 0 unless the stock " + "parameter was used and stock data was " + "collected at least once.\n" + "Use " + "KeepaTime.keepaMinuteToUnixInMillis(int)" + " (long) to get an uncompressed timestamp" + " (Unix epoch time)." + ), + ) + lastRatingUpdate: int | None = Field( + default=None, + description=( + "States the last time we have updated the" + " product rating and review count, in " + "Keepa Time minutes.\n" + "\n" + "Use " + "KeepaTime.keepaMinuteToUnixInMillis(int)" + " (long) to get an uncompressed timestamp" + " (Unix epoch time)." + ), + ) + productType: int | None = Field( + default=None, + description=("Keepa product type Product.ProductType. Must always be evaluated first."), + ) + type: str | None = Field( + default=None, + description=("The item\u2019s type. null if not available."), + ) + hasReviews: bool | None = Field( + default=None, + description=("Whether or not the product has reviews."), + ) + reviews: ReviewObject | None = Field( + default=None, + description=( + "Contains variation specific review and " + "rating counts histories\n" + "as well as a last update timestamp.\n" + "null if not available.\n" + "It is not possible to force an update to" + " the reviews object data.\n" + "For non-variation specific ratings and " + "review data access the csv field.\n" + "Accessible only if the rating parameter " + "was used in the Product Request.\n" + "The ratingCount history has not been " + "updated since April 9th 2025 as that " + "data point was removed by Amazon." + ), + ) + stats: Stats | None = Field( + default=None, + description=( + "Optional field. Only set if the stats " + "parameter was used in the Product " + "Request. Contains statistic values." + ), + ) + offers: list[Offer | None] | None = Field( + default=None, + description=( + "Optional field. Only set if the offers parameter was used in the Product Request." + ), + ) + liveOffersOrder: list[int | None] | None = Field( + default=None, + description=( + "Optional field. Only set if the offers " + "parameter was used in the Product " + "Request.\n" + "\n" + "Contains an ordered array of index " + "positions in the offers array for all " + "Marketplace Offer Objects114 retrieved " + "for this call.\n" + "\n" + "The sequence of integers reflects the " + "ordering of the offers on the Amazon " + "offer page (for all conditions).\n" + "\n" + "Since the offers field contains " + "historical offers as well as current " + "offers, one can use this array to\n" + "\n" + "look up all offers that are currently " + "listed on Amazon in the correct order.\n" + "\n" + "Example:\n" + "[ 3, 5, 2, 18, 15 ] - The offer with the" + " array index 3 of the offers field is " + "currently the first\n" + "\n" + "one listed on the offer listings page on" + " Amazon, followed by the offer with the " + "index 5, and so on.\n" + "\n" + "Example with duplicates:\n" + "[ 3, 5, 2, 18, 5 ] - The second offer, " + "as listed on Amazon, is a lower priced " + "duplicate\n" + "\n" + "of the 6th offer on Amazon. The lower " + "priced one is included in the offers " + "field at index 5." + ), + ) + buyBoxSellerIdHistory: list[str | None] | None = Field( + default=None, + description=( + "Optional field. Only set if the offers " + "parameter was used in the Product " + "Request.\n" + "\n" + "Contains a history of sellerIds that " + "held the Buy Box in the format Keepa " + "time minutes, sellerId, [...].\n" + "\n" + "If no seller qualified for the Buy Box " + 'the sellerId "-1" is used. If it was ' + "hold by an unknown seller (a brand new " + 'one) the sellerId is "-2".\n' + "\n" + 'Example: ["2860926","ATVPDKIKX0DER", \u2026]\n' + "\n" + "Use KeepaTime.keepaMinuteToUnixInMillis(" + "String) (long) to get an uncompressed " + "timestamp (Unix epoch time)." + ), + ) + buyBoxUsedHistory: list[str | None] | None = Field( + default=None, + description=( + "Optional field. Only set if the offers " + "or buybox parameter was used in the " + "Product Request.\n" + "A history of the used buy box winners, " + "containing the sellerIds 159, offer " + "sub-condition and FBA status in the " + "format:\n" + "Keepa time minutes, seller id, " + "condition, isFBA, [\u2026].\n" + "If no seller qualified for the used buy " + 'box the sellerId "" (empty String) is ' + "used.\n" + "\n" + "condition can have the following values:" + "\n" + "\u201c2\u201d - Used - Like New, \u201c3\u201d - Used - Very" + " Good, \u201c4\u201d - Used - Good, \u201c5\u201d - Used - " + "Acceptable\n" + "isFBA is either \u201c1\u201d - offer is FBA or " + "\u201c0\u201d - offer is merchant fulfilled.\n" + "Example: [\u201c2860926\u201d, \u201cATVPDKIKX0DER\u201d, " + "\u201c4\u201d, \u201c1\u201d, \u2026]\n" + "\n" + "Use KeepaTime.keepaMinuteToUnixInMillis(" + "String) (long) to get an uncompressed " + "timestamp (Unix epoch time)." + ), + ) + isRedirectASIN: bool | None = Field( + default=None, + description=( + "Only valid if the offers parameter was " + "used in the Product Request.\n" + "Boolean indicating if the ASIN will be " + "redirected to another one on Amazon\n" + "(example: the ASIN has the color black " + "variation, which is not available any " + "more\n" + "and is redirected on Amazon to the color" + " red)." + ), + ) + isSNS: bool | None = Field( + default=None, + description=( + "Only valid if the offers parameter was " + "used in the Product Request. Boolean " + "indicating if the product's Buy Box is " + "available for subscribe and save." + ), + ) + suggestedLowerPrice: int | None = Field( + default=None, + description=("Suggested Lower Price for the Buy Box, if the buy box is suppressed."), + ) + competitivePriceThreshold: int | None = Field( + default=None, + description=( + "Competitive Price Threshold (CPT) for the Buy Box, if the buy box is suppressed." + ), + ) + buyBoxEligibleOfferCounts: list[int | None] | None = Field( + default=None, + description=( + "If buyBoxEligibleOfferCounts is " + "available, it represents an array of " + "integers, each entry indicating the " + "total number of offers eligible for the " + "Buy Box across specified offer " + "conditions and fulfillment channels. " + "This array contains eight elements, " + "indexed as follows:\n" + "\n" + "0: New FBA\n" + "\n" + "1: New FBM\n" + "\n" + "2: Used FBA\n" + "\n" + "3: Used FBM\n" + "\n" + "4: Collectible FBA\n" + "\n" + "5: Collectible FBM\n" + "\n" + "6: Refurbished FBA\n" + "\n" + "7: Refurbished FBM" + ), + ) + hazardousMaterials: list[HazardousMaterial | None] | None = Field( + default=None, + description=("The hazardous material type of this product, if applicable."), + ) + offersSuccessful: bool | None = Field( + default=None, + description=( + "Only valid if the offers parameter was " + "used in the Product Request. Boolean " + "indicating if the system was able to " + "retrieve fresh offer information." + ), + ) + frequentlyBoughtTogether: list[str | None] | None = Field( + default=None, + description=( + "One or two \u201cFrequently Bought Together\u201d " + "ASINs. null if not available. Field is " + "updated when the offers parameter was " + "used." + ), + ) + isMerchOnDemand: bool | None = Field( + default=None, + description=("True if this product is an Amazon Merch on Demand product"), + ) + isHeatSensitive: bool | None = Field( + default=None, + description=("Indicates if the item is heat sensitive (e.g. meltable)."), + ) + returnRate: int | None = Field( + default=None, + description=( + "Indicates the return rate of this " + "product.\n" + "\n" + "- null if the return rate is unavailable" + " or average.\n" + "\n" + "- 1 for a low return rate.\n" + "\n" + "- 2 for a high return rate." + ), + ) + promotions: list[PromotionObject | None] | None = Field( + default=None, + description=( + "Contains current promotions for this " + "product. Only Amazon US promotions by " + "Amazon (not 3rd party) are collected. In" + " rare cases data can be incomplete." + ), + ) + variations: list[VariationObject | None] | None = Field( + default=None, + description=( + "Contains the dimension attributes for up" + " to 50 variations of this product. Only " + "available on parent ASINs." + ), + ) + availabilityAmazon: int | None = Field( + default=None, + description=("Availability of the Amazon offer Product.AvailabilityType."), + ) + coupon: list[int | None] | None = Field( + default=None, + description=( + "Contains coupon details if any are " + "available for the product. null if not " + "available.\n" + "Integer array with always two entries: " + "The first entry is the discount of a one" + " time coupon, the second is a subscribe " + "and save coupon.\n" + "Entry value is either 0 if no coupon of " + "that type is offered, positive for an " + "absolute discount or negative for a " + "percentage discount.\n" + "The coupons field is always accessible, " + "but only updated if the offers parameter" + " was used in the Product Request.\n" + "\n" + "Example:\n" + "\n" + "[ 200, -15 ] - Both coupon types " + "available: $2 one time discount or 15% " + "for subscribe and save.\n" + "\n" + "[ -7, 0 ] - Only one time coupon type is" + " available offering a 7% discount." + ), + ) + couponHistory: list[int | None] | None = Field( + default=None, + description=( + "Contains historical values for the " + "coupon field, if available. null if not " + "available. We started tracking coupon " + "history on June 15th 2024.\n" + "Format: [ keepaTime, one-time coupon, " + "subscribe and save coupon, keepaTime, \u2026 " + "]\n" + "\n" + "Example:\n" + "\n" + "[2711319, 200, -15, \u2026]" + ), + ) + newPriceIsMAP: bool | None = Field( + default=None, + description=( + "Whether or not the current new price is " + "MAP restricted. Can be used to " + "differentiate out of stock vs. MAP " + "restricted prices (as in both cases the " + "current price is -1)." + ), + ) + fbaFees: FBAFeesObject | None = Field( + default=None, + description=( + "FBA fees for this product. If FBA fee " + "information has not yet been collected " + "for this product the field will be null." + ), + ) + salesRanks: dict[int, list[int | None] | None] | None = Field( + default=None, + description=( + "Contains subcategory rank histories. " + "Each key represents the categoryId of " + "the rank with the history in the " + "corresponding value." + ), + ) + salesRankReference: int | None = Field( + default=None, + description=("The category node id of the main sales rank. -1 if not available."), + ) + salesRankReferenceHistory: list[int | None] | None = Field( + default=None, + description=( + "The category node id history of the main" + " sales rank (format: timestamp, " + "categoryId, [\u2026]). null if not available." + ), + ) + availabilityAmazonDelay: list[int | None] | None = Field( + default=None, + description=( + "Amazon offer shipping delay. Integer " + "array with 2 entries, indicating min and" + " max shipping delay in hours." + ), + ) + audienceRating: str | None = Field( + default=None, + description=( + "Audience rating. The rating suggests the" + " age for which the media is appropriate." + "\n" + "\n" + "Example: PG-13 (Parents Strongly " + "Cautioned)" + ), + ) + unitCount: UnitCountObject | None = Field( + default=None, + description=("Unit Count information"), + ) + scent: str | None = Field( + default=None, + description=( + "The scent of the product. Describes the " + "fragrance associated with the product.\n" + "\n" + 'Example: "Lavender"' + ), + ) + shortDescription: str | None = Field( + default=None, + description=( + 'A brief description of the product.\n\nExample: "A soothing lavender-scented candle."' + ), + ) + activeIngredients: str | None = Field( + default=None, + description=( + "Active ingredients present in the " + "product.\n" + "\n" + 'Example: "Lavender essential oil, Soy ' + 'wax"' + ), + ) + specialIngredients: str | None = Field( + default=None, + description=( + "Special ingredients used in the product " + "that may have unique properties.\n" + "\n" + 'Example: "Beeswax blend, Natural dyes"' + ), + ) + itemForm: str | None = Field( + default=None, + description=( + 'The form or physical state of the item.\n\nExample: "Liquid", "Solid", "Gel"' + ), + ) + itemTypeKeyword: str | None = Field( + default=None, + description=( + 'Keywords describing the type or category of the item.\n\nExample: "body-lotions"' + ), + ) + recommendedUsesForProduct: str | None = Field( + default=None, + description=( + "Recommended uses for the product to " + "guide customers.\n" + "\n" + 'Example: "Aromatherapy, Home Decoration"' + ), + ) + pattern: str | None = Field( + default=None, + description=( + 'The pattern or design featured on the product.\n\nExample: "Striped", "Floral"' + ), + ) + brandStoreName: str | None = Field( + default=None, + description=( + "The store name of the item\u2019s brand. null if not available.\n\nExample: Hot Wheels" + ), + ) + brandStoreUrl: str | None = Field( + default=None, + description=( + "The brand store URL path. null if not " + "available. To get the full URL, prepend " + "the Amazon domain of the respective " + "locale (e.g. https//www.amazon.com).\n" + "\n" + "Example: /stores/LEGO/page/017EF856-965D" + "-4B56-A171-EA61CAFF45DD" + ), + ) + brandStoreUrlName: str | None = Field( + default=None, + description=( + "The brand store Name from the URL path. " + "null if not available.\n" + "\n" + "Example: LEGO (from the URL: /stores/LEG" + "O/page/017EF856-965D-4B56-A171-EA61CAFF4" + "5DD)" + ), + ) + videos: list[Video | None] | None = Field( + default=None, + description=( + "Provides metadata for videos associated " + "with the product.\n" + "\n" + "The videos parameter is mandatory for " + "access. Each object in the array " + "represents\n" + "the metadata for a single video. " + "Metadata can be retrieved for all image " + "carousel videos\n" + "and up to 10 community videos from the " + "product listing\u2019s Videos section. To " + "request live\n" + "data for this field, the offers " + "parameter must also be included. Returns" + " null\n" + "if unavailable.\n" + "\n" + "Example:\n" + "\n" + '"videos": [{\n' + '"title": "Compressed Air Duster",\n' + '"image": "31XBcVI7oTL.jpg",\n' + '"duration": 36,\n' + '"creator": "Seller",\n' + '"name": "Innovation",\n' + '"url": "https://m.media-amazon.com/image' + "s/S/vse-vms-transcoding-artifact-us-east" + "-1-prd/d8d8f97e-aa42-42d2-aa4c-6ab1b006e" + 'db5/default.jobtemplate.hls.m3u8"\n' + "}]" + ), + ) + aPlus: list[APlus | None] | None = Field( + default=None, + description=( + "Provides A+ Content of this product.\n" + "\n" + "The aplus parameter is mandatory for " + "access. To request live\n" + "data for this field, the offers " + "parameter must also be included. Returns" + " null\n" + "if unavailable." + ), + ) + specificUsesForProduct: list[str | None] | None = Field( + default=None, + description=( + "Specific uses for the product, providing" + " detailed applications.\n" + "\n" + 'Example: {"Relaxation", "Decoration"}' + ), + ) + websiteDisplayGroupName: str | None = Field( + default=None, + description=( + "A categorization name of products that " + "behave similarly,\n" + "influencing how sales rank is calculated" + " and displayed,\n" + "especially for product variations.\n" + "\n" + 'Example: {"apparel", "kitchen"}' + ), + ) + websiteDisplayGroup: str | None = Field( + default=None, + description=( + "A categorization of products that behave" + " similarly,\n" + "influencing how sales rank is calculated" + " and displayed,\n" + "especially for product variations.\n" + "\n" + 'Example: {"appareldisplayonwebsite", ' + '"kitchendisplayonwebsite"}' + ), + ) + formats: list[Format | None] | None = Field( + default=None, + description=( + "For books only: An array listing other " + "available formats or bindings of a book," + " excluding the current format." + ), + ) + businessDiscount: int | None = Field( + default=None, + description=("The highest business discount percentage, if available.\n\nExample: 14"), + ) + lastBusinessDiscountUpdate: int | None = Field( + default=None, + description=("KeepaTime timestamp of the last business discount percentage update."), + ) + safetyWarning: str | None = Field( + default=None, + description=( + "Safety warnings associated with the " + "product to inform users of potential " + "hazards.\n" + "\n" + 'Example: "Keep away from open flames."' + ), + ) + productBenefit: str | None = Field( + default=None, + description=( + "Benefits of using the product, " + "highlighting its advantages.\n" + "\n" + 'Example: "Promotes relaxation and stress' + ' relief."' + ), + ) + batteriesRequired: bool | None = Field( + default=None, + description=( + "Indicates whether batteries are required" + " for the product to function.\n" + "\n" + "Example: true or false" + ), + ) + batteriesIncluded: bool | None = Field( + default=None, + description=( + "Indicates whether batteries are included" + " with the product upon purchase.\n" + "\n" + "Example: true or false" + ), + ) + targetAudienceKeyword: str | None = Field( + default=None, + description=( + "Keywords describing the target audience " + "for the product.\n" + "\n" + 'Example: "Adults, Gift for Her"' + ), + ) + style: str | None = Field( + default=None, + description=( + "The style of the product, which may " + "influence its aesthetic appeal.\n" + "\n" + 'Example: "Modern", "Vintage"' + ), + ) + includedComponents: str | None = Field( + default=None, + description=( + "Components included with the product, " + "detailing what is provided upon " + "purchase.\n" + "\n" + 'Example: "Candle, Wick, Box"' + ), + ) + materials: list[str | None] | None = Field( + default=None, + description=( + "Material of the product, specifying the " + "primary substances used in its " + "construction.\n" + "\n" + "Example: [ \u201cSteel\u201d, \u201cWood\u201d ]" + ), + ) + material: str | None = Field( + default=None, + description=("@deprecated use the field materials instead"), + ) + specialFeatures: list[str | None] | None = Field( + default=None, + description=( + "The special features of the product.\n" + "\n" + "Example: [ \u201cFast Charging\u201d, " + "\u201cLightweight\u201d ]" + ), + ) + deals: list[DealDetails | None] | None = Field( + default=None, + description=( + "Provides metadata for active deals " + "associated with the product\u2019s buy box. " + "undefined if unavailable." + ), + ) + csv: list[list[int | None] | None] | None = Field( + default=None, + description=( + "Integer[][] - two dimensional price " + "history array.\n" + "\n" + "First dimension: Product.CsvType\n" + "\n" + "Second dimension:\n" + "\n" + "Each array has the format timestamp, " + "price, [\u2026]. To get an uncompressed " + "timestamp use KeepaTime.keepaMinuteToUni" + "xInMillis(int).\n" + "\n" + 'Example: "csv[0]": [411180,4900, ... ]\n' + "\n" + "timestamp: 411180 => 1318510800000\n" + "\n" + "price: 4900 => $ 49.00 (if domainId is " + "5, Japan, then price: 4900 => \u00a5 4900)\n" + "\n" + "A price of '-1' means that there was no " + "offer at the given timestamp (e.g. out " + "of stock)." + ), + ) + + +class ProductFinderRequest(KeepaBackendModel): + """Backend ``ProductFinderRequest`` model generated from the Keepa Java schema.""" + + author: list[str | None] | None = None + availabilityAmazon: list[int | None] | None = None + avg365_AMAZON_lte: int | None = None + avg365_AMAZON_gte: int | None = None + avg365_BUY_BOX_SHIPPING_lte: int | None = None + avg365_BUY_BOX_SHIPPING_gte: int | None = None + avg365_BUY_BOX_USED_SHIPPING_lte: int | None = None + avg365_BUY_BOX_USED_SHIPPING_gte: int | None = None + avg365_COLLECTIBLE_lte: int | None = None + avg365_COLLECTIBLE_gte: int | None = None + avg365_COUNT_COLLECTIBLE_lte: int | None = None + avg365_COUNT_COLLECTIBLE_gte: int | None = None + avg365_COUNT_NEW_lte: int | None = None + avg365_COUNT_NEW_gte: int | None = None + avg365_COUNT_REFURBISHED_lte: int | None = None + avg365_COUNT_REFURBISHED_gte: int | None = None + avg365_COUNT_REVIEWS_lte: int | None = None + avg365_COUNT_REVIEWS_gte: int | None = None + avg365_COUNT_USED_lte: int | None = None + avg365_COUNT_USED_gte: int | None = None + avg365_EBAY_NEW_SHIPPING_lte: int | None = None + avg365_EBAY_NEW_SHIPPING_gte: int | None = None + avg365_EBAY_USED_SHIPPING_lte: int | None = None + avg365_EBAY_USED_SHIPPING_gte: int | None = None + avg365_LIGHTNING_DEAL_lte: int | None = None + avg365_LIGHTNING_DEAL_gte: int | None = None + avg365_LISTPRICE_lte: int | None = None + avg365_LISTPRICE_gte: int | None = None + avg365_NEW_lte: int | None = None + avg365_NEW_gte: int | None = None + avg365_NEW_FBA_lte: int | None = None + avg365_NEW_FBA_gte: int | None = None + avg365_NEW_FBM_SHIPPING_lte: int | None = None + avg365_NEW_FBM_SHIPPING_gte: int | None = None + avg365_PRIME_EXCL_lte: int | None = None + avg365_PRIME_EXCL_gte: int | None = None + avg365_RATING_lte: int | None = None + avg365_RATING_gte: int | None = None + avg365_REFURBISHED_lte: int | None = None + avg365_REFURBISHED_gte: int | None = None + avg365_REFURBISHED_SHIPPING_lte: int | None = None + avg365_REFURBISHED_SHIPPING_gte: int | None = None + avg365_RENT_lte: int | None = None + avg365_RENT_gte: int | None = None + avg365_SALES_lte: int | None = None + avg365_SALES_gte: int | None = None + avg365_TRADE_IN_lte: int | None = None + avg365_TRADE_IN_gte: int | None = None + avg365_USED_lte: int | None = None + avg365_USED_gte: int | None = None + avg365_USED_ACCEPTABLE_SHIPPING_lte: int | None = None + avg365_USED_ACCEPTABLE_SHIPPING_gte: int | None = None + avg365_USED_GOOD_SHIPPING_lte: int | None = None + avg365_USED_GOOD_SHIPPING_gte: int | None = None + avg365_USED_NEW_SHIPPING_lte: int | None = None + avg365_USED_NEW_SHIPPING_gte: int | None = None + avg365_USED_VERY_GOOD_SHIPPING_lte: int | None = None + avg365_USED_VERY_GOOD_SHIPPING_gte: int | None = None + avg365_WAREHOUSE_lte: int | None = None + avg365_WAREHOUSE_gte: int | None = None + avg180_AMAZON_lte: int | None = None + avg180_AMAZON_gte: int | None = None + avg180_BUY_BOX_SHIPPING_lte: int | None = None + avg180_BUY_BOX_SHIPPING_gte: int | None = None + avg180_BUY_BOX_USED_SHIPPING_lte: int | None = None + avg180_BUY_BOX_USED_SHIPPING_gte: int | None = None + avg180_COLLECTIBLE_lte: int | None = None + avg180_COLLECTIBLE_gte: int | None = None + avg180_COUNT_COLLECTIBLE_lte: int | None = None + avg180_COUNT_COLLECTIBLE_gte: int | None = None + avg180_COUNT_NEW_lte: int | None = None + avg180_COUNT_NEW_gte: int | None = None + avg180_COUNT_REFURBISHED_lte: int | None = None + avg180_COUNT_REFURBISHED_gte: int | None = None + avg180_COUNT_REVIEWS_lte: int | None = None + avg180_COUNT_REVIEWS_gte: int | None = None + avg180_COUNT_USED_lte: int | None = None + avg180_COUNT_USED_gte: int | None = None + avg180_EBAY_NEW_SHIPPING_lte: int | None = None + avg180_EBAY_NEW_SHIPPING_gte: int | None = None + avg180_EBAY_USED_SHIPPING_lte: int | None = None + avg180_EBAY_USED_SHIPPING_gte: int | None = None + avg180_LIGHTNING_DEAL_lte: int | None = None + avg180_LIGHTNING_DEAL_gte: int | None = None + avg180_LISTPRICE_lte: int | None = None + avg180_LISTPRICE_gte: int | None = None + avg180_NEW_lte: int | None = None + avg180_NEW_gte: int | None = None + avg180_NEW_FBA_lte: int | None = None + avg180_NEW_FBA_gte: int | None = None + avg180_NEW_FBM_SHIPPING_lte: int | None = None + avg180_NEW_FBM_SHIPPING_gte: int | None = None + avg180_PRIME_EXCL_lte: int | None = None + avg180_PRIME_EXCL_gte: int | None = None + avg180_RATING_lte: int | None = None + avg180_RATING_gte: int | None = None + avg180_REFURBISHED_lte: int | None = None + avg180_REFURBISHED_gte: int | None = None + avg180_REFURBISHED_SHIPPING_lte: int | None = None + avg180_REFURBISHED_SHIPPING_gte: int | None = None + avg180_RENT_lte: int | None = None + avg180_RENT_gte: int | None = None + avg180_SALES_lte: int | None = None + avg180_SALES_gte: int | None = None + avg180_TRADE_IN_lte: int | None = None + avg180_TRADE_IN_gte: int | None = None + avg180_USED_lte: int | None = None + avg180_USED_gte: int | None = None + avg180_USED_ACCEPTABLE_SHIPPING_lte: int | None = None + avg180_USED_ACCEPTABLE_SHIPPING_gte: int | None = None + avg180_USED_GOOD_SHIPPING_lte: int | None = None + avg180_USED_GOOD_SHIPPING_gte: int | None = None + avg180_USED_NEW_SHIPPING_lte: int | None = None + avg180_USED_NEW_SHIPPING_gte: int | None = None + avg180_USED_VERY_GOOD_SHIPPING_lte: int | None = None + avg180_USED_VERY_GOOD_SHIPPING_gte: int | None = None + avg180_WAREHOUSE_lte: int | None = None + avg180_WAREHOUSE_gte: int | None = None + avg1_AMAZON_lte: int | None = None + avg1_AMAZON_gte: int | None = None + avg1_BUY_BOX_SHIPPING_lte: int | None = None + avg1_BUY_BOX_SHIPPING_gte: int | None = None + avg1_BUY_BOX_USED_SHIPPING_lte: int | None = None + avg1_BUY_BOX_USED_SHIPPING_gte: int | None = None + avg1_COLLECTIBLE_lte: int | None = None + avg1_COLLECTIBLE_gte: int | None = None + avg1_COUNT_COLLECTIBLE_lte: int | None = None + avg1_COUNT_COLLECTIBLE_gte: int | None = None + avg1_COUNT_NEW_lte: int | None = None + avg1_COUNT_NEW_gte: int | None = None + avg1_COUNT_REFURBISHED_lte: int | None = None + avg1_COUNT_REFURBISHED_gte: int | None = None + avg1_COUNT_REVIEWS_lte: int | None = None + avg1_COUNT_REVIEWS_gte: int | None = None + avg1_COUNT_USED_lte: int | None = None + avg1_COUNT_USED_gte: int | None = None + avg1_EBAY_NEW_SHIPPING_lte: int | None = None + avg1_EBAY_NEW_SHIPPING_gte: int | None = None + avg1_EBAY_USED_SHIPPING_lte: int | None = None + avg1_EBAY_USED_SHIPPING_gte: int | None = None + avg1_LIGHTNING_DEAL_lte: int | None = None + avg1_LIGHTNING_DEAL_gte: int | None = None + avg1_LISTPRICE_lte: int | None = None + avg1_LISTPRICE_gte: int | None = None + avg1_NEW_lte: int | None = None + avg1_NEW_gte: int | None = None + avg1_NEW_FBA_lte: int | None = None + avg1_NEW_FBA_gte: int | None = None + avg1_NEW_FBM_SHIPPING_lte: int | None = None + avg1_NEW_FBM_SHIPPING_gte: int | None = None + avg1_PRIME_EXCL_lte: int | None = None + avg1_PRIME_EXCL_gte: int | None = None + avg1_RATING_lte: int | None = None + avg1_RATING_gte: int | None = None + avg1_REFURBISHED_lte: int | None = None + avg1_REFURBISHED_gte: int | None = None + avg1_REFURBISHED_SHIPPING_lte: int | None = None + avg1_REFURBISHED_SHIPPING_gte: int | None = None + avg1_RENT_lte: int | None = None + avg1_RENT_gte: int | None = None + avg1_SALES_lte: int | None = None + avg1_SALES_gte: int | None = None + avg1_TRADE_IN_lte: int | None = None + avg1_TRADE_IN_gte: int | None = None + avg1_USED_lte: int | None = None + avg1_USED_gte: int | None = None + avg1_USED_ACCEPTABLE_SHIPPING_lte: int | None = None + avg1_USED_ACCEPTABLE_SHIPPING_gte: int | None = None + avg1_USED_GOOD_SHIPPING_lte: int | None = None + avg1_USED_GOOD_SHIPPING_gte: int | None = None + avg1_USED_NEW_SHIPPING_lte: int | None = None + avg1_USED_NEW_SHIPPING_gte: int | None = None + avg1_USED_VERY_GOOD_SHIPPING_lte: int | None = None + avg1_USED_VERY_GOOD_SHIPPING_gte: int | None = None + avg1_WAREHOUSE_lte: int | None = None + avg1_WAREHOUSE_gte: int | None = None + avg30_AMAZON_lte: int | None = None + avg30_AMAZON_gte: int | None = None + avg30_BUY_BOX_SHIPPING_lte: int | None = None + avg30_BUY_BOX_SHIPPING_gte: int | None = None + avg30_BUY_BOX_USED_SHIPPING_lte: int | None = None + avg30_BUY_BOX_USED_SHIPPING_gte: int | None = None + avg30_COLLECTIBLE_lte: int | None = None + avg30_COLLECTIBLE_gte: int | None = None + avg30_COUNT_COLLECTIBLE_lte: int | None = None + avg30_COUNT_COLLECTIBLE_gte: int | None = None + avg30_COUNT_NEW_lte: int | None = None + avg30_COUNT_NEW_gte: int | None = None + avg30_COUNT_REFURBISHED_lte: int | None = None + avg30_COUNT_REFURBISHED_gte: int | None = None + avg30_COUNT_REVIEWS_lte: int | None = None + avg30_COUNT_REVIEWS_gte: int | None = None + avg30_COUNT_USED_lte: int | None = None + avg30_COUNT_USED_gte: int | None = None + avg30_EBAY_NEW_SHIPPING_lte: int | None = None + avg30_EBAY_NEW_SHIPPING_gte: int | None = None + avg30_EBAY_USED_SHIPPING_lte: int | None = None + avg30_EBAY_USED_SHIPPING_gte: int | None = None + avg30_LIGHTNING_DEAL_lte: int | None = None + avg30_LIGHTNING_DEAL_gte: int | None = None + avg30_LISTPRICE_lte: int | None = None + avg30_LISTPRICE_gte: int | None = None + avg30_NEW_lte: int | None = None + avg30_NEW_gte: int | None = None + avg30_NEW_FBA_lte: int | None = None + avg30_NEW_FBA_gte: int | None = None + avg30_NEW_FBM_SHIPPING_lte: int | None = None + avg30_NEW_FBM_SHIPPING_gte: int | None = None + avg30_PRIME_EXCL_lte: int | None = None + avg30_PRIME_EXCL_gte: int | None = None + avg30_RATING_lte: int | None = None + avg30_RATING_gte: int | None = None + avg30_REFURBISHED_lte: int | None = None + avg30_REFURBISHED_gte: int | None = None + avg30_REFURBISHED_SHIPPING_lte: int | None = None + avg30_REFURBISHED_SHIPPING_gte: int | None = None + avg30_RENT_lte: int | None = None + avg30_RENT_gte: int | None = None + avg30_SALES_lte: int | None = None + avg30_SALES_gte: int | None = None + avg30_TRADE_IN_lte: int | None = None + avg30_TRADE_IN_gte: int | None = None + avg30_USED_lte: int | None = None + avg30_USED_gte: int | None = None + avg30_USED_ACCEPTABLE_SHIPPING_lte: int | None = None + avg30_USED_ACCEPTABLE_SHIPPING_gte: int | None = None + avg30_USED_GOOD_SHIPPING_lte: int | None = None + avg30_USED_GOOD_SHIPPING_gte: int | None = None + avg30_USED_NEW_SHIPPING_lte: int | None = None + avg30_USED_NEW_SHIPPING_gte: int | None = None + avg30_USED_VERY_GOOD_SHIPPING_lte: int | None = None + avg30_USED_VERY_GOOD_SHIPPING_gte: int | None = None + avg30_WAREHOUSE_lte: int | None = None + avg30_WAREHOUSE_gte: int | None = None + avg7_AMAZON_lte: int | None = None + avg7_AMAZON_gte: int | None = None + avg7_BUY_BOX_SHIPPING_lte: int | None = None + avg7_BUY_BOX_SHIPPING_gte: int | None = None + avg7_BUY_BOX_USED_SHIPPING_lte: int | None = None + avg7_BUY_BOX_USED_SHIPPING_gte: int | None = None + avg7_COLLECTIBLE_lte: int | None = None + avg7_COLLECTIBLE_gte: int | None = None + avg7_COUNT_COLLECTIBLE_lte: int | None = None + avg7_COUNT_COLLECTIBLE_gte: int | None = None + avg7_COUNT_NEW_lte: int | None = None + avg7_COUNT_NEW_gte: int | None = None + avg7_COUNT_REFURBISHED_lte: int | None = None + avg7_COUNT_REFURBISHED_gte: int | None = None + avg7_COUNT_REVIEWS_lte: int | None = None + avg7_COUNT_REVIEWS_gte: int | None = None + avg7_COUNT_USED_lte: int | None = None + avg7_COUNT_USED_gte: int | None = None + avg7_EBAY_NEW_SHIPPING_lte: int | None = None + avg7_EBAY_NEW_SHIPPING_gte: int | None = None + avg7_EBAY_USED_SHIPPING_lte: int | None = None + avg7_EBAY_USED_SHIPPING_gte: int | None = None + avg7_LIGHTNING_DEAL_lte: int | None = None + avg7_LIGHTNING_DEAL_gte: int | None = None + avg7_LISTPRICE_lte: int | None = None + avg7_LISTPRICE_gte: int | None = None + avg7_NEW_lte: int | None = None + avg7_NEW_gte: int | None = None + avg7_NEW_FBA_lte: int | None = None + avg7_NEW_FBA_gte: int | None = None + avg7_NEW_FBM_SHIPPING_lte: int | None = None + avg7_NEW_FBM_SHIPPING_gte: int | None = None + avg7_PRIME_EXCL_lte: int | None = None + avg7_PRIME_EXCL_gte: int | None = None + avg7_RATING_lte: int | None = None + avg7_RATING_gte: int | None = None + avg7_REFURBISHED_lte: int | None = None + avg7_REFURBISHED_gte: int | None = None + avg7_REFURBISHED_SHIPPING_lte: int | None = None + avg7_REFURBISHED_SHIPPING_gte: int | None = None + avg7_RENT_lte: int | None = None + avg7_RENT_gte: int | None = None + avg7_SALES_lte: int | None = None + avg7_SALES_gte: int | None = None + avg7_TRADE_IN_lte: int | None = None + avg7_TRADE_IN_gte: int | None = None + avg7_USED_lte: int | None = None + avg7_USED_gte: int | None = None + avg7_USED_ACCEPTABLE_SHIPPING_lte: int | None = None + avg7_USED_ACCEPTABLE_SHIPPING_gte: int | None = None + avg7_USED_GOOD_SHIPPING_lte: int | None = None + avg7_USED_GOOD_SHIPPING_gte: int | None = None + avg7_USED_NEW_SHIPPING_lte: int | None = None + avg7_USED_NEW_SHIPPING_gte: int | None = None + avg7_USED_VERY_GOOD_SHIPPING_lte: int | None = None + avg7_USED_VERY_GOOD_SHIPPING_gte: int | None = None + avg7_WAREHOUSE_lte: int | None = None + avg7_WAREHOUSE_gte: int | None = None + avg90_AMAZON_lte: int | None = None + avg90_AMAZON_gte: int | None = None + avg90_BUY_BOX_SHIPPING_lte: int | None = None + avg90_BUY_BOX_SHIPPING_gte: int | None = None + avg90_BUY_BOX_USED_SHIPPING_lte: int | None = None + avg90_BUY_BOX_USED_SHIPPING_gte: int | None = None + avg90_COLLECTIBLE_lte: int | None = None + avg90_COLLECTIBLE_gte: int | None = None + avg90_COUNT_COLLECTIBLE_lte: int | None = None + avg90_COUNT_COLLECTIBLE_gte: int | None = None + avg90_COUNT_NEW_lte: int | None = None + avg90_COUNT_NEW_gte: int | None = None + avg90_COUNT_REFURBISHED_lte: int | None = None + avg90_COUNT_REFURBISHED_gte: int | None = None + avg90_COUNT_REVIEWS_lte: int | None = None + avg90_COUNT_REVIEWS_gte: int | None = None + avg90_COUNT_USED_lte: int | None = None + avg90_COUNT_USED_gte: int | None = None + avg90_EBAY_NEW_SHIPPING_lte: int | None = None + avg90_EBAY_NEW_SHIPPING_gte: int | None = None + avg90_EBAY_USED_SHIPPING_lte: int | None = None + avg90_EBAY_USED_SHIPPING_gte: int | None = None + avg90_LIGHTNING_DEAL_lte: int | None = None + avg90_LIGHTNING_DEAL_gte: int | None = None + avg90_LISTPRICE_lte: int | None = None + avg90_LISTPRICE_gte: int | None = None + avg90_NEW_lte: int | None = None + avg90_NEW_gte: int | None = None + avg90_NEW_FBA_lte: int | None = None + avg90_NEW_FBA_gte: int | None = None + avg90_NEW_FBM_SHIPPING_lte: int | None = None + avg90_NEW_FBM_SHIPPING_gte: int | None = None + avg90_PRIME_EXCL_lte: int | None = None + avg90_PRIME_EXCL_gte: int | None = None + avg90_RATING_lte: int | None = None + avg90_RATING_gte: int | None = None + avg90_REFURBISHED_lte: int | None = None + avg90_REFURBISHED_gte: int | None = None + avg90_REFURBISHED_SHIPPING_lte: int | None = None + avg90_REFURBISHED_SHIPPING_gte: int | None = None + avg90_RENT_lte: int | None = None + avg90_RENT_gte: int | None = None + avg90_SALES_lte: int | None = None + avg90_SALES_gte: int | None = None + avg90_TRADE_IN_lte: int | None = None + avg90_TRADE_IN_gte: int | None = None + avg90_USED_lte: int | None = None + avg90_USED_gte: int | None = None + avg90_USED_ACCEPTABLE_SHIPPING_lte: int | None = None + avg90_USED_ACCEPTABLE_SHIPPING_gte: int | None = None + avg90_USED_GOOD_SHIPPING_lte: int | None = None + avg90_USED_GOOD_SHIPPING_gte: int | None = None + avg90_USED_NEW_SHIPPING_lte: int | None = None + avg90_USED_NEW_SHIPPING_gte: int | None = None + avg90_USED_VERY_GOOD_SHIPPING_lte: int | None = None + avg90_USED_VERY_GOOD_SHIPPING_gte: int | None = None + avg90_WAREHOUSE_lte: int | None = None + avg90_WAREHOUSE_gte: int | None = None + backInStock_AMAZON: bool | None = None + backInStock_BUY_BOX_SHIPPING: bool | None = None + backInStock_BUY_BOX_USED_SHIPPING: bool | None = None + backInStock_COLLECTIBLE: bool | None = None + backInStock_COUNT_COLLECTIBLE: bool | None = None + backInStock_COUNT_NEW: bool | None = None + backInStock_COUNT_REFURBISHED: bool | None = None + backInStock_COUNT_REVIEWS: bool | None = None + backInStock_COUNT_USED: bool | None = None + backInStock_EBAY_NEW_SHIPPING: bool | None = None + backInStock_EBAY_USED_SHIPPING: bool | None = None + backInStock_LIGHTNING_DEAL: bool | None = None + backInStock_LISTPRICE: bool | None = None + backInStock_NEW: bool | None = None + backInStock_NEW_FBA: bool | None = None + backInStock_NEW_FBM_SHIPPING: bool | None = None + backInStock_PRIME_EXCL: bool | None = None + backInStock_RATING: bool | None = None + backInStock_REFURBISHED: bool | None = None + backInStock_REFURBISHED_SHIPPING: bool | None = None + backInStock_RENT: bool | None = None + backInStock_SALES: bool | None = None + backInStock_TRADE_IN: bool | None = None + backInStock_USED: bool | None = None + backInStock_USED_ACCEPTABLE_SHIPPING: bool | None = None + backInStock_USED_GOOD_SHIPPING: bool | None = None + backInStock_USED_NEW_SHIPPING: bool | None = None + backInStock_USED_VERY_GOOD_SHIPPING: bool | None = None + backInStock_WAREHOUSE: bool | None = None + binding: list[str | None] | None = None + brand: list[str | None] | None = None + dealType: list[str | None] | None = None + buyBoxIsAmazon: bool | None = None + buyBoxIsFBA: bool | None = None + buyBoxIsUnqualified: bool | None = None + buyBoxSellerId: list[str | None] | None = None + buyBoxUsedCondition: list[int | None] | None = None + buyBoxUsedIsFBA: bool | None = None + buyBoxUsedSellerId: str | None = None + categories_include: list[int | None] | None = None + categories_exclude: list[int | None] | None = None + color: list[str | None] | None = None + historicalParentASIN: str | None = None + couponOneTimeAbsolute_lte: int | None = None + couponOneTimeAbsolute_gte: int | None = None + couponOneTimePercent_lte: int | None = None + couponOneTimePercent_gte: int | None = None + couponSNSPercent_lte: int | None = None + couponSNSPercent_gte: int | None = None + current_AMAZON_lte: int | None = None + current_AMAZON_gte: int | None = None + current_BUY_BOX_SHIPPING_lte: int | None = None + current_BUY_BOX_SHIPPING_gte: int | None = None + current_BUY_BOX_USED_SHIPPING_lte: int | None = None + current_BUY_BOX_USED_SHIPPING_gte: int | None = None + current_COLLECTIBLE_lte: int | None = None + current_COLLECTIBLE_gte: int | None = None + current_COUNT_COLLECTIBLE_lte: int | None = None + current_COUNT_COLLECTIBLE_gte: int | None = None + current_COUNT_NEW_lte: int | None = None + current_COUNT_NEW_gte: int | None = None + current_COUNT_REFURBISHED_lte: int | None = None + current_COUNT_REFURBISHED_gte: int | None = None + current_COUNT_REVIEWS_lte: int | None = None + current_COUNT_REVIEWS_gte: int | None = None + current_COUNT_USED_lte: int | None = None + current_COUNT_USED_gte: int | None = None + current_EBAY_NEW_SHIPPING_lte: int | None = None + current_EBAY_NEW_SHIPPING_gte: int | None = None + current_EBAY_USED_SHIPPING_lte: int | None = None + current_EBAY_USED_SHIPPING_gte: int | None = None + current_LIGHTNING_DEAL_lte: int | None = None + current_LIGHTNING_DEAL_gte: int | None = None + current_LISTPRICE_lte: int | None = None + current_LISTPRICE_gte: int | None = None + current_NEW_lte: int | None = None + current_NEW_gte: int | None = None + current_NEW_FBA_lte: int | None = None + current_NEW_FBA_gte: int | None = None + current_NEW_FBM_SHIPPING_lte: int | None = None + current_NEW_FBM_SHIPPING_gte: int | None = None + current_PRIME_EXCL_lte: int | None = None + current_PRIME_EXCL_gte: int | None = None + current_RATING_lte: int | None = None + current_RATING_gte: int | None = None + current_REFURBISHED_lte: int | None = None + current_REFURBISHED_gte: int | None = None + current_REFURBISHED_SHIPPING_lte: int | None = None + current_REFURBISHED_SHIPPING_gte: int | None = None + current_RENT_lte: int | None = None + current_RENT_gte: int | None = None + current_SALES_lte: int | None = None + current_SALES_gte: int | None = None + current_TRADE_IN_lte: int | None = None + current_TRADE_IN_gte: int | None = None + current_USED_lte: int | None = None + current_USED_gte: int | None = None + current_USED_ACCEPTABLE_SHIPPING_lte: int | None = None + current_USED_ACCEPTABLE_SHIPPING_gte: int | None = None + current_USED_GOOD_SHIPPING_lte: int | None = None + current_USED_GOOD_SHIPPING_gte: int | None = None + current_USED_NEW_SHIPPING_lte: int | None = None + current_USED_NEW_SHIPPING_gte: int | None = None + current_USED_VERY_GOOD_SHIPPING_lte: int | None = None + current_USED_VERY_GOOD_SHIPPING_gte: int | None = None + current_WAREHOUSE_lte: int | None = None + current_WAREHOUSE_gte: int | None = None + delta1_AMAZON_lte: int | None = None + delta1_AMAZON_gte: int | None = None + delta1_BUY_BOX_SHIPPING_lte: int | None = None + delta1_BUY_BOX_SHIPPING_gte: int | None = None + delta1_BUY_BOX_USED_SHIPPING_lte: int | None = None + delta1_BUY_BOX_USED_SHIPPING_gte: int | None = None + delta1_COLLECTIBLE_lte: int | None = None + delta1_COLLECTIBLE_gte: int | None = None + delta1_COUNT_COLLECTIBLE_lte: int | None = None + delta1_COUNT_COLLECTIBLE_gte: int | None = None + delta1_COUNT_NEW_lte: int | None = None + delta1_COUNT_NEW_gte: int | None = None + delta1_COUNT_REFURBISHED_lte: int | None = None + delta1_COUNT_REFURBISHED_gte: int | None = None + delta1_COUNT_REVIEWS_lte: int | None = None + delta1_COUNT_REVIEWS_gte: int | None = None + delta1_COUNT_USED_lte: int | None = None + delta1_COUNT_USED_gte: int | None = None + delta1_EBAY_NEW_SHIPPING_lte: int | None = None + delta1_EBAY_NEW_SHIPPING_gte: int | None = None + delta1_EBAY_USED_SHIPPING_lte: int | None = None + delta1_EBAY_USED_SHIPPING_gte: int | None = None + delta1_LIGHTNING_DEAL_lte: int | None = None + delta1_LIGHTNING_DEAL_gte: int | None = None + delta1_LISTPRICE_lte: int | None = None + delta1_LISTPRICE_gte: int | None = None + delta1_NEW_lte: int | None = None + delta1_NEW_gte: int | None = None + delta1_NEW_FBA_lte: int | None = None + delta1_NEW_FBA_gte: int | None = None + delta1_NEW_FBM_SHIPPING_lte: int | None = None + delta1_NEW_FBM_SHIPPING_gte: int | None = None + delta1_PRIME_EXCL_lte: int | None = None + delta1_PRIME_EXCL_gte: int | None = None + delta1_RATING_lte: int | None = None + delta1_RATING_gte: int | None = None + delta1_REFURBISHED_lte: int | None = None + delta1_REFURBISHED_gte: int | None = None + delta1_REFURBISHED_SHIPPING_lte: int | None = None + delta1_REFURBISHED_SHIPPING_gte: int | None = None + delta1_RENT_lte: int | None = None + delta1_RENT_gte: int | None = None + delta1_SALES_lte: int | None = None + delta1_SALES_gte: int | None = None + delta1_TRADE_IN_lte: int | None = None + delta1_TRADE_IN_gte: int | None = None + delta1_USED_lte: int | None = None + delta1_USED_gte: int | None = None + delta1_USED_ACCEPTABLE_SHIPPING_lte: int | None = None + delta1_USED_ACCEPTABLE_SHIPPING_gte: int | None = None + delta1_USED_GOOD_SHIPPING_lte: int | None = None + delta1_USED_GOOD_SHIPPING_gte: int | None = None + delta1_USED_NEW_SHIPPING_lte: int | None = None + delta1_USED_NEW_SHIPPING_gte: int | None = None + delta1_USED_VERY_GOOD_SHIPPING_lte: int | None = None + delta1_USED_VERY_GOOD_SHIPPING_gte: int | None = None + delta1_WAREHOUSE_lte: int | None = None + delta1_WAREHOUSE_gte: int | None = None + delta30_AMAZON_lte: int | None = None + delta30_AMAZON_gte: int | None = None + delta30_BUY_BOX_SHIPPING_lte: int | None = None + delta30_BUY_BOX_SHIPPING_gte: int | None = None + delta30_BUY_BOX_USED_SHIPPING_lte: int | None = None + delta30_BUY_BOX_USED_SHIPPING_gte: int | None = None + delta30_COLLECTIBLE_lte: int | None = None + delta30_COLLECTIBLE_gte: int | None = None + delta30_COUNT_COLLECTIBLE_lte: int | None = None + delta30_COUNT_COLLECTIBLE_gte: int | None = None + delta30_COUNT_NEW_lte: int | None = None + delta30_COUNT_NEW_gte: int | None = None + delta30_COUNT_REFURBISHED_lte: int | None = None + delta30_COUNT_REFURBISHED_gte: int | None = None + delta30_COUNT_REVIEWS_lte: int | None = None + delta30_COUNT_REVIEWS_gte: int | None = None + delta30_COUNT_USED_lte: int | None = None + delta30_COUNT_USED_gte: int | None = None + delta30_EBAY_NEW_SHIPPING_lte: int | None = None + delta30_EBAY_NEW_SHIPPING_gte: int | None = None + delta30_EBAY_USED_SHIPPING_lte: int | None = None + delta30_EBAY_USED_SHIPPING_gte: int | None = None + delta30_LIGHTNING_DEAL_lte: int | None = None + delta30_LIGHTNING_DEAL_gte: int | None = None + delta30_LISTPRICE_lte: int | None = None + delta30_LISTPRICE_gte: int | None = None + delta30_NEW_lte: int | None = None + delta30_NEW_gte: int | None = None + delta30_NEW_FBA_lte: int | None = None + delta30_NEW_FBA_gte: int | None = None + delta30_NEW_FBM_SHIPPING_lte: int | None = None + delta30_NEW_FBM_SHIPPING_gte: int | None = None + delta30_PRIME_EXCL_lte: int | None = None + delta30_PRIME_EXCL_gte: int | None = None + delta30_RATING_lte: int | None = None + delta30_RATING_gte: int | None = None + delta30_REFURBISHED_lte: int | None = None + delta30_REFURBISHED_gte: int | None = None + delta30_REFURBISHED_SHIPPING_lte: int | None = None + delta30_REFURBISHED_SHIPPING_gte: int | None = None + delta30_RENT_lte: int | None = None + delta30_RENT_gte: int | None = None + delta30_SALES_lte: int | None = None + delta30_SALES_gte: int | None = None + delta30_TRADE_IN_lte: int | None = None + delta30_TRADE_IN_gte: int | None = None + delta30_USED_lte: int | None = None + delta30_USED_gte: int | None = None + delta30_USED_ACCEPTABLE_SHIPPING_lte: int | None = None + delta30_USED_ACCEPTABLE_SHIPPING_gte: int | None = None + delta30_USED_GOOD_SHIPPING_lte: int | None = None + delta30_USED_GOOD_SHIPPING_gte: int | None = None + delta30_USED_NEW_SHIPPING_lte: int | None = None + delta30_USED_NEW_SHIPPING_gte: int | None = None + delta30_USED_VERY_GOOD_SHIPPING_lte: int | None = None + delta30_USED_VERY_GOOD_SHIPPING_gte: int | None = None + delta30_WAREHOUSE_lte: int | None = None + delta30_WAREHOUSE_gte: int | None = None + delta7_AMAZON_lte: int | None = None + delta7_AMAZON_gte: int | None = None + delta7_BUY_BOX_SHIPPING_lte: int | None = None + delta7_BUY_BOX_SHIPPING_gte: int | None = None + delta7_BUY_BOX_USED_SHIPPING_lte: int | None = None + delta7_BUY_BOX_USED_SHIPPING_gte: int | None = None + delta7_COLLECTIBLE_lte: int | None = None + delta7_COLLECTIBLE_gte: int | None = None + delta7_COUNT_COLLECTIBLE_lte: int | None = None + delta7_COUNT_COLLECTIBLE_gte: int | None = None + delta7_COUNT_NEW_lte: int | None = None + delta7_COUNT_NEW_gte: int | None = None + delta7_COUNT_REFURBISHED_lte: int | None = None + delta7_COUNT_REFURBISHED_gte: int | None = None + delta7_COUNT_REVIEWS_lte: int | None = None + delta7_COUNT_REVIEWS_gte: int | None = None + delta7_COUNT_USED_lte: int | None = None + delta7_COUNT_USED_gte: int | None = None + delta7_EBAY_NEW_SHIPPING_lte: int | None = None + delta7_EBAY_NEW_SHIPPING_gte: int | None = None + delta7_EBAY_USED_SHIPPING_lte: int | None = None + delta7_EBAY_USED_SHIPPING_gte: int | None = None + delta7_LIGHTNING_DEAL_lte: int | None = None + delta7_LIGHTNING_DEAL_gte: int | None = None + delta7_LISTPRICE_lte: int | None = None + delta7_LISTPRICE_gte: int | None = None + delta7_NEW_lte: int | None = None + delta7_NEW_gte: int | None = None + delta7_NEW_FBA_lte: int | None = None + delta7_NEW_FBA_gte: int | None = None + delta7_NEW_FBM_SHIPPING_lte: int | None = None + delta7_NEW_FBM_SHIPPING_gte: int | None = None + delta7_PRIME_EXCL_lte: int | None = None + delta7_PRIME_EXCL_gte: int | None = None + delta7_RATING_lte: int | None = None + delta7_RATING_gte: int | None = None + delta7_REFURBISHED_lte: int | None = None + delta7_REFURBISHED_gte: int | None = None + delta7_REFURBISHED_SHIPPING_lte: int | None = None + delta7_REFURBISHED_SHIPPING_gte: int | None = None + delta7_RENT_lte: int | None = None + delta7_RENT_gte: int | None = None + delta7_SALES_lte: int | None = None + delta7_SALES_gte: int | None = None + delta7_TRADE_IN_lte: int | None = None + delta7_TRADE_IN_gte: int | None = None + delta7_USED_lte: int | None = None + delta7_USED_gte: int | None = None + delta7_USED_ACCEPTABLE_SHIPPING_lte: int | None = None + delta7_USED_ACCEPTABLE_SHIPPING_gte: int | None = None + delta7_USED_GOOD_SHIPPING_lte: int | None = None + delta7_USED_GOOD_SHIPPING_gte: int | None = None + delta7_USED_NEW_SHIPPING_lte: int | None = None + delta7_USED_NEW_SHIPPING_gte: int | None = None + delta7_USED_VERY_GOOD_SHIPPING_lte: int | None = None + delta7_USED_VERY_GOOD_SHIPPING_gte: int | None = None + delta7_WAREHOUSE_lte: int | None = None + delta7_WAREHOUSE_gte: int | None = None + delta90_AMAZON_lte: int | None = None + delta90_AMAZON_gte: int | None = None + delta90_BUY_BOX_SHIPPING_lte: int | None = None + delta90_BUY_BOX_SHIPPING_gte: int | None = None + delta90_BUY_BOX_USED_SHIPPING_lte: int | None = None + delta90_BUY_BOX_USED_SHIPPING_gte: int | None = None + delta90_COLLECTIBLE_lte: int | None = None + delta90_COLLECTIBLE_gte: int | None = None + delta90_COUNT_COLLECTIBLE_lte: int | None = None + delta90_COUNT_COLLECTIBLE_gte: int | None = None + delta90_COUNT_NEW_lte: int | None = None + delta90_COUNT_NEW_gte: int | None = None + delta90_COUNT_REFURBISHED_lte: int | None = None + delta90_COUNT_REFURBISHED_gte: int | None = None + delta90_COUNT_REVIEWS_lte: int | None = None + delta90_COUNT_REVIEWS_gte: int | None = None + delta90_COUNT_USED_lte: int | None = None + delta90_COUNT_USED_gte: int | None = None + delta90_EBAY_NEW_SHIPPING_lte: int | None = None + delta90_EBAY_NEW_SHIPPING_gte: int | None = None + delta90_EBAY_USED_SHIPPING_lte: int | None = None + delta90_EBAY_USED_SHIPPING_gte: int | None = None + delta90_LIGHTNING_DEAL_lte: int | None = None + delta90_LIGHTNING_DEAL_gte: int | None = None + delta90_LISTPRICE_lte: int | None = None + delta90_LISTPRICE_gte: int | None = None + delta90_NEW_lte: int | None = None + delta90_NEW_gte: int | None = None + delta90_NEW_FBA_lte: int | None = None + delta90_NEW_FBA_gte: int | None = None + delta90_NEW_FBM_SHIPPING_lte: int | None = None + delta90_NEW_FBM_SHIPPING_gte: int | None = None + delta90_PRIME_EXCL_lte: int | None = None + delta90_PRIME_EXCL_gte: int | None = None + delta90_RATING_lte: int | None = None + delta90_RATING_gte: int | None = None + delta90_REFURBISHED_lte: int | None = None + delta90_REFURBISHED_gte: int | None = None + delta90_REFURBISHED_SHIPPING_lte: int | None = None + delta90_REFURBISHED_SHIPPING_gte: int | None = None + delta90_RENT_lte: int | None = None + delta90_RENT_gte: int | None = None + delta90_SALES_lte: int | None = None + delta90_SALES_gte: int | None = None + delta90_TRADE_IN_lte: int | None = None + delta90_TRADE_IN_gte: int | None = None + delta90_USED_lte: int | None = None + delta90_USED_gte: int | None = None + delta90_USED_ACCEPTABLE_SHIPPING_lte: int | None = None + delta90_USED_ACCEPTABLE_SHIPPING_gte: int | None = None + delta90_USED_GOOD_SHIPPING_lte: int | None = None + delta90_USED_GOOD_SHIPPING_gte: int | None = None + delta90_USED_NEW_SHIPPING_lte: int | None = None + delta90_USED_NEW_SHIPPING_gte: int | None = None + delta90_USED_VERY_GOOD_SHIPPING_lte: int | None = None + delta90_USED_VERY_GOOD_SHIPPING_gte: int | None = None + delta90_WAREHOUSE_lte: int | None = None + delta90_WAREHOUSE_gte: int | None = None + deltaLast_AMAZON_lte: int | None = None + deltaLast_AMAZON_gte: int | None = None + deltaLast_BUY_BOX_SHIPPING_lte: int | None = None + deltaLast_BUY_BOX_SHIPPING_gte: int | None = None + deltaLast_BUY_BOX_USED_SHIPPING_lte: int | None = None + deltaLast_BUY_BOX_USED_SHIPPING_gte: int | None = None + deltaLast_COLLECTIBLE_lte: int | None = None + deltaLast_COLLECTIBLE_gte: int | None = None + deltaLast_COUNT_COLLECTIBLE_lte: int | None = None + deltaLast_COUNT_COLLECTIBLE_gte: int | None = None + deltaLast_COUNT_NEW_lte: int | None = None + deltaLast_COUNT_NEW_gte: int | None = None + deltaLast_COUNT_REFURBISHED_lte: int | None = None + deltaLast_COUNT_REFURBISHED_gte: int | None = None + deltaLast_COUNT_REVIEWS_lte: int | None = None + deltaLast_COUNT_REVIEWS_gte: int | None = None + deltaLast_COUNT_USED_lte: int | None = None + deltaLast_COUNT_USED_gte: int | None = None + deltaLast_EBAY_NEW_SHIPPING_lte: int | None = None + deltaLast_EBAY_NEW_SHIPPING_gte: int | None = None + deltaLast_EBAY_USED_SHIPPING_lte: int | None = None + deltaLast_EBAY_USED_SHIPPING_gte: int | None = None + deltaLast_LIGHTNING_DEAL_lte: int | None = None + deltaLast_LIGHTNING_DEAL_gte: int | None = None + deltaLast_LISTPRICE_lte: int | None = None + deltaLast_LISTPRICE_gte: int | None = None + deltaLast_NEW_lte: int | None = None + deltaLast_NEW_gte: int | None = None + deltaLast_NEW_FBA_lte: int | None = None + deltaLast_NEW_FBA_gte: int | None = None + deltaLast_NEW_FBM_SHIPPING_lte: int | None = None + deltaLast_NEW_FBM_SHIPPING_gte: int | None = None + deltaLast_PRIME_EXCL_lte: int | None = None + deltaLast_PRIME_EXCL_gte: int | None = None + deltaLast_RATING_lte: int | None = None + deltaLast_RATING_gte: int | None = None + deltaLast_REFURBISHED_lte: int | None = None + deltaLast_REFURBISHED_gte: int | None = None + deltaLast_REFURBISHED_SHIPPING_lte: int | None = None + deltaLast_REFURBISHED_SHIPPING_gte: int | None = None + deltaLast_RENT_lte: int | None = None + deltaLast_RENT_gte: int | None = None + deltaLast_SALES_lte: int | None = None + deltaLast_SALES_gte: int | None = None + deltaLast_TRADE_IN_lte: int | None = None + deltaLast_TRADE_IN_gte: int | None = None + deltaLast_USED_lte: int | None = None + deltaLast_USED_gte: int | None = None + deltaLast_USED_ACCEPTABLE_SHIPPING_lte: int | None = None + deltaLast_USED_ACCEPTABLE_SHIPPING_gte: int | None = None + deltaLast_USED_GOOD_SHIPPING_lte: int | None = None + deltaLast_USED_GOOD_SHIPPING_gte: int | None = None + deltaLast_USED_NEW_SHIPPING_lte: int | None = None + deltaLast_USED_NEW_SHIPPING_gte: int | None = None + deltaLast_USED_VERY_GOOD_SHIPPING_lte: int | None = None + deltaLast_USED_VERY_GOOD_SHIPPING_gte: int | None = None + deltaLast_WAREHOUSE_lte: int | None = None + deltaLast_WAREHOUSE_gte: int | None = None + deltaPercent1_AMAZON_lte: int | None = None + deltaPercent1_AMAZON_gte: int | None = None + deltaPercent1_BUY_BOX_SHIPPING_lte: int | None = None + deltaPercent1_BUY_BOX_SHIPPING_gte: int | None = None + deltaPercent1_BUY_BOX_USED_SHIPPING_lte: int | None = None + deltaPercent1_BUY_BOX_USED_SHIPPING_gte: int | None = None + deltaPercent1_COLLECTIBLE_lte: int | None = None + deltaPercent1_COLLECTIBLE_gte: int | None = None + deltaPercent1_COUNT_COLLECTIBLE_lte: int | None = None + deltaPercent1_COUNT_COLLECTIBLE_gte: int | None = None + deltaPercent1_COUNT_NEW_lte: int | None = None + deltaPercent1_COUNT_NEW_gte: int | None = None + deltaPercent1_COUNT_REFURBISHED_lte: int | None = None + deltaPercent1_COUNT_REFURBISHED_gte: int | None = None + deltaPercent1_COUNT_REVIEWS_lte: int | None = None + deltaPercent1_COUNT_REVIEWS_gte: int | None = None + deltaPercent1_COUNT_USED_lte: int | None = None + deltaPercent1_COUNT_USED_gte: int | None = None + deltaPercent1_EBAY_NEW_SHIPPING_lte: int | None = None + deltaPercent1_EBAY_NEW_SHIPPING_gte: int | None = None + deltaPercent1_EBAY_USED_SHIPPING_lte: int | None = None + deltaPercent1_EBAY_USED_SHIPPING_gte: int | None = None + deltaPercent1_LIGHTNING_DEAL_lte: int | None = None + deltaPercent1_LIGHTNING_DEAL_gte: int | None = None + deltaPercent1_LISTPRICE_lte: int | None = None + deltaPercent1_LISTPRICE_gte: int | None = None + deltaPercent1_NEW_lte: int | None = None + deltaPercent1_NEW_gte: int | None = None + deltaPercent1_NEW_FBA_lte: int | None = None + deltaPercent1_NEW_FBA_gte: int | None = None + deltaPercent1_NEW_FBM_SHIPPING_lte: int | None = None + deltaPercent1_NEW_FBM_SHIPPING_gte: int | None = None + deltaPercent1_PRIME_EXCL_lte: int | None = None + deltaPercent1_PRIME_EXCL_gte: int | None = None + deltaPercent1_RATING_lte: int | None = None + deltaPercent1_RATING_gte: int | None = None + deltaPercent1_REFURBISHED_lte: int | None = None + deltaPercent1_REFURBISHED_gte: int | None = None + deltaPercent1_REFURBISHED_SHIPPING_lte: int | None = None + deltaPercent1_REFURBISHED_SHIPPING_gte: int | None = None + deltaPercent1_RENT_lte: int | None = None + deltaPercent1_RENT_gte: int | None = None + deltaPercent1_SALES_lte: int | None = None + deltaPercent1_SALES_gte: int | None = None + deltaPercent1_TRADE_IN_lte: int | None = None + deltaPercent1_TRADE_IN_gte: int | None = None + deltaPercent1_USED_lte: int | None = None + deltaPercent1_USED_gte: int | None = None + deltaPercent1_USED_ACCEPTABLE_SHIPPING_lte: int | None = None + deltaPercent1_USED_ACCEPTABLE_SHIPPING_gte: int | None = None + deltaPercent1_USED_GOOD_SHIPPING_lte: int | None = None + deltaPercent1_USED_GOOD_SHIPPING_gte: int | None = None + deltaPercent1_USED_NEW_SHIPPING_lte: int | None = None + deltaPercent1_USED_NEW_SHIPPING_gte: int | None = None + deltaPercent1_USED_VERY_GOOD_SHIPPING_lte: int | None = None + deltaPercent1_USED_VERY_GOOD_SHIPPING_gte: int | None = None + deltaPercent1_WAREHOUSE_lte: int | None = None + deltaPercent1_WAREHOUSE_gte: int | None = None + deltaPercent30_AMAZON_lte: int | None = None + deltaPercent30_AMAZON_gte: int | None = None + deltaPercent30_BUY_BOX_SHIPPING_lte: int | None = None + deltaPercent30_BUY_BOX_SHIPPING_gte: int | None = None + deltaPercent30_BUY_BOX_USED_SHIPPING_lte: int | None = None + deltaPercent30_BUY_BOX_USED_SHIPPING_gte: int | None = None + deltaPercent30_COLLECTIBLE_lte: int | None = None + deltaPercent30_COLLECTIBLE_gte: int | None = None + deltaPercent30_COUNT_COLLECTIBLE_lte: int | None = None + deltaPercent30_COUNT_COLLECTIBLE_gte: int | None = None + deltaPercent30_COUNT_NEW_lte: int | None = None + deltaPercent30_COUNT_NEW_gte: int | None = None + deltaPercent30_COUNT_REFURBISHED_lte: int | None = None + deltaPercent30_COUNT_REFURBISHED_gte: int | None = None + deltaPercent30_COUNT_REVIEWS_lte: int | None = None + deltaPercent30_COUNT_REVIEWS_gte: int | None = None + deltaPercent30_COUNT_USED_lte: int | None = None + deltaPercent30_COUNT_USED_gte: int | None = None + deltaPercent30_EBAY_NEW_SHIPPING_lte: int | None = None + deltaPercent30_EBAY_NEW_SHIPPING_gte: int | None = None + deltaPercent30_EBAY_USED_SHIPPING_lte: int | None = None + deltaPercent30_EBAY_USED_SHIPPING_gte: int | None = None + deltaPercent30_LIGHTNING_DEAL_lte: int | None = None + deltaPercent30_LIGHTNING_DEAL_gte: int | None = None + deltaPercent30_LISTPRICE_lte: int | None = None + deltaPercent30_LISTPRICE_gte: int | None = None + deltaPercent30_NEW_lte: int | None = None + deltaPercent30_NEW_gte: int | None = None + deltaPercent30_NEW_FBA_lte: int | None = None + deltaPercent30_NEW_FBA_gte: int | None = None + deltaPercent30_NEW_FBM_SHIPPING_lte: int | None = None + deltaPercent30_NEW_FBM_SHIPPING_gte: int | None = None + deltaPercent30_PRIME_EXCL_lte: int | None = None + deltaPercent30_PRIME_EXCL_gte: int | None = None + deltaPercent30_RATING_lte: int | None = None + deltaPercent30_RATING_gte: int | None = None + deltaPercent30_REFURBISHED_lte: int | None = None + deltaPercent30_REFURBISHED_gte: int | None = None + deltaPercent30_REFURBISHED_SHIPPING_lte: int | None = None + deltaPercent30_REFURBISHED_SHIPPING_gte: int | None = None + deltaPercent30_RENT_lte: int | None = None + deltaPercent30_RENT_gte: int | None = None + deltaPercent30_SALES_lte: int | None = None + deltaPercent30_SALES_gte: int | None = None + deltaPercent30_TRADE_IN_lte: int | None = None + deltaPercent30_TRADE_IN_gte: int | None = None + deltaPercent30_USED_lte: int | None = None + deltaPercent30_USED_gte: int | None = None + deltaPercent30_USED_ACCEPTABLE_SHIPPING_lte: int | None = None + deltaPercent30_USED_ACCEPTABLE_SHIPPING_gte: int | None = None + deltaPercent30_USED_GOOD_SHIPPING_lte: int | None = None + deltaPercent30_USED_GOOD_SHIPPING_gte: int | None = None + deltaPercent30_USED_NEW_SHIPPING_lte: int | None = None + deltaPercent30_USED_NEW_SHIPPING_gte: int | None = None + deltaPercent30_USED_VERY_GOOD_SHIPPING_lte: int | None = None + deltaPercent30_USED_VERY_GOOD_SHIPPING_gte: int | None = None + deltaPercent30_WAREHOUSE_lte: int | None = None + deltaPercent30_WAREHOUSE_gte: int | None = None + deltaPercent7_AMAZON_lte: int | None = None + deltaPercent7_AMAZON_gte: int | None = None + deltaPercent7_BUY_BOX_SHIPPING_lte: int | None = None + deltaPercent7_BUY_BOX_SHIPPING_gte: int | None = None + deltaPercent7_BUY_BOX_USED_SHIPPING_lte: int | None = None + deltaPercent7_BUY_BOX_USED_SHIPPING_gte: int | None = None + deltaPercent7_COLLECTIBLE_lte: int | None = None + deltaPercent7_COLLECTIBLE_gte: int | None = None + deltaPercent7_COUNT_COLLECTIBLE_lte: int | None = None + deltaPercent7_COUNT_COLLECTIBLE_gte: int | None = None + deltaPercent7_COUNT_NEW_lte: int | None = None + deltaPercent7_COUNT_NEW_gte: int | None = None + deltaPercent7_COUNT_REFURBISHED_lte: int | None = None + deltaPercent7_COUNT_REFURBISHED_gte: int | None = None + deltaPercent7_COUNT_REVIEWS_lte: int | None = None + deltaPercent7_COUNT_REVIEWS_gte: int | None = None + deltaPercent7_COUNT_USED_lte: int | None = None + deltaPercent7_COUNT_USED_gte: int | None = None + deltaPercent7_EBAY_NEW_SHIPPING_lte: int | None = None + deltaPercent7_EBAY_NEW_SHIPPING_gte: int | None = None + deltaPercent7_EBAY_USED_SHIPPING_lte: int | None = None + deltaPercent7_EBAY_USED_SHIPPING_gte: int | None = None + deltaPercent7_LIGHTNING_DEAL_lte: int | None = None + deltaPercent7_LIGHTNING_DEAL_gte: int | None = None + deltaPercent7_LISTPRICE_lte: int | None = None + deltaPercent7_LISTPRICE_gte: int | None = None + deltaPercent7_NEW_lte: int | None = None + deltaPercent7_NEW_gte: int | None = None + deltaPercent7_NEW_FBA_lte: int | None = None + deltaPercent7_NEW_FBA_gte: int | None = None + deltaPercent7_NEW_FBM_SHIPPING_lte: int | None = None + deltaPercent7_NEW_FBM_SHIPPING_gte: int | None = None + deltaPercent7_PRIME_EXCL_lte: int | None = None + deltaPercent7_PRIME_EXCL_gte: int | None = None + deltaPercent7_RATING_lte: int | None = None + deltaPercent7_RATING_gte: int | None = None + deltaPercent7_REFURBISHED_lte: int | None = None + deltaPercent7_REFURBISHED_gte: int | None = None + deltaPercent7_REFURBISHED_SHIPPING_lte: int | None = None + deltaPercent7_REFURBISHED_SHIPPING_gte: int | None = None + deltaPercent7_RENT_lte: int | None = None + deltaPercent7_RENT_gte: int | None = None + deltaPercent7_SALES_lte: int | None = None + deltaPercent7_SALES_gte: int | None = None + deltaPercent7_TRADE_IN_lte: int | None = None + deltaPercent7_TRADE_IN_gte: int | None = None + deltaPercent7_USED_lte: int | None = None + deltaPercent7_USED_gte: int | None = None + deltaPercent7_USED_ACCEPTABLE_SHIPPING_lte: int | None = None + deltaPercent7_USED_ACCEPTABLE_SHIPPING_gte: int | None = None + deltaPercent7_USED_GOOD_SHIPPING_lte: int | None = None + deltaPercent7_USED_GOOD_SHIPPING_gte: int | None = None + deltaPercent7_USED_NEW_SHIPPING_lte: int | None = None + deltaPercent7_USED_NEW_SHIPPING_gte: int | None = None + deltaPercent7_USED_VERY_GOOD_SHIPPING_lte: int | None = None + deltaPercent7_USED_VERY_GOOD_SHIPPING_gte: int | None = None + deltaPercent7_WAREHOUSE_lte: int | None = None + deltaPercent7_WAREHOUSE_gte: int | None = None + deltaPercent90_AMAZON_lte: int | None = None + deltaPercent90_AMAZON_gte: int | None = None + deltaPercent90_BUY_BOX_SHIPPING_lte: int | None = None + deltaPercent90_BUY_BOX_SHIPPING_gte: int | None = None + deltaPercent90_BUY_BOX_USED_SHIPPING_lte: int | None = None + deltaPercent90_BUY_BOX_USED_SHIPPING_gte: int | None = None + deltaPercent90_COLLECTIBLE_lte: int | None = None + deltaPercent90_COLLECTIBLE_gte: int | None = None + deltaPercent90_COUNT_COLLECTIBLE_lte: int | None = None + deltaPercent90_COUNT_COLLECTIBLE_gte: int | None = None + deltaPercent90_COUNT_NEW_lte: int | None = None + deltaPercent90_COUNT_NEW_gte: int | None = None + deltaPercent90_COUNT_REFURBISHED_lte: int | None = None + deltaPercent90_COUNT_REFURBISHED_gte: int | None = None + deltaPercent90_COUNT_REVIEWS_lte: int | None = None + deltaPercent90_COUNT_REVIEWS_gte: int | None = None + deltaPercent90_COUNT_USED_lte: int | None = None + deltaPercent90_COUNT_USED_gte: int | None = None + deltaPercent90_EBAY_NEW_SHIPPING_lte: int | None = None + deltaPercent90_EBAY_NEW_SHIPPING_gte: int | None = None + deltaPercent90_EBAY_USED_SHIPPING_lte: int | None = None + deltaPercent90_EBAY_USED_SHIPPING_gte: int | None = None + deltaPercent90_LIGHTNING_DEAL_lte: int | None = None + deltaPercent90_LIGHTNING_DEAL_gte: int | None = None + deltaPercent90_LISTPRICE_lte: int | None = None + deltaPercent90_LISTPRICE_gte: int | None = None + deltaPercent90_NEW_lte: int | None = None + deltaPercent90_NEW_gte: int | None = None + deltaPercent90_NEW_FBA_lte: int | None = None + deltaPercent90_NEW_FBA_gte: int | None = None + deltaPercent90_NEW_FBM_SHIPPING_lte: int | None = None + deltaPercent90_NEW_FBM_SHIPPING_gte: int | None = None + deltaPercent90_PRIME_EXCL_lte: int | None = None + deltaPercent90_PRIME_EXCL_gte: int | None = None + deltaPercent90_RATING_lte: int | None = None + deltaPercent90_RATING_gte: int | None = None + deltaPercent90_REFURBISHED_lte: int | None = None + deltaPercent90_REFURBISHED_gte: int | None = None + deltaPercent90_REFURBISHED_SHIPPING_lte: int | None = None + deltaPercent90_REFURBISHED_SHIPPING_gte: int | None = None + deltaPercent90_RENT_lte: int | None = None + deltaPercent90_RENT_gte: int | None = None + deltaPercent90_SALES_lte: int | None = None + deltaPercent90_SALES_gte: int | None = None + deltaPercent90_TRADE_IN_lte: int | None = None + deltaPercent90_TRADE_IN_gte: int | None = None + deltaPercent90_USED_lte: int | None = None + deltaPercent90_USED_gte: int | None = None + deltaPercent90_USED_ACCEPTABLE_SHIPPING_lte: int | None = None + deltaPercent90_USED_ACCEPTABLE_SHIPPING_gte: int | None = None + deltaPercent90_USED_GOOD_SHIPPING_lte: int | None = None + deltaPercent90_USED_GOOD_SHIPPING_gte: int | None = None + deltaPercent90_USED_NEW_SHIPPING_lte: int | None = None + deltaPercent90_USED_NEW_SHIPPING_gte: int | None = None + deltaPercent90_USED_VERY_GOOD_SHIPPING_lte: int | None = None + deltaPercent90_USED_VERY_GOOD_SHIPPING_gte: int | None = None + deltaPercent90_WAREHOUSE_lte: int | None = None + deltaPercent90_WAREHOUSE_gte: int | None = None + edition: list[str | None] | None = None + fbaFees_lte: int | None = None + fbaFees_gte: int | None = None + format: list[str | None] | None = None + genre: list[str | None] | None = None + hasParentASIN: bool | None = None + hasReviews: bool | None = None + isAdultProduct: bool | None = None + isEligibleForSuperSaverShipping: bool | None = None + isEligibleForTradeIn: bool | None = None + isHighestOffer: bool | None = None + isLowestOffer: bool | None = None + isLowest_AMAZON: bool | None = None + isLowest_BUY_BOX_SHIPPING: bool | None = None + isLowest_BUY_BOX_USED_SHIPPING: bool | None = None + isLowest_COLLECTIBLE: bool | None = None + isLowest_COUNT_COLLECTIBLE: bool | None = None + isLowest_COUNT_NEW: bool | None = None + isLowest_COUNT_REFURBISHED: bool | None = None + isLowest_COUNT_REVIEWS: bool | None = None + isLowest_COUNT_USED: bool | None = None + isLowest_EBAY_NEW_SHIPPING: bool | None = None + isLowest_EBAY_USED_SHIPPING: bool | None = None + isLowest_LIGHTNING_DEAL: bool | None = None + isLowest_LISTPRICE: bool | None = None + isLowest_NEW: bool | None = None + isLowest_NEW_FBA: bool | None = None + isLowest_NEW_FBM_SHIPPING: bool | None = None + isLowest_PRIME_EXCL: bool | None = None + isLowest_RATING: bool | None = None + isLowest_REFURBISHED: bool | None = None + isLowest_REFURBISHED_SHIPPING: bool | None = None + isLowest_RENT: bool | None = None + isLowest_SALES: bool | None = None + isLowest_TRADE_IN: bool | None = None + isLowest_USED: bool | None = None + isLowest_USED_ACCEPTABLE_SHIPPING: bool | None = None + isLowest_USED_GOOD_SHIPPING: bool | None = None + isLowest_USED_NEW_SHIPPING: bool | None = None + isLowest_USED_VERY_GOOD_SHIPPING: bool | None = None + isLowest_WAREHOUSE: bool | None = None + isPrimeExclusive: bool | None = None + isSNS: bool | None = None + itemDimension_lte: int | None = None + itemDimension_gte: int | None = None + itemHeight_lte: int | None = None + itemHeight_gte: int | None = None + itemLength_lte: int | None = None + itemLength_gte: int | None = None + itemWeight_lte: int | None = None + itemWeight_gte: int | None = None + itemWidth_lte: int | None = None + itemWidth_gte: int | None = None + label: list[str | None] | None = None + languages: list[str | None] | None = None + lastOffersUpdate_lte: int | None = None + lastOffersUpdate_gte: int | None = None + lastPriceChange_lte: int | None = None + lastPriceChange_gte: int | None = None + lastPriceChange_AMAZON_lte: int | None = None + lastPriceChange_AMAZON_gte: int | None = None + lastPriceChange_NEW_lte: int | None = None + lastPriceChange_NEW_gte: int | None = None + lastPriceChange_USED_lte: int | None = None + lastPriceChange_USED_gte: int | None = None + lastPriceChange_SALES_lte: int | None = None + lastPriceChange_SALES_gte: int | None = None + lastPriceChange_LISTPRICE_lte: int | None = None + lastPriceChange_LISTPRICE_gte: int | None = None + lastPriceChange_COLLECTIBLE_lte: int | None = None + lastPriceChange_COLLECTIBLE_gte: int | None = None + lastPriceChange_REFURBISHED_lte: int | None = None + lastPriceChange_REFURBISHED_gte: int | None = None + lastPriceChange_NEW_FBM_SHIPPING_lte: int | None = None + lastPriceChange_NEW_FBM_SHIPPING_gte: int | None = None + lastPriceChange_LIGHTNING_DEAL_lte: int | None = None + lastPriceChange_LIGHTNING_DEAL_gte: int | None = None + lastPriceChange_WAREHOUSE_lte: int | None = None + lastPriceChange_WAREHOUSE_gte: int | None = None + lastPriceChange_NEW_FBA_lte: int | None = None + lastPriceChange_NEW_FBA_gte: int | None = None + lastPriceChange_COUNT_NEW_lte: int | None = None + lastPriceChange_COUNT_NEW_gte: int | None = None + lastPriceChange_COUNT_USED_lte: int | None = None + lastPriceChange_COUNT_USED_gte: int | None = None + lastPriceChange_COUNT_REFURBISHED_lte: int | None = None + lastPriceChange_COUNT_REFURBISHED_gte: int | None = None + lastPriceChange_COUNT_COLLECTIBLE_lte: int | None = None + lastPriceChange_COUNT_COLLECTIBLE_gte: int | None = None + lastPriceChange_RATING_lte: int | None = None + lastPriceChange_RATING_gte: int | None = None + lastPriceChange_COUNT_REVIEWS_lte: int | None = None + lastPriceChange_COUNT_REVIEWS_gte: int | None = None + lastPriceChange_BUY_BOX_SHIPPING_lte: int | None = None + lastPriceChange_BUY_BOX_SHIPPING_gte: int | None = None + lastPriceChange_USED_NEW_SHIPPING_lte: int | None = None + lastPriceChange_USED_NEW_SHIPPING_gte: int | None = None + lastPriceChange_USED_VERY_GOOD_SHIPPING_lte: int | None = None + lastPriceChange_USED_VERY_GOOD_SHIPPING_gte: int | None = None + lastPriceChange_USED_GOOD_SHIPPING_lte: int | None = None + lastPriceChange_USED_GOOD_SHIPPING_gte: int | None = None + lastPriceChange_USED_ACCEPTABLE_SHIPPING_lte: int | None = None + lastPriceChange_USED_ACCEPTABLE_SHIPPING_gte: int | None = None + lastPriceChange_REFURBISHED_SHIPPING_lte: int | None = None + lastPriceChange_REFURBISHED_SHIPPING_gte: int | None = None + lastPriceChange_EBAY_NEW_SHIPPING_lte: int | None = None + lastPriceChange_EBAY_NEW_SHIPPING_gte: int | None = None + lastPriceChange_EBAY_USED_SHIPPING_lte: int | None = None + lastPriceChange_EBAY_USED_SHIPPING_gte: int | None = None + lastPriceChange_TRADE_IN_lte: int | None = None + lastPriceChange_TRADE_IN_gte: int | None = None + lastPriceChange_RENT_lte: int | None = None + lastPriceChange_RENT_gte: int | None = None + lastPriceChange_BUY_BOX_USED_SHIPPING_lte: int | None = None + lastPriceChange_BUY_BOX_USED_SHIPPING_gte: int | None = None + lastPriceChange_PRIME_EXCL_lte: int | None = None + lastPriceChange_PRIME_EXCL_gte: int | None = None + lastRatingUpdate_lte: int | None = None + lastRatingUpdate_gte: int | None = None + lastUpdate_lte: int | None = None + lastUpdate_gte: int | None = None + lightningEnd_lte: int | None = None + lightningEnd_gte: int | None = None + lightningStart_lte: int | None = None + lightningStart_gte: int | None = None + listedSince_lte: int | None = None + listedSince_gte: int | None = None + manufacturer: list[str | None] | None = None + model: list[str | None] | None = None + newPriceIsMAP: bool | None = None + nextUpdate_lte: int | None = None + nextUpdate_gte: int | None = None + numberOfItems_lte: int | None = None + numberOfItems_gte: int | None = None + numberOfPages_lte: int | None = None + numberOfPages_gte: int | None = None + numberOfTrackings_lte: int | None = None + numberOfTrackings_gte: int | None = None + offerCountFBA_lte: int | None = None + offerCountFBA_gte: int | None = None + offerCountFBM_lte: int | None = None + offerCountFBM_gte: int | None = None + outOfStockPercentage90_BB_lte: int | None = None + outOfStockPercentage90_BB_gte: int | None = None + outOfStockPercentage90_BB_USED_lte: int | None = None + outOfStockPercentage90_BB_USED_gte: int | None = None + outOfStockPercentage90_NEW_lte: int | None = None + outOfStockPercentage90_NEW_gte: int | None = None + outOfStockPercentage90_USED_lte: int | None = None + outOfStockPercentage90_USED_gte: int | None = None + outOfStockPercentageInInterval_lte: int | None = None + outOfStockPercentageInInterval_gte: int | None = None + packageDimension_lte: int | None = None + packageDimension_gte: int | None = None + packageHeight_lte: int | None = None + packageHeight_gte: int | None = None + packageLength_lte: int | None = None + packageLength_gte: int | None = None + packageQuantity_lte: int | None = None + packageQuantity_gte: int | None = None + packageWeight_lte: int | None = None + packageWeight_gte: int | None = None + packageWidth_lte: int | None = None + packageWidth_gte: int | None = None + partNumber: list[str | None] | None = None + platform: list[str | None] | None = None + productGroup: list[str | None] | None = None + productType: list[int | None] | None = None + publicationDate_lte: int | None = None + publicationDate_gte: int | None = None + publisher: list[str | None] | None = None + releaseDate_lte: int | None = None + releaseDate_gte: int | None = None + rootCategory: list[str | None] | None = None + salesRankDrops180_lte: int | None = None + salesRankDrops180_gte: int | None = None + salesRankDrops30_lte: int | None = None + salesRankDrops30_gte: int | None = None + salesRankDrops365_lte: int | None = None + salesRankDrops365_gte: int | None = None + salesRankDrops90_lte: int | None = None + salesRankDrops90_gte: int | None = None + salesRankReference: int | None = None + salesRankTopPct_lte: int | None = None + salesRankTopPct_gte: int | None = None + sellerIds: list[str | None] | None = None + sellerIdsLowestFBA: list[str | None] | None = None + sellerIdsLowestFBM: list[str | None] | None = None + size: list[str | None] | None = None + studio: list[str | None] | None = None + title: str | None = None + title_flag: str | None = None + totalOfferCount_lte: int | None = None + totalOfferCount_gte: int | None = None + trackingSince_lte: int | None = None + trackingSince_gte: int | None = None + monthlySold_lte: int | None = None + monthlySold_gte: int | None = None + buyBoxIsPreorder: bool | None = None + buyBoxIsBackorder: bool | None = None + buyBoxIsPrimeExclusive: bool | None = None + buyBoxIsPrimeEligible: bool | None = None + type: list[str | None] | None = None + warehouseCondition: list[int | None] | None = None + singleVariation: bool | None = None + outOfStockPercentage90_lte: int | None = None + outOfStockPercentage90_gte: int | None = None + variationCount_lte: int | None = None + variationCount_gte: int | None = None + imageCount_lte: int | None = None + imageCount_gte: int | None = None + buyBoxStatsAmazon30_lte: int | None = None + buyBoxStatsAmazon30_gte: int | None = None + buyBoxStatsAmazon90_lte: int | None = None + buyBoxStatsAmazon90_gte: int | None = None + buyBoxStatsAmazon180_lte: int | None = None + buyBoxStatsAmazon180_gte: int | None = None + buyBoxStatsAmazon365_lte: int | None = None + buyBoxStatsAmazon365_gte: int | None = None + buyBoxStatsTopSeller30_lte: int | None = None + buyBoxStatsTopSeller30_gte: int | None = None + buyBoxStatsTopSeller90_lte: int | None = None + buyBoxStatsTopSeller90_gte: int | None = None + buyBoxStatsTopSeller180_lte: int | None = None + buyBoxStatsTopSeller180_gte: int | None = None + buyBoxStatsTopSeller365_lte: int | None = None + buyBoxStatsTopSeller365_gte: int | None = None + buyBoxStatsSellerCount30_lte: int | None = None + buyBoxStatsSellerCount30_gte: int | None = None + buyBoxStatsSellerCount90_lte: int | None = None + buyBoxStatsSellerCount90_gte: int | None = None + buyBoxStatsSellerCount180_lte: int | None = None + buyBoxStatsSellerCount180_gte: int | None = None + buyBoxStatsSellerCount365_lte: int | None = None + buyBoxStatsSellerCount365_gte: int | None = None + isHazMat: bool | None = None + isHeatSensitive: bool | None = None + buyBoxStandardDeviation30_lte: int | None = None + buyBoxStandardDeviation30_gte: int | None = None + buyBoxStandardDeviation90_lte: int | None = None + buyBoxStandardDeviation90_gte: int | None = None + buyBoxStandardDeviation365_lte: int | None = None + buyBoxStandardDeviation365_gte: int | None = None + flipability30_lte: int | None = None + flipability30_gte: int | None = None + flipability90_lte: int | None = None + flipability90_gte: int | None = None + flipability365_lte: int | None = None + flipability365_gte: int | None = None + itemTypeKeyword: list[str | None] | None = None + targetAudienceKeyword: list[str | None] | None = None + itemForm: list[str | None] | None = None + scent: str | None = None + unitType: str | None = None + unitValue_lte: float | None = None + unitValue_gte: float | None = None + pattern: str | None = None + batteriesRequired: bool | None = None + batteriesIncluded: bool | None = None + style: str | None = None + material: str | None = None + lastBusinessDiscountUpdate_lte: int | None = None + lastBusinessDiscountUpdate_gte: int | None = None + businessDiscount_lte: int | None = None + businessDiscount_gte: int | None = None + activeIngredients: list[str | None] | None = None + specialIngredients: list[str | None] | None = None + isMerchOnDemand: bool | None = None + hasMainVideo: bool | None = None + hasAPlus: bool | None = None + hasAPlusFromManufacturer: bool | None = None + videoCount_lte: int | None = None + videoCount_gte: int | None = None + brandStoreName: list[str | None] | None = None + brandStoreUrlName: list[str | None] | None = None + returnRate: list[int | None] | None = None + deltaPercent90_monthlySold_lte: int | None = None + deltaPercent90_monthlySold_gte: int | None = None + outOfStockCountAmazon30_lte: int | None = None + outOfStockCountAmazon30_gte: int | None = None + outOfStockCountAmazon90_lte: int | None = None + outOfStockCountAmazon90_gte: int | None = None + variationReviewCount_lte: int | None = None + variationReviewCount_gte: int | None = None + variationRatingCount_lte: int | None = None + variationRatingCount_gte: int | None = None + websiteDisplayGroupName: list[str | None] | None = None + websiteDisplayGroup: list[str | None] | None = None + availabilityAmazonMinDelayInDays_lte: int | None = None + availabilityAmazonMinDelayInDays_gte: int | None = None + blackList: list[int | None] | None = None + isDeal: bool | None = None + launchpad: bool | None = None + historicalSellerIds: list[str | None] | None = None + buyBoxShippingCountry: list[str | None] | None = None + sellerIdsFBA: list[str | None] | None = None + sellerIdsFBM: list[str | None] | None = None + buyBoxStatsTopSellerId30: list[str | None] | None = None + buyBoxStatsTopSellerId90: list[str | None] | None = None + buyBoxStatsTopSellerId180: list[str | None] | None = None + buyBoxStatsTopSellerId365: list[str | None] | None = None + buyBoxEligibleOfferCountsNewFBA_lte: int | None = None + buyBoxEligibleOfferCountsNewFBA_gte: int | None = None + buyBoxEligibleOfferCountsNewFBM_lte: int | None = None + buyBoxEligibleOfferCountsNewFBM_gte: int | None = None + buyBoxEligibleOfferCountsUsedFBA_lte: int | None = None + buyBoxEligibleOfferCountsUsedFBA_gte: int | None = None + buyBoxEligibleOfferCountsUsedFBM_lte: int | None = None + buyBoxEligibleOfferCountsUsedFBM_gte: int | None = None + srAvg000_gte: int | None = None + srAvg001_gte: int | None = None + srAvg002_gte: int | None = None + srAvg003_gte: int | None = None + srAvg004_gte: int | None = None + srAvg005_gte: int | None = None + srAvg006_gte: int | None = None + srAvg007_gte: int | None = None + srAvg008_gte: int | None = None + srAvg009_gte: int | None = None + srAvg010_gte: int | None = None + srAvg011_gte: int | None = None + srAvg100_gte: int | None = None + srAvg101_gte: int | None = None + srAvg102_gte: int | None = None + srAvg103_gte: int | None = None + srAvg104_gte: int | None = None + srAvg105_gte: int | None = None + srAvg106_gte: int | None = None + srAvg107_gte: int | None = None + srAvg108_gte: int | None = None + srAvg109_gte: int | None = None + srAvg110_gte: int | None = None + srAvg111_gte: int | None = None + srAvg200_gte: int | None = None + srAvg201_gte: int | None = None + srAvg202_gte: int | None = None + srAvg203_gte: int | None = None + srAvg204_gte: int | None = None + srAvg205_gte: int | None = None + srAvg206_gte: int | None = None + srAvg207_gte: int | None = None + srAvg208_gte: int | None = None + srAvg209_gte: int | None = None + srAvg210_gte: int | None = None + srAvg211_gte: int | None = None + srAvg000_lte: int | None = None + srAvg001_lte: int | None = None + srAvg002_lte: int | None = None + srAvg003_lte: int | None = None + srAvg004_lte: int | None = None + srAvg005_lte: int | None = None + srAvg006_lte: int | None = None + srAvg007_lte: int | None = None + srAvg008_lte: int | None = None + srAvg009_lte: int | None = None + srAvg010_lte: int | None = None + srAvg011_lte: int | None = None + srAvg100_lte: int | None = None + srAvg101_lte: int | None = None + srAvg102_lte: int | None = None + srAvg103_lte: int | None = None + srAvg104_lte: int | None = None + srAvg105_lte: int | None = None + srAvg106_lte: int | None = None + srAvg107_lte: int | None = None + srAvg108_lte: int | None = None + srAvg109_lte: int | None = None + srAvg110_lte: int | None = None + srAvg111_lte: int | None = None + srAvg200_lte: int | None = None + srAvg201_lte: int | None = None + srAvg202_lte: int | None = None + srAvg203_lte: int | None = None + srAvg204_lte: int | None = None + srAvg205_lte: int | None = None + srAvg206_lte: int | None = None + srAvg207_lte: int | None = None + srAvg208_lte: int | None = None + srAvg209_lte: int | None = None + srAvg210_lte: int | None = None + srAvg211_lte: int | None = None + minMatch: dict[str, int | None] | None = None + sort: list[list[str | None] | None] | None = None + perPage: int | None = None + page: int | None = None + + +class PromotionObject(KeepaBackendModel): + """Backend ``PromotionObject`` model generated from the Keepa Java schema.""" + + type: PromotionType | None = Field( + default=None, + description=("The type of promotion"), + ) + amount: int | None = None + discountPercent: int | None = None + snsBulkDiscountPercent: int | None = None + sellerId: str | None = None + + +class Request(KeepaBackendModel): + """Backend ``Request`` model generated from the Keepa Java schema.""" + + parameter: dict[str, str | None] | None = None + postData: str | None = None + path: str | None = None + + +class RequestError(KeepaBackendModel): + """Backend ``RequestError`` model generated from the Keepa Java schema.""" + + pass + + +class Response(KeepaBackendModel): + """Backend ``Response`` model generated from the Keepa Java schema.""" + + timestamp: int | None = Field( + default=None, + description=("Server time when response was sent."), + ) + tokensLeft: int | None = Field( + default=None, + description=( + "States how many ASINs may be requested " + "before the assigned API contingent is " + "depleted.\n" + "If the contingent is depleted, HTTP " + "status code 503 will be delivered with " + "the message:\n" + '"You are submitting requests too quickly' + ' and your requests are being throttled."' + ), + ) + refillIn: int | None = Field( + default=None, + description=( + "Milliseconds till new tokens are " + "generated. Use this if your contingent " + "is depleted to wait before you try a new" + " request. Tokens are generated every 5 " + "minutes." + ), + ) + refillRate: int | None = Field( + default=None, + description=("Token refill rate per minute."), + ) + requestTime: int | None = Field( + default=None, + description=( + "total time the request took (local, " + "including latencies and connection " + "establishment), in milliseconds" + ), + ) + processingTimeInMs: int | None = Field( + default=None, + description=("time the request's processing took (remote), in milliseconds"), + ) + tokenFlowReduction: float | None = Field( + default=None, + description=("Token flow reduction"), + ) + tokensConsumed: int | None = Field( + default=None, + description=("Tokens used for call"), + ) + status: ResponseStatus | None = Field( + default=None, + description=("Status of the response."), + ) + statusCode: int | None = Field( + default=None, + description=("HTTP Status code of the response."), + ) + products: list[Product | None] | None = Field( + default=None, + description=("Results of the product request"), + ) + categories: dict[int, Category | None] | None = Field( + default=None, + description=("Results of the category lookup and search"), + ) + categoryParents: dict[int, Category | None] | None = Field( + default=None, + description=("Results of the category lookup and search includeParents parameter"), + ) + deals: DealResponse | None = Field( + default=None, + description=("Results of the deals request"), + ) + bestSellersList: BestSellers | None = Field( + default=None, + description=("Results of the best sellers request"), + ) + sellers: dict[str, Seller | None] | None = Field( + default=None, + description=("Results of the deals request"), + ) + trackings: list[Tracking | None] | None = Field( + default=None, + description=("Results of get and add tracking operations"), + ) + notifications: list[Notification | None] | None = Field( + default=None, + description=("Results of get and add tracking operations"), + ) + asinList: list[str | None] | None = Field( + default=None, + description=( + "A list of ASINs. Result of, but not limited to, the get tracking list operation" + ), + ) + totalResults: int | None = Field( + default=None, + description=("Estimated count of all matched products."), + ) + sellerIdList: list[str | None] | None = Field( + default=None, + description=("A list of sellerIds."), + ) + lightningDeals: list[LightningDeal | None] | None = Field( + default=None, + description=("A list of lightning deals."), + ) + error: RequestError | None = Field( + default=None, + description=("Contains information about any error that might have occurred."), + ) + additional: str | None = Field( + default=None, + description=("Contains request specific additional output."), + ) + + +class ReviewObject(KeepaBackendModel): + """Backend ``ReviewObject`` model generated from the Keepa Java schema.""" + + lastUpdate: int | None = Field( + default=None, + description=( + "The last time the information was " + "updated, in Keepa Time minutes.\n" + "\n" + "Use " + "KeepaTime.keepaMinuteToUnixInMillis(int)" + " (long) to get an uncompressed timestamp" + " (Unix epoch time)." + ), + ) + ratingCount: list[int | None] | None = Field( + default=None, + description=( + "Contains historical values of the " + "ratingCount field. null if it has no " + "value.\n" + "The most recent value is at the end of " + "the array. The first value is the " + "oldest.\n" + "Data points alternate in this pattern: [" + " keepaTime, ratingCount, ... ]\n" + "The ratingCount history has not been " + "updated since April 9th 2025 as that " + "data point was removed by Amazon." + ), + ) + reviewCount: list[int | None] | None = Field( + default=None, + description=( + "Contains historical values of the " + "reviewCount field. null if it has no " + "value.\n" + "The most recent value is at the end of " + "the array. The first value is the " + "oldest.\n" + "Data points alternate in this pattern: [" + " keepaTime, ratingCount, ... ]" + ), + ) + + +class Seller(KeepaBackendModel): + """Backend ``Seller`` model generated from the Keepa Java schema.""" + + trackedSince: int | None = Field( + default=None, + description=( + "States the time we have started tracking" + " this seller, in Keepa Time minutes.\n" + "\n" + "Use " + "KeepaTime.keepaMinuteToUnixInMillis(int)" + " (long) to get an uncompressed timestamp" + " (Unix epoch time)." + ), + ) + domainId: int | None = Field( + default=None, + description=("The domainId of the products Amazon locale\nAmazonLocale"), + ) + sellerId: str | None = Field( + default=None, + description=( + "The seller id of the merchant.\n\nExample: A2L77EE7U53NWQ (Amazon.com Warehouse Deals)" + ), + ) + sellerName: str | None = Field( + default=None, + description=("The name of seller.\n\nExample: Amazon Warehouse Deals"), + ) + csv: list[list[int | None] | None] | None = Field( + default=None, + description=( + "Two dimensional history array that " + "contains history data for this seller. " + "First dimension index:\n" + "\n" + "MerchantCsvType\n" + "\n" + "0 - RATING: The merchant's rating in " + "percent, Integer from 0 to 100.\n" + "1 - RATING_COUNT: The merchant's total " + "rating count, Integer." + ), + ) + lastUpdate: int | None = Field( + default=None, + description=( + "States the time of our last update of " + "this seller, in Keepa Time minutes.\n" + "\n" + "Use " + "KeepaTime.keepaMinuteToUnixInMillis(int)" + " (long) to get an uncompressed timestamp" + " (Unix epoch time)." + ), + ) + shipsFromChina: bool | None = Field( + default=None, + description=("Indicating whether or not this seller ships from China."), + ) + hasFBA: bool | None = Field( + default=None, + description=( + "Boolean value indicating whether or not " + "the seller currently has FBA listings.\n" + "\n" + "This value is usually correct, but could" + " be set to false even if the seller has " + "FBA listings, since we are not always " + "aware of all\n" + "\n" + "seller listings. This can especially be " + "the case with sellers with only a few " + "listings consisting of slow-selling " + "products." + ), + ) + totalStorefrontAsins: list[int | None] | None = Field( + default=None, + description=( + "Contains the number of storefront ASINs," + " based on our database, if available and" + " the last update of that metric.\n" + "\n" + "Is null if not available (no storefront " + "was ever retrieved). This field is " + "available in the\n" + "\n" + "default Request Seller Information " + "(storefront parameter is not required).\n" + "\n" + "Use " + "KeepaTime.keepaMinuteToUnixInMillis(int)" + " (long) to get an uncompressed timestamp" + " (Unix epoch time).\n" + "\n" + "Has the format: [ last update of the " + "storefront in Keepa Time minutes, the " + "count of storefront ASINs ]\n" + "\n" + "Example: [2711319, 1200]" + ), + ) + asinList: list[str | None] | None = Field( + default=None, + description=( + "Only available if the storefront " + "parameter was used and only updated if " + "the update parameter was utilized.\n" + "\n" + "String array containing up to 100,000 " + "storefront ASINs, sorted by freshest " + "first. The corresponding\n" + "\n" + "time stamps can be found in the " + "asinListLastSeen field.\n" + "\n" + 'Example: ["B00M0QVG3W", "B00M4KCH2A"]' + ), + ) + asinListLastSeen: list[int | None] | None = Field( + default=None, + description=( + "Only available if the storefront " + "parameter was used and only updated if " + "the update parameter was utilized.\n" + "\n" + "Contains the last time (in Keepa Time " + "minutes) we were able to verify each " + "ASIN in the asinList field.\n" + "\n" + "asinList and asinListLastSeen share the " + "same indexation, so the corresponding " + "time stamp\n" + "\n" + "for asinList[10] would be " + "asinListLastSeen[10].\n" + "\n" + "\n" + "Use " + "KeepaTime.keepaMinuteToUnixInMillis(int)" + " (long) to get an uncompressed timestamp" + " (Unix epoch time).\n" + "\n" + "\n" + "\n" + "Example: [2711319, 2711311]" + ), + ) + totalStorefrontAsinsCSV: list[int | None] | None = Field( + default=None, + description=( + "Only available if the storefront " + "parameter was used and only updated if " + "the update parameter was utilized.\n" + "\n" + "Contains the total amount of listings of" + " this seller. Includes historical data\n" + "\n" + "asinList and asinListLastSeen share the " + "same indexation, so the corresponding " + "time stamp\n" + "\n" + "for asinList[10] would be " + "asinListLastSeen[10]. Has the format: " + "Keepa Time minutes, count, ...\n" + "\n" + "\n" + "Use " + "KeepaTime.keepaMinuteToUnixInMillis(int)" + " (long) to get an uncompressed timestamp" + " (Unix epoch time).\n" + "\n" + "\n" + "\n" + "Example: [2711319, 1200, 2711719, 1187]" + ), + ) + sellerCategoryStatistics: list[MerchantCategoryStatistics | None] | None = Field( + default=None, + description=( + "Statistics about the primary categories " + "of this seller. Based on our often " + "incomplete and outdated product offers " + "data." + ), + ) + sellerBrandStatistics: list[MerchantBrandStatistics | None] | None = Field( + default=None, + description=( + "Statistics about the primary brands of " + "this seller. Based on our often " + "incomplete and outdated product offers " + "data." + ), + ) + avgBuyBoxCompetitors: float | None = Field( + default=None, + description=( + "Average number of sellers competing for " + "the Buy Box of this seller's products " + "(this seller included)." + ), + ) + buyBoxNewOwnershipRate: int | None = Field( + default=None, + description=("Average New Buy Box ownership percentage"), + ) + buyBoxUsedOwnershipRate: int | None = Field( + default=None, + description=("Average Used Buy Box ownership percentage"), + ) + competitors: list[Competitors | None] | None = Field( + default=None, + description=( + "The top five sellers most commonly offering the same products as this seller." + ), + ) + address: list[str | None] | None = Field( + default=None, + description=( + "The business address. Each entry of the " + "array contains one address line.\n" + "The last entry contains the 2 letter " + "country code. null if not available.\n" + "Example: [123 Main Street, New York, NY," + " 10001, US]" + ), + ) + recentFeedback: list[FeedbackObject | None] | None = Field( + default=None, + description=( + "Contains up to 5 of the most recent " + "customer feedbacks.\n" + "Each feedback object in the array " + "contains the following fields" + ), + ) + lastRatingUpdate: int | None = Field( + default=None, + description=( + "States the time of our last rating data " + "update of this seller, in Keepa Time " + "minutes.\n" + "\n" + "Use " + "KeepaTime.keepaMinuteToUnixInMillis(int)" + " (long) to get an uncompressed timestamp" + " (Unix epoch time)." + ), + ) + neutralRating: list[int | None] | None = Field( + default=None, + description=( + "Contains the neutral percentage ratings " + "for the last 30 days, 90 days, 365 days " + "and lifetime, in that order.\n" + "A neutral rating is a 3 star rating.\n" + "Example: [1, 1, 1, 2]" + ), + ) + negativeRating: list[int | None] | None = Field( + default=None, + description=( + "Contains the negative percentage ratings" + " for the last 30 days, 90 days, 365 days" + " and lifetime, in that order.\n" + "A negative rating is a 1 or 2 star " + "rating.\n" + "Example: [3, 1, 1, 3]" + ), + ) + positiveRating: list[int | None] | None = Field( + default=None, + description=( + "Contains the positive percentage ratings" + " for the last 30 days, 90 days, 365 days" + " and lifetime, in that order.\n" + "A positive rating is a 4 or 5 star " + "rating.\n" + "Example: [96, 98, 98, 95]" + ), + ) + ratingCount: list[int | None] | None = Field( + default=None, + description=( + "Contains the rating counts for the last " + "30 days, 90 days, 365 days and lifetime," + " in that order.\n" + "Example: [3, 10, 98, 321]" + ), + ) + customerServicesAddress: list[str | None] | None = Field( + default=None, + description=( + "The customer services address. Each " + "entry of the array contains one address " + "line.\n" + "The last entry contains the 2 letter " + "country code. null if not available.\n" + "Example: [123 Main Street, New York, NY," + " 10001, US]" + ), + ) + tradeNumber: str | None = Field( + default=None, + description=("The Trade Register Number. null if not available.\nExample: HRB 123 456"), + ) + businessName: str | None = Field( + default=None, + description=("The business name. null if not available.\nExample: Keepa GmbH"), + ) + vatID: str | None = Field( + default=None, + description=("The VAT number. null if not available.\nExample: DE123456789"), + ) + phoneNumber: str | None = Field( + default=None, + description=("The phone number. null if not available.\nExample: 800 1234 567"), + ) + businessType: str | None = Field( + default=None, + description=( + "The business type. null if not available.\nExample: Unternehmen in Privatbesitz" + ), + ) + shareCapital: str | None = Field( + default=None, + description=("The share capital. null if not available.\nExample: 25000"), + ) + representative: str | None = Field( + default=None, + description=( + "The name of the business representative." + " null if not available.\n" + "Example: Max Mustermann" + ), + ) + email: str | None = Field( + default=None, + description=( + "The email address of the business. null if not available.\nExample: info@keepa.com" + ), + ) + currentRating: int | None = None + currentRatingCount: int | None = None + ratingsLast30Days: int | None = None + + +class Stats(KeepaBackendModel): + """Backend ``Stats`` model generated from the Keepa Java schema.""" + + current: list[int | None] | None = Field( + default=None, + description=( + "Contains the prices / ranks of the " + "product of the time we last updated it.\n" + "\n" + "Uses Product.CsvType indexing.\n" + "\n" + "The price is an integer of the " + "respective Amazon locale's smallest " + "currency unit (e.g. euro cents or yen).\n" + "If no offer was available in the given " + "interval (e.g. out of stock) the price " + "has the value -1." + ), + ) + avg: list[int | None] | None = Field( + default=None, + description=( + "Contains the weighted mean for the " + "interval specified in the product " + "request's stats parameter.\n" + "\n" + "Uses Product.CsvType indexing.\n" + "\n" + "If no offer was available in the given " + "interval or there is insufficient data " + "it has the value -1." + ), + ) + avg30: list[int | None] | None = Field( + default=None, + description=( + "Contains the weighted mean for the last " + "30 days.\n" + "\n" + "Uses Product.CsvType indexing.\n" + "\n" + "If no offer was available in the given " + "interval or there is insufficient data " + "it has the value -1." + ), + ) + avg90: list[int | None] | None = Field( + default=None, + description=( + "Contains the weighted mean for the last " + "90 days.\n" + "\n" + "Uses Product.CsvType indexing.\n" + "\n" + "If no offer was available in the given " + "interval or there is insufficient data " + "it has the value -1." + ), + ) + avg180: list[int | None] | None = Field( + default=None, + description=( + "Contains the weighted mean for the last " + "180 days.\n" + "\n" + "Uses Product.CsvType indexing.\n" + "\n" + "If no offer was available in the given " + "interval or there is insufficient data " + "it has the value -1." + ), + ) + avg365: list[int | None] | None = Field( + default=None, + description=( + "Contains the weighted mean for the last " + "365 days.\n" + "\n" + "Uses Product.CsvType indexing.\n" + "\n" + "If no offer was available in the given " + "interval or there is insufficient data " + "it has the value -1." + ), + ) + atIntervalStart: list[int | None] | None = Field( + default=None, + description=( + "Contains the prices registered at the " + "start of the interval specified in the " + "product request's stats parameter.\n" + "\n" + "Uses Product.CsvType indexing.\n" + "\n" + "If no offer was available in the given " + "interval or there is insufficient data " + "it has the value -1." + ), + ) + min: list[list[int | None] | None] | None = Field( + default=None, + description=( + "Contains the lowest prices registered " + "for this product.\n" + "\n" + "First dimension uses Product.CsvType " + "indexing\n" + "\n" + "Second dimension is either null, if " + "there is no data available for the price" + " type, or\n" + "an array of the size 2 with the first " + "value being the time of the extreme " + "point (in Keepa time minutes) and the " + "second one the respective extreme value." + "\n" + "\n" + "Use " + "KeepaTime.keepaMinuteToUnixInMillis(int)" + " (long) to get an uncompressed timestamp" + " (Unix epoch time)." + ), + ) + minInInterval: list[list[int | None] | None] | None = Field( + default=None, + description=( + "Contains the lowest prices registered in" + " the interval specified in the product " + "request's stats parameter.\n" + "\n" + "First dimension uses Product.CsvType " + "indexing\n" + "\n" + "Second dimension is either null, if " + "there is no data available for the price" + " type, or\n" + "an array of the size 2 with the first " + "value being the time of the extreme " + "point (in Keepa time minutes) and the " + "second one the respective extreme value." + "\n" + "\n" + "Use " + "KeepaTime.keepaMinuteToUnixInMillis(int)" + " (long) to get an uncompressed timestamp" + " (Unix epoch time)." + ), + ) + max: list[list[int | None] | None] | None = Field( + default=None, + description=( + "Contains the highest prices registered " + "for this product.\n" + "\n" + "First dimension uses Product.CsvType " + "indexing\n" + "\n" + "Second dimension is either null, if " + "there is no data available for the price" + " type, or\n" + "an array of the size 2 with the first " + "value being the time of the extreme " + "point (in Keepa time minutes) and the " + "second one the respective extreme value." + "\n" + "\n" + "Use " + "KeepaTime.keepaMinuteToUnixInMillis(int)" + " (long) to get an uncompressed timestamp" + " (Unix epoch time)." + ), + ) + maxInInterval: list[list[int | None] | None] | None = Field( + default=None, + description=( + "Contains the highest prices registered " + "in the interval specified in the product" + " request's stats parameter.\n" + "\n" + "First dimension uses Product.CsvType " + "indexing\n" + "\n" + "Second dimension is either null, if " + "there is no data available for the price" + " type, or\n" + "an array of the size 2 with the first " + "value being the time of the extreme " + "point (in Keepa time minutes) and the " + "second one the respective extreme value." + "\n" + "\n" + "Use " + "KeepaTime.keepaMinuteToUnixInMillis(int)" + " (long) to get an uncompressed timestamp" + " (Unix epoch time)." + ), + ) + isLowest: list[bool | None] | None = Field( + default=None, + description=( + "Whether the current price is the " + "all-time lowest price.\n" + "\n" + "Uses Product.CsvType indexing" + ), + ) + isLowest90: list[bool | None] | None = Field( + default=None, + description=( + "Whether the current price is the lowest " + "price in the last 90 days.\n" + "\n" + "Uses Product.CsvType indexing" + ), + ) + outOfStockCountAmazon30: int | None = Field( + default=None, + description=("Number of times in the last 30 days Amazon went out of stock."), + ) + outOfStockCountAmazon90: int | None = Field( + default=None, + description=("Number of times in the last 90 days Amazon went out of stock."), + ) + deltaPercent90_monthlySold: int | None = Field( + default=None, + description=( + "Contains the difference in percent " + "between the current monthlySold value " + "and the average value of the last 90 " + "days.\n" + "The value 0 means it did not change or " + "could not be calculated." + ), + ) + outOfStockPercentageInInterval: list[int | None] | None = Field( + default=None, + description=( + "Contains the out of stock percentage in " + "the interval specified in the product " + "request's stats parameter.\n" + "\n" + "Uses Product.CsvType indexing.\n" + "\n" + "It has the value -1 if there is " + "insufficient data or the CsvType is not " + "a price.\n" + "\n" + "Examples: 0 = never was out of stock, " + "100 = was out of stock 100% of the time," + " 25 = was out of stock 25% of the time" + ), + ) + outOfStockPercentage90: list[int | None] | None = Field( + default=None, + description=( + "Contains the 90 day out of stock " + "percentage\n" + "\n" + "Uses Product.CsvType indexing.\n" + "\n" + "It has the value -1 if there is " + "insufficient data or the CsvType is not " + "a price.\n" + "\n" + "Examples: 0 = never was out of stock, " + "100 = was out of stock 100% of the time," + " 25 = was out of stock 25% of the time" + ), + ) + outOfStockPercentage30: list[int | None] | None = Field( + default=None, + description=( + "Contains the 30 day out of stock " + "percentage\n" + "\n" + "Uses Product.CsvType indexing.\n" + "\n" + "It has the value -1 if there is " + "insufficient data or the CsvType is not " + "a price.\n" + "\n" + "Examples: 0 = never was out of stock, " + "100 = was out of stock 100% of the time," + " 25 = was out of stock 25% of the time" + ), + ) + lightningDealInfo: list[int | None] | None = Field( + default=None, + description=( + "Can be used to identify past, upcoming " + "and current lightning deal offers.\n" + "\n" + "Has the format [startDate, endDate] (if " + "not null, always array length 2). *null*" + " if the product never had a lightning " + "deal. Both timestamps are in UTC and " + "Keepa time minutes.\n" + "\n" + "If there is a upcoming lightning deal, " + "only startDate is be set (endDate has " + "value -1)\n" + "\n" + "If there is a current lightning deal, " + "both startDate and endDate will be set. " + "startDate will be older than the current" + " time, but endDate will be a future " + "date.\n" + "\n" + "If there is only a past deal, both " + "startDate and endDate will be set in the" + " past.\n" + "\n" + "Use " + "KeepaTime.keepaMinuteToUnixInMillis(int)" + " (long) to get an uncompressed timestamp" + " (Unix epoch time)." + ), + ) + totalOfferCount: int | None = Field( + default=None, + description=( + "the total count of offers this product " + "has (all conditions combined). The offer" + " count per condition can be found in the" + " current field." + ), + ) + lastOffersUpdate: int | None = Field( + default=None, + description=( + "the last time the offers information was" + " updated. Use " + "KeepaTime.keepaMinuteToUnixInMillis(int)" + " (long) to get an uncompressed timestamp" + " (Unix epoch time)." + ), + ) + stockPerCondition3rdFBA: list[int | None] | None = Field( + default=None, + description=( + "Contains the total stock available per " + "item condition (of the retrieved offers)" + " for 3rd party FBA\n" + "(fulfillment by Amazon, Warehouse Deals " + "included) or FBM (fulfillment by " + "merchant) offers. Uses the " + "Offer.OfferCondition indexing." + ), + ) + stockPerConditionFBM: list[int | None] | None = Field( + default=None, + description=( + "Contains the total stock available per " + "item condition (of the retrieved offers)" + " for 3rd party FBM\n" + "(fulfillment by Amazon, Warehouse Deals " + "included) or FBM (fulfillment by " + "merchant) offers. Uses the " + "Offer.OfferCondition indexing." + ), + ) + stockAmazon: int | None = Field( + default=None, + description=( + "Only set when the offers parameter was " + "used. The stock of Amazon, if Amazon has" + " an offer. Max. reported stock is 10. " + "Otherwise -2." + ), + ) + stockBuyBox: int | None = Field( + default=None, + description=( + "Only set when the offers parameter was " + "used. The stock of buy box offer. Max. " + "reported stock is 10. If the buy box is " + "empty/unqualified: -2." + ), + ) + retrievedOfferCount: int | None = Field( + default=None, + description=( + "Only set when the offers parameter was " + "used. The count of actually retrieved " + "offers for this request." + ), + ) + buyBoxPrice: int | None = Field( + default=None, + description=( + "Only set when the offers parameter was " + "used. The buy box price, if existent. " + "Otherwise -2." + ), + ) + buyBoxShipping: int | None = Field( + default=None, + description=( + "Only set when the offers parameter was " + "used. The buy box shipping cost, if " + "existent. Otherwise -2." + ), + ) + buyBoxIsUnqualified: bool | None = Field( + default=None, + description=( + "Only set when the offers parameter was " + "used. Whether or not a seller won the " + "buy box. If there are only sellers with " + "bad offers none qualifies for the buy " + "box." + ), + ) + buyBoxIsShippable: bool | None = Field( + default=None, + description=( + "Only set when the offers parameter was " + "used. Whether or not the buy box is " + "listed as being shippable." + ), + ) + buyBoxIsPreorder: bool | None = Field( + default=None, + description=("Only set when the offers parameter was used. If the buy box is a pre-order."), + ) + buyBoxIsFBA: bool | None = Field( + default=None, + description=( + "Only set when the offers parameter was " + "used. Whether or not the buy box is " + "fulfilled by Amazon." + ), + ) + lastBuyBoxUpdate: int | None = Field( + default=None, + description=( + "The last time the Buy Box price was " + "updated. Use " + "KeepaTime.keepaMinuteToUnixInMillis(int)" + " (long) to get an uncompressed timestamp" + " (Unix epoch time)." + ), + ) + buyBoxIsUsed: bool | None = Field( + default=None, + description=( + "Only set when the offers parameter was " + "used. Whether or not there is a used buy" + " box offer." + ), + ) + buyBoxIsBackorder: bool | None = Field( + default=None, + description=("Whether the buy box offer is back-ordered"), + ) + buyBoxIsAmazon: bool | None = Field( + default=None, + description=( + "Only set when the offers parameter was used. If Amazon is the seller in the buy box." + ), + ) + buyBoxIsMAP: bool | None = Field( + default=None, + description=( + "Only set when the offers parameter was " + "used. If the buy box price is hidden on " + "Amazon due to MAP restrictions (minimum " + "advertised price)." + ), + ) + buyBoxMinOrderQuantity: int | None = Field( + default=None, + description=( + "The minimum order quantity of the buy box. -1 if not available, 0 if no limit exists." + ), + ) + buyBoxMaxOrderQuantity: int | None = Field( + default=None, + description=( + "The maximum order quantity of the buy box. -1 if not available, 0 if no limit exists." + ), + ) + buyBoxAvailabilityMessage: str | None = Field( + default=None, + description=( + "The availability message of the buy box." + " null if not available.\n" + "Example: \u201cIn Stock.\u201d" + ), + ) + buyBoxSellerId: str | None = Field( + default=None, + description=('The seller id of the buy box offer, if existent. Otherwise "-2" or null'), + ) + buyBoxShippingCountry: str | None = Field( + default=None, + description=( + "The default shipping country of the buy " + "box seller. null if not available. " + "Example: \u201cUS\u201d" + ), + ) + buyBoxIsPrimeExclusive: bool | None = Field( + default=None, + description=("If the buy box is Prime exclusive. null if not available."), + ) + buyBoxIsFreeShippingEligible: bool | None = Field( + default=None, + description=("If the buy box is free shipping eligible. null if not available."), + ) + buyBoxIsPrimeEligible: bool | None = Field( + default=None, + description=("If the buy box is Prime eligible. null if not available."), + ) + buyBoxIsPrimePantry: bool | None = Field( + default=None, + description=("If the buy box is a Prime Pantry offer. null if not available."), + ) + buyBoxStats: dict[str, BuyBoxStatsObject | None] | None = Field( + default=None, + description=( + "A map containing buy box statistics for " + "the interval specified. Each key " + "represents the sellerId of the buy box " + "seller and each object a buy box " + "statistics object." + ), + ) + buyBoxSavingBasis: int | None = Field( + default=None, + description=( + "The buy box saving basis price (strikethrough, typical price). null if unavailable." + ), + ) + buyBoxSavingBasisType: str | None = Field( + default=None, + description=( + "The buy box new strikethrough price\u2019s " + "reference type (either LISTPRICE or " + "WASPRICE) if it exists. null if " + "unavailable." + ), + ) + buyBoxSavingPercentage: int | None = Field( + default=None, + description=( + "The buy box new price\u2019s stated " + "percentage discount, if it exists. null " + "if unavailable." + ), + ) + buyBoxUsedPrice: int | None = Field( + default=None, + description=( + "Only set when the offers parameter was " + "used. Price of the used buy box, if " + 'existent. Otherwise "-1" or null' + ), + ) + buyBoxUsedShipping: int | None = Field( + default=None, + description=( + "Only set when the offers parameter was " + "used. Shipping cost of the used buy box," + ' if existent. Otherwise "-1" or null' + ), + ) + buyBoxUsedSellerId: str | None = Field( + default=None, + description=( + "Only set when the offers parameter was " + "used. Seller id of the used buy box, if " + "existent. Otherwise null." + ), + ) + buyBoxUsedIsFBA: bool | None = Field( + default=None, + description=( + "Only set when the offers parameter was " + "used. Whether or not the used buy box is" + " fulfilled by Amazon." + ), + ) + buyBoxUsedCondition: int | None = Field( + default=None, + description=( + "Only set when the offers parameter was " + "used. The used sub type condition of the" + " used buy box offer\n" + "\n" + "The Offer.OfferCondition condition of " + "the offered product. Integer value:\n" + "\n" + "2 - Used - Like New\n" + "\n" + "3 - Used - Very Good\n" + "\n" + "4 - Used - Good\n" + "\n" + "5 - Used - Acceptable\n" + "\n" + "Note: Open Box conditions will be coded " + "as Used conditions." + ), + ) + buyBoxUsedStats: dict[str, BuyBoxStatsObject | None] | None = Field( + default=None, + description=( + "A map containing used buy box statistics" + " for the interval specified. Each key " + "represents the sellerId of the used buy " + "box seller and each object a buy box " + "statistics object." + ), + ) + isAddonItem: bool | None = Field( + default=None, + description=( + "Only set when the offers parameter was " + "used. If the product is an add-on item " + "(add-on Items ship with orders that " + "include $25 or more of items shipped by " + "Amazon)." + ), + ) + sellerIdsLowestFBA: list[str | None] | None = Field( + default=None, + description=( + "Only set when the offers parameter was " + "used. Contains the seller ids (if any) " + "of the lowest priced live FBA offer(s). " + "Multiple entries if multiple offers " + "share the same price." + ), + ) + sellerIdsLowestFBM: list[str | None] | None = Field( + default=None, + description=( + "Only set when the offers parameter was " + "used. Contains the seller ids (if any) " + "of the lowest priced live FBM offer(s). " + "Multiple entries if multiple offers " + "share the same price." + ), + ) + offerCountFBA: int | None = Field( + default=None, + description=( + "Only set when the offers parameter was used. Count of retrieved live FBA offers." + ), + ) + offerCountFBM: int | None = Field( + default=None, + description=( + "Only set when the offers parameter was used. Count of retrieved live FBM offers." + ), + ) + salesRankDrops30: int | None = Field( + default=None, + description=( + "The count of sales rank drops (from high" + " value to low value) within the last 30 " + "days which are considered to indicate " + "sales." + ), + ) + salesRankDrops90: int | None = Field( + default=None, + description=( + "The count of sales rank drops (from high" + " value to low value) within the last 90 " + "days which are considered to indicate " + "sales." + ), + ) + salesRankDrops180: int | None = Field( + default=None, + description=( + "The count of sales rank drops (from high" + " value to low value) within the last 180" + " days which are considered to indicate " + "sales." + ), + ) + salesRankDrops365: int | None = Field( + default=None, + description=( + "The count of sales rank drops (from high" + " value to low value) within the last 365" + " days which are considered to indicate " + "sales." + ), + ) + + +class Tracking(KeepaBackendModel): + """Backend ``Tracking`` model generated from the Keepa Java schema.""" + + asin: str | None = Field( + default=None, + description=("The tracked product ASIN"), + ) + createDate: int | None = Field( + default=None, + description=("Creation date of the tracking in KeepaTime minutes"), + ) + ttl: int | None = Field( + default=None, + description=( + "The time to live in hours until the " + "tracking expires and is deleted. When " + "setting the value through the Add " + "Tracking request it is in relation to " + "the time of request,\n" + "when retrieving the tracking object it " + "is relative to the createDate. Possible " + "values:\n" + "\n" + "any positive integer: time to live in " + "hours\n" + "\n" + "0: never expires\n" + "\n" + "any negative integer (only when setting " + "the value):\n" + "\n" + "tracking already exists: keep the " + "original ttl\n" + "tracking is new: use the absolute value " + "as ttl" + ), + ) + expireNotify: bool | None = Field( + default=None, + description=( + "Trigger a notification if tracking " + "expires or is removed by the system " + "(e.g. product deprecated)" + ), + ) + mainDomainId: int | None = Field( + default=None, + description=( + "The main Amazon locale of this tracking " + "determines the currency used for all " + "desired prices.\n" + "\n" + "Integer value for the Amazon locale " + "AmazonLocale" + ), + ) + thresholdValues: list[TrackingThresholdValue | None] | None = Field( + default=None, + description=("Contains all settings for price or value related tracking criteria"), + ) + notifyIf: list[TrackingNotifyIf | None] | None = Field( + default=None, + description=("Contains specific, meta tracking criteria, like out of stock."), + ) + notificationType: list[bool | None] | None = Field( + default=None, + description=( + "Determines through which channels we " + "will send notifications.\n" + "Must be a boolean array with the length " + "of the NotificationType enum.\n" + "Uses NotificationType indexing " + "NotificationType. True means the channel" + " will be used.\n" + "\n" + "Our Tracking API currently only supports" + " notifications through push webhooks or " + "API pull request. Other channels will " + "follow soon.\n" + "\n" + "Example: Only notify via API: [ false, " + "false, false, false, false, true, false " + "]\n" + "\n" + "\n" + "boolean[] notificationType = new boolean" + "[Tracking.NotificationType.values.length" + "];\n" + "\n" + "notificationType[Tracking.NotificationTy" + "pe.API.ordinal()] = true;" + ), + ) + notificationCSV: list[int | None] | None = Field( + default=None, + description=( + "A history of past notifications of this " + "tracking. Each past notification " + "consists of 5 entries, in the format:\n" + "\n" + "[AmazonLocale, Product.CsvType, " + "NotificationType, " + "TrackingNotificationCause, KeepaTime]" + ), + ) + individualNotificationInterval: int | None = Field( + default=None, + description=( + "A tracking specific rearm timer.\n" + "\n" + "-1 = use default notification timer of " + "the user account (changeable via the " + "website settings)\n" + "0 = never notify a desired price more " + "than once\n" + "larger than 0 = rearm the desired price " + "after x minutes." + ), + ) + isActive: bool | None = Field( + default=None, + description=( + "Whether or not the tracking is active. A" + " tracking is automatically deactivated " + "if the corresponding API account is no " + "longer sufficiently paid for." + ), + ) + updateInterval: int | None = Field( + default=None, + description=( + "The update interval, in hours. " + "Determines how often our system will " + "trigger a product update. A setting of 1" + " hour will not trigger an update exactly" + " every 60 minutes, but as close to that " + "as it is efficient for our system. " + "Throughout a day it will be updated 24 " + "times, but the updates are not perfectly" + " distributed." + ), + ) + metaData: str | None = Field( + default=None, + description=( + "The meta data of this tracking (max " + "length is 500 characters). Used to " + "assign some text to this tracking, like " + "a user reference or a memo." + ), + ) + + +class TrackingNotifyIf(KeepaBackendModel): + """Backend ``TrackingNotifyIf`` model generated from the Keepa Java schema.""" + + domain: int | None = Field( + default=None, + description=("Integer value of the AmazonLocale for this NotifyIf"), + ) + csvType: int | None = Field( + default=None, + description=("The Product.CsvType for this threshold value"), + ) + notifyIfType: int | None = Field( + default=None, + description=("The NotifyIfType"), + ) + + +class TrackingRequest(KeepaBackendModel): + """Backend ``TrackingRequest`` model generated from the Keepa Java schema.""" + + asin: str | None = Field( + default=None, + description=("The product ASIN to track"), + ) + ttl: int | None = Field( + default=None, + description=( + "The time to live in hours until the " + "tracking expires and is deleted.\n" + "When setting the value through the Add " + "Tracking request it is in relation to " + "the time of request. Possible values:\n" + "\n" + "any positive integer: time to live in " + "hours\n" + "\n" + "0: never expires\n" + "\n" + "any negative integer:\n" + "\n" + "tracking already exists: keep the " + "original ttl\n" + "tracking is new: use the absolute value " + "as ttl" + ), + ) + expireNotify: bool | None = Field( + default=None, + description=( + "Trigger a notification if tracking " + "expires or is removed by the system " + "(e.g. product deprecated)" + ), + ) + desiredPricesInMainCurrency: bool | None = Field( + default=None, + description=( + "Whether or not all desired prices are in" + " the currency of the mainDomainId. If " + "false they will be converted." + ), + ) + mainDomainId: int | None = Field( + default=None, + description=( + "The main Amazon locale of this tracking " + "determines the currency used for all " + "desired prices.\n" + "\n" + "Integer value for the Amazon locale " + "AmazonLocale" + ), + ) + thresholdValues: list[TrackingThresholdValue | None] | None = Field( + default=None, + description=("Contains all settings for price or value related tracking criteria"), + ) + notifyIf: list[TrackingNotifyIf | None] | None = Field( + default=None, + description=("Contains specific, meta tracking criteria, like out of stock."), + ) + notificationType: list[bool | None] | None = Field( + default=None, + description=( + "Determines through which channels we " + "will send notifications.\n" + "\n" + "Uses NotificationType indexing " + "Tracking.NotificationType. True means " + "the channel will be used." + ), + ) + individualNotificationInterval: int | None = Field( + default=None, + description=( + "A tracking specific rearm timer.\n" + "\n" + "-1 = use default notification timer of " + "the user account (changeable via the " + "website settings)\n" + "0 = never notify a desired price more " + "than once\n" + "larger than 0 = rearm the desired price " + "after x minutes." + ), + ) + updateInterval: int | None = Field( + default=None, + description=( + "The update interval, in hours. " + "Determines how often our system will " + "trigger a product update. A setting of 1" + "\n" + "hour will not trigger an update exactly " + "every 60 minutes, but as close to that " + "as it is efficient for our system.\n" + "Throughout a day it will be updated 24 " + "times, but the updates are not perfectly" + " distributed.\n" + "\n" + "Possible values: Any integer between 0 " + "and 25. Default is 1." + ), + ) + metaData: str | None = Field( + default=None, + description=( + "Meta data of this tracking (max length " + "is 500 characters). You can use this to " + "store any string with this tracking." + ), + ) + + +class TrackingThresholdValue(KeepaBackendModel): + """Backend ``TrackingThresholdValue`` model generated from the Keepa Java schema.""" + + thresholdValueCSV: list[int | None] | None = Field( + default=None, + description=( + "The history of threshold values (or " + "desired prices). Only for existing " + "tracking!\n" + "\n" + "Format: [KeepaTime, value]" + ), + ) + thresholdValue: int | None = Field( + default=None, + description=("The threshold value (or desired price). Only for creating a tracking!"), + ) + domain: int | None = Field( + default=None, + description=( + "Integer value of the AmazonLocale this " + "threshold value belongs to. Regardless " + "of the locale, the threshold value is " + "always in the currency of the " + "mainDomainId." + ), + ) + csvType: int | None = Field( + default=None, + description=("Integer value of the Product.CsvType for this threshold value"), + ) + isDrop: bool | None = Field( + default=None, + description=( + "Whether or not this tracking threshold " + "value tracks value drops (true) or value" + " increases (false)" + ), + ) + minDeltaAbsolute: int | None = Field( + default=None, + description=("not yet available."), + ) + minDeltaPercentage: int | None = Field( + default=None, + description=("not yet available."), + ) + deltasAreBetweenNotifications: bool | None = Field( + default=None, + description=("not yet available."), + ) + + +class UnitCountObject(KeepaBackendModel): + """Backend ``UnitCountObject`` model generated from the Keepa Java schema.""" + + unitValue: float | None = None + unitType: str | None = None + eachUnitCount: float | None = None + + +class VariationAttributeObject(KeepaBackendModel): + """Backend ``VariationAttributeObject`` model generated from the Keepa Java schema.""" + + dimension: str | None = Field( + default=None, + description=("dimension type, e.g. Color"), + ) + value: str | None = Field( + default=None, + description=("dimension value, e.g. Red"), + ) + + +class VariationObject(KeepaBackendModel): + """Backend ``VariationObject`` model generated from the Keepa Java schema.""" + + asin: str | None = Field( + default=None, + description=("Variation ASIN"), + ) + attributes: list[VariationAttributeObject | None] | None = Field( + default=None, + description=("This variation ASIN's dimension attributes"), + ) + image: str | None = Field( + default=None, + description=("This variation ASIN's swatch image"), + ) + + +class Video(KeepaBackendModel): + """Backend ``Video`` model generated from the Keepa Java schema.""" + + title: str | None = None + image: str | None = Field( + default=None, + description=("Full Amazon image path:\n\nhttps://m.media-amazon.com/images/I/image"), + ) + duration: int | None = Field( + default=None, + description=("in seconds"), + ) + creator: VideoCreatorType | None = None + name: str | None = None + url: str | None = None + + +__all__ = [ + "BACKEND_COMMIT", + "KeepaBackendModel", + "AmazonLocale", + "AvailabilityType", + "CsvType", + "DealInterval", + "DealState", + "MerchantCsvType", + "NotificationType", + "NotifyIfType", + "OfferCondition", + "ProductType", + "PromotionType", + "ResponseStatus", + "TrackingNotificationCause", + "VideoCreatorType", + "APlus", + "APlusModule", + "BestSellers", + "BuyBoxStatsObject", + "Category", + "CategoryTreeEntry", + "Competitors", + "Deal", + "DealDetails", + "DealRequest", + "DealResponse", + "FBAFeesObject", + "FeedbackObject", + "Format", + "HazardousMaterial", + "Image", + "LightningDeal", + "MerchantBrandStatistics", + "MerchantCategoryStatistics", + "Notification", + "Offer", + "OfferDuplicate", + "Product", + "ProductFinderRequest", + "PromotionObject", + "Request", + "RequestError", + "Response", + "ReviewObject", + "Seller", + "Stats", + "Tracking", + "TrackingNotifyIf", + "TrackingRequest", + "TrackingThresholdValue", + "UnitCountObject", + "VariationAttributeObject", + "VariationObject", + "Video", +] + +_MODELS: list[type[KeepaBackendModel]] = [ + APlus, + APlusModule, + BestSellers, + BuyBoxStatsObject, + Category, + CategoryTreeEntry, + Competitors, + Deal, + DealDetails, + DealRequest, + DealResponse, + FBAFeesObject, + FeedbackObject, + Format, + HazardousMaterial, + Image, + LightningDeal, + MerchantBrandStatistics, + MerchantCategoryStatistics, + Notification, + Offer, + OfferDuplicate, + Product, + ProductFinderRequest, + PromotionObject, + Request, + RequestError, + Response, + ReviewObject, + Seller, + Stats, + Tracking, + TrackingNotifyIf, + TrackingRequest, + TrackingThresholdValue, + UnitCountObject, + VariationAttributeObject, + VariationObject, + Video, +] +for _model in _MODELS: + _model.model_rebuild() diff --git a/tests/test_async_interface.py b/tests/test_async_interface.py index 062da94..15258d5 100644 --- a/tests/test_async_interface.py +++ b/tests/test_async_interface.py @@ -2,10 +2,10 @@ Test the asynchronous interface to the keepa backend. """ -from pathlib import Path import datetime import os import warnings +from pathlib import Path import numpy as np import pandas as pd @@ -101,13 +101,6 @@ async def test_update_status(api: keepa.AsyncKeepa) -> None: assert api.status.tokensLeft -@pytest.mark.asyncio -async def test_update_status(api: keepa.AsyncKeepa) -> None: - assert api.status.tokensLeft is None - await api.update_status() - assert api.status.tokensLeft - - @pytest.mark.asyncio async def test_wait_for_tokens(api: keepa.AsyncKeepa) -> None: assert api.status.tokensLeft is None @@ -161,7 +154,7 @@ async def test_product_finder_query(api: keepa.AsyncKeepa) -> None: perPage=50, categories_exclude=["1055398"], ) - asins = api.product_finder(product_parms) + asins = await api.product_finder(product_parms) assert asins diff --git a/tests/test_backend_models.py b/tests/test_backend_models.py new file mode 100644 index 0000000..dc30767 --- /dev/null +++ b/tests/test_backend_models.py @@ -0,0 +1,360 @@ +"""Tests for optional Pydantic backend model responses.""" + +import inspect +import json +from pathlib import Path +from typing import Any + +import pytest + +import keepa +from keepa import backend_models + +PROJECT_ROOT = Path(__file__).resolve().parents[1] + + +def _ready_api() -> keepa.Keepa: + api = keepa.Keepa("x" * 64, check_key=False) + api.tokens_left = 100 + api.status.refillRate = 100 + api.status.refillIn = 0 + return api + + +def test_query_extra_params_default_is_not_mutable() -> None: + assert inspect.signature(keepa.Keepa.query).parameters["extra_params"].default is None + + +def test_backend_aliases_round_trip() -> None: + image = backend_models.Image.model_validate({"l": "https://example.com/image.jpg"}) + + assert image.l_ == "https://example.com/image.jpg" + assert image.model_dump(exclude_none=True, by_alias=True) == { + "l": "https://example.com/image.jpg" + } + + +def test_generated_backend_api_docs_are_complete() -> None: + reference = (PROJECT_ROOT / "docs/source/backend_model_reference.rst").read_text() + + for export_name in backend_models.__all__: + assert f"keepa.models.backend.{export_name}" in reference + + +@pytest.mark.parametrize( + ("client", "reference_name"), + [(keepa.Keepa, "sync_client.rst"), (keepa.AsyncKeepa, "async_client.rst")], +) +def test_public_client_api_docs_are_complete(client: type, reference_name: str) -> None: + reference = (PROJECT_ROOT / "docs/source" / reference_name).read_text() + public_members = { + name + for name, member in inspect.getmembers(client) + if not name.startswith("_") + and (inspect.isfunction(member) or inspect.ismethod(member) or isinstance(member, property)) + } + + for member_name in public_members: + assert f"{client.__name__}.{member_name}" in reference + + +def test_query_typed_response(monkeypatch: pytest.MonkeyPatch) -> None: + api = _ready_api() + + def fake_request(request_type: str, payload: dict[str, Any], **kwargs: Any) -> dict[str, Any]: + assert request_type == "product" + return { + "products": [ + { + "asin": "B000000000", + "domainId": 1, + "csv": None, + "unknownFutureField": "kept", + } + ] + } + + monkeypatch.setattr(api, "_request", fake_request) + + products = api.query("B000000000", history=False, progress_bar=False, typed=True) + + assert isinstance(products[0], backend_models.Product) + assert products[0].asin == "B000000000" + assert products[0].unknownFutureField == "kept" + + +def test_query_default_response_remains_dict(monkeypatch: pytest.MonkeyPatch) -> None: + api = _ready_api() + + def fake_request(request_type: str, payload: dict[str, Any], **kwargs: Any) -> dict[str, Any]: + return {"products": [{"asin": "B000000000", "domainId": 1, "csv": None}]} + + monkeypatch.setattr(api, "_request", fake_request) + + products = api.query("B000000000", history=False, progress_bar=False) + + assert isinstance(products[0], dict) + + +def test_raw_query_cannot_be_typed() -> None: + api = _ready_api() + + with pytest.raises(ValueError, match="typed=True"): + api.query("B000000000", raw=True, typed=True, progress_bar=False) + + +def test_category_lookup_typed_response(monkeypatch: pytest.MonkeyPatch) -> None: + api = _ready_api() + + def fake_request(request_type: str, payload: dict[str, Any], **kwargs: Any) -> dict[str, Any]: + return {"categories": {"1": {"catId": 1, "name": "Root"}}} + + monkeypatch.setattr(api, "_request", fake_request) + + categories = api.category_lookup(0, typed=True) + + assert isinstance(categories["1"], backend_models.Category) + assert categories["1"].name == "Root" + + +def test_category_search_typed_response(monkeypatch: pytest.MonkeyPatch) -> None: + api = _ready_api() + + def fake_request(request_type: str, payload: dict[str, Any], **kwargs: Any) -> dict[str, Any]: + return {"categories": {"1": {"catId": 1, "name": "Root"}}} + + monkeypatch.setattr(api, "_request", fake_request) + + categories = api.search_for_categories("root", typed=True) + + assert isinstance(categories["1"], backend_models.Category) + + +def test_seller_query_typed_response(monkeypatch: pytest.MonkeyPatch) -> None: + api = _ready_api() + + def fake_request(request_type: str, payload: dict[str, Any], **kwargs: Any) -> dict[str, Any]: + return { + "sellers": { + "A2L77EE7U53NWQ": { + "sellerId": "A2L77EE7U53NWQ", + "sellerName": "Amazon Warehouse", + "trackedSince": 1, + } + } + } + + monkeypatch.setattr(api, "_request", fake_request) + + sellers = api.seller_query("A2L77EE7U53NWQ", typed=True) + + assert isinstance(sellers["A2L77EE7U53NWQ"], backend_models.Seller) + assert sellers["A2L77EE7U53NWQ"].trackedSince == 1 + + +def test_deals_typed_response(monkeypatch: pytest.MonkeyPatch) -> None: + api = _ready_api() + + def fake_request(request_type: str, payload: dict[str, Any], **kwargs: Any) -> dict[str, Any]: + return {"deals": {"dr": [{"asin": "B000000000"}], "categoryIds": [1]}} + + monkeypatch.setattr(api, "_request", fake_request) + + deals = api.deals({"page": 0, "domainId": 1}, typed=True) + + assert isinstance(deals, backend_models.DealResponse) + assert isinstance(deals.dr[0], backend_models.Deal) + assert deals.dr[0].asin == "B000000000" + + +def test_deals_accepts_generated_request_model(monkeypatch: pytest.MonkeyPatch) -> None: + api = _ready_api() + + def fake_request(request_type: str, payload: dict[str, Any], **kwargs: Any) -> dict[str, Any]: + selection = json.loads(payload["selection"]) + assert selection["page"] == 0 + assert selection["includeCategories"] == [123] + return {"deals": {"dr": [{"asin": "B000000000"}]}} + + monkeypatch.setattr(api, "_request", fake_request) + + deals = api.deals(backend_models.DealRequest(page=0, includeCategories=[123])) + + assert deals["dr"][0]["asin"] == "B000000000" + + +def test_product_finder_accepts_generated_request_model( + monkeypatch: pytest.MonkeyPatch, +) -> None: + api = _ready_api() + + def fake_request(request_type: str, payload: dict[str, Any], **kwargs: Any) -> dict[str, Any]: + selection = json.loads(payload["selection"]) + assert selection["author"] == ["jim butcher"] + assert selection["perPage"] == 50 + return {"asinList": ["B000HRMAR2"]} + + monkeypatch.setattr(api, "_request", fake_request) + + asins = api.product_finder(backend_models.ProductFinderRequest(author=["jim butcher"])) + + assert asins == ["B000HRMAR2"] + + +def test_best_sellers_typed_response(monkeypatch: pytest.MonkeyPatch) -> None: + api = _ready_api() + + def fake_request(request_type: str, payload: dict[str, Any], **kwargs: Any) -> dict[str, Any]: + assert request_type == "bestsellers" + return { + "bestSellersList": { + "domainId": 1, + "categoryId": 123, + "asinList": ["B000000000"], + } + } + + monkeypatch.setattr(api, "_request", fake_request) + + best_sellers = api.best_sellers_query("123", typed=True) + + assert isinstance(best_sellers, backend_models.BestSellers) + assert best_sellers.categoryId == 123 + assert best_sellers.asinList == ["B000000000"] + + +def test_best_sellers_default_response_remains_asin_list( + monkeypatch: pytest.MonkeyPatch, +) -> None: + api = _ready_api() + + def fake_request(request_type: str, payload: dict[str, Any], **kwargs: Any) -> dict[str, Any]: + return {"bestSellersList": {"asinList": ["B000000000"]}} + + monkeypatch.setattr(api, "_request", fake_request) + + assert api.best_sellers_query("123") == ["B000000000"] + + +@pytest.mark.asyncio +async def test_async_query_typed_response(monkeypatch: pytest.MonkeyPatch) -> None: + api = await keepa.AsyncKeepa.create("x" * 64) + api.tokens_left = 100 + api.status.refillRate = 100 + api.status.refillIn = 0 + + async def fake_product_query(*args: Any, **kwargs: Any) -> dict[str, Any]: + return {"products": [{"asin": "B000000000", "domainId": 1, "csv": None}]} + + monkeypatch.setattr(api, "_product_query", fake_product_query) + + products = await api.query("B000000000", history=False, progress_bar=False, typed=True) + + assert isinstance(products[0], backend_models.Product) + assert products[0].asin == "B000000000" + + +@pytest.mark.asyncio +async def test_async_category_endpoints_typed_response( + monkeypatch: pytest.MonkeyPatch, +) -> None: + api = await keepa.AsyncKeepa.create("x" * 64) + + async def fake_request( + request_type: str, payload: dict[str, Any], **kwargs: Any + ) -> dict[str, Any]: + if request_type == "category": + assert payload["parents"] == 0 + return {"categories": {"1": {"catId": 1, "name": "Root"}}} + + monkeypatch.setattr(api, "_request", fake_request) + + search = await api.search_for_categories("root", typed=True) + lookup = await api.category_lookup(0, typed=True) + + assert isinstance(search["1"], backend_models.Category) + assert isinstance(lookup["1"], backend_models.Category) + + +@pytest.mark.asyncio +async def test_async_seller_query_typed_response(monkeypatch: pytest.MonkeyPatch) -> None: + api = await keepa.AsyncKeepa.create("x" * 64) + + async def fake_request(*args: Any, **kwargs: Any) -> dict[str, Any]: + return {"sellers": {"SELLER": {"sellerId": "SELLER", "trackedSince": 1}}} + + monkeypatch.setattr(api, "_request", fake_request) + + sellers = await api.seller_query("SELLER", typed=True) + + assert isinstance(sellers["SELLER"], backend_models.Seller) + + +@pytest.mark.asyncio +async def test_async_deals_typed_response(monkeypatch: pytest.MonkeyPatch) -> None: + api = await keepa.AsyncKeepa.create("x" * 64) + + async def fake_request(*args: Any, **kwargs: Any) -> dict[str, Any]: + return {"deals": {"dr": [{"asin": "B000000000"}]}} + + monkeypatch.setattr(api, "_request", fake_request) + + deals = await api.deals({"page": 0, "domainId": 1}, typed=True) + + assert isinstance(deals, backend_models.DealResponse) + assert isinstance(deals.dr[0], backend_models.Deal) + + +@pytest.mark.asyncio +async def test_async_deals_accepts_generated_request_model( + monkeypatch: pytest.MonkeyPatch, +) -> None: + api = await keepa.AsyncKeepa.create("x" * 64) + + async def fake_request(*args: Any, **kwargs: Any) -> dict[str, Any]: + payload = args[1] + selection = json.loads(payload["selection"]) + assert selection["page"] == 0 + assert selection["includeCategories"] == [123] + return {"deals": {"dr": [{"asin": "B000000000"}]}} + + monkeypatch.setattr(api, "_request", fake_request) + + deals = await api.deals(backend_models.DealRequest(page=0, includeCategories=[123])) + + assert deals["dr"][0]["asin"] == "B000000000" + + +@pytest.mark.asyncio +async def test_async_product_finder_accepts_generated_request_model( + monkeypatch: pytest.MonkeyPatch, +) -> None: + api = await keepa.AsyncKeepa.create("x" * 64) + + async def fake_request(*args: Any, **kwargs: Any) -> dict[str, Any]: + payload = args[1] + selection = json.loads(payload["selection"]) + assert selection["author"] == ["jim butcher"] + assert selection["perPage"] == 50 + return {"asinList": ["B000HRMAR2"]} + + monkeypatch.setattr(api, "_request", fake_request) + + asins = await api.product_finder(backend_models.ProductFinderRequest(author=["jim butcher"])) + + assert asins == ["B000HRMAR2"] + + +@pytest.mark.asyncio +async def test_async_best_sellers_typed_response(monkeypatch: pytest.MonkeyPatch) -> None: + api = await keepa.AsyncKeepa.create("x" * 64) + + async def fake_request(*args: Any, **kwargs: Any) -> dict[str, Any]: + return {"bestSellersList": {"categoryId": 123, "asinList": ["B000000000"]}} + + monkeypatch.setattr(api, "_request", fake_request) + + best_sellers = await api.best_sellers_query("123", typed=True) + + assert isinstance(best_sellers, backend_models.BestSellers) + assert best_sellers.asinList == ["B000000000"] diff --git a/tests/test_backend_schema.py b/tests/test_backend_schema.py index c3dde51..e289609 100644 --- a/tests/test_backend_schema.py +++ b/tests/test_backend_schema.py @@ -2,11 +2,13 @@ import ast import dataclasses +import importlib.util import json import math import os import re import subprocess +import sys from pathlib import Path from typing import Any @@ -15,6 +17,7 @@ import keepa import keepa.models.product_params +from keepa import backend_models from keepa.constants import _SELLER_TIME_DATA_KEYS, DCODES, SCODES, csv_indices from keepa.models.domain import Domain from keepa.models.status import Status @@ -26,6 +29,10 @@ "https://raw.githubusercontent.com/keepacom/api_backend" f"/{BACKEND_COMMIT}/src/main/java/com/keepa/api/backend" ) +BACKEND_CONTENTS_URL = ( + "https://api.github.com/repos/keepacom/api_backend/contents/" + "src/main/java/com/keepa/api/backend/structs" +) BACKEND_SOURCE_DIR_ENV = "KEEPA_BACKEND_SOURCE_DIR" JAVA_TYPE_TO_PYTHON_TYPE = { @@ -44,6 +51,18 @@ # Keepa's backend marks RATING as non-price, but this library intentionally # scales it like a price-like value so users get star ratings instead of 0-50. CSV_PRICE_FLAG_OVERRIDES = {"RATING": True} +PROJECT_ROOT = Path(__file__).resolve().parents[1] + + +def _backend_model_generator() -> Any: + module_path = PROJECT_ROOT / "utilities" / "generate-backend-models.py" + spec = importlib.util.spec_from_file_location("keepa_backend_model_generator", module_path) + if spec is None or spec.loader is None: + raise AssertionError(f"Could not load backend model generator from {module_path}") + module = importlib.util.module_from_spec(spec) + sys.modules[spec.name] = module + spec.loader.exec_module(module) + return module def _backend_source(filename: str) -> str: @@ -61,6 +80,25 @@ def _backend_source(filename: str) -> str: return response.text +def _backend_struct_files() -> list[str]: + source_dir = os.environ.get(BACKEND_SOURCE_DIR_ENV) + if source_dir is not None and _local_backend_checkout_matches_commit(): + return sorted(path.name for path in (Path(source_dir) / "structs").glob("*.java")) + + try: + response = requests.get( + BACKEND_CONTENTS_URL, + params={"ref": BACKEND_COMMIT}, + timeout=20, + ) + except requests.RequestException as exc: + pytest.skip(f"Could not list Keepa backend structs: {exc}") + + if response.status_code != 200: + pytest.skip(f"Could not list Keepa backend structs: HTTP {response.status_code}") + return sorted(item["name"] for item in response.json() if item["name"].endswith(".java")) + + def _local_backend_checkout_matches_commit() -> bool: source_dir = os.environ.get(BACKEND_SOURCE_DIR_ENV) if source_dir is None: @@ -105,6 +143,66 @@ def _java_fields(java_source: str) -> dict[str, str]: return {name: java_type.strip() for java_type, name in fields} +def _java_declarations(java_source: str) -> dict[str, tuple[str, str]]: + java_source = _strip_java_comments(java_source) + declarations = {} + pattern = re.compile(r"public\s+(?:static\s+)?(?:final\s+)?(class|enum)\s+(\w+)") + for match in pattern.finditer(java_source): + brace_start = java_source.find("{", match.end()) + if brace_start < 0: + continue + brace_end = _matching_brace(java_source, brace_start) + declarations[match.group(2)] = ( + match.group(1), + java_source[brace_start + 1 : brace_end], + ) + return declarations + + +def _strip_java_comments(java_source: str) -> str: + java_source = re.sub(r"/\*.*?\*/", "", java_source, flags=re.S) + return re.sub(r"//.*", "", java_source) + + +def _matching_brace(source: str, brace_start: int) -> int: + depth = 0 + for index in range(brace_start, len(source)): + if source[index] == "{": + depth += 1 + elif source[index] == "}": + depth -= 1 + if depth == 0: + return index + raise AssertionError("Could not find matching Java brace") + + +def _top_level_java_class_fields(class_body: str) -> set[str]: + class_body = _remove_nested_java_declarations(class_body) + return { + match.group(2) + for match in re.finditer( + r"public\s+(?!(?:static|final|transient)\b)([\w\[\].<>, ?]+)\s+(\w+)" + r"(?:\s*=\s*[^;]+)?\s*;", + class_body, + ) + } + + +def _remove_nested_java_declarations(class_body: str) -> str: + pattern = re.compile(r"public\s+(?:static\s+)?(?:final\s+)?(?:class|enum)\s+\w+") + output = [] + cursor = 0 + for match in pattern.finditer(class_body): + brace_start = class_body.find("{", match.end()) + if brace_start < 0: + continue + brace_end = _matching_brace(class_body, brace_start) + output.append(class_body[cursor : match.start()]) + cursor = brace_end + 1 + output.append(class_body[cursor:]) + return "".join(output) + + def _java_enum_values(java_source: str, enum_name: str) -> list[str]: match = re.search(rf"public\s+enum\s+{enum_name}\s*\{{(.*?)\}}", java_source, re.S) if match is None: @@ -113,6 +211,32 @@ def _java_enum_values(java_source: str, enum_name: str) -> list[str]: return [value.strip() for value in values_text.split(",") if value.strip()] +def _java_enum_values_from_body(enum_body: str) -> list[str]: + constants_text = enum_body.split(";", 1)[0] + values = [] + for item in _split_top_level(constants_text, ","): + match = re.match(r"\s*([A-Za-z_][A-Za-z0-9_]*)", item) + if match is not None: + values.append(match.group(1)) + return values + + +def _split_top_level(text: str, separator: str) -> list[str]: + parts = [] + depth = 0 + start = 0 + for index, char in enumerate(text): + if char in "(<[": + depth += 1 + elif char in ")>]": + depth -= 1 + elif char == separator and depth == 0: + parts.append(text[start:index]) + start = index + 1 + parts.append(text[start:]) + return parts + + def _response_status_codes(java_source: str) -> dict[str, str]: cases = re.findall( r"case\s+(\d+):\s*response\.status\s*=\s*ResponseStatus\.([A-Z_]+);", @@ -185,6 +309,31 @@ def test_status_codes_match_backend_commit() -> None: assert SCODES == backend_status_codes +def test_response_status_model_enum_matches_backend_commit() -> None: + backend_statuses = _java_enum_values(_backend_source("KeepaAPI.java"), "ResponseStatus") + + assert [status.value for status in backend_models.ResponseStatus] == backend_statuses + + +def test_backend_models_are_generated_from_pinned_backend_commit() -> None: + assert backend_models.BACKEND_COMMIT == BACKEND_COMMIT + + +def test_backend_model_exports_are_public_and_complete() -> None: + declarations = {} + for file_name in _backend_struct_files(): + declarations.update(_java_declarations(_backend_source(f"structs/{file_name}"))) + + expected_exports = { + "BACKEND_COMMIT", + "KeepaBackendModel", + "ResponseStatus", + *declarations, + } + + assert set(backend_models.__all__) == expected_exports + + def test_status_model_fields_match_backend_response_token_fields() -> None: backend_fields = _java_fields(_backend_source("structs/Response.java")) token_status_fields = {"tokensLeft", "refillIn", "refillRate", "timestamp"} @@ -220,6 +369,76 @@ def test_csv_indices_match_backend_commit() -> None: assert csv_indices == backend_csv_types +def test_backend_models_cover_all_generated_struct_declarations() -> None: + declarations = {} + for file_name in _backend_struct_files(): + declarations.update(_java_declarations(_backend_source(f"structs/{file_name}"))) + + for name, (kind, body) in declarations.items(): + model_or_enum = getattr(backend_models, name) + if kind == "enum": + assert [member.value for member in model_or_enum] == _java_enum_values_from_body(body) + continue + + backend_field_names = _top_level_java_class_fields(body) + model_field_names = { + field.alias or field_name for field_name, field in model_or_enum.model_fields.items() + } + assert model_field_names == backend_field_names + + +def test_backend_model_fields_have_specific_types() -> None: + for export_name in backend_models.__all__: + model = getattr(backend_models, export_name) + if not isinstance(model, type) or not issubclass(model, backend_models.KeepaBackendModel): + continue + for field in model.model_fields.values(): + assert field.annotation is not Any, f"{model.__name__}.{field.alias} is untyped" + assert "typing.Any" not in str(field.annotation), ( + f"{model.__name__}.{field.alias} contains an untyped value" + ) + + +def test_backend_model_field_descriptions_match_backend_javadocs() -> None: + generator = _backend_model_generator() + sources = { + file_name: _backend_source(f"structs/{file_name}") for file_name in _backend_struct_files() + } + documented_fields = 0 + + for declaration in generator._collect_declarations(sources): + if declaration.kind != "class": + continue + + model = getattr(backend_models, declaration.name) + for field in generator._class_fields(declaration.body): + if field.description is None: + continue + + documented_fields += 1 + model_field = model.model_fields[generator._python_field_name(field.name)] + assert model_field.description == field.description, ( + f"{declaration.name}.{field.name} description does not match backend Javadoc" + ) + + assert documented_fields >= 400 + + +def test_backend_model_field_descriptions_are_cleaned_for_users() -> None: + assert backend_models.Product.model_fields["asin"].description == "The ASIN of the product" + assert ( + backend_models.Deal.model_fields["deltaPercent"].description + == "Same as delta, but given in percent instead of absolute values.\n\n" + "First dimension uses Product.CsvType, second dimension DealInterval" + ) + assert ( + backend_models.Seller.model_fields["csv"].description + == "Two dimensional history array that contains history data for this seller. First " + "dimension index:\n\nMerchantCsvType\n\n0 - RATING: The merchant's rating in percent, " + "Integer from 0 to 100.\n1 - RATING_COUNT: The merchant's total rating count, Integer." + ) + + def test_product_params_accept_backend_fields_and_reject_unknown_fields() -> None: params = keepa.ProductParams( activeIngredients=["ceramide"], diff --git a/tests/test_interface.py b/tests/test_interface.py index 8066a3f..bf7c331 100644 --- a/tests/test_interface.py +++ b/tests/test_interface.py @@ -2,11 +2,11 @@ Test the synchronous interface to the keepa backend. """ -from pathlib import Path import datetime -from itertools import chain import os import warnings +from itertools import chain +from pathlib import Path import numpy as np import pandas as pd @@ -14,8 +14,7 @@ import requests import keepa -from keepa import keepa_minutes_to_time -from keepa import Keepa +from keepa import Keepa, keepa_minutes_to_time # reduce the request limit for testing keepa.keepa_sync.REQLIM = 2 @@ -257,7 +256,7 @@ def test_productquery_offers(api: keepa.Keepa) -> None: def test_productquery_only_live_offers(api: keepa.Keepa) -> None: - """Tests that no historical offer data was returned from response if only_live_offers param was specified.""" + """Test only_live_offers omits historical offer data.""" max_offers = 20 request = api.query(PRODUCT_ASIN, offers=max_offers, only_live_offers=True, history=False) diff --git a/utilities/generate-backend-models.py b/utilities/generate-backend-models.py new file mode 100644 index 0000000..4d6b110 --- /dev/null +++ b/utilities/generate-backend-models.py @@ -0,0 +1,511 @@ +"""Generate permissive Pydantic models from Keepa backend Java structs.""" + +from __future__ import annotations + +import html +import json +import keyword +import os +import re +import subprocess +import textwrap +from dataclasses import dataclass +from pathlib import Path + +import requests + +BACKEND_COMMIT = "6e524d13bc25bdbe49be24d59a4b28feb9f98e5d" +BACKEND_SOURCE_DIR_ENV = "KEEPA_BACKEND_SOURCE_DIR" +BACKEND_RAW_BASE = ( + "https://raw.githubusercontent.com/keepacom/api_backend" + f"/{BACKEND_COMMIT}/src/main/java/com/keepa/api/backend" +) +BACKEND_CONTENTS_URL = ( + "https://api.github.com/repos/keepacom/api_backend/contents/" + "src/main/java/com/keepa/api/backend/structs" +) + +ROOT = Path(__file__).resolve().parents[1] +OUTPUT = ROOT / "src" / "keepa" / "models" / "backend.py" +DOC_OUTPUT = ROOT / "docs" / "source" / "backend_model_reference.rst" + +JAVA_TYPE_MAP = { + "String": "str", + "Integer": "int", + "int": "int", + "Long": "int", + "long": "int", + "Short": "int", + "short": "int", + "Byte": "int", + "byte": "int", + "Double": "float", + "double": "float", + "Float": "float", + "float": "float", + "Boolean": "bool", + "boolean": "bool", +} + + +def main() -> None: + """Generate backend Pydantic models from the pinned Keepa Java source.""" + struct_files = _backend_struct_files() + sources = {file_name: _backend_source(f"structs/{file_name}") for file_name in struct_files} + keepa_api_source = _backend_source("KeepaAPI.java") + declarations = _collect_declarations(sources) + enum_values = _collect_enum_values(declarations, keepa_api_source) + class_names = {declaration.name for declaration in declarations if declaration.kind == "class"} + enum_names = set(enum_values) + export_names = [ + "BACKEND_COMMIT", + "KeepaBackendModel", + *sorted(enum_values), + *[ + declaration.name + for declaration in sorted(declarations, key=lambda item: item.name) + if declaration.kind == "class" + ], + ] + + lines = [ + '"""Pydantic models generated from Keepa backend Java structs."""', + "", + "# Generated by utilities/generate-backend-models.py; do not edit by hand.", + "", + "from __future__ import annotations", + "", + "from enum import Enum", + "", + "from pydantic import BaseModel, ConfigDict, Field", + "", + f'BACKEND_COMMIT = "{BACKEND_COMMIT}"', + "", + "", + "class KeepaBackendModel(BaseModel):", + ' """Base model for backend response/request structures."""', + "", + ' model_config = ConfigDict(extra="allow", coerce_numbers_to_str=True)', + ] + + for enum_name, values in sorted(enum_values.items()): + lines.extend(_render_enum(enum_name, values)) + + for declaration in sorted(declarations, key=lambda item: item.name): + if declaration.kind != "class": + continue + lines.extend(_render_model(declaration, class_names, enum_names)) + + lines.extend( + [ + "", + "", + "__all__ = [", + *[f' "{name}",' for name in export_names], + "]", + "", + "_MODELS: list[type[KeepaBackendModel]] = [", + *[ + f" {declaration.name}," + for declaration in sorted(declarations, key=lambda item: item.name) + if declaration.kind == "class" + ], + "]", + "for _model in _MODELS:", + " _model.model_rebuild()", + "", + ] + ) + + OUTPUT.write_text("\n".join(lines)) + _write_backend_docs(declarations, enum_values) + + +def _write_backend_docs(declarations: list[Declaration], enum_values: dict[str, list[str]]) -> None: + model_names = [ + "KeepaBackendModel", + *sorted(declaration.name for declaration in declarations if declaration.kind == "class"), + ] + lines = [ + ".. Generated by utilities/generate-backend-models.py; do not edit by hand.", + "", + "Backend Model API", + "=================", + "Every model and enum is generated from the pinned Keepa backend schema.", + "", + "Models", + "------", + "", + ".. autosummary::", + " :toctree: api/backend/models", + " :template: pydantic_model", + "", + *[f" keepa.models.backend.{name}" for name in model_names], + "", + "Enums", + "-----", + "", + ".. autosummary::", + " :toctree: api/backend/enums", + " :template: enum", + "", + *[f" keepa.models.backend.{name}" for name in sorted(enum_values)], + "", + "Schema Metadata", + "---------------", + "", + ".. autosummary::", + " :toctree: api/backend/data", + "", + " keepa.models.backend.BACKEND_COMMIT", + "", + ] + DOC_OUTPUT.write_text("\n".join(lines)) + + +@dataclass(frozen=True) +class Declaration: + """Java declaration parsed from the backend source.""" + + kind: str + name: str + body: str + + +def _backend_source(filename: str) -> str: + local_file = _local_backend_file(filename) + if local_file is not None: + return local_file.read_text() + + response = requests.get(f"{BACKEND_RAW_BASE}/{filename}", timeout=20) + response.raise_for_status() + return response.text + + +def _backend_struct_files() -> list[str]: + source_dir = os.environ.get(BACKEND_SOURCE_DIR_ENV) + if source_dir is not None: + source_path = Path(source_dir) + if _local_backend_checkout_matches_commit(source_path): + return sorted(path.name for path in (source_path / "structs").glob("*.java")) + + response = requests.get(BACKEND_CONTENTS_URL, params={"ref": BACKEND_COMMIT}, timeout=20) + response.raise_for_status() + return sorted(item["name"] for item in response.json() if item["name"].endswith(".java")) + + +def _local_backend_file(filename: str) -> Path | None: + source_dir = os.environ.get(BACKEND_SOURCE_DIR_ENV) + if source_dir is None: + return None + source_path = Path(source_dir) + local_file = source_path / filename + if local_file.is_file() and _local_backend_checkout_matches_commit(source_path): + return local_file + return None + + +def _local_backend_checkout_matches_commit(source_path: Path) -> bool: + try: + result = subprocess.run( + ["git", "-C", str(source_path), "rev-parse", "--show-toplevel"], + check=True, + capture_output=True, + text=True, + ) + repo_root = Path(result.stdout.strip()) + result = subprocess.run( + ["git", "-C", str(repo_root), "rev-parse", "HEAD"], + check=True, + capture_output=True, + text=True, + ) + except (OSError, subprocess.CalledProcessError): + return False + return result.stdout.strip() == BACKEND_COMMIT + + +def _collect_declarations(sources: dict[str, str]) -> list[Declaration]: + declarations = [] + declaration_pattern = re.compile(r"public\s+(?:static\s+)?(?:final\s+)?(class|enum)\s+(\w+)") + for source in sources.values(): + source_without_comments = _strip_comments_preserve_length(source) + for match in declaration_pattern.finditer(source_without_comments): + brace_start = source_without_comments.find("{", match.end()) + if brace_start < 0: + continue + brace_end = _matching_brace(source_without_comments, brace_start) + declarations.append( + Declaration(match.group(1), match.group(2), source[brace_start + 1 : brace_end]) + ) + return declarations + + +def _strip_comments_preserve_length(source: str) -> str: + def replacement(match: re.Match[str]) -> str: + return "".join("\n" if char == "\n" else " " for char in match.group(0)) + + source = re.sub(r"/\*.*?\*/", replacement, source, flags=re.S) + return re.sub(r"//.*", replacement, source) + + +def _strip_comments(source: str) -> str: + source = re.sub(r"/\*.*?\*/", "", source, flags=re.S) + return re.sub(r"//.*", "", source) + + +def _matching_brace(source: str, brace_start: int) -> int: + depth = 0 + for index in range(brace_start, len(source)): + if source[index] == "{": + depth += 1 + elif source[index] == "}": + depth -= 1 + if depth == 0: + return index + raise ValueError("No matching brace") + + +def _collect_enum_values( + declarations: list[Declaration], keepa_api_source: str +) -> dict[str, list[str]]: + values = { + declaration.name: _enum_values(declaration.body) + for declaration in declarations + if declaration.kind == "enum" + } + response_status = re.search( + r"public\s+enum\s+ResponseStatus\s*\{(.*?)\}", keepa_api_source, re.S + ) + if response_status is not None: + values["ResponseStatus"] = _enum_values(response_status.group(1)) + return values + + +def _enum_values(enum_body: str) -> list[str]: + constants_text = enum_body.split(";", 1)[0] + constants_text = re.sub(r"/\*.*?\*/", "", constants_text, flags=re.S) + values = [] + for item in _split_top_level(constants_text, ","): + item = item.strip() + if not item: + continue + match = re.match(r"([A-Za-z_][A-Za-z0-9_]*)", item) + if match is not None: + values.append(match.group(1)) + return values + + +def _split_top_level(text: str, separator: str) -> list[str]: + parts = [] + depth = 0 + start = 0 + for index, char in enumerate(text): + if char in "(<[": + depth += 1 + elif char in ")>]": + depth -= 1 + elif char == separator and depth == 0: + parts.append(text[start:index]) + start = index + 1 + parts.append(text[start:]) + return parts + + +def _render_enum(name: str, values: list[str]) -> list[str]: + lines = ["", "", f"class {name}(str, Enum):"] + lines.append(' """Backend enum generated from the Keepa Java schema."""') + lines.append("") + if not values: + lines.append(" pass") + for value in values: + lines.append(f' {value} = "{value}"') + return lines + + +def _render_model( + declaration: Declaration, class_names: set[str], enum_names: set[str] +) -> list[str]: + fields = _class_fields(declaration.body) + lines = ["", "", f"class {declaration.name}(KeepaBackendModel):"] + lines.append( + f' """Backend ``{declaration.name}`` model generated from the Keepa Java schema."""' + ) + lines.append("") + if not fields: + lines.append(" pass") + return lines + seen = set() + for field in fields: + if field.name in seen: + continue + seen.add(field.name) + python_name = _python_field_name(field.name) + annotation = _python_type(field.java_type, class_names, enum_names) + lines.extend(_render_field(python_name, annotation, field)) + return lines + + +def _render_field(python_name: str, annotation: str, field: FieldDeclaration) -> list[str]: + if python_name == field.name and field.description is None: + return [f" {python_name}: {annotation} | None = None"] + + if field.description is None: + return [ + f' {python_name}: {annotation} | None = Field(default=None, alias="{field.name}")' + ] + + lines = [f" {python_name}: {annotation} | None = Field("] + lines.append(" default=None,") + if python_name != field.name: + lines.append(f" alias={json.dumps(field.name)},") + lines.append(" description=(") + lines.extend(_string_literal_lines(field.description, indent=" ")) + lines.append(" ),") + lines.append(" )") + return lines + + +def _string_literal_lines(text: str, indent: str, width: int = 40) -> list[str]: + lines = [] + for line in text.splitlines(keepends=True) or [""]: + for chunk in textwrap.wrap( + line, + width=width, + break_long_words=True, + break_on_hyphens=False, + replace_whitespace=False, + drop_whitespace=False, + ): + lines.append(f"{indent}{json.dumps(chunk, ensure_ascii=True)}") + return lines + + +def _python_field_name(name: str) -> str: + if not name.isidentifier() or keyword.iskeyword(name) or name == "l": + return f"{name}_" + return name + + +@dataclass(frozen=True) +class FieldDeclaration: + """Java field parsed from a backend struct.""" + + java_type: str + name: str + description: str | None + + +def _class_fields(class_body: str) -> list[FieldDeclaration]: + class_body = _remove_nested_declarations(class_body) + class_body_without_comments = _strip_comments_preserve_length(class_body) + fields = [] + for match in re.finditer( + r"public\s+(?!(?:static|final|transient)\b)([\w\[\].<>, ?]+)\s+(\w+)" + r"(?:\s*=\s*[^;]+)?\s*;", + class_body_without_comments, + ): + java_type = " ".join(match.group(1).replace("?", "").split()) + fields.append( + FieldDeclaration( + java_type=java_type, + name=match.group(2), + description=_field_description(class_body, match.start()), + ) + ) + return fields + + +def _remove_nested_declarations(class_body: str) -> str: + declaration_pattern = re.compile(r"public\s+(?:static\s+)?(?:final\s+)?(?:class|enum)\s+\w+") + class_body_without_comments = _strip_comments_preserve_length(class_body) + output = [] + cursor = 0 + for match in declaration_pattern.finditer(class_body_without_comments): + brace_start = class_body_without_comments.find("{", match.end()) + if brace_start < 0: + continue + brace_end = _matching_brace(class_body_without_comments, brace_start) + output.append(class_body[cursor : match.start()]) + cursor = brace_end + 1 + output.append(class_body[cursor:]) + return "".join(output) + + +def _field_description(class_body: str, field_start: int) -> str | None: + prefix = class_body[:field_start] + comments = list(re.finditer(r"/\*\*((?:(?!\*/).)*)\*/", prefix, re.S)) + if not comments: + return None + + comment = comments[-1] + between_comment_and_field = prefix[comment.end() :].strip() + if between_comment_and_field: + annotation_lines = [ + line.strip() for line in between_comment_and_field.splitlines() if line.strip() + ] + if not annotation_lines or any(not line.startswith("@") for line in annotation_lines): + return None + + return _clean_javadoc(comment.group(1)) + + +def _clean_javadoc(comment: str) -> str | None: + lines = [] + for line in comment.splitlines(): + lines.append(re.sub(r"^\s*\*\s?", "", line).rstrip()) + text = "\n".join(lines).strip() + if not text: + return None + + def replace_link(match: re.Match[str]) -> str: + target = match.group(1).replace("#", ".").lstrip(".") + label = match.group(2) + return label.strip() if label is not None else target + + text = re.sub(r"\{@(?:code|literal)\s+([^}]*)\}", r"\1", text) + text = re.sub(r"\{@link\s+([^}\s]+)(?:\s+([^}]+))?\}", replace_link, text) + text = re.sub(r"\(([^)\n]+)\)\}", r"(\1)", text) + text = re.sub(r"(?i)<\s*br\s*/?\s*>", "\n", text) + text = re.sub(r"(?i)", "\n\n", text) + text = re.sub(r"<[^>]+>", "", text) + text = html.unescape(text) + text = re.sub(r"`([^`]+)`", r"\1", text) + text = re.sub(r"_([^_\n]+)_", r"\1", text) + text = re.sub(r"[ \t]+", " ", text) + text = re.sub(r"\n{3,}", "\n\n", text) + text = "\n".join(line.strip() for line in text.splitlines()).strip() + return text or None + + +def _python_type(java_type: str, class_names: set[str], enum_names: set[str]) -> str: + java_type = java_type.strip() + if java_type.endswith("[][]"): + item_type = _python_type(java_type[:-4], class_names, enum_names) + return f"list[list[{item_type} | None] | None]" + if java_type.endswith("[]"): + item_type = _python_type(java_type[:-2], class_names, enum_names) + return f"list[{item_type} | None]" + + generic_match = re.match(r"(?:HashMap|LinkedHashMap)<(.+)>", java_type) + if generic_match is not None: + key_type, value_type = _split_top_level(generic_match.group(1), ",") + return ( + f"dict[{_python_type(key_type, class_names, enum_names)}, " + f"{_python_type(value_type, class_names, enum_names)} | None]" + ) + + if java_type == "KeepaAPI.ResponseStatus": + return "ResponseStatus" + if java_type in JAVA_TYPE_MAP: + return JAVA_TYPE_MAP[java_type] + if java_type in class_names or java_type in enum_names: + return java_type + nested_type = java_type.rsplit(".", 1)[-1] + if nested_type in class_names or nested_type in enum_names: + return nested_type + raise ValueError(f"Unsupported Java type: {java_type}") + + +if __name__ == "__main__": + main() diff --git a/uv.lock b/uv.lock index f0899b8..e5a7cac 100644 --- a/uv.lock +++ b/uv.lock @@ -197,6 +197,21 @@ wheels = [ {url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z"} ] +[[package]] +dependencies = [ + {name = "pydantic"}, + {name = "pydantic-settings"}, + {name = "sphinx", version = "8.1.3", source = {registry = "https://pypi.org/simple"}, marker = "python_full_version < '3.11'"}, + {name = "sphinx", version = "8.2.3", source = {registry = "https://pypi.org/simple"}, marker = "python_full_version == '3.11.*'"}, + {name = "sphinx", version = "9.1.0", source = {registry = "https://pypi.org/simple"}, marker = "python_full_version >= '3.12'"} +] +name = "autodoc-pydantic" +source = {registry = "https://pypi.org/simple"} +version = "2.2.0" +wheels = [ + {url = "https://files.pythonhosted.org/packages/7b/df/87120e2195f08d760bc5cf8a31cfa2381a6887517aa89453b23f1ae3354f/autodoc_pydantic-2.2.0-py3-none-any.whl", hash = "sha256:8c6a36fbf6ed2700ea9c6d21ea76ad541b621fbdf16b5a80ee04673548af4d95", size = 34001, upload-time = "2024-04-27T10:57:00.542Z"} +] + [[package]] name = "babel" sdist = {url = "https://files.pythonhosted.org/packages/7d/6b/d52e42361e1aa00709585ecc30b3f9684b3ab62530771402248b1b1d6240/babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d", size = 9951852, upload-time = "2025-02-01T15:17:41.026Z"} @@ -860,27 +875,37 @@ version = "1.5.dev0" provides-extras = ["doc", "test"] requires-dist = [ {name = "aiohttp"}, + {name = "autodoc-pydantic", marker = "extra == 'doc'", specifier = "==2.2.0"}, {name = "matplotlib", marker = "extra == 'test'"}, {name = "numpy", specifier = ">=1.9.3"}, - {name = "numpydoc", marker = "extra == 'doc'", specifier = "==1.7.0"}, + {name = "numpydoc", marker = "extra == 'doc'", specifier = "==1.10.0"}, {name = "pandas", specifier = "<=3.0"}, {name = "pandas", marker = "extra == 'test'"}, - {name = "pydantic"}, - {name = "pydata-sphinx-theme", marker = "extra == 'doc'", specifier = "==0.15.4"}, + {name = "pydantic", specifier = ">=2"}, + {name = "pydata-sphinx-theme", marker = "extra == 'doc'", specifier = "==0.19.0"}, {name = "pytest", marker = "extra == 'test'"}, {name = "pytest-asyncio", marker = "extra == 'test'"}, {name = "pytest-cov", marker = "extra == 'test'"}, {name = "pytest-rerunfailures", marker = "extra == 'test'"}, {name = "requests", specifier = ">=2.2"}, - {name = "sphinx", marker = "extra == 'doc'", specifier = "==7.3.7"}, + {name = "sphinx", marker = "python_full_version == '3.11.*' and extra == 'doc'", specifier = "==8.2.3"}, + {name = "sphinx", marker = "python_full_version < '3.11' and extra == 'doc'", specifier = "==8.1.3"}, + {name = "sphinx", marker = "python_full_version >= '3.12' and extra == 'doc'", specifier = "==9.1.0"}, + {name = "sphinx-copybutton", marker = "extra == 'doc'", specifier = "==0.5.2"}, + {name = "tomli", marker = "python_full_version < '3.11' and extra == 'doc'"}, {name = "tqdm"} ] [package.optional-dependencies] doc = [ + {name = "autodoc-pydantic"}, {name = "numpydoc"}, {name = "pydata-sphinx-theme"}, - {name = "sphinx"} + {name = "sphinx", version = "8.1.3", source = {registry = "https://pypi.org/simple"}, marker = "python_full_version < '3.11'"}, + {name = "sphinx", version = "8.2.3", source = {registry = "https://pypi.org/simple"}, marker = "python_full_version == '3.11.*'"}, + {name = "sphinx", version = "9.1.0", source = {registry = "https://pypi.org/simple"}, marker = "python_full_version >= '3.12'"}, + {name = "sphinx-copybutton"}, + {name = "tomli", marker = "python_full_version < '3.11'"} ] test = [ {name = "matplotlib"}, @@ -1449,16 +1474,17 @@ wheels = [ [[package]] dependencies = [ - {name = "sphinx"}, - {name = "tabulate"}, + {name = "sphinx", version = "8.1.3", source = {registry = "https://pypi.org/simple"}, marker = "python_full_version < '3.11'"}, + {name = "sphinx", version = "8.2.3", source = {registry = "https://pypi.org/simple"}, marker = "python_full_version == '3.11.*'"}, + {name = "sphinx", version = "9.1.0", source = {registry = "https://pypi.org/simple"}, marker = "python_full_version >= '3.12'"}, {name = "tomli", marker = "python_full_version < '3.11'"} ] name = "numpydoc" -sdist = {url = "https://files.pythonhosted.org/packages/76/69/d745d43617a476a5b5fb7f71555eceaca32e23296773c35decefa1da5463/numpydoc-1.7.0.tar.gz", hash = "sha256:866e5ae5b6509dcf873fc6381120f5c31acf13b135636c1a81d68c166a95f921", size = 87575, upload-time = "2024-03-28T13:06:49.029Z"} +sdist = {url = "https://files.pythonhosted.org/packages/e9/3c/dfccc9e7dee357fb2aa13c3890d952a370dd0ed071e0f7ed62ed0df567c1/numpydoc-1.10.0.tar.gz", hash = "sha256:3f7970f6eee30912260a6b31ac72bba2432830cd6722569ec17ee8d3ef5ffa01", size = 94027, upload-time = "2025-12-02T16:39:12.937Z"} source = {registry = "https://pypi.org/simple"} -version = "1.7.0" +version = "1.10.0" wheels = [ - {url = "https://files.pythonhosted.org/packages/f0/fa/dcfe0f65660661db757ee9ebd84e170ff98edd5d80235f62457d9088f85f/numpydoc-1.7.0-py3-none-any.whl", hash = "sha256:5a56419d931310d79a06cfc2a126d1558700feeb9b4f3d8dcae1a8134be829c9", size = 62813, upload-time = "2024-03-28T13:06:45.483Z"} + {url = "https://files.pythonhosted.org/packages/62/5e/3a6a3e90f35cea3853c45e5d5fb9b7192ce4384616f932cf7591298ab6e1/numpydoc-1.10.0-py3-none-any.whl", hash = "sha256:3149da9874af890bcc2a82ef7aae5484e5aa81cb2778f08e3c307ba6d963721b", size = 69255, upload-time = "2025-12-02T16:39:11.561Z"} ] [[package]] @@ -1882,23 +1908,38 @@ wheels = [ {url = "https://files.pythonhosted.org/packages/48/f7/925f65d930802e3ea2eb4d5afa4cb8730c8dc0d2cb89a59dc4ed2fcb2d74/pydantic_core-2.41.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c173ddcd86afd2535e2b695217e82191580663a1d1928239f877f5a1649ef39f", size = 2147775, upload-time = "2025-10-14T10:23:45.406Z"} ] +[[package]] +dependencies = [ + {name = "pydantic"}, + {name = "python-dotenv"}, + {name = "typing-inspection"} +] +name = "pydantic-settings" +sdist = {url = "https://files.pythonhosted.org/packages/5c/b5/8f48e906c3e0205276e8bd8cb7512217a87b2685304d64be27cad5b3019f/pydantic_settings-2.14.2.tar.gz", hash = "sha256:c19dd64b19097f1de80184f0cc7b0272a13ae6e170cbf240a3e27e381ed14a5f", size = 237700, upload-time = "2026-06-19T13:44:56.324Z"} +source = {registry = "https://pypi.org/simple"} +version = "2.14.2" +wheels = [ + {url = "https://files.pythonhosted.org/packages/77/c1/6e422f34e569cf8e18df68d1939c81c099d2b61e4f7d9621c8a77560799c/pydantic_settings-2.14.2-py3-none-any.whl", hash = "sha256:a20c97b37910b6550d5ea50fbcc2d4187defe58cd57070b73863d069419c9440", size = 61715, upload-time = "2026-06-19T13:44:55.02Z"} +] + [[package]] dependencies = [ {name = "accessible-pygments"}, {name = "babel"}, {name = "beautifulsoup4"}, {name = "docutils"}, - {name = "packaging"}, {name = "pygments"}, - {name = "sphinx"}, + {name = "sphinx", version = "8.1.3", source = {registry = "https://pypi.org/simple"}, marker = "python_full_version < '3.11'"}, + {name = "sphinx", version = "8.2.3", source = {registry = "https://pypi.org/simple"}, marker = "python_full_version == '3.11.*'"}, + {name = "sphinx", version = "9.1.0", source = {registry = "https://pypi.org/simple"}, marker = "python_full_version >= '3.12'"}, {name = "typing-extensions"} ] name = "pydata-sphinx-theme" -sdist = {url = "https://files.pythonhosted.org/packages/67/ea/3ab478cccacc2e8ef69892c42c44ae547bae089f356c4b47caf61730958d/pydata_sphinx_theme-0.15.4.tar.gz", hash = "sha256:7762ec0ac59df3acecf49fd2f889e1b4565dbce8b88b2e29ee06fdd90645a06d", size = 2400673, upload-time = "2024-06-25T19:28:45.041Z"} +sdist = {url = "https://files.pythonhosted.org/packages/39/7e/e3defb93f30557ae825a3b3fbc2c6728301e5e9505a85e00d8503345c42c/pydata_sphinx_theme-0.19.0.tar.gz", hash = "sha256:148ba092bd6937b3321385dc482cc69a29ad2ede36547b4f010ade782bc6a062", size = 5004933, upload-time = "2026-06-15T09:31:02.268Z"} source = {registry = "https://pypi.org/simple"} -version = "0.15.4" +version = "0.19.0" wheels = [ - {url = "https://files.pythonhosted.org/packages/e7/d3/c622950d87a2ffd1654208733b5bd1c5645930014abed8f4c0d74863988b/pydata_sphinx_theme-0.15.4-py3-none-any.whl", hash = "sha256:2136ad0e9500d0949f96167e63f3e298620040aea8f9c74621959eda5d4cf8e6", size = 4640157, upload-time = "2024-06-25T19:28:42.383Z"} + {url = "https://files.pythonhosted.org/packages/54/77/bdb5a4c0e8e33c08f31fd175a5af0bc5e29f1b43ffa9e963f4c4f9bfbc97/pydata_sphinx_theme-0.19.0-py3-none-any.whl", hash = "sha256:5d7dfe3beb0facc88b5d78ff4a4c948f214cc0e03aae27e7fc58286e963b588b", size = 6201132, upload-time = "2026-06-15T09:30:59.966Z"} ] [[package]] @@ -1990,6 +2031,15 @@ wheels = [ {url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z"} ] +[[package]] +name = "python-dotenv" +sdist = {url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z"} +source = {registry = "https://pypi.org/simple"} +version = "1.2.2" +wheels = [ + {url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z"} +] + [[package]] name = "pytz" sdist = {url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884, upload-time = "2025-03-25T02:25:00.538Z"} @@ -2014,6 +2064,27 @@ wheels = [ {url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z"} ] +[[package]] +name = "roman-numerals" +sdist = {url = "https://files.pythonhosted.org/packages/ae/f9/41dc953bbeb056c17d5f7a519f50fdf010bd0553be2d630bc69d1e022703/roman_numerals-4.1.0.tar.gz", hash = "sha256:1af8b147eb1405d5839e78aeb93131690495fe9da5c91856cb33ad55a7f1e5b2", size = 9077, upload-time = "2025-12-17T18:25:34.381Z"} +source = {registry = "https://pypi.org/simple"} +version = "4.1.0" +wheels = [ + {url = "https://files.pythonhosted.org/packages/04/54/6f679c435d28e0a568d8e8a7c0a93a09010818634c3c3907fc98d8983770/roman_numerals-4.1.0-py3-none-any.whl", hash = "sha256:647ba99caddc2cc1e55a51e4360689115551bf4476d90e8162cf8c345fe233c7", size = 7676, upload-time = "2025-12-17T18:25:33.098Z"} +] + +[[package]] +dependencies = [ + {name = "roman-numerals", marker = "python_full_version == '3.11.*'"} +] +name = "roman-numerals-py" +sdist = {url = "https://files.pythonhosted.org/packages/cb/b5/de96fca640f4f656eb79bbee0e79aeec52e3e0e359f8a3e6a0d366378b64/roman_numerals_py-4.1.0.tar.gz", hash = "sha256:f5d7b2b4ca52dd855ef7ab8eb3590f428c0b1ea480736ce32b01fef2a5f8daf9", size = 4274, upload-time = "2025-12-17T18:25:41.153Z"} +source = {registry = "https://pypi.org/simple"} +version = "4.1.0" +wheels = [ + {url = "https://files.pythonhosted.org/packages/27/2c/daca29684cbe9fd4bc711f8246da3c10adca1ccc4d24436b17572eb2590e/roman_numerals_py-4.1.0-py3-none-any.whl", hash = "sha256:553114c1167141c1283a51743759723ecd05604a1b6b507225e91dc1a6df0780", size = 4547, upload-time = "2025-12-17T18:25:40.136Z"} +] + [[package]] name = "six" sdist = {url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z"} @@ -2043,30 +2114,109 @@ wheels = [ [[package]] dependencies = [ - {name = "alabaster"}, - {name = "babel"}, - {name = "colorama", marker = "sys_platform == 'win32'"}, - {name = "docutils"}, - {name = "imagesize"}, - {name = "jinja2"}, - {name = "packaging"}, - {name = "pygments"}, - {name = "requests"}, - {name = "snowballstemmer"}, - {name = "sphinxcontrib-applehelp"}, - {name = "sphinxcontrib-devhelp"}, - {name = "sphinxcontrib-htmlhelp"}, - {name = "sphinxcontrib-jsmath"}, - {name = "sphinxcontrib-qthelp"}, - {name = "sphinxcontrib-serializinghtml"}, + {name = "alabaster", marker = "python_full_version < '3.11'"}, + {name = "babel", marker = "python_full_version < '3.11'"}, + {name = "colorama", marker = "python_full_version < '3.11' and sys_platform == 'win32'"}, + {name = "docutils", marker = "python_full_version < '3.11'"}, + {name = "imagesize", marker = "python_full_version < '3.11'"}, + {name = "jinja2", marker = "python_full_version < '3.11'"}, + {name = "packaging", marker = "python_full_version < '3.11'"}, + {name = "pygments", marker = "python_full_version < '3.11'"}, + {name = "requests", marker = "python_full_version < '3.11'"}, + {name = "snowballstemmer", marker = "python_full_version < '3.11'"}, + {name = "sphinxcontrib-applehelp", marker = "python_full_version < '3.11'"}, + {name = "sphinxcontrib-devhelp", marker = "python_full_version < '3.11'"}, + {name = "sphinxcontrib-htmlhelp", marker = "python_full_version < '3.11'"}, + {name = "sphinxcontrib-jsmath", marker = "python_full_version < '3.11'"}, + {name = "sphinxcontrib-qthelp", marker = "python_full_version < '3.11'"}, + {name = "sphinxcontrib-serializinghtml", marker = "python_full_version < '3.11'"}, {name = "tomli", marker = "python_full_version < '3.11'"} ] name = "sphinx" -sdist = {url = "https://files.pythonhosted.org/packages/b7/0a/b88033900b1582f5ed8f880263363daef968d1cd064175e32abfd9714410/sphinx-7.3.7.tar.gz", hash = "sha256:a4a7db75ed37531c05002d56ed6948d4c42f473a36f46e1382b0bd76ca9627bc", size = 7094808, upload-time = "2024-04-19T04:44:48.297Z"} +resolution-markers = [ + "python_full_version < '3.11'" +] +sdist = {url = "https://files.pythonhosted.org/packages/6f/6d/be0b61178fe2cdcb67e2a92fc9ebb488e3c51c4f74a36a7824c0adf23425/sphinx-8.1.3.tar.gz", hash = "sha256:43c1911eecb0d3e161ad78611bc905d1ad0e523e4ddc202a58a821773dc4c927", size = 8184611, upload-time = "2024-10-13T20:27:13.93Z"} +source = {registry = "https://pypi.org/simple"} +version = "8.1.3" +wheels = [ + {url = "https://files.pythonhosted.org/packages/26/60/1ddff83a56d33aaf6f10ec8ce84b4c007d9368b21008876fceda7e7381ef/sphinx-8.1.3-py3-none-any.whl", hash = "sha256:09719015511837b76bf6e03e42eb7595ac8c2e41eeb9c29c5b755c6b677992a2", size = 3487125, upload-time = "2024-10-13T20:27:10.448Z"} +] + +[[package]] +dependencies = [ + {name = "alabaster", marker = "python_full_version == '3.11.*'"}, + {name = "babel", marker = "python_full_version == '3.11.*'"}, + {name = "colorama", marker = "python_full_version == '3.11.*' and sys_platform == 'win32'"}, + {name = "docutils", marker = "python_full_version == '3.11.*'"}, + {name = "imagesize", marker = "python_full_version == '3.11.*'"}, + {name = "jinja2", marker = "python_full_version == '3.11.*'"}, + {name = "packaging", marker = "python_full_version == '3.11.*'"}, + {name = "pygments", marker = "python_full_version == '3.11.*'"}, + {name = "requests", marker = "python_full_version == '3.11.*'"}, + {name = "roman-numerals-py", marker = "python_full_version == '3.11.*'"}, + {name = "snowballstemmer", marker = "python_full_version == '3.11.*'"}, + {name = "sphinxcontrib-applehelp", marker = "python_full_version == '3.11.*'"}, + {name = "sphinxcontrib-devhelp", marker = "python_full_version == '3.11.*'"}, + {name = "sphinxcontrib-htmlhelp", marker = "python_full_version == '3.11.*'"}, + {name = "sphinxcontrib-jsmath", marker = "python_full_version == '3.11.*'"}, + {name = "sphinxcontrib-qthelp", marker = "python_full_version == '3.11.*'"}, + {name = "sphinxcontrib-serializinghtml", marker = "python_full_version == '3.11.*'"} +] +name = "sphinx" +resolution-markers = [ + "python_full_version == '3.11.*'" +] +sdist = {url = "https://files.pythonhosted.org/packages/38/ad/4360e50ed56cb483667b8e6dadf2d3fda62359593faabbe749a27c4eaca6/sphinx-8.2.3.tar.gz", hash = "sha256:398ad29dee7f63a75888314e9424d40f52ce5a6a87ae88e7071e80af296ec348", size = 8321876, upload-time = "2025-03-02T22:31:59.658Z"} source = {registry = "https://pypi.org/simple"} -version = "7.3.7" +version = "8.2.3" wheels = [ - {url = "https://files.pythonhosted.org/packages/b4/fa/130c32ed94cf270e3d0b9ded16fb7b2c8fea86fa7263c29a696a30c1dde7/sphinx-7.3.7-py3-none-any.whl", hash = "sha256:413f75440be4cacf328f580b4274ada4565fb2187d696a84970c23f77b64d8c3", size = 3335650, upload-time = "2024-04-19T04:44:43.839Z"} + {url = "https://files.pythonhosted.org/packages/31/53/136e9eca6e0b9dc0e1962e2c908fbea2e5ac000c2a2fbd9a35797958c48b/sphinx-8.2.3-py3-none-any.whl", hash = "sha256:4405915165f13521d875a8c29c8970800a0141c14cc5416a38feca4ea5d9b9c3", size = 3589741, upload-time = "2025-03-02T22:31:56.836Z"} +] + +[[package]] +dependencies = [ + {name = "alabaster", marker = "python_full_version >= '3.12'"}, + {name = "babel", marker = "python_full_version >= '3.12'"}, + {name = "colorama", marker = "python_full_version >= '3.12' and sys_platform == 'win32'"}, + {name = "docutils", marker = "python_full_version >= '3.12'"}, + {name = "imagesize", marker = "python_full_version >= '3.12'"}, + {name = "jinja2", marker = "python_full_version >= '3.12'"}, + {name = "packaging", marker = "python_full_version >= '3.12'"}, + {name = "pygments", marker = "python_full_version >= '3.12'"}, + {name = "requests", marker = "python_full_version >= '3.12'"}, + {name = "roman-numerals", marker = "python_full_version >= '3.12'"}, + {name = "snowballstemmer", marker = "python_full_version >= '3.12'"}, + {name = "sphinxcontrib-applehelp", marker = "python_full_version >= '3.12'"}, + {name = "sphinxcontrib-devhelp", marker = "python_full_version >= '3.12'"}, + {name = "sphinxcontrib-htmlhelp", marker = "python_full_version >= '3.12'"}, + {name = "sphinxcontrib-jsmath", marker = "python_full_version >= '3.12'"}, + {name = "sphinxcontrib-qthelp", marker = "python_full_version >= '3.12'"}, + {name = "sphinxcontrib-serializinghtml", marker = "python_full_version >= '3.12'"} +] +name = "sphinx" +resolution-markers = [ + "python_full_version >= '3.12'" +] +sdist = {url = "https://files.pythonhosted.org/packages/cd/bd/f08eb0f4eed5c83f1ba2a3bd18f7745a2b1525fad70660a1c00224ec468a/sphinx-9.1.0.tar.gz", hash = "sha256:7741722357dd75f8190766926071fed3bdc211c74dd2d7d4df5404da95930ddb", size = 8718324, upload-time = "2025-12-31T15:09:27.646Z"} +source = {registry = "https://pypi.org/simple"} +version = "9.1.0" +wheels = [ + {url = "https://files.pythonhosted.org/packages/73/f7/b1884cb3188ab181fc81fa00c266699dab600f927a964df02ec3d5d1916a/sphinx-9.1.0-py3-none-any.whl", hash = "sha256:c84fdd4e782504495fe4f2c0b3413d6c2bf388589bb352d439b2a3bb99991978", size = 3921742, upload-time = "2025-12-31T15:09:25.561Z"} +] + +[[package]] +dependencies = [ + {name = "sphinx", version = "8.1.3", source = {registry = "https://pypi.org/simple"}, marker = "python_full_version < '3.11'"}, + {name = "sphinx", version = "8.2.3", source = {registry = "https://pypi.org/simple"}, marker = "python_full_version == '3.11.*'"}, + {name = "sphinx", version = "9.1.0", source = {registry = "https://pypi.org/simple"}, marker = "python_full_version >= '3.12'"} +] +name = "sphinx-copybutton" +sdist = {url = "https://files.pythonhosted.org/packages/fc/2b/a964715e7f5295f77509e59309959f4125122d648f86b4fe7d70ca1d882c/sphinx-copybutton-0.5.2.tar.gz", hash = "sha256:4cf17c82fb9646d1bc9ca92ac280813a3b605d8c421225fd9913154103ee1fbd", size = 23039, upload-time = "2023-04-14T08:10:22.998Z"} +source = {registry = "https://pypi.org/simple"} +version = "0.5.2" +wheels = [ + {url = "https://files.pythonhosted.org/packages/9e/48/1ea60e74949eecb12cdd6ac43987f9fd331156388dcc2319b45e2ebb81bf/sphinx_copybutton-0.5.2-py3-none-any.whl", hash = "sha256:fb543fd386d917746c9a2c50360c7905b605726b9355cd26e9974857afeae06e", size = 13343, upload-time = "2023-04-14T08:10:20.844Z"} ] [[package]] @@ -2123,15 +2273,6 @@ wheels = [ {url = "https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331", size = 92072, upload-time = "2024-07-29T01:10:08.203Z"} ] -[[package]] -name = "tabulate" -sdist = {url = "https://files.pythonhosted.org/packages/ec/fe/802052aecb21e3797b8f7902564ab6ea0d60ff8ca23952079064155d1ae1/tabulate-0.9.0.tar.gz", hash = "sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c", size = 81090, upload-time = "2022-10-06T17:21:48.54Z"} -source = {registry = "https://pypi.org/simple"} -version = "0.9.0" -wheels = [ - {url = "https://files.pythonhosted.org/packages/40/44/4a5f08c96eb108af5cb50b41f76142f0afa346dfa99d5296fe7202a11854/tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f", size = 35252, upload-time = "2022-10-06T17:21:44.262Z"} -] - [[package]] name = "tomli" sdist = {url = "https://files.pythonhosted.org/packages/52/ed/3f73f72945444548f33eba9a87fc7a6e969915e7b1acc8260b30e1f76a2f/tomli-2.3.0.tar.gz", hash = "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549", size = 17392, upload-time = "2025-10-08T22:01:47.119Z"}