انتقل إلى المحتوى
Mohamed Tahiri
Miami --:--
2023 → · مفتوح المصدر

SDKs, Alexa and Home Assistant

A non-profit platform publishes daily timetables for thousands of venues. I wrote its API contract, generated two typed SDKs with 100% coverage, took over and re-architected its Alexa skill, and carried its Home Assistant integration.

Role
SDK author, Alexa lead
Clients
Python, TypeScript, Alexa, Home Assistant
Usage
~3,300 downloads / month
Quality
1,300+ tests, 100% coverage

دراسة الحالة هذه متاحة بالفرنسية والإنجليزية فقط.

clients for one API
4
SDK downloads a month
~3,300
line and branch coverage
100%
tests across the four projects
1,300+

The context

A non-profit platform publishes daily timetables for thousands of community venues. Each venue keeps its own yearly schedule, shown on screens on site, in a mobile app, and more and more on smart speakers and in smart homes. The API existed, but with no documentation and no official library: every client redid the same maths, with the same bugs.

One contract, four clients

APIPHP · SYMFONYCI checksSPECTRAL · AJV · NIGHTLY PRODOpenAPI 3.1 specHAND-WRITTEN · VERSIONEDPython generatorJINJA2 · RUFFTS generatorTEMPLATES · BIOMEPython SDKPYPI · SYNC + ASYNCTypeScript SDKNPM · ZERO DEPSHome AssistantHACS · CORE IN REVIEWAlexa skillLAMBDA · 7 LOCALESCAPTUREPINNED · SHA-256CI REGENERATES AND FAILS ON ANY DIFF

It all starts from an OpenAPI 3.1 spec I wrote by hand from the backend’s code. It only describes what clients use, but it describes it exactly. Two in-house generators turn it into a Python SDK and a TypeScript SDK, which in turn power the Home Assistant integration and the Alexa skill.

What was hard

A hand-written contract that doesn’t lie

A hand-written spec drifts at the backend’s first change. Three nets stop that. Custom Spectral rules reject any operation without an id or a description, since those descriptions become the SDKs’ documentation. AJV validates real captured responses with every object closed (additionalProperties: false injected everywhere): an undocumented field fails CI. And every night, a job captures fresh production responses and validates them again.

On the SDK side, the spec is pinned by tag and SHA-256. CI regenerates the code and fails on any difference: you cannot publish a library that doesn’t match the contract.

Generated SDKs that don’t feel generated

I didn’t use openapi-generator: you can tell a generic SDK. My generators (Jinja2 and ruff for Python, templates and Biome for TypeScript) produce the types, resources and client. Transport, errors and timetable maths stay hand-written. And what OpenAPI cannot express becomes a type:

search.test-d.ts
// 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 })

The client retries with backoff and jitter, honours Retry-After up to 60 seconds, cancels cleanly through AbortSignal, and never follows a redirect, so the token can’t leak to another domain.

Time, the real enemy

Each venue publishes wall-clock times, “05:42”, day by day, in its own time zone. Clients need instants. In between: daylight saving changes, events after midnight, relative offsets and invalid entries. Python relies on zoneinfo, TypeScript on Intl alone, with no dependency. An invalid time becomes null rather than a wrong value, and a reference test checks that both SDKs compute the same lunar calendar, month by month, from 1990 to 2060.

A chime on the minute, or not at all

I took over an Alexa skill written by a contractor and re-architected it: JavaScript to strict TypeScript in thirteen PRs, data in DynamoDB, and above all scheduled audio announcements that must play on time, in each venue’s time zone.

Scheduler1 ENTRY PER VENUE × EVENTDaily refresh04:05 LOCAL TIMETriggerLAMBDA · FAN-OUTSQS queue30 S VISIBILITYDead lettersALARM · NEVER REPLAYEDWorkerDROPS > 5 MIN LATEDynamoDBTOKENS · DEVICESAlexaROUTINE → SPEAKERAFTER 3 TRIESLATE IS WORSE THAN NEVER

A late announcement is worse than a missed one. Past five minutes, the worker drops the message. SQS visibility is cut to 30 seconds so that three attempts fit in the window, and the dead-letter queue raises an alarm but is never replayed. Concurrent writes (voice, website, a widget) only touch their own fields, under a version number, and replay their diff on conflict.

The bar

100% coverage, enforced

Lines and branches, in both SDKs. mypy and pyright in strict mode, a 100% typed public API, Python 3.10 to 3.14, Node 22 to 26, Deno and Bun tested.

Releases without secrets

PyPI and npm through OIDC, with provenance. Actions pinned by SHA, audited workflows, no long-lived token.

Two people for production

For Alexa, a production deploy needs a tag and the approval of someone other than the person who started it. The deploy role can only update code.

Home Assistant

The community integration is the one running in people’s homes: I wrote 60 of its 74 merged PRs, with 394 tests, 100% coverage and a UI in six languages. A version for Home Assistant core, co-written, is in review with the maintainers. There, each entity schedules its own next value change instead of polling the API every minute.

What I took away

A good contract is worth more than any generator, and time is the most treacherous domain I know. The rest is tooling, so nobody has to think about it again.

التقنيات

OpenAPI 3.1PythonTypeScriptSpectralAJVpytestVitestAWS LambdaDynamoDBEventBridgeSQSAlexa Skills KitHome AssistantGitHub ActionsSentry
المشروع التاليBuddy →
تواصل

لنبنِ شيئًا معًا.

أو اترك رسالة