api guide¶
supermercapy exposes its supported public names from the top-level supermercapy package, and each store's own models from supermercapy.<store>. the package ships py.typed.
the shared client¶
every client subclasses BaseClient, which owns the transport, the retry loop, the request pacing, and the standard interface. you construct a client directly, construction performs no i/o, and the client closes its connection pool through close() or a with block.
Client(
<store binding, if the store has one>,
*,
language=None,
timeout=10.0,
retry_policy=None,
min_request_interval=None,
transport=None,
user_agent=None,
)
language accepts a Language member or its string value and must be one the client declares. timeout accepts a positive finite number or an httpx.Timeout. transport accepts an httpx.BaseTransport, such as a CacheTransport or a test double. min_request_interval sets the minimum gap between request starts, defaulting to the client's own default_min_request_interval. user_agent replaces the client's default. carrefour, bonpreu and alcampo default to a browser's user agent, because their edges refuse other ones.
the leading positional argument differs per store, because the binding does:
| client | binding argument | other keywords |
|---|---|---|
Mercadona |
warehouse |
none |
Consum |
zone=None |
drop_sponsored=True |
Plusfresc |
center=12 |
none |
Bonarea |
none | none |
Carrefour |
sale_point=None |
catalog="food" |
Lidl |
region=None |
zone="PEN", family=None |
Bonpreu |
none | none |
Dia |
postal_code=None |
none |
Eroski |
none | none |
Caprabo |
none | none |
Aldi |
region="pen" |
none |
Ahorramas |
none | none |
Alcampo |
store=None |
none |
Condis |
picking_centre="718" |
none |
class attributes¶
these describe a client before one is built, so a program can plan against a store without constructing it.
| attribute | type | meaning |
|---|---|---|
store_name |
str |
the lowercase store key, also used in error messages |
capabilities |
frozenset[Capability] |
every optional operation and data shape the store supports |
supported_languages |
frozenset[Language] |
the languages the storefront answers in |
default_language |
Language |
the language used when none is passed |
default_user_agent |
str |
the user agent sent unless one is passed |
default_min_request_interval |
float |
the pacing the store needs by default |
default_page_size |
int |
the page size used when none is passed |
max_page_size |
optional int |
the largest page the store accepts, or None when it caps none |
supports(capability) is a classmethod returning whether capabilities contains one flag.
instance properties¶
| property | type | meaning |
|---|---|---|
store_id |
optional str |
the binding as a string, or None when the store binds nothing |
language |
Language |
the selected language |
min_request_interval |
float |
the minimum seconds between request starts |
user_agent |
str |
the user agent every request carries |
is_closed |
bool |
whether close() has run |
required methods¶
every client implements these four. a store that cannot answer one of them honestly is not in supermercapy.
| method | return type |
|---|---|
search_products(query, *, page_size=None, cursor=None) |
SearchResult |
get_product(product_id) |
Product |
get_categories() |
tuple[Category, ...] |
get_category(category_id) |
Category |
a client may add typed keyword arguments after the standard ones, such as Consum.search_products(..., order_by=, filters=) or Carrefour.search_products(..., sort=). the standard arguments keep their meaning everywhere.
methods the base class implements¶
| method | return type | what it does |
|---|---|---|
iter_search(query, *, page_size=None) |
Iterator[ProductSummary] |
follows next_cursor until it is None |
get_catalog() |
tuple[ProductSummary, ...] |
drains iter_catalog() and deduplicates by id |
download(source, destination) |
Path |
streams a Photo or an image url to a file |
get_catalog() requires Capability.CATALOG like iter_catalog() does.
optional methods¶
each of these raises UnsupportedOperationError before any i/o unless the client declares the matching capability.
| method | capability | return type |
|---|---|---|
from_postal_code(postal_code, **options) |
POSTAL_CODE |
a client |
list_stores(postal_code=None) |
STORES |
tuple[Store, ...] |
iter_catalog() |
CATALOG |
Iterator[ProductSummary] |
get_product_by_ean(ean) |
EAN_LOOKUP |
Product |
get_new_arrivals() |
NEW_ARRIVALS |
tuple[ProductSummary, ...] |
get_offers() |
OFFERS |
tuple[ProductSummary, ...] |
get_home() |
HOME |
tuple[HomeSection, ...] |
from_postal_code() is a classmethod taking the same keyword options as the constructor plus whatever the store's own constructor accepts. it validates a five-digit spanish postcode before requesting anything. consum, plusfresc, carrefour, lidl, dia and condis answer when they do not serve a postcode, and there it raises OutOfCoverageError.
capabilities¶
Capability is a Flag enum with eleven members. seven of them unlock one optional method each, and the conformance suite asserts that a client declares one exactly when it overrides that method. the other four describe response data. they say whether a field can ever hold a value for that store, which is what tells an absent value apart from one the store never publishes.
| flag | kind | meaning |
|---|---|---|
POSTAL_CODE |
operation | a postcode resolves to a binding |
STORES |
operation | the stores, zones, or warehouses can be listed |
CATALOG |
operation | the whole assortment can be enumerated |
EAN_LOOKUP |
operation | a barcode resolves to a product |
NEW_ARRIVALS |
operation | the storefront publishes a novelties feed |
OFFERS |
operation | the storefront publishes a discounts feed |
HOME |
operation | the home page's sections are machine-readable |
EAN |
data | products carry a barcode |
NUTRITION |
data | products carry ingredients, allergens, or a nutrition table |
PROMOTIONS |
data | products carry offers, member prices, or campaign badges |
FUTURE_PRICES |
data | a price that starts later is published before it applies |
capability matrix¶
no means the store publishes nothing supermercapy could map onto that capability, not that the work is pending. the store pages explain each no in place, and a test parses this table against every client's declared capabilities, so it cannot drift from the code.
| capability | mercadona | consum | plusfresc | bonarea | carrefour | lidl | bonpreu | dia | eroski | caprabo | aldi | ahorramas | alcampo | condis |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
POSTAL_CODE |
yes | yes | yes | no | yes | yes | no | yes | no | no | yes | no | no | yes |
STORES |
yes | yes | yes | no | yes | yes | no | no | no | no | no | no | yes | no |
CATALOG |
yes | yes | yes | yes | no | yes | no | yes | yes | yes | yes | yes | no | yes |
EAN_LOOKUP |
no | yes | no | no | yes | no | no | no | no | no | no | no | no | no |
NEW_ARRIVALS |
yes | yes | yes | no | no | no | yes | yes | no | no | no | no | yes | yes |
OFFERS |
no | yes | yes | no | no | yes | yes | yes | no | no | yes | yes | yes | yes |
HOME |
yes | no | no | no | yes | no | no | no | no | no | no | no | no | no |
EAN |
yes | yes | no | no | yes | yes | no | no | no | no | no | no | no | no |
NUTRITION |
yes | no | yes | yes | yes | no | yes | yes | yes | yes | no | no | yes | yes |
PROMOTIONS |
no | yes | yes | no | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes |
FUTURE_PRICES |
no | no | no | no | no | yes | no | no | no | no | yes | no | no | no |
pagination¶
the contract is one page plus an opaque cursor. search_products() takes cursor=None for the first page and returns next_cursor, which is None at the end. iter_search() follows it. the cursor is a string because its meaning is the store's business, and five different meanings are in use:
| style | stores | what the cursor holds |
|---|---|---|
| page | mercadona | the next zero-based page number |
| page and row | eroski, caprabo | the storefront page, and the rows of it already returned when a smaller page was cut from it: 3 or 3:5 |
| offset | consum, carrefour, lidl, dia, aldi, ahorramás, condis | the number of rows consumed so far |
| token | bonpreu, alcampo | the continuation token the storefront issued |
| none | plusfresc, bonàrea | an offset into rows the client already holds |
the none style covers storefronts that take no paging parameter at all and answer with the whole result set. those clients slice the response themselves, so walking their cursor re-requests the same response. use iter_search() there, which streams one response without asking again.
SearchResult carries page_size, total_hits when the store reports one, next_cursor, has_more, and truncated. truncated marks a page that sat against an upstream result ceiling with matches still unreached, which happens on plusfresc at exactly one hundred rows, on carrefour beyond its start cap, on mercadona past its thousand-hit page limit, and on condis once the next offset would pass its ceiling of 2,494.
exceptions¶
every failure supermercapy raises inherits from SupermercapyError.
SupermercapyError
├── ConfigurationError(ValueError)
│ └── UnsupportedOperationError
├── TransportError
│ ├── RateLimitError
│ ├── NotFoundError
│ │ └── NotAvailableError
│ ├── OutOfCoverageError
│ ├── BlockedError
│ │ └── ChallengedError
│ └── AuthenticationError
└── InvalidResponseError
see the error model for what each one means and which attributes it carries.
models¶
all models are frozen, slotted, keyword-only dataclasses. collections are tuples, ids are str, money and quantities are Decimal, and timestamps are datetime. store packages subclass them to add store-specific fields, and each client's return annotations narrow to its own subclasses.
| model | fields |
|---|---|
ProductSummary |
id, name, brand, ean, slug, url, pack_size_text, thumbnail, price, availability, category_ids, promotions, is_new, is_sponsored, is_variable_weight |
Product |
every ProductSummary field plus photos, description, legal_name, origin, storage, usage, category_path, nutrition, requires_age_check |
Price |
amount, previous, unit_price, unit_price_unit, unit_price_text, reference, currency, tax_percentage, is_discounted, discount_percentage, valid_from, valid_until, is_approximate |
UnitPrice |
amount, unit |
Availability |
available, status, max_quantity, min_quantity, increment |
Promotion |
id, description, kind, price, requires_quantity, starts_at, ends_at, member_only |
Nutrition |
ingredients, allergens, values, per, nutri_score, raw_html |
NutritionValue |
name, per_100, per_serving, unit |
Photo |
url, kind, alt |
Category |
id, name, parent_id, level, slug, image_url, product_count, children, products |
SearchResult |
query, products, page_size, total_hits, next_cursor, truncated, and the has_more property |
Store |
id, name, kind, address, postal_code, city, province, latitude, longitude |
HomeSection |
layout, title, products |
Photo.url is a complete url. a store whose cdn can resize an image exposes that through its own photo subclass, not through this field.
prices¶
Price.amount is what one selling unit costs now. the three unit_price fields keep the store's own unit price untouched. unit_price is the store's figure, unit_price_unit its unit exactly as sent ("L", "1 Kg", "100 Gr", "dc", "PER_1KG"), and unit_price_text its display string where it sends one. they differ from store to store, so they don't compare.
Price.reference restates that figure in a shared unit. it is a UnitPrice, whose amount is what one unit costs, and Unit is a string enum.
Unit |
value | what the stores' units become |
|---|---|---|
KILOGRAM |
"kg" |
per kilogram as is, per gram ×1000, per 100 g ×10 |
LITRE |
"l" |
per litre as is, per 100 ml ×10, per centilitre ×100, per millilitre ×1000 |
PIECE |
"piece" |
per unit (ud, u., un, each) as is, per dozen ÷12 |
DOSE |
"dose" |
per dose (dosis, do, d.) and per wash (lavado, lv) as is |
METRE |
"m" |
per metre as is |
SQUARE_METRE |
"m2" |
per square metre as is |
a wash counts as one dose, because detergent labels use the two words for the same thing.
the conversion is Decimal arithmetic. the package rounds the result half up to four decimal places, then drops trailing zeros down to two places. 6.2 per 100 g becomes 62.00 per kilogram, and 2.625 per dozen becomes 0.2188 per piece.
the reference follows amount. when a promotion replaces the price and the store sends a promotional unit price, the reference comes from the promotional one.
reference is None when the store published no unit price, or published one in a unit the package doesn't recognise. None means unknown, never zero. drop those rows before sorting, and only compare amounts whose unit matches.
no reference comes from a product name or a free-text pack size. carrefour's search index is the one place the package computes a unit price itself, by dividing the price by a quantity the index sends as a number. each store page lists the units that store reports.
serialising models¶
to_dict(value) converts a model, a list or tuple of models, or a dict of them into json-compatible python, and to_json(value, indent=None) returns the same as a json string. Decimal becomes a string so prices stay exact. see saving results.
controlled values¶
Language is a string enum with SPANISH = "es", CATALAN = "ca", ENGLISH = "en", and VALENCIAN = "vl". a client rejects a language outside its supported_languages with ConfigurationError.
Unit is a string enum with KILOGRAM = "kg", LITRE = "l", PIECE = "piece", DOSE = "dose", METRE = "m", and SQUARE_METRE = "m2". see prices.
RetryPolicy is a frozen dataclass of max_attempts=3, backoff_factor=0.25, max_delay=5.0, and jitter_ratio=0.1. see reliability and request behavior for how the client applies them.
version¶
supermercapy.__version__ holds the installed package version, which is the version of the supermercapy distribution.