SDK, Alexa et Home Assistant
Une plateforme associative publie chaque jour les horaires de milliers de lieux. J’ai écrit le contrat de son API, généré deux SDK typés à 100 % de couverture, repris et réarchitecturé sa skill Alexa, et porté son intégration Home Assistant.
- clients pour une même API
- 4
- téléchargements des SDK par mois
- ~3 300
- de couverture, lignes et branches
- 100 %
- tests sur les quatre projets
- 1 300+
Le contexte
Une plateforme associative publie les horaires quotidiens de milliers de lieux communautaires. Chaque lieu tient son planning annuel, affiché sur des écrans sur place, dans une app mobile, et de plus en plus sur des enceintes et dans la maison connectée. L’API existait, mais sans documentation ni bibliothèque officielle : chaque client refaisait les mêmes calculs, avec les mêmes bugs.
Un contrat, quatre clients
Tout part d’une spec OpenAPI 3.1 que j’ai écrite à la main à partir du code du backend. Elle ne décrit que ce que les clients utilisent, mais elle le décrit exactement. Deux générateurs maison en tirent un SDK Python et un SDK TypeScript, qui alimentent à leur tour l’intégration Home Assistant et la skill Alexa.
Ce qui était difficile
Un contrat écrit à la main qui ne ment pas
Une spec écrite à la main dérive au premier changement du backend. Trois filets l’en empêchent. Des règles Spectral maison refusent toute opération sans identifiant ni description, puisque ces descriptions deviennent la documentation des SDK. AJV valide de vraies réponses capturées en fermant chaque objet (additionalProperties: false injecté partout) : un champ non documenté fait échouer la CI. Et chaque nuit, un job capture des réponses fraîches en production et les revalide.
Côté SDK, la spec est épinglée par tag et par empreinte SHA-256. La CI régénère le code et échoue au moindre écart : impossible de publier une bibliothèque qui ne correspond pas au contrat.
Des SDK générés, mais pas génériques
Je n’ai pas pris openapi-generator : un SDK générique, ça se sent. Mes générateurs (Jinja2 et ruff côté Python, templates et Biome côté TypeScript) produisent les types, les ressources et le client. Le transport, les erreurs et les calculs d’horaires restent écrits à la main. Et ce qu’OpenAPI ne sait pas exprimer devient un type :
// simplified: OpenAPI cannot say "a position OR words", the generated union can
client.venues.search({ lat: 48.85, lon: 2.35 }) // ok
client.venues.search({ words: 'paris' }) // ok
// @ts-expect-error a latitude needs its longitude
client.venues.search({ lat: 48.85 })Le client reprend les erreurs avec backoff et jitter, respecte Retry-After jusqu’à 60 secondes, s’annule proprement avec AbortSignal, et ne suit jamais une redirection, pour que le jeton ne parte pas vers un autre domaine.
L’heure, le vrai ennemi
Chaque lieu publie des heures murales, « 05:42 », jour par jour, dans son fuseau. Les clients ont besoin d’instants. Entre les deux : les changements d’heure, les événements après minuit, les décalages relatifs et les saisies invalides. Python s’appuie sur zoneinfo, TypeScript sur Intl seul, sans dépendance. Une heure invalide devient null plutôt qu’une fausse valeur, et un test de référence vérifie que les deux SDK calculent le même calendrier lunaire, mois par mois, de 1990 à 2060.
Un son à la minute près, ou pas du tout
J’ai repris une skill Alexa écrite par un prestataire et je l’ai réarchitecturée : passage de JavaScript à TypeScript strict en treize PR, données dans DynamoDB, et surtout des annonces sonores programmées qui doivent sonner à l’heure, dans le fuseau de chaque lieu.
Une annonce en retard est pire qu’une annonce manquée. Au-delà de cinq minutes, le worker jette le message. La visibilité SQS est réduite à 30 secondes pour que trois essais tiennent dans la fenêtre, et la file d’erreurs déclenche une alarme mais n’est jamais rejouée. Les écritures concurrentes (la voix, le site, un widget) ne modifient que leurs champs, sous un numéro de version, et rejouent leur diff en cas de conflit.
Le niveau d’exigence
Lignes et branches, dans les deux SDK. mypy et pyright en strict, API publique typée à 100 %, Python 3.10 à 3.14, Node 22 à 26, Deno et Bun testés.
PyPI et npm via OIDC, avec provenance. Actions épinglées par SHA, workflows audités, aucun jeton longue durée.
Côté Alexa, déployer en production exige un tag et l’accord de quelqu’un d’autre que celui qui lance. Le rôle de déploiement ne peut que mettre à jour du code.
Home Assistant
L’intégration communautaire est celle qui tourne chez les gens : j’ai écrit 60 de ses 74 PR fusionnées, avec 394 tests, 100 % de couverture et une interface en six langues. Une version pour le cœur de Home Assistant, coécrite, est en revue chez les mainteneurs. Chaque entité y planifie son prochain changement de valeur au lieu d’interroger l’API toutes les minutes.
Ce que j’en retiens
Un bon contrat vaut plus que n’importe quel générateur, et le temps est le domaine le plus traître que je connaisse. Le reste, c’est de l’outillage, pour que plus personne n’ait à y penser.