Sélection courante + favoris + images servables (sortie prête Hermes)

Deux outils MCP pour qu'Hermes n'ait plus de scripts à écrire :
- hf_next_delivery() : prochaine box RÉELLEMENT sélectionnée (≈4 recettes,
  pas le menu complet) + date/cutoff ; erreur stricte si introuvable
  (jamais de repli propose). Saute les semaines PAUSED via next_delivery.
- hf_favorites() : recettes favorites du compte. Champ is_favorite ajouté
  partout (hf_get_menu inclus).

Endpoints découverts (probe CDP) :
- sélection : GET /gw/my-deliveries/menu -> meals[].selection.quantity>0
- favoris   : GET /gw/cfs/v2/favorites/recipe -> items[].object_id
(GET /gw/v1/carts/{week} renvoie 404 : pas la lecture de sélection.)

Images : URLs recettes CloudFront (502) réécrites vers
img.hellofresh.com/.../hellofresh_s3/... (hellofresh/images.py),
appliqué dans Recipe.summary() -> profite à tous les outils.

README : procédure de ré-auth CDP clarifiée (refresh tokens rotatifs,
backups inutiles, page /login, profil Chrome dédié).

Outils de re-découverte : tools/probe_selection.py, tools/probe_menu_capture.py
This commit is contained in:
jerem
2026-06-18 14:18:40 +02:00
parent 4b1eb9f52c
commit 03e281a810
8 changed files with 489 additions and 10 deletions

View File

@@ -255,6 +255,69 @@ class HelloFreshClient:
recipes.extend(Recipe.from_api(r) for r in raw)
return recipes
def get_current_selection(self, week: str) -> list[Recipe]:
"""Recettes RÉELLEMENT dans la box de la semaine (sélection courante, ≠ menu complet).
Source : `GET my-deliveries/menu` (BFF) → `meals[*]` ; un repas est sélectionné quand
`selection.quantity > 0`. On en extrait les ids puis on récupère les détails complets
(ingrédients/allergènes) comme pour le menu. Lève une exception si rien n'est
sélectionné (box vide / non finalisée) — pas de repli vers une proposition.
"""
ep = self._ep.get("current_selection")
if not ep:
raise EndpointsNotConfigured("Endpoint 'current_selection' manquant.")
sub = self._subscription_info()
params = {
"country": self._country,
"locale": self._locale,
"subscription": sub["sub_id"],
"product-sku": sub["sku"],
"week": week,
}
data = self._request("GET", ep, params=params).json()
meals = data.get("meals", []) if isinstance(data, dict) else []
idx_by_id: dict[str, int | None] = {}
for m in meals:
sel = m.get("selection")
qty = sel.get("quantity") if isinstance(sel, dict) else None
if qty and qty > 0:
rid = (m.get("recipe") or {}).get("id")
if rid:
idx_by_id[str(rid)] = m.get("index")
if not idx_by_id:
raise RuntimeError(
f"Aucune recette sélectionnée pour {week} (box vide ou non finalisée ?)."
)
recipes = self._fetch_details(list(idx_by_id))
if not recipes:
raise RuntimeError(f"Détails de la sélection {week} introuvables.")
for r in recipes:
r.course_index = idx_by_id.get(r.id)
return recipes
def favorite_ids(self) -> set[str]:
"""Ids des recettes favorites du compte (best-effort : set() si indispo)."""
ep = self._ep.get("favorites")
if not ep:
return set()
try:
data = self._request("GET", ep, params={
"country": self._country, "locale": self._locale, "ids": ""}).json()
items = data.get("items", []) if isinstance(data, dict) else (data or [])
return {str(it.get("object_id")) for it in items if it.get("object_id")}
except Exception:
return set()
def get_favorites(self) -> list[Recipe]:
"""Recettes favorites du compte, complètes. Liste vide si aucun favori."""
ids = self.favorite_ids()
if not ids:
return []
recipes = self._fetch_details(list(ids))
for r in recipes:
r.is_favorite = True
return recipes
def selection_indices(self, week: str, recipe_ids: list[str]) -> tuple[list[int], list[str]]:
"""Mappe des ids de recette vers leurs index de course pour la semaine.