ahorramás¶
supermercapy.ahorramas.Ahorramas reads ahorramás's online supermarket at www.ahorramas.com. the shop runs salesforce commerce cloud behind cloudflare and publishes no product api, so the client reads what the storefront's own scripts read:
- listings come from the html grid the "más resultados" button loads,
Search-UpdateGrid, one tile per product with the prices, the unit price, the promotion callouts and the purchase limits. - a product's record is the json its page asks for when the quantity changes,
Product-Variation, which adds the photos, the promotion ids and dates, the general attributes and the description. - the category tree is the menu every full page carries. the client reads the home page's.
nothing binds. the header offers a postcode box, and entering one stores a delivery postcode and a preferred shop in the anonymous session. in october 2026 the first 48 results for "leche" and every one of their prices were identical with no postcode, with 28001 and with 28903, and the shop refused 08001 as outside the delivery area. so the postcode decides delivery, not the catalog, and POSTAL_CODE and STORES are not declared.
from supermercapy import Ahorramas
with Ahorramas() as ahorramas:
page = ahorramas.search_products("leche", page_size=24) # one request
for row in page.products:
row.price.amount, row.price.reference, row.promotions
milk = ahorramas.get_product(page.products[0].id) # one request
milk.photos, milk.general_info, milk.category_ids
tree = ahorramas.get_categories() # one request, about 790 kb
lamb = ahorramas.get_category("cordero_y_cabrito") # one more
capabilities¶
| capability | supported | how |
|---|---|---|
CATALOG |
yes | iter_catalog walks the catalog's root category a hundred products at a time |
OFFERS |
yes | get_offers walks the menu's "ofertas" branch to its end |
PROMOTIONS |
yes | price drops and multi-buys become Promotion records with their price and dates |
NUTRITION |
no | the record has ingredient, nutrition and allergen fields, and all fifteen products sampled in october 2026 left them empty |
POSTAL_CODE / STORES |
no | a postcode picks a delivery shop and changes no price or product, see above |
NEW_ARRIVALS |
no | the record's novelty field was empty on every product sampled, and no category collects novelties |
HOME |
no | the home page carries no product tiles; its carousel is filled by a separate script request |
EAN / EAN_LOOKUP |
no | no barcode appears in any response |
FUTURE_PRICES |
no | a promotion's dates bound the price that applies now |
extensions¶
| method | what it does |
|---|---|
get_category_products(category_id, page_size=, cursor=) |
one listing page of a category, its subtree included, without reading the menu |
the product model adds category_names, the names of the categories a product is filed under from the top down, badges such as "SIN GLUTEN" or "PESO VARIABLE", average_weight in kilograms for products sold by weight, and, from a record, general_info, every filled general attribute as a (label, value) pair. categories add path, their address on the site, and url.
request counts¶
| call | requests | size |
|---|---|---|
search_products, get_category_products |
one per page | about 11 kb per product |
get_product |
one | about 30 kb |
get_categories |
one | about 790 kb |
get_category |
one, plus the menu once per client | about 11 kb per product |
get_offers |
one per hundred offers, plus an empty page when the last is full | 1,000 to 1,500 offers in october 2026, a megabyte per hundred |
iter_catalog |
one per hundred products | a full walk on 4 october 2026 read 7,579 products in 76 requests and 88 mb, in nine minutes, because each page took about seven seconds to answer |
the sizes are the html and json after decompression. the shop compresses its answers, so the wire carries about a fourteenth of that: ten requests that decompress to 2.7 mb arrived as 190 kb in october 2026.
quirks¶
the grid is the listing. Search-UpdateGrid answers html tiles and nothing else around them. it takes q or cgid, start and sz, so the cursor is an offset. the grid publishes no total, so total_hits is None, and it keeps its "more results" button on its last page, so a page shorter than page_size is the end and a full last page costs one empty request more.
pages can be large. the storefront answered 300 tiles in one 3.9 mb response, so it caps nothing a client is likely to ask for. max_page_size is the client's own cap of 100, to keep one response near a megabyte, and the walks use it.
an unknown category is not an error. a cgid the storefront does not know answers the whole catalog, unfiltered, with http 200. every grid carries its sort links, which repeat the request's filters, so the client checks that they name the category it asked for and raises NotFoundError when they do not. a blank query also answers the whole catalog, so the client refuses one before sending it.
category ids are the menu's link ids. they are what cgid takes, and they do not always match the path: cordero_y_cabrito lives at /frescos/carniceria/cordero-y-lechal/. get_categories builds the tree from the link paths and takes each id from its link. the client skips the "ver todo" links, because some point into another branch.
the analytics price is the old one. each tile carries an analytics attribute whose price is the list price during a price drop. price.amount reads the price the tile shows, and a struck-through price fills previous.
products sold by weight are priced per kilogram. fresh meat, fish and produce show a price per kilogram, a PESO VARIABLE badge, and purchase steps in kilograms, such as 0.19 for one banana. is_variable_weight is set, average_weight is the weight of an average piece, and price.reference is the price per kilogram. the record's own measure fields disagree with the tile for these products, sometimes by a factor of five, so the client never reads them for a weighed product.
units. tiles print the unit price as 0,83€/LITRO, and the units seen were LITRO, KILO, Kg, UNIDAD, DOCENA, LAVADO, and KG.PESO ESC on canned and jarred food, per kilogram of drained weight, so price.reference restates them per litre, kilogram, piece or dose. a record carries the same figure and unit as two separate fields, which fill unit_price and unit_price_unit and leave unit_price_text empty.
promotions carry their dates. a callout reads Bajada de precio a 0.48€ (01/10/26 - 28/10/26) or Comprando 2, la unidad te sale a 0.50€ (24/09/26 - 28/10/26). each becomes a Promotion with its price, its start and end dates at midnight utc, and, for a multi-buy, requires_quantity. a record adds the promotion's id and its name in kind, such as Pack 2 unidades.
an unknown product is a server error. Product-Variation answers an id it does not know with http 500 and an error document, the same answer a fault would get. the client reads that document as NotFoundError and does not retry it. a 500 without it is retried as usual.
sponsored tiles. the full search page mixes retail-media tiles into its results. the grid the client reads had none in october 2026, and a tile the storefront flags as sponsored comes back with is_sponsored set.
cloudflare. the shop answered the library's own user agent in october 2026 without a challenge. a challenge, whether a cf-mitigated: challenge header or the "just a moment" page, raises ChallengedError after one request, and any other 403 raises BlockedError. min_request_interval defaults to one second, because a catalog walk is close to fifty pages of a megabyte each.
robots.txt. ahorramás's robots.txt disallows /on/demandware.store/Sites-Ahorramas-Site/es/Product-Variation*, which get_product reads, along with Search-ShowAjax*, /buscador?*, *&prefn1*, *?pmax*, *ver-todo*, the cart and the login. the client never sends the others, and Search-UpdateGrid is not disallowed. the owner chose to use these paths.
non-goals¶
- the delivery postcode. entering one changes the session and the delivery slots, not the catalog, so the client never sends one.
- nutrition. the fields exist and were empty everywhere. the client parses nothing until a product carries them.
- carts, lists and accounts. the tiles carry add-to-cart and wishlist endpoints. the client never calls them.