generated api¶
this page renders signatures and docstrings from the installed package. the api guide explains the interface and the capability matrix, reliability gives the request counts, and the store pages cover each storefront's own models and extensions.
serialisation¶
supermercapy.to_dict ¶
to_dict(value: Any) -> Any
convert a model, or a collection of them, to json-compatible python.
a dataclass becomes a dict of its fields in declaration order, so the
result of a model is a dict and the result of a list or tuple of models,
such as get_catalog(), is a list. tuples, lists, and sets become lists
(sets are sorted by the string form of each converted item so the output is
deterministic), and dict keys become strings. Decimal becomes a string
such as "1.69": a float would round the price the storefront published.
dates and times become iso 8601 strings and enums become their value.
strings, numbers, booleans, and None pass through. anything else raises
TypeError naming its type.
supermercapy.to_json ¶
to_json(value: Any, *, indent: int | None = None) -> str
return value as a json string, converted by :func:to_dict.
non-ascii text is kept as written rather than escaped.
base client¶
supermercapy.BaseClient ¶
BaseClient(*, language: Language | str | None = None, timeout: float | Timeout = 10.0, retry_policy: RetryPolicy | None = None, min_request_interval: float | None = None, transport: BaseTransport | None = None, user_agent: str | None = None)
Bases: ABC
shared transport, retries, pacing, and the standard storefront interface.
subclasses set the class attributes, implement the four required methods,
override the optional methods whose capability they declare, and adjust
transport behaviour through the _prime, _prepare_request,
_classify, _on_auth_expired, and _on_challenge hooks.
store_id
property
¶
store_id: str | None
return the store, warehouse, or zone this client is bound to.
min_request_interval
property
¶
min_request_interval: float
return the minimum time between request starts in seconds.
supports
classmethod
¶
supports(capability: Capability) -> bool
return whether this store's client declares capability.
search_products
abstractmethod
¶
search_products(query: str, *, page_size: int | None = None, cursor: str | None = None) -> SearchResult
return one page of summaries; pass next_cursor back to continue.
get_product
abstractmethod
¶
get_product(product_id: str | int) -> Product
return a complete product record by store id.
get_categories
abstractmethod
¶
get_categories() -> tuple[Category, ...]
return the root of the storefront category tree.
get_category
abstractmethod
¶
get_category(category_id: str | int) -> Category
return one category with its children and listed products.
iter_search ¶
iter_search(query: str, *, page_size: int | None = None) -> Iterator[ProductSummary]
yield summaries for query across every page.
get_catalog ¶
get_catalog() -> tuple[ProductSummary, ...]
return every catalog summary once, keyed on product id.
download ¶
download(source: Photo | str, destination: str | PathLike[str]) -> Path
stream an image to destination through an atomic replacement.
from_postal_code
classmethod
¶
from_postal_code(postal_code: str, *, language: Language | str | None = None, timeout: float | Timeout = 10.0, retry_policy: RetryPolicy | None = None, min_request_interval: float | None = None, transport: BaseTransport | None = None, user_agent: str | None = None) -> Self
resolve a postal code to a store binding and return a client.
list_stores ¶
list_stores(postal_code: str | None = None) -> tuple[Store, ...]
return the stores or zones the storefront can be bound to.
iter_catalog ¶
iter_catalog() -> Iterator[ProductSummary]
yield every catalog summary, possibly with duplicates.
get_product_by_ean ¶
get_product_by_ean(ean: str) -> Product
return a complete product record by its ean.
get_new_arrivals ¶
get_new_arrivals() -> tuple[ProductSummary, ...]
return the products the storefront presents as new.
get_home ¶
get_home() -> tuple[HomeSection, ...]
return the ordered sections of the storefront home page.
supermercapy.RetryPolicy
dataclass
¶
RetryPolicy(max_attempts: int = 3, backoff_factor: float = 0.25, max_delay: float = 5.0, jitter_ratio: float = 0.1)
bounded retry settings for connection and transient http failures.
store clients¶
supermercapy.Mercadona ¶
Mercadona(warehouse: str, *, language: Language | str | None = None, timeout: float | Timeout = 10.0, retry_policy: RetryPolicy | None = None, min_request_interval: float | None = None, transport: BaseTransport | None = None, user_agent: str | None = None)
Bases: BaseClient
a reusable synchronous client scoped to one mercadona warehouse.
from_postal_code
classmethod
¶
from_postal_code(postal_code: str, *, language: Language | str | None = None, timeout: float | Timeout = 10.0, retry_policy: RetryPolicy | None = None, min_request_interval: float | None = None, transport: BaseTransport | None = None, user_agent: str | None = None) -> Self
resolve a postal code with one request and return a scoped client.
search_products ¶
search_products(query: str, *, page_size: int | None = None, cursor: str | None = None, top_level_category_id: str | int | None = None) -> MercadonaSearchResult
search one page of product summaries.
the cursor is the zero-based page number as a string; None requests
the first page.
get_product ¶
get_product(product_id: str | int) -> MercadonaProduct
return a complete product record by id.
get_categories ¶
get_categories() -> tuple[MercadonaCategory, ...]
return the storefront category tree.
get_category ¶
get_category(category_id: str | int) -> MercadonaCategory
return one category with its groups and their products.
only the second level has a category page: a top-level id answers
http 404 there. on that 404 the category tree is fetched, one more
request, and a root found in it is returned as the tree spells it,
with its second-level categories as children and no products. an id
the tree does not hold as a root, such as one of the groups below a
second-level category, raises :class:~supermercapy.NotFoundError.
list_stores ¶
list_stores(postal_code: str | None = None) -> tuple[Store, ...]
resolve postal_code to its warehouse and return it as a store.
iter_catalog ¶
iter_catalog() -> Iterator[MercadonaProductSummary]
yield summaries from the category tree and each listed group.
get_catalog ¶
get_catalog() -> tuple[MercadonaProductSummary, ...]
collect deduplicated summaries from all listed category groups.
get_new_arrivals ¶
get_new_arrivals() -> tuple[MercadonaProductSummary, ...]
return the current new-arrival product summaries.
get_indexed_catalog ¶
get_indexed_catalog() -> CatalogResult
collect the complete algolia index through top-category partitions.
an index that exposes no category facets is partitioned by score ranges instead: numeric filters work on every index, and a range that reports more hits than the result cap is halved until it fits. records without a score, or more tied scores than the cap, leave the result unreconciled rather than silently short.
download_photo ¶
download_photo(photo: MercadonaPhoto, destination: str | PathLike[str], *, width: int | None = None, height: int | None = None, fit: PhotoFit | str = CROP) -> Path
download a photo at an optional size through an atomic replacement.
supermercapy.Consum ¶
Consum(zone: int | str | None = None, *, drop_sponsored: bool = True, language: Language | str | None = None, timeout: float | Timeout = 10.0, retry_policy: RetryPolicy | None = None, min_request_interval: float | None = None, transport: BaseTransport | None = None, user_agent: str | None = None)
Bases: BaseClient
a reusable synchronous client, optionally scoped to one consum zone.
a zone narrows the assortment to what one store carries; prices are the
same nationwide. an unbound client sees a default assortment, not a
superset of the zones. an unrecognised zone is answered with a catalog
rather than an error, so resolve one with :meth:from_postal_code or
:meth:list_stores instead of guessing.
drop_sponsored
property
¶
drop_sponsored: bool
whether injected retail-media rows are removed from listings.
from_postal_code
classmethod
¶
from_postal_code(postal_code: str, *, method: DeliveryMethod | str = HOME, drop_sponsored: bool = True, language: Language | str | None = None, timeout: float | Timeout = 10.0, retry_policy: RetryPolicy | None = None, min_request_interval: float | None = None, transport: BaseTransport | None = None, user_agent: str | None = None) -> Self
resolve a postal code with one request and return a scoped client.
the first area the storefront lists for the postal code wins; use
:meth:list_stores when a postal code maps to more than one.
search_products ¶
search_products(query: str, *, page_size: int | None = None, cursor: str | None = None, order_by: SortOrder | int | None = None, filters: ProductFilters | None = None, include_filters: bool = False) -> ConsumSearchResult
search one page of products.
the cursor is the next offset as a string; None starts at zero.
get_categories ¶
get_categories() -> tuple[ConsumCategory, ...]
return the whole category tree, already nested, in one request.
get_category ¶
get_category(category_id: str | int) -> ConsumCategory
return one category from the tree, with its first page of products.
the tree carries no products, so this costs one request for the tree and one for the listing.
list_stores ¶
list_stores(postal_code: str | None = None, *, method: DeliveryMethod | str = HOME) -> tuple[ConsumStore, ...]
return the areas serving postal_code for one delivery method.
iter_catalog ¶
iter_catalog() -> Iterator[ConsumProduct]
yield the whole catalog by walking offsets a hundred rows at a time.
get_product_by_ean ¶
get_product_by_ean(ean: str) -> ConsumProduct
return one product by its ean, in a single request.
get_new_arrivals ¶
get_new_arrivals() -> tuple[ConsumProduct, ...]
return every product the storefront flags as new, newest first.
the listing is walked a hundred rows per request until it ends.
get_offers ¶
get_offers(*, include_deferred: bool = False) -> tuple[ConsumProduct, ...]
return every product carrying an immediate discount.
the listing is walked a hundred rows per request until it ends, which
was eleven requests for a thousand offers in october 2026.
include_deferred widens the result to products that credit money
back instead of cutting the shelf price.
get_category_products ¶
get_category_products(category_id: str | int, *, page_size: int | None = None, cursor: str | None = None, order_by: SortOrder | int | None = None, filters: ProductFilters | None = None, include_filters: bool = False) -> ConsumSearchResult
list one page of a category, descendants included.
get_products ¶
get_products(codes: Iterable[str | int]) -> tuple[ConsumProduct, ...]
return several products in one request, in the order given.
codes the selected zone does not carry are omitted from the result rather than raising, so compare what comes back against what you asked for.
get_groups ¶
get_groups() -> tuple[ConsumGroup, ...]
return the promotional campaigns and their nested subgroups.
get_group_products ¶
get_group_products(code: str, *, page_size: int | None = None, cursor: str | None = None, order_by: SortOrder | int | None = None) -> ConsumSearchResult
list one page of a campaign, selected by its :class:ConsumGroup code.
get_sort_orders ¶
get_sort_orders() -> tuple[SortOption, ...]
return the sort orders the storefront currently advertises.
suggest ¶
suggest(query: str, *, limit: int = 5) -> tuple[Suggestion, ...]
return spelling completions for a partial query.
suggest_tags ¶
suggest_tags(query: str) -> tuple[Suggestion, ...]
return the refinement tags the search box offers for a query.
download_photo ¶
download_photo(photo: ConsumPhoto, destination: str | PathLike[str], *, size: ImageSize | str = MEDIUM) -> Path
download one image at a rendition through an atomic replacement.
supermercapy.Plusfresc ¶
Plusfresc(center: int | str = DEFAULT_CENTER, *, language: Language | str | None = None, timeout: float | Timeout = 10.0, retry_policy: RetryPolicy | None = None, min_request_interval: float | None = None, transport: BaseTransport | None = None, user_agent: str | None = None)
Bases: BaseClient
a reusable synchronous client scoped to one plusfresc preparation center.
a center decides the assortment; prices looked the same across the two
centers compared during reconnaissance, but only one category was checked,
so do not assume they are national. resolve one with
:meth:from_postal_code or :meth:list_stores rather than guessing;
center 12 (lleida) is the storefront's own default.
the catalog, the category tree, and the postcode lookup need no credentials. search, product details, and the extended sheet need a guest token, which the client mints on demand with an unauthenticated post and refreshes a minute before it expires.
store_id
property
¶
store_id: str | None
return the center id as a string; the same value as center.
from_postal_code
classmethod
¶
from_postal_code(postal_code: str, *, language: Language | str | None = None, timeout: float | Timeout = 10.0, retry_policy: RetryPolicy | None = None, min_request_interval: float | None = None, transport: BaseTransport | None = None, user_agent: str | None = None) -> Self
resolve a postal code with one request and return a scoped client.
plusfresc delivers around lleida, barcelona, and tarragona only; a
postcode outside that footprint is answered with an http 409 and
raises :class:~supermercapy.OutOfCoverageError.
search_products ¶
search_products(query: str, *, page_size: int | None = None, cursor: str | None = None) -> PlusfrescSearchResult
search one page of products.
the backend takes no paging parameter of any kind and answers with at
most a hundred rows, so every page is carved out of one response and
the cursor is an offset into rows already fetched. a page whose
truncated flag is set sat at the cap, which means total_hits
is a floor rather than a total and the rest of the matches cannot be
reached; walk the category tree instead when you need them all.
the query travels as a path segment, so the characters the backend
cannot route there (/ \ % & : * ? < > +) are sent as spaces and
trailing dots are dropped; query on the result keeps the original.
get_product ¶
get_product(product_id: str | int) -> PlusfrescProduct
return one product by its six-digit item id.
the composite placement id a listing row calls id is not accepted
upstream and raises :class:~supermercapy.NotFoundError.
get_categories ¶
get_categories() -> tuple[PlusfrescCategory, ...]
return the whole category tree, already nested, in one request.
get_category ¶
get_category(category_id: str | int, *, page_size: int | None = None) -> PlusfrescCategory
return one category with its direct children and a page of products.
the tree carries no products, so this costs one request for the node
and one for its listing. the node endpoint names no parent, so
parent_id is None here; :meth:get_categories sets it.
iter_catalog ¶
iter_catalog() -> Iterator[PlusfrescProduct]
yield the whole catalogue from the one listing of the Root node.
the backend answers with every product of the center in a single
uncached response of several megabytes, so this costs one request
and no token. a product placed under more than one category comes
back once per placement; :meth:get_catalog deduplicates by id.
gate repeated walks behind :meth:get_catalog_version.
list_stores ¶
list_stores(postal_code: str | None = None) -> tuple[PlusfrescStore, ...]
return every store and locker, or only those of one postcode's center.
a locker is listed under the center that prepares its orders, so every
id here can be passed straight back to the constructor.
get_new_arrivals ¶
get_new_arrivals() -> tuple[PlusfrescProduct, ...]
return the products the storefront collects under "novetats".
no row carries a novelty flag of its own, so membership of that category is what marks a product new here.
get_offers ¶
get_offers(*, highlighted: bool = False) -> tuple[PlusfrescProduct, ...]
return the products of the web-offers category.
highlighted swaps it for the smaller carousel of featured
campaigns the storefront promotes on its home page.
get_category_products ¶
get_category_products(category_id: str | int, *, page_size: int | None = None, cursor: str | None = None) -> PlusfrescSearchResult
list one page of a category, descendants included.
the endpoint answers with the whole category at once and takes no
paging parameter, so the page is carved out client-side just as it is
for search. Root is the whole catalogue, which is several
megabytes; gate it behind :meth:get_catalog_version.
centers ¶
centers() -> tuple[PlusfrescStore, ...]
return the eight preparation centers, without their lockers.
get_catalog_version ¶
get_catalog_version() -> date
return the date the catalogue last changed.
the storefront caches its whole tree until this date moves, and so can a caller: nothing else here is cacheable, every listing is served uncached and the catalogue root alone is several megabytes.
get_units ¶
get_units() -> tuple[PlusfrescUnit, ...]
return the unit-of-measure codes a product's unit_measure uses.
download_photo ¶
download_photo(photo: PlusfrescPhoto, destination: str | PathLike[str], *, size: ImageSize | str = LARGE) -> Path
download one image at a rendition through an atomic replacement.
supermercapy.Bonarea ¶
Bonarea(*, language: Language | str | None = None, timeout: float | Timeout = 10.0, retry_policy: RetryPolicy | None = None, min_request_interval: float | None = None, transport: BaseTransport | None = None, user_agent: str | None = None)
Bases: BaseClient
a reusable synchronous client for the bonàrea online shop.
there is nothing to bind: the catalogue, the prices and the stock figures
are national, so no postal code, zone or store selects them. language is
the only axis, and it is the one piece of session state the storefront
keeps — see :meth:_prime.
every read is a form post to /{language}/shop/{action}, and none of
them paginates: one search returns every hit and one category listing
returns every article, so responses are large and paging happens here.
search_products ¶
search_products(query: str, *, page_size: int | None = None, cursor: str | None = None) -> BonareaSearchResult
return one page of hits, sliced from the whole result set.
the storefront takes no page, size or sort parameter and answers every
query with all of its hits at once, so a cursor walk re-requests the
same response; :meth:iter_search streams it in one request instead.
iter_search ¶
iter_search(query: str, *, page_size: int | None = None) -> Iterator[BonareaProduct]
yield every hit for query from the single response search returns.
page_size is validated and then ignored: there are no pages to
fetch, so streaming never issues a second request.
get_product ¶
get_product(product_id: str | int) -> BonareaProduct
return one article, in either the 13*5361 or 13_5361 spelling.
an unknown id is answered with {"article": null} and http 200
rather than a 404, which becomes a :class:NotFoundError here.
get_categories ¶
get_categories() -> tuple[BonareaCategory, ...]
return the whole category tree, already nested, in one request.
every listing response embeds the tree, so asking for a blank listing is the cheapest way to fetch it.
get_category ¶
get_category(category_id: str | int) -> BonareaCategory
return one category with its children and every article it lists.
one request answers both, because a listing response carries the whole tree alongside its articles. a menu-level category resolves to a node with children and no products: only the level the tree calls a listing level returns articles.
an unknown reference makes the storefront throw, and it answers with
an http 500 error page rather than a 404. that page is not retried,
and the id is checked against the tree with one more request before
it is reported as :class:NotFoundError; an id the tree does know
re-raises the original error.
iter_catalog ¶
iter_catalog() -> Iterator[BonareaProduct]
yield every article by walking one listing per branch of the tree.
one request fetches the tree and one more follows for each listing
node, which is 493 of them at the time of writing. articles listed
under more than one category are yielded more than once;
:meth:get_catalog deduplicates them by id.
new_products ¶
new_products(category_id: str | int | None = None) -> tuple[BonareaProduct, ...]
return the articles badged as new, deduplicated by id.
bonàrea publishes no novelties endpoint, so this filters listings on
the :attr:Characteristic.NEW badge. one category costs one request;
passing none walks the whole catalog.
price_drops ¶
price_drops(category_id: str | int | None = None) -> tuple[BonareaProduct, ...]
return the articles badged as reduced, deduplicated by id.
the badge is the only price-drop signal bonàrea publishes: no previous
price accompanies it. the cost is the same as :meth:new_products.
delivery_zones ¶
delivery_zones(province: str | int, town: str | None = None) -> tuple[str, ...]
return the postal codes bonàrea delivers to inside one province.
province is the two-digit spanish province code, and town
narrows the answer to one town. this is delivery coverage only: it
selects nothing, because the catalogue and its prices are national.
get_product_sheet ¶
get_product_sheet(product_id: str | int) -> str
return the storefront's pre-rendered product modal as raw html.
:meth:get_product is the structured route; this is the fallback for
anything the json omits but the modal renders.
download_photo ¶
download_photo(photo: BonareaPhoto, destination: str | PathLike[str], *, width: int | None = None, height: int | None = None) -> Path
download one image, optionally resized by the cdn, atomically.
omitting both dimensions downloads the original upload; the storefront itself asks for 500 px in a grid and 1000 px on a product page.
supermercapy.Carrefour ¶
Carrefour(sale_point: str | int | None = None, *, catalog: str = DEFAULT_CATALOG, language: Language | str | None = None, timeout: float | Timeout = 10.0, retry_policy: RetryPolicy | None = None, min_request_interval: float | None = None, transport: BaseTransport | None = None, user_agent: str | None = None)
Bases: BaseClient
a reusable synchronous client for carrefour's spanish food storefront.
the client talks to two hosts. searching, autocomplete and every lookup by id or barcode go to the empathy search index carrefour's own storefront calls, which is unauthenticated, needs no cookies and is the only path that honours a per-request sale point. product pages, category listings, stock figures and the sale-point directory come from the storefront itself, which sits behind cloudflare.
that gate is a user-agent check, so the default user agent here is a
browser string rather than supermercapy's own; replacing it with a library
user agent makes every storefront request fail with http 403. cold
requests are also only about three times in four reliable, so the first
storefront request of a session warms a cookie jar with one html fetch
and :meth:_prime renews it every twenty-five minutes. requests that
never touch the storefront never pay for a warm-up.
a sale point changes both prices and assortment, exactly like mercadona's
warehouse. bind one with :meth:from_postal_code or pick an id out of
:meth:list_stores; leaving it unset searches carrefour's national
superset, which prices nothing.
sale_point
property
¶
sale_point: str | None
return the six-character sale point this client prices against.
store_id
property
¶
store_id: str | None
return the sale point id; the same value as :attr:sale_point.
from_postal_code
classmethod
¶
from_postal_code(postal_code: str, *, catalog: str = DEFAULT_CATALOG, language: Language | str | None = None, timeout: float | Timeout = 10.0, retry_policy: RetryPolicy | None = None, min_request_interval: float | None = None, transport: BaseTransport | None = None, user_agent: str | None = None) -> Self
bind the sale point that serves postal_code.
carrefour publishes no postcode-to-sale-point resolver: the postcode endpoint answers with physical shops, whose ids belong to a different namespace, and the storefront picks a sale point by mutating session state. so this costs two requests — the shops near the postcode, then the drive directory — and binds the sale point in the same postcode if there is one, otherwise the one nearest the closest shop.
a postcode carrefour does not serve raises
:class:~supermercapy.OutOfCoverageError.
search_products ¶
search_products(query: str, *, page_size: int | None = None, cursor: str | None = None, sort: str | None = None) -> CarrefourSearchResult
search one page of products on the direct index.
the query is lowercased before it is sent, as carrefour's own client
does, so both share the cdn's cache key. sort takes the index's
own spelling — "price asc", "best_sellers_food desc" — and an
unsortable field is refused upstream rather than ignored.
one query reaches at most 2,998 documents: the offset ceiling is
2,498 and a page at most 500 rows. a page whose truncated flag is
set sat against that ceiling with matches left over.
get_product ¶
get_product(product_id: str | int) -> CarrefourProduct
return one complete product, nutrition included, from its page.
ids come in four unrelated spellings and are opaque strings, never integers. the slug in a product url is not load-bearing: this asks for a placeholder one and follows the redirect carrefour answers with, which is also how an unknown id is detected — it redirects to the home page instead of to a product.
get_product_by_ean ¶
get_product_by_ean(ean: str) -> CarrefourProduct
return one complete product by its barcode.
the index has no barcode endpoint; a barcode is matched as ordinary free text, which returns exactly one document when it is known. the document is then fetched again as a page, because nutrition lives nowhere else, and the barcode is checked against it so a fuzzy text match cannot answer with the wrong product.
get_categories ¶
get_categories(*, deep: bool = False) -> tuple[CarrefourCategory, ...]
return the food departments from the storefront's navigation menu.
one request returns the ten departments, named in the client's
language. deep=True adds each department's aisles at one more
request per department. the menu stops at the aisles; the leaves
under an aisle are the children :meth:get_category returns for it.
the menu answers without a warm session, so neither costs the
warm-up page unless cloudflare challenges the first request.
get_category ¶
get_category(category_id: str | int, *, offset: int = 0) -> CarrefourCategory
return one category with its children and one page of its products.
everything comes from the category's listing page, which answers any
placeholder slug, so this costs one page fetch. the page is fixed at
24 products and floors offset to a multiple of 24; the page size
cannot be set from the url at all. an unknown id redirects to the
home page, which raises :class:~supermercapy.NotFoundError.
list_stores ¶
list_stores(postal_code: str | None = None) -> tuple[CarrefourStore, ...]
return the sale points, or the physical shops near one postcode.
without a postcode this is the drive directory, and every id in it can be passed straight back to the constructor. with one it is the shops near that postcode, ordered by distance; those ids identify buildings rather than sale points and the constructor does not accept them.
get_home ¶
get_home() -> tuple[CarrefourHomeSection, ...]
return the product carousels of the supermarket home page.
the page is a cms page whose carousels arrive already filled, in page order, with their titles in the cms documents beside them. it is also the warm-up page, so a cold client fetches it twice.
get_category_products ¶
get_category_products(category_url: str, *, offset: int = 0) -> CarrefourListing
return one rendered page of a category listing by its url.
the url comes from a :class:CarrefourCategory. sponsored rows are
dropped: the ad-injected carousels are never read and a card carrying
an ad id inside the grid is flagged is_sponsored.
get_stock ¶
get_stock(product_id: str | int) -> int | None
return how many units of a product the session's sale point holds.
stock is published by carrefour's own proxied search and by nothing
else. that proxy ignores store, so this figure belongs to the
session's default sale point rather than to :attr:sale_point; the
two agree only when the session already sits on that sale point.
the proxy also refuses a request without a session parameter with
a firewall block rather than an api error, so one is always sent; its
value is never read.
suggest ¶
suggest(query: str, *, limit: int = 5) -> tuple[str, ...]
return the query completions the search box would offer.
download_photo ¶
download_photo(photo: CarrefourPhoto, destination: str | PathLike[str], *, width: int | None = None) -> Path
download one image, optionally re-rendered at width pixels.
supermercapy.Lidl ¶
Lidl(region: int | str | None = None, *, zone: PriceZone | str = PENINSULA, family: ProductFamily | str | None = None, language: Language | str | None = None, timeout: float | Timeout = 10.0, retry_policy: RetryPolicy | None = None, min_request_interval: float | None = None, transport: BaseTransport | None = None, user_agent: str | None = None)
Bases: BaseClient
a reusable synchronous client for lidl's spanish storefront.
lidl publishes two assortments through one api: the mail-order shop, priced
per delivery zone, and the in-store grocery range, priced per offer
region. both geographies travel inside every response, so binding one is
a client-side selection that costs no extra request; resolve a pair with
:meth:from_postal_code or pick one from :meth:list_stores.
grocery prices are promotional only. lidl publishes this week's and next week's campaign prices and permanent reductions, not a full shelf-price list, so a grocery product outside a campaign carries no price at all.
the capability set follows the reconnaissance: EAN_LOOKUP is absent
because no endpoint takes an ean, NUTRITION because no ingredient,
allergen, or nutrition field exists anywhere in the public apis, and
NEW_ARRIVALS and HOME because their surfaces are cms html with no
product json behind them.
family
property
¶
family: ProductFamily | None
return the assortment half listings are filtered to, if any.
store_id
property
¶
store_id: str | None
return the offer region as a string; the same value as region.
from_postal_code
classmethod
¶
from_postal_code(postal_code: str, *, family: ProductFamily | str | None = None, language: Language | str | None = None, timeout: float | Timeout = 10.0, retry_policy: RetryPolicy | None = None, min_request_interval: float | None = None, transport: BaseTransport | None = None, user_agent: str | None = None) -> Self
bind the offer region and price zone of the nearest store.
the stores api has no postal-code filter, so this pages the whole
spanish store list once and matches on it: a store in the same postal
code first, then the first store of the same province. a postal code
with neither raises :class:~supermercapy.OutOfCoverageError.
search_products ¶
search_products(query: str, *, page_size: int | None = None, cursor: str | None = None, family: ProductFamily | str | None = None, sort: str | None = None) -> LidlSearchResult
search one page of both assortments.
the cursor is the next offset as a string; None starts at zero.
family narrows the page to one assortment, defaulting to the
client's own filter. the search index holds groceries, but only for
real keyword queries.
get_product ¶
get_product(product_id: str | int) -> LidlProduct
return one product, by either its parent or its variant id.
the detail endpoint accepts parent ids only, so a variant id — the parent plus three digits — is normalised before the request.
get_categories ¶
get_categories() -> tuple[LidlCategory, ...]
return the roots of the shop category tree, read from one facet.
there is no category endpoint: /q/api/category is routed but
answers http 404 for spain, so the roots come from the category
facet of an empty listing. that facet does not expand them, so their
children are empty; :meth:get_category returns one level more.
grocery items are not in the tree — their taxonomy is
:attr:LidlProduct.category_path.
get_category ¶
get_category(category_id: str | int) -> LidlCategory
return one shop category at any depth, its children, and a page.
list_stores ¶
list_stores(postal_code: str | None = None) -> tuple[LidlStore, ...]
return every spanish store, or the ones in one postal code.
the api pages two hundred and fifty stores at a time and takes no postal-code filter, so a postal code is matched on the full list.
iter_catalog ¶
iter_catalog(*, family: ProductFamily | str | None = None) -> Iterator[LidlProduct]
yield every catalogued product, hydrating one sitemap id at a time.
the sitemap holds about six thousand shop ids and six hundred grocery
ids, and each one costs a request, so pass family — or bind one on
the client — to walk half of it. ids the storefront has already
retired are skipped rather than raising.
get_offers ¶
get_offers(*, week: OfferWeek | str = CURRENT) -> tuple[LidlProduct, ...]
return one week's grocery campaign in a single request.
week names one of the campaign pages lidl publishes:
current, next, weekend, weekend_next,
permanent_cuts, or other_brands. next week's page prices its
products through :attr:LidlProduct.future_prices rather than
:attr:~supermercapy.Product.price, and from sunday so does the
current page, which already lists the week that starts on monday.
get_campaign_products ¶
get_campaign_products(slug: str, campaign_id: str | int) -> tuple[LidlProduct, ...]
return the products of any /c/<slug>/a<id> campaign page.
campaign listings have no json endpoint, so the page is fetched as
html and each tile's data-grid-data attribute — the same gridbox a
search hit carries — is parsed out of it. the pages are half a
megabyte and more, so cache what comes back.
iter_catalog_ids ¶
iter_catalog_ids(*, family: ProductFamily | str | None = None) -> Iterator[str]
yield the product ids of the sitemap, one request for all of them.
the leading digits split the two assortments — 10 is the online
shop, 11 the in-store grocery range — so a family is filtered
without fetching anything.
get_leaflets ¶
get_leaflets(*, region: int | str | None = None, store: str | None = None) -> tuple[LidlLeaflet, ...]
return the leaflets on offer, narrowed to a region or a store.
region defaults to the client's binding; pass 0 for the
national ones and leave both arguments out on an unbound client to
enumerate every leaflet.
get_leaflet ¶
get_leaflet(identifier: str, *, region: int | str | None = None, store: str | None = None) -> LidlLeaflet
return one leaflet with its pages, by slug or by uuid.
food leaflets carry page images and no product data at all; only bazar
leaflets fill :attr:LidlLeaflet.products. slugs embed the week and a
random suffix and change every week, so resolve one through
:meth:get_leaflets rather than caching it.
supermercapy.Bonpreu ¶
Bonpreu(*, language: Language | str | None = None, timeout: float | Timeout = 10.0, retry_policy: RetryPolicy | None = None, min_request_interval: float | None = None, transport: BaseTransport | None = None, user_agent: str | None = None)
Bases: BaseClient
a reusable synchronous client for the bonpreu and esclat online shop.
the storefront runs on ocado smart platform and serves anonymous json from one host, with no key, no binding and no postcode step: the tenant has a single region, and a delivery destination a hundred kilometres away resolves to that same region with the cart reporting its assortment unchanged. language is the only lever, and it travels as a cookie.
an aws waf budget is the real constraint. it is per ip, worth twenty to
thirty requests cold in september 2026 and eight to thirteen in october,
and once spent it answers http 202 with an empty body for tens of minutes;
pacing delays it but does not prevent it, and every challenged request
appears to spend from it again. so a challenge raises
:class:~supermercapy.ChallengedError after one request and is never retried,
and min_request_interval defaults to two seconds. the category tree
was the one path never seen challenged, which makes :meth:get_categories
the cheap first call.
the same waf refuses /api/ outright to a user agent it does not take
for a browser, with cloudfront's http 403 "request blocked" page, so the
default user agent is a browser string. that refusal raises
:class:~supermercapy.BlockedError; the origin's own 403, a missing csrf
token, is told apart by the requestid header it carries.
CATALOG is deliberately not declared. the catalogue is twenty-one
thousand products and the only ways to walk it — paging every leaf, or the
sitemap plus one sheet request each — both need far more requests than the
budget allows, so an iter_catalog that always dies part way through
would be a promise the store cannot keep. EAN is absent because no
barcode exists anywhere in the api, and POSTAL_CODE, STORES and
HOME because there is nothing for them to select or serve.
region_id
property
¶
region_id: str
return the one region the tenant serves; there is nothing to select.
search_products ¶
search_products(query: str, *, page_size: int | None = None, cursor: str | None = None, category_id: str | int | None = None, sort: SortOption | str | None = None, filters: object = None) -> BonpreuSearchResult
search one page of products.
the cursor is the storefront's own nextPageToken, passed back
verbatim. it is session scoped, so it only works from the client that
produced it — the cookies that scope it live on this client and are
replayed for you. category_id narrows the search to one category,
sort reorders it, and filters takes a mapping of filter group
id to one or more attribute ids.
queries longer than fifty characters are truncated, as the storefront truncates its own.
get_product ¶
get_product(product_id: str | int) -> BonpreuProduct
return one product sheet by its numeric retailerProductId.
the internal product uuid that listings, similar and related speak is
not accepted here; it is kept on
:attr:~supermercapy.bonpreu.BonpreuProduct.product_uuid.
get_categories ¶
get_categories(*, depth: int = MAX_CATEGORY_DEPTH) -> tuple[BonpreuCategory, ...]
return the whole category tree, nested, in one request.
this is the one path that kept answering while the challenge was
active, so treat it as the cheap call and the health check. depth
trims the tree to fewer levels; the default is the full four.
get_category ¶
get_category(category_id: str | int, *, page_size: int | None = None, sort: SortOption | str | None = None, filters: object = None) -> BonpreuCategory
return one category with its children and its first page of products.
an unknown category answers http 404, which raises
:class:~supermercapy.NotFoundError. until september 2026 it answered with
the root pseudo-category and a generic page instead, so the category
the listing reports serving is still checked against the one that was
asked for, and a mismatch raises the same error.
get_new_arrivals ¶
get_new_arrivals() -> tuple[BonpreuProduct, ...]
return every product the storefront collects under "novetats".
membership of that category is what marks a product new here; the
isNew flag on a row is about the row, not the category. the
category is read in pages of three hundred, the largest the listing
serves, until the count it reports on the first page is reached: two
requests for the 510 products it held in october 2026.
get_offers ¶
get_offers() -> tuple[BonpreuProduct, ...]
return the first page of the shop-wide promotions listing.
:meth:get_promotions is the same listing with paging, a category
filter, and the page's own filter and sort metadata.
get_promotions ¶
get_promotions(*, category_id: str | int | None = None, retailer_category_id: str | int | None = None, page_size: int | None = None, cursor: str | None = None, sort: SortOption | str | None = None, filters: object = None) -> BonpreuSearchResult
return one page of the promotions listing, whole shop or one category.
unlike the category listing this one takes either identifier: a
category_id uuid or the hierarchical retailer_category_id code
such as "0301".
suggest ¶
suggest(term: str, *, limit: int = DEFAULT_SUGGESTION_LIMIT) -> tuple[str, ...]
return the autocomplete suggestions for a partial search term.
get_similar ¶
get_similar(product_id: str | int) -> tuple[BonpreuProduct, ...]
return the products the storefront offers as similar to one product.
the endpoint answers with internal uuids alone, so the rows are resolved with one batch request; an empty answer costs nothing more.
get_related ¶
get_related(product_id: str | int, *, limit: int | None = None, high_relevance_only: bool = False) -> tuple[BonpreuProduct, ...]
return the products the storefront cross-sells with one product.
supermercapy.Dia ¶
Dia(postal_code: str | None = None, *, language: Language | str | None = None, timeout: float | Timeout = 10.0, retry_policy: RetryPolicy | None = None, min_request_interval: float | None = None, transport: BaseTransport | None = None, user_agent: str | None = None)
Bases: BaseClient
a reusable synchronous client for dia's online shop at www.dia.es.
every call is anonymous json under /api/v1/. the session cookie the
server sets carries the two things worth choosing, the delivery postcode
and the language, so the client sets both on its own session before its
first catalogue request and restates them while it lives. a postcode
changes the assortment and the stock, and now and then a price; an
unbound client sees the storefront's default postcode, 28041 (madrid) in
october 2026.
the home page and every other html page refuse a library client with an
akamai "access denied" page; the json api does not, and answers a library
user agent. both refusals raise :class:~supermercapy.BlockedError.
STORES is left out because there is no store list: the postcode check
names one store id and nothing else about it. EAN and EAN_LOOKUP
are left out because no barcode is published, HOME because the home
page is html behind the refusal, and FUTURE_PRICES because nothing
carries a date.
postal_code
property
¶
postal_code: str | None
return the postcode the session is bound to, or None for the default.
physical_store_id
property
¶
physical_store_id: str | None
return the store serving the postcode, as :meth:from_postal_code found.
from_postal_code
classmethod
¶
from_postal_code(postal_code: str, *, language: Language | str | None = None, timeout: float | Timeout = 10.0, retry_policy: RetryPolicy | None = None, min_request_interval: float | None = None, transport: BaseTransport | None = None, user_agent: str | None = None) -> Self
check that dia delivers to a postcode and return a client bound to it.
two requests: the storefront's own delivery check, which also names
the store that would serve the postcode, and the move of the client's
anonymous session to it. a postcode dia does not deliver to raises
:class:~supermercapy.OutOfCoverageError after the first.
search_products ¶
search_products(query: str, *, page_size: int | None = None, cursor: str | None = None) -> DiaSearchResult
search one page of products.
the cursor is the next row offset as a string. the storefront pages by
number with at least thirty rows a page and refuses a page above
fifty, so the client picks the page that holds the rows asked for and
trims it, which keeps any page_size from one to a thousand exact.
get_product ¶
get_product(product_id: str | int) -> DiaProduct
return one product sheet by its sku.
dia answers an unknown sku with http 500 rather than 404, so a 500
from the sheet is checked with one search for the sku: no hit raises
:class:~supermercapy.NotFoundError, a hit means the sheet really
failed and raises :class:~supermercapy.TransportError.
get_categories ¶
get_categories() -> tuple[DiaCategory, ...]
return the two-level category tree in one request.
get_category ¶
get_category(category_id: str | int) -> DiaCategory
return one category with its children and its first listing page.
this costs one request for the tree and one for the listing. a
top-level category lists its children one after another, so its first
page holds the first child's products; product_count is the whole
category's. "novedades y recomendados" has no listing of its own (the
storefront redirects it to its first child), so it comes back without
products.
iter_catalog ¶
iter_catalog() -> Iterator[DiaProduct]
yield every product by walking each top-level category in turn.
one request for the tree, then one per twenty rows of each child
category: an estimated 450 requests for the 6,500 rows the tree held in
october 2026. a product listed in two categories is yielded twice;
:meth:get_catalog keeps one.
get_new_arrivals ¶
get_new_arrivals() -> tuple[DiaProduct, ...]
return the products of the storefront's own "novedades" category.
every row of it carries the "novedad" stamp. it costs one request for
the tree and one per twenty products: three for the 38 it held in
october 2026. rows stamped "novedad" also turn up elsewhere with
is_new set, and not all of them are in this category.
get_offers ¶
get_offers() -> tuple[DiaProduct, ...]
return every product on offer, across every category.
the offers page groups its rows by category and shows ten of each, so the client reads the first group's id from it and then walks each category's own offer listing, which chains to the next category: one request, then one per category with offers and one more per extra page of twenty-four, about thirty in october 2026. a product offered in two categories is returned once.
get_category_products ¶
get_category_products(category_id: str | int, *, cursor: str | None = None) -> DiaSearchResult
return one page of a category's listing, twenty rows at a time.
without a cursor this costs one request for the tree and one for the
page; with the next_cursor of an earlier page it costs one. a
top-level category walks its children one after another, and each
page's category_id names the child it came from.
get_category_offers ¶
get_category_offers(category_id: str | int) -> tuple[DiaProduct, ...]
return every product on offer in one category, top-level or not.
one request per twenty-four offers.
supermercapy.Eroski ¶
Eroski(*, language: Language | str | None = None, timeout: float | Timeout = 10.0, retry_policy: RetryPolicy | None = None, min_request_interval: float | None = None, transport: BaseTransport | None = None, user_agent: str | None = None)
Bases: TapestryClient[EroskiProduct, EroskiCategory, EroskiSearchResult]
a reusable synchronous client for eroski's online supermarket.
the storefront is server-rendered html with no json api, so every method reads markup: listings from the twenty-tile fragments the storefront's own infinite scroll loads, products from their pages, and the category tree from the menu every full page carries. full pages weigh close to a megabyte and fragments about a quarter of one, so the client paces itself at one request a second by default.
nothing binds. the anonymous session is pinned to one shop, 157
(named bilbondo), which sets the prices and the regional range,
and choosing another one needs an account, so store_id is None and
every product carries the shop_id that priced it. the storefront also
speaks basque, galician and german, which :class:~supermercapy.Language
does not hold.
supermercapy.Caprabo ¶
Caprabo(*, language: Language | str | None = None, timeout: float | Timeout = 10.0, retry_policy: RetryPolicy | None = None, min_request_interval: float | None = None, transport: BaseTransport | None = None, user_agent: str | None = None)
Bases: TapestryClient[CapraboProduct, CapraboCategory, CapraboSearchResult]
a reusable synchronous client for caprabo's online supermarket.
caprabo belongs to the eroski group and runs the same storefront build on its own host: the same paths, the same category ids and the same markup, read by the same parsers. its catalog is its own, though, and so are its regional products, which carry their own ids and prices. the client paces itself at one request a second by default.
nothing binds. the anonymous session is pinned to one shop, 8284
(caprabo's platform), and choosing another one needs an account, so
store_id is None and every product carries the shop_id that
priced it. the storefront speaks spanish and catalan, and translates
product names into catalan as well as the menu.
supermercapy.Aldi ¶
Aldi(region: Region | str = PENINSULA, *, language: Language | str | None = None, timeout: float | Timeout = 10.0, retry_policy: RetryPolicy | None = None, min_request_interval: float | None = None, transport: BaseTransport | None = None, user_agent: str | None = None)
Bases: BaseClient
a reusable synchronous client for aldi's spanish product catalogue.
aldi runs no online shop. www.aldi.es publishes the fixed assortment
and the weekly offers with prices, and reads both from a public algolia
index, one per price region: the península, the balearic islands and the
canary islands. search, product records, category listings and the
catalogue come straight from that index with the site's own search-only
key; the category tree and the offers come from the json the site embeds
in its pages.
a region is the only binding and a postcode maps to one by its province
without a request, so :meth:from_postal_code is a local lookup. regions
change prices and, for the canary islands, the assortment.
EAN, EAN_LOOKUP and NUTRITION are left out because no record
carries a barcode, ingredients or a nutrition table. STORES is left
out because a store binds nothing: prices follow the region. there is no
novelties feed and no machine-readable home page.
from_postal_code
classmethod
¶
from_postal_code(postal_code: str, *, language: Language | str | None = None, timeout: float | Timeout = 10.0, retry_policy: RetryPolicy | None = None, min_request_interval: float | None = None, transport: BaseTransport | None = None, user_agent: str | None = None) -> Self
return a client for the price region of a postcode's province.
this makes no request: balearic postcodes (07) map to the balearic region, las palmas (35) and santa cruz de tenerife (38) to the canary one, and every other province, ceuta and melilla included, to the península.
search_products ¶
search_products(query: str, *, page_size: int | None = None, cursor: str | None = None) -> AldiSearchResult
search one page of the region's index.
the cursor is the next row offset as a string; algolia serves any window of up to a thousand rows.
get_product ¶
get_product(product_id: str | int) -> AldiProduct
return one product record by its numeric object id, in one request.
get_categories ¶
get_categories(*, deep: bool = False) -> tuple[AldiCategory, ...]
return the top-level categories of the site's product overview.
one request reads the overview page. deep=True adds each
category's children at one more request per category, twenty more in
october 2026. categories the site hides, such as an out-of-season
"verano", are left out.
get_category ¶
get_category(category_id: str | int) -> AldiCategory
return one category with its children and every product it lists.
two requests: the category page, for the node, its children and the filter the site lists it with, and one query of the region's index for up to a thousand products. a top-level page lists no products of its own, so its products are those of all its children together.
iter_catalog ¶
iter_catalog() -> Iterator[AldiProduct]
yield every record of the region's index, a thousand per request.
three requests for the 2,342 records of the península index in october 2026.
get_offers ¶
get_offers() -> tuple[AldiProduct, ...]
return this week's offers, once each, in the order the page lists them.
one request for the region's offers page, which embeds every record
it shows. :meth:get_offer_groups keeps the page's sections and
their dates.
get_offer_groups ¶
get_offer_groups() -> tuple[AldiOfferGroup, ...]
return the sections of this week's offers page with their dates.
get_category_products ¶
get_category_products(key: str, *, page_size: int | None = None, cursor: str | None = None) -> AldiSearchResult
return one page of the products filed under a category key.
the key is what a product's category_ids and
:attr:AldiCategory.key hold, so this reaches a product's category
in one request without its page.
supermercapy.Ahorramas ¶
Ahorramas(*, language: Language | str | None = None, timeout: float | Timeout = 10.0, retry_policy: RetryPolicy | None = None, min_request_interval: float | None = None, transport: BaseTransport | None = None, user_agent: str | None = None)
Bases: BaseClient
a reusable synchronous client for ahorramás's online supermarket.
the shop runs salesforce commerce cloud behind cloudflare and publishes no product api. listings come from the html grid its "more results" button loads, product records from the json its product page asks for when a quantity changes, and the category tree from the menu every full page carries. both of the storefront's own endpoints sit under paths its robots.txt disallows, which the store's documentation lists.
nothing binds. the shop serves one assortment at one price whatever the address, publishes no postcode step, and speaks spanish only. the client paces itself at one request a second, because a catalog walk is close to fifty pages of a megabyte each.
NUTRITION is not declared: the product record has ingredient,
nutrition and allergen fields, and every product sampled in october 2026
left them empty.
search_products ¶
search_products(query: str, *, page_size: int | None = None, cursor: str | None = None) -> AhorramasSearchResult
search one page of products; the cursor is the next offset.
the grid publishes no total, so total_hits is None, and a page
shorter than page_size is the last one.
get_product ¶
get_product(product_id: str | int) -> AhorramasProduct
return one product's record, the json its page reads, in one request.
an unknown id is answered with http 500 and an error document, the
same answer a server fault gets, so both raise
:class:~supermercapy.NotFoundError.
get_categories ¶
get_categories() -> tuple[AhorramasCategory, ...]
return the category tree from the home page's menu, about 790 kb.
the tree is kept on the client, so a later :meth:get_category needs
no second copy.
get_category ¶
get_category(category_id: str | int, *, page_size: int | None = None) -> AhorramasCategory
return one category with its children and its first listing page.
the name and the children come from the menu, which a client that has
not read it yet reads first: two requests, then one per category. an
id the menu does not hold raises :class:~supermercapy.NotFoundError.
no product count is published.
iter_catalog ¶
iter_catalog() -> Iterator[AhorramasProduct]
yield every product by walking the catalogue's root a hundred at a time.
the root category holds the whole catalogue: a full walk on 4 october 2026 read 7,579 products in 76 requests of about a megabyte each, and took nine minutes because each page took about seven seconds.
get_offers ¶
get_offers() -> tuple[AhorramasProduct, ...]
return every product in the menu's offers branch, walked to the end.
get_category_products ¶
get_category_products(category_id: str | int, *, page_size: int | None = None, cursor: str | None = None) -> AhorramasSearchResult
return one listing page of a category, its subtree included.
the id is the menu's, such as "cordero_y_cabrito", and the call
needs no menu. the storefront answers an unknown id with the whole
catalogue rather than an error, so the category the grid reports
serving is checked and a mismatch raises
:class:~supermercapy.NotFoundError.
supermercapy.Alcampo ¶
Alcampo(store: str | AlcampoStore | None = None, *, language: Language | str | None = None, timeout: float | Timeout = 10.0, retry_policy: RetryPolicy | None = None, min_request_interval: float | None = None, transport: BaseTransport | None = None, user_agent: str | None = None)
Bases: BaseClient
a reusable synchronous client for the alcampo online shop.
the storefront runs on ocado smart platform, like bonpreu's, and serves
anonymous json from one host with no key. prices and assortment follow a
region, one fulfilment centre behind a group of click-and-collect points;
a new anonymous session lands in the "vaguada" region in madrid. an
unbound client reads that default region and costs no extra request.
binding a client to one of the points :meth:list_stores returns moves
the client's own anonymous session to that point's region, the way the
storefront's own region picker does, before the first request.
an aws waf budget is the constraint, as on bonpreu: it is per ip, seven or
eight requests to the product pages service per half hour in october
2026, and once spent that service answers http 202 with an empty body for
tens of minutes. a challenge raises :class:~supermercapy.ChallengedError
after one request and is never retried, and min_request_interval
defaults to two seconds. the same waf refuses search and the product sheet
to a user agent it does not take for a browser, so the default user agent
is a browser string.
CATALOG is not declared: the product sitemap lists more than fifty
thousand products, and no walk of the catalogue fits the budget.
POSTAL_CODE is absent because the storefront resolves an address to a
region from browser-side coordinates and its postcode services answer
nothing for this tenant, and EAN because no barcode exists in the
api.
store_id
property
¶
store_id: str | None
return the click-and-collect point this client is bound to.
None means unbound: the client reads the region a new anonymous
session lands in.
region_id
property
¶
region_id: str | None
return the region the client's prices and assortment come from.
a client bound by a bare store id learns its region on the first
request, and returns None until then.
search_products ¶
search_products(query: str, *, page_size: int | None = None, cursor: str | None = None, category_id: str | int | None = None, sort: SortOption | str | None = None, filters: object = None) -> AlcampoSearchResult
search one page of products.
the cursor is the storefront's own nextPageToken, passed back
verbatim. it is session scoped, so it only works from the client that
produced it. category_id narrows the search to one category,
sort reorders it, and filters takes a mapping of filter group
id to one or more attribute ids.
queries longer than fifty characters are truncated, as the storefront truncates its own.
get_product ¶
get_product(product_id: str | int) -> AlcampoProduct
return one product sheet by its numeric retailerProductId.
the internal product uuid that listings, similar and related speak is
not accepted here; it is kept on
:attr:~supermercapy.alcampo.AlcampoProduct.product_uuid.
get_categories ¶
get_categories(*, depth: int = MAX_CATEGORY_DEPTH) -> tuple[AlcampoCategory, ...]
return the whole category tree, nested, in one request.
depth trims the tree to fewer levels; the default is the full
four.
get_category ¶
get_category(category_id: str | int, *, page_size: int | None = None, sort: SortOption | str | None = None, filters: object = None) -> AlcampoCategory
return one category with its children and its first page of products.
an unknown category raises :class:~supermercapy.NotFoundError, and
so does a listing that echoes back a category other than the one asked
for. the category's product_count is the storefront's own count
and can exceed what the bound region lists.
list_stores ¶
list_stores(postal_code: str | None = None) -> tuple[AlcampoStore, ...]
return the click-and-collect points, with the region behind each.
one request returns every point in the country. with a
postal_code the list keeps the points in its province, those in
the postal code itself first.
get_new_arrivals ¶
get_new_arrivals() -> tuple[AlcampoProduct, ...]
return every product the storefront's "nuevo producto" filter selects.
the filter is the storefront's own, offered on every listing in its
dummyValue group; applied to the whole-shop listing it is the
novelties list. it is read in pages of three hundred until no page
follows: one request for the 75 products it held in october 2026.
get_offers ¶
get_offers() -> tuple[AlcampoProduct, ...]
return the first page of the region's promotions listing.
:meth:get_promotions is the same listing with paging, a category
filter, and the page's own filter and sort metadata.
get_promotions ¶
get_promotions(*, category_id: str | int | None = None, retailer_category_id: str | int | None = None, page_size: int | None = None, cursor: str | None = None, sort: SortOption | str | None = None, filters: object = None) -> AlcampoSearchResult
return one page of the promotions listing, whole shop or one category.
the listing is keyed on the client's region. unlike the category
listing it takes either identifier: a category_id uuid or the
retailer_category_id code such as "OC1701".
suggest ¶
suggest(term: str, *, limit: int = DEFAULT_SUGGESTION_LIMIT) -> tuple[str, ...]
return the autocomplete suggestions for a partial search term.
get_similar ¶
get_similar(product_id: str | int) -> tuple[AlcampoProduct, ...]
return the products the storefront offers as similar to one product.
the endpoint answers with internal uuids alone, so the rows are resolved with one batch request; an empty answer costs nothing more.
get_related ¶
get_related(product_id: str | int, *, limit: int | None = None, high_relevance_only: bool = False) -> tuple[AlcampoProduct, ...]
return the products the storefront cross-sells with one product.
supermercapy.Condis ¶
Condis(picking_centre: str | int = DEFAULT_PICKING_CENTRE, *, language: Language | str | None = None, timeout: float | Timeout = 10.0, retry_policy: RetryPolicy | None = None, min_request_interval: float | None = None, transport: BaseTransport | None = None, user_agent: str | None = None)
Bases: BaseClient
a reusable synchronous client for condis's online shop, condisline.
two hosts answer. the empathy search index at api.empathy.co serves
search, category listings, the novelty and offer facets, and suggestions,
with no key and no cookie; every request names a picking centre, which
decides the assortment. the storefront at compraonline.condis.es
renders product sheets and the category tree on the server, behind an
anonymous sign-in that every first page request redirects through; the
client follows it once per session, as a browser does, and never logs in.
a client is bound to a picking centre, 718 by default, which is the
centre an anonymous visitor gets. :meth:from_postal_code resolves
another through the storefront's own postcode picker.
picking_centre
property
¶
picking_centre: str
return the picking centre whose assortment the index answers with.
store_id
property
¶
store_id: str
return the picking centre; the same value as :attr:picking_centre.
from_postal_code
classmethod
¶
from_postal_code(postal_code: str, *, language: Language | str | None = None, timeout: float | Timeout = 10.0, retry_policy: RetryPolicy | None = None, min_request_interval: float | None = None, transport: BaseTransport | None = None, user_agent: str | None = None) -> Self
bind the picking centre the storefront assigns to postal_code.
the storefront resolves a postcode with a next.js server action,
fetchPostalCodeById, whose id changes with every deployment and is
published only inside the page's scripts. so this reads the home page,
then its scripts in the order the page lists them until one names the
action, then calls it. in october 2026 that was eighteen requests: the
home page through the anonymous sign-in (five), twelve scripts of
about 1.4 mb together, and the call. a postcode condis does not
deliver to raises :class:~supermercapy.OutOfCoverageError.
search_products ¶
search_products(query: str, *, page_size: int | None = None, cursor: str | None = None, filters: Iterable[str] | None = None) -> CondisSearchResult
search one page of the index.
the cursor is the offset of the next row. filters takes the
index's own "facet:value" strings, as its facets name them, such
as "on_sale:true" or "filterCategory:c07", each sent as one
filter parameter. one query reaches at most 2,494 rows plus one
page; a page against that ceiling with rows left is flagged
truncated.
get_product ¶
get_product(product_id: str | int) -> CondisProduct
return one product sheet, read from its server-rendered page.
the slug in a product url is not checked, so this asks for a
placeholder one and reads the real address from the page. an id the
shop does not know renders the page without a product, which raises
:class:~supermercapy.NotFoundError.
get_categories ¶
get_categories() -> tuple[CondisCategory, ...]
return the category tree, three levels deep.
the tree is the storefront's own navigation, read from the home page: one request of about 250 kb, plus the anonymous sign-in on a client's first storefront request.
get_category ¶
get_category(category_id: str | int, *, page_size: int | None = None) -> CondisCategory
return one category with its children and its first page of rows.
product_count is the number of rows the index holds for the
category at the bound picking centre; :meth:get_category_products
pages through them. two requests: the tree, then the rows.
iter_catalog ¶
iter_catalog() -> Iterator[CondisProduct]
yield every row of every top-level category at the picking centre.
each top-level category is walked five hundred rows at a time; the largest held 1,541 rows in october 2026, under the index's offset ceiling. a category too large to walk to its end is walked through its children instead.
get_new_arrivals ¶
get_new_arrivals() -> tuple[CondisProduct, ...]
return every row the index flags is_novelty.
one request for the 57 it held in october 2026.
get_offers ¶
get_offers() -> tuple[CondisProduct, ...]
return every row on sale or on promotion, once each.
on_sale marks a reduced price and on_promotion a multibuy
such as "llévate 6 y paga 5"; the two facets are read one after the
other, five hundred rows a request, about three requests in october
2026.
get_category_products ¶
get_category_products(category_id: str | int, *, page_size: int | None = None, cursor: str | None = None) -> CondisSearchResult
return one page of a category's rows, any level of the tree.
the id is the path-shaped one the tree uses, such as
"c07__cat00210003"; an unknown id answers an empty page.
suggest ¶
suggest(term: str) -> tuple[str, ...]
return the index's query suggestions for a partial term.
cross-store search¶
supermercapy.search_all ¶
search_all(query: str, *, stores: str | type[BaseClient] | Iterable[str | type[BaseClient]] | None = None, postal_code: str | None = None, page_size: int | None = None, max_workers: int | None = None, timeout: float | Timeout = 10.0, retry_policy: RetryPolicy | None = None, min_request_interval: float | None = None, user_agent: str | None = None, transport: BaseTransport | None = None) -> SearchAllResult
search several stores in parallel and report each one separately.
every store gets its own client on its own thread, and every client is
closed before this returns. with a postal_code, the stores that declare
Capability.POSTAL_CODE bind through from_postal_code(), which costs
requests of its own; the rest are built unbound. without one, a store that
cannot be built unbound, such as mercadona, is skipped. a store that does
not serve the postal code reports OUT_OF_COVERAGE, and one that raises
any other :class:SupermercapyError reports FAILED; neither hides what
the other stores found. any other exception is a bug and propagates.
stores takes store names, client classes, or a mix, and the result
keeps their order: ALL_CLIENTS order by default, never the order the
stores happened to finish in. page_size is capped at each store's
max_page_size, and None leaves every store its own default.
max_workers defaults to one thread per store. the remaining keywords
reach every client's constructor; transport is shared by all of them
and stays open, because the caller who made it owns it.
invalid arguments raise :class:ConfigurationError before any request.
supermercapy.SearchAllResult
dataclass
¶
SearchAllResult(*, query: str, postal_code: str | None, stores: tuple[StoreSearch, ...])
what every store asked by :func:search_all answered, in order.
pages
property
¶
pages: dict[str, SearchResult]
the page each store that answered returned, keyed on store name.
products
property
¶
products: tuple[tuple[str, ProductSummary], ...]
every summary found, paired with its store name, store by store.
failed
property
¶
failed: tuple[StoreSearch, ...]
the stores that raised; skipped and out-of-coverage stores are not.
supermercapy.StoreSearch
dataclass
¶
StoreSearch(*, store: str, status: SearchStatus, store_id: str | None = None, result: SearchResult | None = None, reason: str | None = None, error_type: str | None = None)
one store's part of a :func:search_all call.
store_id is the binding the search ran against, when the store has
one. reason says why a store has no result: the exception message for
a failure, or why it was skipped. error_type names the exception class,
so the record stays plain data that :func:to_json can write.
supermercapy.SearchStatus ¶
Bases: StrEnum
how one store's part of a :func:search_all call ended.
SKIPPED
class-attribute
instance-attribute
¶
SKIPPED = 'skipped'
the store was not asked, because it cannot be built without a binding.
OUT_OF_COVERAGE
class-attribute
instance-attribute
¶
OUT_OF_COVERAGE = 'out_of_coverage'
the store does not serve the postal code.
FAILED
class-attribute
instance-attribute
¶
FAILED = 'failed'
the store raised a :class:SupermercapyError.
matching¶
supermercapy.group_same ¶
group_same(listings: Iterable[ListingLike], *, threshold: float = SAME_THRESHOLD) -> tuple[ProductGroup, ...]
split listings into groups of the same product, at most one per store.
this is the shape search_all(...).products comes in. every listing
lands in exactly one group, and a listing that matched nothing is a group
of its own, and listings without a store are never kept apart by
store. two groups join only when every pair across them scores at
least threshold, so one strong pair never drags in a listing that
matches only half the group; listings that share a barcode stand in for
each other there, so a product a store renamed still joins through its
twin. groups keep the order of their first listing, and listings keep
their input order inside a group.
supermercapy.find_same ¶
find_same(product: ListingLike, candidates: Iterable[ListingLike], *, threshold: float = SAME_THRESHOLD) -> tuple[Match, ...]
return the candidates that are the same product as product.
candidates from product's own store are skipped, and each other store
keeps only its best candidate, so the answer reads "this product at those
stores". matches come best first.
supermercapy.pair_same ¶
pair_same(left: Iterable[ListingLike], right: Iterable[ListingLike], *, threshold: float = SAME_THRESHOLD) -> tuple[Match, ...]
pair each listing of left with at most one of right.
the best-scoring pairs are taken first and no listing is used twice, so
two arbitrary lists, such as a basket at one store and a search page at
another, come back as one-to-one matches in left's order. listings
that found no partner at threshold are left out.
supermercapy.score_same ¶
score_same(a: ListingLike, b: ListingLike) -> Match
score whether a and b are the same product.
equal barcodes settle it at 1.0. otherwise the score is half the name
likeness, plus a quarter when the brands agree and a quarter when the pack
sizes agree, so a pair whose brand or size cannot be confirmed never
reaches :data:SAME_THRESHOLD. a brand, a pack size, a barcode or a
container (can, bottle, brick, jar, squeeze bottle) that contradicts
halves the score.
the names are compared without brand, pack size, packaging and stop
words, in spanish and catalan alike, and only their differences count: a
descriptor such as "refresco" or "soluble" costs little, any
other word costs enough to fall below the threshold on its own, and a
number or a negation ("0%", "sin lactosa") costs most. the name
likeness is 1 / (1 + cost).
supermercapy.find_alternatives ¶
find_alternatives(product: ListingLike, candidates: Iterable[ListingLike], *, threshold: float = ALTERNATIVE_THRESHOLD) -> tuple[Match, ...]
return the candidates that are comparable alternatives to product.
any store counts, product's own included, and so does any brand. the
answer comes cheapest first by price.reference; candidates without
one come last, best score first.
supermercapy.score_alternative ¶
score_alternative(a: ListingLike, b: ListingLike) -> Match
score whether b is a comparable alternative to a.
the score is the likeness of what the two names say the product is, with
both brands taken out, so a store brand competes on equal terms; size does
not count, because alternatives are compared per unit. it is 1.0 for
the same description, stays above :data:ALTERNATIVE_THRESHOLD while the
names differ only by descriptors, and falls below it for one other word.
it is halved when both carry a price.reference in different units,
since those prices cannot be compared, and it is 0 when either name
says nothing once its brand is gone.
supermercapy.Listing
dataclass
¶
Listing(*, store: str | None = None, product: ProductSummary)
one product at one store, as search_all().products pairs them.
store is None when the caller passed a bare summary.
supermercapy.Match
dataclass
¶
Match(*, kind: MatchKind, score: float, reasons: tuple[str, ...], left: Listing, right: Listing)
one listing judged against another.
score is in [0, 1], rounded to two places. reasons lists
every signal that counted, agreeing or not: "ean", "brand",
"size", "name 0.82", "brand differs", "size unknown", and
for an alternative "kind 0.90", "unit l" or "store brand".
supermercapy.MatchKind ¶
Bases: StrEnum
which question a :class:Match answers.
supermercapy.ProductGroup
dataclass
¶
ProductGroup(*, listings: tuple[Listing, ...], score: float | None = None, reasons: tuple[str, ...] = ())
listings :func:group_same judged to be one product, one per store.
a group is as sure as its weakest pair: score and reasons are that
pair's, among the pairs that reached the threshold. a listing that matched
nothing is a group of one, with score None and no reasons.
supermercapy.match.SAME_THRESHOLD
module-attribute
¶
SAME_THRESHOLD = 0.8
the default score from which two listings count as the same product.
supermercapy.match.ALTERNATIVE_THRESHOLD
module-attribute
¶
ALTERNATIVE_THRESHOLD = 0.6
the default score from which a listing counts as a comparable alternative.
async and caching¶
supermercapy.AsyncClient ¶
AsyncClient(source: ClientT | Callable[..., ClientT], /, *args: Any, **kwargs: Any)
Bases: Generic[ClientT]
run one store client from asyncio without blocking the event loop.
every call runs the synchronous client in a worker thread of the loop's
default executor, so retries, pacing, warm-ups and errors behave exactly
as they do without asyncio. calls on one AsyncClient run one at a
time, first come first served, because a client keeps per-session state
such as guest tokens, csrf tokens and warm-up clocks that two threads
could interleave; concurrency comes from awaiting several clients at once.
pass a store client, or a callable that builds one and its arguments,
such as AsyncClient(Mercadona.from_postal_code, "28001"). a callable
runs in a worker thread when the async with block starts or, without
one, on the first call, so neither the connection pool nor a postcode
lookup blocks the loop. leaving the block or awaiting :meth:aclose
closes the client.
client
property
¶
client: ClientT
return the wrapped synchronous client.
read its properties, such as store_id, here. calling one of its
request methods directly would block the event loop and skip the
one-call-at-a-time rule; use :meth:run instead.
run
async
¶
run(call: Callable[[ClientT], ResultT]) -> ResultT
run call with the client in a worker thread and return its result.
this reaches everything the awaitable methods do not: store extensions,
and the extra keywords a store adds to a standard method, with the
store's own return types, as in
await lidl.run(lambda client: client.get_leaflets()).
iterate
async
¶
iterate(call: Callable[[ClientT], Iterable[ItemT]]) -> AsyncGenerator[ItemT, None]
yield what an iterator-returning method of the client yields.
the method and each step of its iterator run in a worker thread, so a page is fetched only when the loop asks for its first item, and leaving the loop early costs no request. other calls on this client may run between two steps.
search_products
async
¶
search_products(query: str, *, page_size: int | None = None, cursor: str | None = None) -> SearchResult
await :meth:BaseClient.search_products.
get_product
async
¶
get_product(product_id: str | int) -> Product
await :meth:BaseClient.get_product.
get_categories
async
¶
get_categories() -> tuple[Category, ...]
await :meth:BaseClient.get_categories.
get_category
async
¶
get_category(category_id: str | int) -> Category
await :meth:BaseClient.get_category.
iter_search ¶
iter_search(query: str, *, page_size: int | None = None) -> AsyncGenerator[ProductSummary, None]
iterate :meth:BaseClient.iter_search without blocking the loop.
iter_catalog ¶
iter_catalog() -> AsyncGenerator[ProductSummary, None]
iterate :meth:BaseClient.iter_catalog without blocking the loop.
get_catalog
async
¶
get_catalog() -> tuple[ProductSummary, ...]
await :meth:BaseClient.get_catalog, the whole walk in one call.
list_stores
async
¶
list_stores(postal_code: str | None = None) -> tuple[Store, ...]
await :meth:BaseClient.list_stores.
get_product_by_ean
async
¶
get_product_by_ean(ean: str) -> Product
await :meth:BaseClient.get_product_by_ean.
get_new_arrivals
async
¶
get_new_arrivals() -> tuple[ProductSummary, ...]
await :meth:BaseClient.get_new_arrivals.
supermercapy.CacheTransport ¶
CacheTransport(*, ttl: float, max_bytes: int = _DEFAULT_MAX_BYTES, methods: Iterable[str] = ('GET',), transport: BaseTransport | None = None)
Bases: BaseTransport
keep successful json answers in memory for ttl seconds.
pass one as a client's transport. a client closes its transport when
it closes, so give each client its own, or lend one to
:func:~supermercapy.search_all, which leaves it open. a request is
answered from memory when the same method, url, body and request headers,
cookie and authorization aside, were answered within ttl
seconds; anything else goes to transport. the default sends through
httpx.HTTPTransport, directly or through the proxy that
HTTP_PROXY, HTTPS_PROXY, ALL_PROXY and NO_PROXY name.
only a 2xx response with a json content type is stored, so errors,
redirects, images, and the html pages that warm sessions up or carry csrf
tokens always reach the storefront. a stored copy drops set-cookie,
so a cached answer never rewinds the client's session. GET is the only
method cached unless methods names more, which is how bonàrea's form
posts and mercadona's search join in.
the storefronts' own cache-control headers are ignored, since nearly
all of them forbid caching, and ttl is the one freshness rule. stored
bodies are bounded by max_bytes, and the least recently used go first.
product models¶
supermercapy.ProductSummary
dataclass
¶
ProductSummary(*, id: str, name: str, brand: str | None = None, ean: str | None = None, slug: str | None = None, url: str | None = None, pack_size_text: str | None = None, thumbnail: Photo | None = None, price: Price = Price(), availability: Availability = Availability(), category_ids: tuple[str, ...] = (), promotions: tuple[Promotion, ...] = (), is_new: bool = False, is_sponsored: bool = False, is_variable_weight: bool = False)
partial product data returned by listing operations.
supermercapy.Product
dataclass
¶
Product(*, id: str, name: str, brand: str | None = None, ean: str | None = None, slug: str | None = None, url: str | None = None, pack_size_text: str | None = None, thumbnail: Photo | None = None, price: Price = Price(), availability: Availability = Availability(), category_ids: tuple[str, ...] = (), promotions: tuple[Promotion, ...] = (), is_new: bool = False, is_sponsored: bool = False, is_variable_weight: bool = False, photos: tuple[Photo, ...] = (), description: str | None = None, legal_name: str | None = None, origin: str | None = None, storage: str | None = None, usage: str | None = None, category_path: tuple[Category, ...] = (), nutrition: Nutrition | None = None, requires_age_check: bool = False)
supermercapy.Price
dataclass
¶
Price(*, amount: Decimal | None = None, previous: Decimal | None = None, unit_price: Decimal | None = None, unit_price_unit: str | None = None, unit_price_text: str | None = None, reference: UnitPrice | None = None, currency: str = 'EUR', tax_percentage: Decimal | None = None, is_discounted: bool = False, discount_percentage: Decimal | None = None, valid_from: datetime | None = None, valid_until: datetime | None = None, is_approximate: bool = False)
what one selling unit costs now, plus its unit price.
unit_price, unit_price_unit and unit_price_text keep the
store's own figure, unit and wording. reference restates that figure
per one kilogram, litre, piece, dose, metre or square metre, so prices
compare across stores; None means the store published no unit price
the package can read, not that it is zero.
supermercapy.UnitPrice
dataclass
¶
UnitPrice(*, amount: Decimal, unit: Unit)
what one :class:Unit costs: one kilogram, one litre, one piece.
supermercapy.Availability
dataclass
¶
Availability(*, available: bool | None = None, status: str | None = None, max_quantity: Decimal | None = None, min_quantity: Decimal | None = None, increment: Decimal | None = None)
whether a product can be bought and in what quantities.
supermercapy.Promotion
dataclass
¶
Promotion(*, id: str | None = None, description: str | None = None, kind: str | None = None, price: Decimal | None = None, requires_quantity: int | None = None, starts_at: datetime | None = None, ends_at: datetime | None = None, member_only: bool = False)
one offer attached to a product.
supermercapy.Nutrition
dataclass
¶
Nutrition(*, ingredients: str | None = None, allergens: str | None = None, values: tuple[NutritionValue, ...] = (), per: str | None = None, nutri_score: str | None = None, raw_html: str | None = None)
ingredient, allergen, and nutrition-table data for a product.
supermercapy.NutritionValue
dataclass
¶
NutritionValue(*, name: str, per_100: str | None = None, per_serving: str | None = None, unit: str | None = None)
one row of a nutrition table.
supermercapy.Photo
dataclass
¶
Photo(*, url: str, kind: str | None = None, alt: str | None = None)
one product image addressed by its full url.
listing models¶
supermercapy.Category
dataclass
¶
Category(*, id: str, name: str, parent_id: str | None = None, level: int | None = None, slug: str | None = None, image_url: str | None = None, product_count: int | None = None, children: tuple[Category, ...] = (), products: tuple[ProductSummary, ...] = ())
one category node with optional children and product summaries.
supermercapy.SearchResult
dataclass
¶
SearchResult(*, query: str, products: tuple[ProductSummary, ...], page_size: int, total_hits: int | None = None, next_cursor: str | None = None, truncated: bool = False)
one page of product summaries with an opaque continuation cursor.
supermercapy.HomeSection
dataclass
¶
HomeSection(*, layout: str, title: str | None = None, products: tuple[ProductSummary, ...] = ())
one ordered section of a storefront home page.
supermercapy.Store
dataclass
¶
Store(*, id: str, name: str, kind: str | None = None, address: str | None = None, postal_code: str | None = None, city: str | None = None, province: str | None = None, latitude: float | None = None, longitude: float | None = None)
one physical store, warehouse, or delivery zone.
controlled values¶
supermercapy.Capability ¶
Bases: Flag
optional behaviour a store client declares on its class.
operation capabilities correspond to one optional client method each.
data-shape capabilities say whether a field can ever be populated for the
store, so a None value can be told apart from "never available".
supermercapy.Language ¶
Bases: StrEnum
storefront languages; each client declares which ones it supports.
supermercapy.Unit ¶
Bases: StrEnum
the units a :class:UnitPrice is expressed in.
a dozen is twelve pieces, and a lavado (one wash) is one dose, since detergent shelf labels price either per wash or per dose for the same thing.
exceptions¶
supermercapy.SupermercapyError ¶
Bases: Exception
base class for all supermercapy errors.
supermercapy.ConfigurationError ¶
Bases: SupermercapyError, ValueError
the client or a request was configured with an invalid value.
supermercapy.UnsupportedOperationError ¶
UnsupportedOperationError(capability: Capability, store: str)
supermercapy.TransportError ¶
TransportError(message: str, *, status_code: int | None = None)
supermercapy.RateLimitError ¶
RateLimitError(message: str, *, retry_after: float | None = None)
supermercapy.NotFoundError ¶
NotFoundError(message: str, *, status_code: int | None = 404)
supermercapy.NotAvailableError ¶
NotAvailableError(message: str, *, status_code: int | None = 404)
supermercapy.OutOfCoverageError ¶
OutOfCoverageError(message: str, *, status_code: int | None = None)
supermercapy.BlockedError ¶
BlockedError(message: str, *, status_code: int | None = None, retry_after: float | None = None, permanent: bool = False)
supermercapy.ChallengedError ¶
ChallengedError(message: str, *, status_code: int | None = None, suggested_backoff: float = 1800.0)
supermercapy.AuthenticationError ¶
AuthenticationError(message: str, *, status_code: int | None = None)
supermercapy.InvalidResponseError ¶
Bases: SupermercapyError
the upstream response is not valid json or has no usable structure.