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.

language property

language: Language

return the selected storefront language.

store_id property

store_id: str | None

return the store, warehouse, or zone this client is bound to.

is_closed property

is_closed: bool

return whether the client has been closed.

min_request_interval property

min_request_interval: float

return the minimum time between request starts in seconds.

user_agent property

user_agent: str

return the user agent sent with every request.

supports classmethod

supports(capability: Capability) -> bool

return whether this store's client declares capability.

close

close() -> None

close the http connection pool.

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(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_offers

get_offers() -> tuple[ProductSummary, ...]

return the products currently on offer.

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.

warehouse property

warehouse: str

return the normalized warehouse code.

store_id property

store_id: str

return the warehouse code; the same value as 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_home

get_home() -> tuple[MercadonaHomeSection, ...]

return ordered storefront home sections.

get_season

get_season(season_id: str) -> Season

return one seasonal product collection by id.

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.

zone property

zone: int | None

return the zone this client is scoped to, if any.

store_id property

store_id: str | None

return the zone id as a string; the same value as zone.

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_product

get_product(product_id: str | int) -> ConsumProduct

return one product by its code.

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.

center property

center: int

return the preparation center this client is scoped to.

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(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.

catalog property

catalog: str

return the catalog searches are scoped to; food by default.

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.

region property

region: int | None

return the offer region grocery prices are read for, if any.

zone property

zone: str

return the delivery zone shop prices are read for.

family property

family: ProductFamily | None

return the assortment half listings are filtered to, if any.

store property

store: LidlStore | None

return the store :meth:from_postal_code resolved, 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(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.

store_id property

store_id: str | None

return the bound postcode; the same value as postal_code.

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.

region property

region: Region

return the price region this client reads.

store_id property

store_id: str

return the region code, such as "pen".

index_name property

index_name: str

return the algolia index of the client's region.

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(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.

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.

OK class-attribute instance-attribute

OK = 'ok'

the store answered; result holds its page.

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.

SAME class-attribute instance-attribute

SAME = 'same'

the same brand, product and pack size, at another store.

ALTERNATIVE class-attribute instance-attribute

ALTERNATIVE = 'alternative'

the same kind of product under any brand, compared per unit.

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.

aclose async

aclose() -> None

close the wrapped client once the calls already queued have run.

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(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.

get_offers async

get_offers() -> tuple[ProductSummary, ...]

await :meth:BaseClient.get_offers.

get_home async

get_home() -> tuple[HomeSection, ...]

await :meth:BaseClient.get_home.

download async

download(source: Photo | str, destination: str | PathLike[str]) -> Path

await :meth:BaseClient.download.

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.

handle_request

handle_request(request: Request) -> Response

answer request from memory, or send it and keep what may be kept.

clear

clear() -> None

forget every stored response.

close

close() -> None

forget every stored response and close the wrapped transport.

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)

Bases: ProductSummary

complete product data returned by a product-detail operation.

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.

has_more property

has_more: bool

whether passing next_cursor back would fetch another page.

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)

Bases: ConfigurationError

the store's client does not implement an optional operation.

supermercapy.TransportError

TransportError(message: str, *, status_code: int | None = None)

Bases: SupermercapyError

a request failed before supermercapy received a usable response.

supermercapy.RateLimitError

RateLimitError(message: str, *, retry_after: float | None = None)

Bases: TransportError

the upstream service kept returning http 429.

supermercapy.NotFoundError

NotFoundError(message: str, *, status_code: int | None = 404)

Bases: TransportError

the requested resource does not exist.

supermercapy.NotAvailableError

NotAvailableError(message: str, *, status_code: int | None = 404)

Bases: NotFoundError

the resource exists but is not offered by the selected store or zone.

supermercapy.OutOfCoverageError

OutOfCoverageError(message: str, *, status_code: int | None = None)

Bases: TransportError

the postal code is not served by the store.

supermercapy.BlockedError

BlockedError(message: str, *, status_code: int | None = None, retry_after: float | None = None, permanent: bool = False)

Bases: TransportError

the upstream service refused the request with a bot-protection rule.

supermercapy.ChallengedError

ChallengedError(message: str, *, status_code: int | None = None, suggested_backoff: float = 1800.0)

Bases: BlockedError

the upstream service answered with a browser challenge instead of data.

supermercapy.AuthenticationError

AuthenticationError(message: str, *, status_code: int | None = None)

Bases: TransportError

the client could not obtain or refresh the credentials a store needs.

supermercapy.InvalidResponseError

Bases: SupermercapyError

the upstream response is not valid json or has no usable structure.