Metadata-Version: 2.4
Name: productinformationapi
Version: 0.2.0
Summary: Product information and stock checks with Product Information API
Author-email: HustleGotReal <contact@hustlegotreal.com>
License-Expression: MIT
Project-URL: Homepage, https://productinformationapi.com
Keywords: product-information,stock,scraping,api,sdk
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Operating System :: OS Independent
Classifier: Typing :: Typed
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Dynamic: license-file

# Product Information API for Python

Product information, stock checks and AI product discovery in Python 3.11+, with type hints and no runtime dependencies.

## Install

```sh
python -m pip install productinformationapi
```

Or install the distributed wheel with `python -m pip install ./productinformationapi-0.2.0-py3-none-any.whl`.

## Quick start

Use a tenant API key from your Product Information API account. MCP OAuth access tokens are scoped
to MCP and cannot authenticate these REST requests. Keep the key in your server environment:

```sh
export PRODUCTINFORMATIONAPI_API_KEY='your-api-key'
```

```python
from productinformationapi import ProductInformationAPI

client = ProductInformationAPI()  # Reads PRODUCTINFORMATIONAPI_API_KEY.
url = "https://www.amazon.com/dp/B0D3BCR3V7"

product = client.get_product_information(url)
print((product.get("productInformation") or {}).get("title"), product["creditsCharged"])

stock = client.check_stock(url)
print(stock["inStock"], stock["offers"], stock["creditsRemaining"])
```

Methods return the complete API response as a dictionary. Response keys retain the API's camelCase
names. The client is synchronous; in an asyncio application, use a worker thread:

```python
import asyncio

stock = await asyncio.to_thread(client.check_stock, url)
```

Cancelling the asyncio waiter does not cancel the worker thread's HTTP request; its socket timeout
still applies.

## Bulk

```python
batch = client.check_stock_bulk([
    "https://www.amazon.com/dp/B0D3BCR3V7",
    {"productUrl": "https://www.amazon.co.uk/dp/B0D3BCR3V7", "sourceSite": "amazon_gb"},
])

for row in batch["results"]:
    if row["httpStatus"] == 200:
        print(row["index"], row["result"])
    else:
        print(row["index"], row["httpStatus"], row["result"]["error"])

# Product information uses the same inputs:
products = client.get_product_information_bulk([
    "https://www.amazon.com/dp/B0D3BCR3V7",
])
```

Bulk calls return inline. A successful HTTP response can contain failed items; inspect each
`httpStatus`. The current default server limit is 100 items per batch. Items are never silently
split, retried, or reordered. Large batches may require a longer HTTP timeout.

## FindProducts

Start an asynchronous search with required natural-language `criteria`, exact `count` (integer 1–100;
booleans are rejected), ISO alpha-2 `country`, and `supplierScope`. Country is normalized to uppercase.
Each new search runs an AI agent. Availability depends on the API and your tenant's `products:find` access.

```python
import time
from uuid import uuid4
from productinformationapi import FindProductsRequest, FindProductsResult

request: FindProductsRequest = {
    "criteria": "Get 10 Christmas items that cost less than 15 USD from Amazon",
    "count": 10,
    "country": "US",
    "supplierScope": {"mode": "supported", "includeSupplierIds": ["amazon_us"]},
}
search_key = str(uuid4())  # Persist this key and request before starting.
search: FindProductsResult = client.find_products(request, idempotency_key=search_key)

# Explicit, bounded polling; each call reads the same search and never restarts it.
for _ in range(40):
    if search["status"] not in ("queued", "running"):
        break
    time.sleep(min(5, max(1, search["pollAfterSeconds"])))
    search = client.get_find_products(search["searchId"], timeout=10)

if search["status"] == "succeeded":
    if search["foundCount"] != search["requestedCount"] or len(search["products"]) != search["requestedCount"]:
        raise ValueError("Invalid exact-count result")
    print(search["products"])
elif search["status"] in ("queued", "running"):
    print("Still running; save the search ID to read later", search["searchId"], search["foundCount"])
else:
    print("Incomplete", search["status"], search["foundCount"], search["requestedCount"], search["reason"])
    print(search["products"], search["credits"]["charged"])  # Qualifying partial products may be charged.

# When you want to stop this search:
# search = client.cancel_find_products(search["searchId"])
```

`FindProductsRequest`, `FindProductsResult` and product/evidence types are exported `TypedDict`s;
their keys retain the API's camelCase names. HTTP 200/202 delivers a snapshot; only `succeeded` means
exactly the requested number of distinct qualifying product families. `queued`/`running` products
are provisional and `foundCount` can decrease after revalidation. `unfulfilled`, `stopped` and
`failed` are terminal shortfalls with counts, products and a reason. Cancellation is idempotent and
may first return `cancelRequested: True` while dispatched work finishes. It cannot undo committed
success. An HTTP timeout or cancelled asyncio waiter does not cancel the search job.

- `supplierScope: {"mode": "supported", "includeSupplierIds": ["amazon_us", "walmart_us"]}` searches
  the included suppliers together; `count` is the total. Omit includes for any available supplier in
  the target market, or subtract IDs with `excludeSupplierIds`.
- `supplierScope: {"mode": "open_web", "includeDomains": ["shop.example"], "excludeDomains": ["excluded.example"], "excludeSupplierIds": ["amazon_us"]}`
  enables public-web discovery, including unregistered merchants with `supplierId: None`. All three
  filters are optional. Domain filters cannot be used in supported mode, and open-web mode cannot
  include supplier IDs. Lists are unique with at most 64 entries; explicit inclusions cannot be empty.
- Country means selling/delivery market, not dispatch origin. Optional `postcode` specifies a
  destination; optional `dispatchCountry` requires origin evidence. Inspect `effectiveMarket` and
  product `marketEvidence`; a default site destination does not prove delivery to every address.
  Prices retain observed currency and are never converted to satisfy criteria.
- `count` is exact. Conflicting requested quantities in `criteria` require a corrected request;
  pack sizes and price amounts are separate. Variants and repeat offers do not fill extra slots.
- Optional `maxCredits` is a positive integer customer-credit ceiling. The API quotes/reserves the
  requested product count and settles qualifying products actually delivered, including labeled
  partial results, refunding the rest. Inspect `credits["quoted"]`, `"reserved"`, `"charged"`, and `"refunded"`.
  Server execution and provider-spending limits can stop work before the count is reached.

Start requires the keyword argument `idempotency_key`, never a key in the request dictionary.
After an uncertain start response, an explicit retry with the same key and identical request replays
the same job; changed input gives 409. Reads, cancellation and replay do not add product charges.
Terminal results never resume. Results are retained for 24 hours (`expiresAt`); retry identities for
30 days. A 410 raises `APIError` and never starts replacement work. Do not resubmit an old start after
30 days. Use a new key only when intentionally starting a new paid search.

## Options and errors

```python
from uuid import uuid4
from productinformationapi import APIError

request_key = str(uuid4())  # Retain this key if you retry this exact request.
try:
    stock = client.check_stock(
        url,
        idempotency_key=request_key,
        timeout=120,
        request_timeout_ms=30_000,
    )
    print(stock["inStock"])
except APIError as error:
    print(error.status, error.code, error.request_id, error.retry_after_s)
```

- Constructor: `ProductInformationAPI(api_key=None, *, base_url="https://api.productinformationapi.com", timeout=120)`.
- All methods accept `idempotency_key` and `timeout` in seconds. `timeout` is the standard-library
  socket timeout for blocking operations, not a guaranteed total wall-clock deadline.
- FindProducts start requires `idempotency_key`; other methods keep it optional.
- Singular known-URL methods accept `source_site`. `get_product_information` also accepts
  `include_gpsr=True` for Amazon DE GPSR details. Bulk requests do not accept GPSR options.
- `request_timeout_ms` applies only to singular stock requests, from 1000 to 100000 milliseconds.
  It shortens the server execution budget. Leave additional time in `timeout` for server cleanup.
- Timeouts do not prove the server did no work or charged no credits. There are no automatic
  retries. Reuse an idempotency key only for the same operation and payload; keep bulk items in
  the same order on a retry. Keys must contain 1–255 visible ASCII characters.
- `APIError` exposes `status`, `code`, `request_id`, `retryable`, `retry_after_s`, and the parsed
  response as `body`. HTML edge failures still produce `APIError`. Bulk item errors remain in
  `batch["results"]`. Transport failures retain standard-library exceptions such as `URLError`
  and `TimeoutError`.

## Webhooks

Scrape-result webhooks and asynchronous known-URL scrape jobs are not currently implemented by the
API. Known-URL methods return results directly; FindProducts uses explicit asynchronous
start/read/cancel methods. No webhook URL option is available.

## Support and license

Visit [Product Information API](https://productinformationapi.com) or contact
[contact@hustlegotreal.com](mailto:contact@hustlegotreal.com).

This SDK is MIT licensed. API access requires a separate account and is subject to the service's
terms and credit usage.
