command line

installing supermercapy also installs a supermercapy command, and python -m supermercapy runs the same program.

supermercapy --version
supermercapy --help
supermercapy search --help

tables go to standard output and fit the terminal width. notes about a store, such as why it could not answer, go to standard error one line each, so a pipe carries the table alone. a name too long for its column ends in …. prices appear exactly as the storefront published them, with no rounding and no currency sign, and every store prices in euros. the unit price column shows the normalised price.reference, per kg, l, piece, dose, m or m2, so rows compare across stores. when a store states a unit price the client cannot normalise, the column shows the store's own figure.

every command takes --json, which swaps the table for one json document. see json output.

the examples below are real runs from 4 october 2026 in an 80-column terminal. the products and prices will have changed since.

stores

supermercapy stores lists every store with its client class, the constructor argument it binds through, and the capabilities it declares. it makes no request.

$ supermercapy stores
store      class      binds to                      capabilities
mercadona  Mercadona  warehouse (required)          postal_code, stores,
                                                    catalog, new_arrivals, home,
                                                    ean, nutrition
consum     Consum     zone (optional)               postal_code, stores,
                                                    catalog, ean_lookup,
                                                    new_arrivals, offers, ean,
                                                    promotions
plusfresc  Plusfresc  center (default 12)           postal_code, stores,
                                                    catalog, new_arrivals,
                                                    offers, nutrition,
                                                    promotions
bonarea    Bonarea    nothing                       catalog, nutrition
carrefour  Carrefour  sale point (optional)         postal_code, stores,
                                                    ean_lookup, home, ean,
                                                    nutrition, promotions
lidl       Lidl       region (optional)             postal_code, stores,
                                                    catalog, offers, ean,
                                                    promotions, future_prices
bonpreu    Bonpreu    nothing                       new_arrivals, offers,
                                                    nutrition, promotions
dia        Dia        postal code (optional)        postal_code, catalog,
                                                    new_arrivals, offers,
                                                    nutrition, promotions
eroski     Eroski     nothing                       catalog, nutrition,
                                                    promotions
caprabo    Caprabo    nothing                       catalog, nutrition,
                                                    promotions
aldi       Aldi       region (default pen)          postal_code, catalog,
                                                    offers, promotions,
                                                    future_prices
ahorramas  Ahorramas  nothing                       catalog, offers, promotions
alcampo    Alcampo    store (optional)              stores, new_arrivals,
                                                    offers, nutrition,
                                                    promotions
condis     Condis     picking centre (default 718)  postal_code, catalog,
                                                    new_arrivals, offers,
                                                    nutrition, promotions

a store whose binding is required cannot answer anything until --postal-code gives it one. today that is mercadona alone.

supermercapy search QUERY searches every store in parallel through search_all() and prints up to five products from each. --store NAME searches only the named store, and repeating it adds more, in the order given. --limit N asks each store for N products, capped at the most that store accepts. --postal-code binds every store that declares POSTAL_CODE before it searches, and the rest ignore it.

$ supermercapy search leche --postal-code 08013 --limit 3
store      id         name                                     price  unit price
mercadona  10381      Leche semidesnatada Hacendado             4.98  0.83/l
mercadona  10379      Leche entera Hacendado                    5.76  0.96/l
mercadona  10721      Leche semidesnatada sin lactosa Hacend…   5.64  0.94/l
consum     7080604    Leche Semidesnatada Brik                  0.84  0.84/l
consum     7080596    Leche Entera Brik                         0.96  0.96/l
consum     5583190    Leche Semidesnatada Brik                  1.25  1.25/l
plusfresc  000426     Llet semidesnatada ASTURIANA, 1 l         1.17  1.17/l
plusfresc  000428     Llet desnatada ASTURIANA, 1 l             1.13  1.13/l
plusfresc  000857     Formatge de vaca PYRÉNÉE llet crua MOU…  27.99  27.99/kg
bonarea    13*0005    Leche desnatada paq. de 6 brics           5.22  0.87/l
bonarea    13*0020    Leche semidesnatada Calcio paq. de 6 b…    7.2  1.20/l
bonarea    13*0004    Leche semidesnatada paq. de 6 brics       5.34  0.89/l
carrefour  521007071  Leche semidesnatada Carrefour brik 1 l.   0.84  0.84/l
carrefour  521006992  Leche entera Carrefour brik 1 l.          0.96  0.96/l
carrefour  714713105  Leche semidesnatada Carrefour sin lact…   0.94  0.94/l
lidl       100408539  Espumador de leche »SMSP 500 A1«         17.99
lidl       100411764  Espumador de leche                       36.99
lidl       100411470  Cafetera »CAFFETTIERA«                   14.99
bonpreu    49637      DOLCE GUSTO Càpsules de cafè Espresso …   8.55  0.28/piece
bonpreu    49644      DOLCE GUSTO Càpsules de cafè amb llet     8.55  0.28/piece
bonpreu    25833      STARBUCKS Càpsules de cafè Espresso Ro…   3.99  0.40/piece
dia        504P6      Leche semidesnatada Dia Láctea pack 6 …   5.04  0.84/l
dia        608P6      Leche entera Dia Láctea pack 6 x 1 L      5.76  0.96/l
dia        130063P6   Leche semidesnatada sin lactosa Dia Lá…   5.34  0.89/l
eroski     18672295   Leche entera del País Vasco EROSKI, br…   1.15
eroski     18672311   Leche semidesnatada del PaÍs Vasco ERO…   1.06
eroski     26084350   Leche entera BOMILK, brik 1 litro         0.96
caprabo    18581678   Leche semidesnatada de Cataluña EROSKI…   0.99
caprabo    735399     Leche semidesnatada ASTURIANA, brik 1 …   1.09
caprabo    13150131   Leche semidesnatada sin lactosa PASCUA…   1.29
aldi       872200     Leche entera sin lactosa                  1.03
aldi       828100     Leche evaporada                           0.92  2.71/kg
aldi       82000      Leche con calcio semidesnatada            1.05
ahorramas  70865      Leche Alipende 1l semidesnatada           0.83  0.83/l
ahorramas  70864      Leche Alipende 1l entera                  0.96  0.96/l
ahorramas  70866      Leche Alipende 1l desnatada               0.81  0.81/l
alcampo    54180      AUCHAN Leche semidesnatada de vaca 6 x…   5.28  0.88/l
alcampo    54178      AUCHAN Leche entera de vaca 6 x 1 l Pr…   5.76  0.96/l
alcampo    99193      L.R. Leche entera 6 x 1 l.                5.70  0.95/l
condis     704049     LECHE CONDIS ENTERA 1 L                   0.99  0.99/l
condis     704048     LECHE CONDIS SEMIDESNATADA 1 L            0.87  0.87/l
condis     704005     LECHE ATO ENTERA BRIK 1 L                 1.28  1.28/l

that run made 45 requests to the fourteen stores. each store spends one on the search itself, and the rest go to postcode bindings and session warm-ups, 19 of them at condis. request counts gives each store's share. without --postal-code it makes 15 and skips mercadona, which notes why on standard error:

mercadona: skipped: needs a postal code to bind a warehouse

a store that does not serve the postcode notes out of coverage, and one with nothing to show notes no results. neither counts as a failure. naming mercadona with --store and no --postal-code is a usage error, because it could never answer.

compare

supermercapy compare QUERY runs the same search as search, then uses group_same() to line up the listings that are the same brand, product and pack at different stores. each group is one block, cheapest store first, and the command prints only groups that reach two stores. it takes the same --store and --postal-code flags as search, asks each store for 10 products unless --limit says otherwise, and takes --threshold from 0 to 1, 0.8 by default. the score column is the group's weakest pair.

$ supermercapy compare "cola cao"
group  score  store      id                name                price  unit price
1      0.81   bonarea    13*0092606        Cacao intantáneo …   4.35  11.36/kg
              alcampo    16806             COLACAO Cacao en …   4.74  12.38/kg
              carrefour  530913230         Cacao soluble Col…   4.95  12.9243/kg
              consum     7360985           Cacao Soluble        4.99  13.03/kg
              plusfresc  002510            Cacau en pols COL…   4.99  13.02/kg
              bonpreu    00633             COLACAO Cacau sol…   4.99  13.03/kg
              ahorramas  60035             Cacao Colacao 383g   4.99  13.03/kg
              condis     206070            CACAO COLACAO EN …   4.99  13.03/kg
              dia        64150             ColaCao 383 g           5  13.05/kg
              eroski     23666878          Cacao soluble ori…   5.00  13.05/kg
              caprabo    23666878          Cacao soluble ori…   5.00  13.05/kg
2      0.81   alcampo    16919             COLACAO Cacao en …   6.55  8.62/kg
              bonarea    13*0091234        Cacao instantáneo…   6.75  8.77/kg
              consum     7331569           Cacao Soluble        6.99  9.20/kg
              plusfresc  002511            Cacau en pols COL…   6.99  9.19/kg
              bonpreu    04215             COLACAO Cacau sol…   6.99  9.20/kg
              ahorramas  59329             Cacao Colacao 760g   6.99  9.20/kg
              condis     206071            CACAO COLACAO EN …   6.99  9.20/kg
              dia        161437            ColaCao 760 g           7  9.21/kg
              eroski     23666886          Cacao soluble COL…   7.00  9.21/kg
              caprabo    23666886          Cacao soluble COL…   7.00  9.21/kg

those are the first two of fifteen groups. that run made 15 requests, as many as the same search makes without a postcode, and grouping added none. besides the usual store notes, standard error says how many listings matched nothing:

mercadona: skipped: needs a postal code to bind a warehouse
aldi: no results
49 of 119 products matched no other store

compare leaves out a product the stores name too differently to match, instead of guessing. matching explains how pairs are scored and what precision and recall to expect.

product

supermercapy product STORE ID prints one product, one field per line. the id is the one search prints. fields the store does not publish are left out, and --json prints the whole record, nutrition and photos included.

$ supermercapy product mercadona 10381 --postal-code 08013
store       mercadona, warehouse bcn1
id          10381
name        Leche semidesnatada Hacendado
brand       Hacendado
ean         8402001002090
price       4.98 (was 5.04)
unit price  0.83/l
available   yes
category    Huevos, leche y mantequilla
url         https://tienda.mercadona.es/product/10381/leche-semidesnatada-hacendado-pack-6

that cost two requests, one to bind the postcode and one for the product. without --postal-code, a product costs one request at most stores. plusfresc fetches a guest token first, carrefour reads a warm-up page and follows one redirect, and condis follows the four redirects of its anonymous sign-in, so they spend two, three and five.

categories

supermercapy categories STORE prints the tree the store's get_categories() returns, one category per line, each child indented under its parent. how deep it goes is up to the store. carrefour's menu returns its departments alone:

$ supermercapy categories carrefour
id           name
cat20968591  Ofertas
cat20002     Frescos
cat20001     La Despensa
cat20003     Bebidas
cat20005     Droguería y limpieza
cat20004     Cuidado personal e Higiene
cat21449123  Congelados
cat20006     Bebé
cat20007     Mascotas
cat20008     Parafarmacia

that is one request. every other store's tree costs one too, except at condis, which reads it from a page behind its anonymous sign-in and spends five on a new client. --postal-code adds the binding.

ean

supermercapy ean EAN looks up a barcode of 8 to 14 digits in every store that declares EAN_LOOKUP, consum and carrefour today, in parallel.

$ supermercapy ean 8431876011937
store      id         name                                     price  unit price
carrefour  521007071  Leche semidesnatada Carrefour brik 1 l.   0.84  0.84/l
consum: not found

that lookup made five requests. consum filters its listing on the barcode in one. carrefour spent four, because it matches the barcode in its search index and then reads the product page after a warm-up page and a redirect. a store that does not carry the barcode notes not found, which is not a failure.

json output

with --json, a command prints exactly one json document on standard output, and the notes on standard error stay as they are. decimals are strings, so "0.84" keeps the published figure. saving results covers the conversion.

command document
stores a list with one object per store: store, class, binding, binding_required, and capabilities
search the SearchAllResult that search_all() returned
compare an object with query, postal_code, threshold, groups, unmatched, and stores: each group has its listings, score, and reasons, each unmatched entry is a store and its product, and each store entry is a search entry without its result
product the Product
categories the list of root categories, children nested inside each
ean an object with ean, postal_code, and stores, one entry per store

each ean entry has store, status, store_id, product, reason, and error_type, the same fields as a search entry with product in place of result. its status is ok, not_found, skipped, out_of_coverage, or failed.

supermercapy search leche --store consum --json | jq -r '.stores[0].result.products[].name'

exit codes

code meaning
0 every store asked answered, or was skipped, out of coverage, or without a match
1 a store failed, and whatever the other stores found is still printed
2 a usage error, such as a missing argument, an invalid postcode, or a store that needs --postal-code
130 interrupted with ctrl-c

a failure is one line on standard error, naming the store, the exception class, and its message, with no traceback:

lidl: TransportError: GET https://www.lidl.es/q/api/search returned HTTP 503