Outils MCP async (fix Playwright/asyncio) + hf_account_info + auth vérifiée

- server.py : outils passés en async + déport thread (anyio.to_thread.run_sync).
  Le SDK mcp 1.27.2 appelle les outils sync directement dans la boucle asyncio,
  ce qui cassait l'API sync de Playwright. Transport configurable via
  ANTICOCO_TRANSPORT (défaut streamable-http, stdio pour Claude Code local).
- api.py : nouvelle méthode account_info() (client, abonnement, adresse,
  prochaine livraison) + outil MCP hf_account_info (lecture seule).
- auth.py : auth_status() valide désormais le token par un vrai appel API
  (200 vs 401) au lieu de supposer "token présent = connecté", et n'ouvre plus
  de navigateur. _is_logged_in() utilise un signal positif (cookie apiV2Auth
  non expiré) au lieu de l'absence de champ mot de passe. Supprime les faux
  positifs "connecté" sur session morte (important pour le homelab/Hermes).
This commit is contained in:
jerem
2026-06-18 11:31:56 +02:00
parent 5d3899fdfb
commit e37a27cc1a
3 changed files with 206 additions and 75 deletions

View File

@@ -146,6 +146,65 @@ class HelloFreshClient:
"customer_id": str(cust.get("id", "")),
"sku": str(prod.get("sku", ""))}
def account_info(self) -> dict[str, Any]:
"""Résumé lisible du compte : client, abonnement, adresse, prochaine livraison."""
ep = self._ep.get("subscriptions")
if not ep:
raise EndpointsNotConfigured("Endpoint 'subscriptions' manquant.")
data = self._request("GET", ep, params={"country": self._country}).json()
items = data.get("items", []) if isinstance(data, dict) else (data or [])
if not items:
raise RuntimeError("Aucun abonnement actif sur ce compte.")
sub = items[0]
cust = sub.get("customer") or {}
ptype = sub.get("productType") or {}
prod = sub.get("product") or {}
ship = sub.get("shippingAddress") or {}
def _addr(a: dict[str, Any]) -> dict[str, Any]:
return {
"name": " ".join(p for p in (a.get("firstName"), a.get("lastName")) if p),
"address": a.get("address1"),
"postcode": a.get("postcode"),
"city": a.get("city"),
"country": (a.get("country") or {}).get("iso2Code"),
"phone": a.get("phone"),
}
unit_price = prod.get("unitPrice")
return {
"customer": {
"id": cust.get("id"),
"email": cust.get("email"),
"first_name": cust.get("firstName"),
"last_name": cust.get("lastName"),
"locale": cust.get("locale"),
"loyalty_points": (cust.get("loyalty") or {}).get("value"),
},
"subscription": {
"id": sub.get("id"),
"active": sub.get("isActive"),
"paused_at": sub.get("pausedAt"),
"canceled_at": sub.get("canceledAt"),
"blocked": sub.get("isBlocked"),
"sku": prod.get("sku") or ptype.get("handle"),
"product_name": ptype.get("productName"),
"meals": (ptype.get("specs") or {}).get("meals"),
"people": (ptype.get("specs") or {}).get("size"),
"box_price_eur": unit_price / 100 if isinstance(unit_price, int) else None,
"preset": sub.get("preset"),
"delivery_weekday": sub.get("deliveryWeekday"),
"delivery_interval": sub.get("deliveryInterval"),
"payment_method": sub.get("paymentMethod"),
},
"shipping_address": _addr(ship),
"next_delivery": {
"week": sub.get("nextDeliveryWeek"),
"date": sub.get("nextDelivery"),
"cutoff": sub.get("nextCutoffDate"),
},
}
def _menu_courses(self, week: str) -> list[dict]:
"""Courses bruts du menu (chacun : index + recipe). Base de l'écriture."""
params = {