Skip to Content

SDK IronFlock

Le SDK IronFlock permet à vos applications edge d’interagir avec la plateforme IronFlock. Il existe en trois langages — Python, JavaScript et Go — avec les mêmes fonctionnalités, documentés côte à côte ci-dessous. Il gère l’authentification automatiquement lorsqu’il s’exécute sur un appareil enregistré et fournit des fonctions pour publier des données, interroger l’historique, appeler des procédures distantes entre appareils et mettre à jour les métadonnées des appareils.

SDKPackagePrérequis
Pythonironflock sur PyPIPython 3.8+
JavaScriptironflock sur npmNode.js 18+ ou navigateur moderne
Gogithub.com/RecordEvolution/ironflock-goGo 1.27.2+

Installation

pip install ironflock

Ou ajoutez ironflock au requirements.txt de votre application.

Démarrage rapide

import asyncio from datetime import datetime, timezone from ironflock import IronFlock async def main(): while True: await ironflock.publish_to_table("sensordata", { "tsp": datetime.now(timezone.utc).isoformat(), "temperature": 22.5, "humidity": 60 }) await asyncio.sleep(5) ironflock = IronFlock(mainFunc=main) ironflock.run()

Lorsqu’il est utilisé à l’intérieur d’un conteneur d’application IronFlock, le SDK lit automatiquement les identifiants de connexion depuis l’environnement — aucune configuration manuelle n’est nécessaire. Chaque ligne que vous écrivez dans une table porte un tsp, son horodatage (voir publishToTable).

Options du constructeur

ironflock = IronFlock( mainFunc=main, # async function to run after connecting serial_number="abc123", # override device serial (optional) reconnect_window=60, # seconds a table operation rides out a platform restart (optional) )
ParamètreDescription
mainFuncUne fonction asynchrone qui s’exécute une fois la connexion établie
serial_numberRemplace le numéro de série de l’appareil envoyé avec les publications et les écritures dans les tables. Par défaut, la variable d’environnement DEVICE_SERIAL_NUMBER. Ne change pas l’identité avec laquelle la connexion s’authentifie tant que l’agent de l’appareil injecte des identifiants propres à l’app
reconnect_windowDurée en secondes pendant laquelle une opération sur une table attend le retour de la plateforme après un redémarrage avant de lever une erreur. Par défaut 60 ; 0 la désactive (voir Fiabilité de la connexion)

Publier des données

publishToTable / publish_to_table

Publie un enregistrement de données dans une table de flotte. Le nom de la table doit correspondre à une table définie dans le data-template.yml de votre application. Le SDK achemine automatiquement les données vers la bonne base de données du projet.

from datetime import datetime, timezone await ironflock.publish_to_table("sensordata", { "tsp": datetime.now(timezone.utc).isoformat(), "temperature": 22.5, "humidity": 60, "device_id": "sensor-001" })

Le backend de données lit la ligne depuis le premier argument : un dict en Python, le premier élément du tableau en JavaScript, le premier argument positionnel en Go (une map ou une struct). Les arguments nommés supplémentaires — en Go, une valeur ironflock.Kwargs parmi les arguments — ne sont lus que pour les colonnes que le data template leur associe (kwargs.<name>).

Chaque ligne a besoin d’un tsp. Chaque table possède une colonne tsp obligatoire, l’horodatage de la ligne — par ex. une chaîne ISO 8601, ou en Go un time.Time, envoyé en RFC 3339 en UTC —, que le backend de données lit dans la ligne, sauf si le data template l’associe à une autre source. Il abandonne une ligne publiée sans tsp, avec pour seule trace une entrée dans la table error-logs de l’app — la publication acquittée semble réussie. Une ligne qu’il refuse faute d’espace de stockage est abandonnée sans même cette trace. Utilisez appendToTable / append_to_table lorsque vous devez savoir si une ligne a été stockée.

Lignes partielles dans les tables d’entités. Dans une table dotée d’une clé d’entité (maintainLatestFlagFor), une ligne peut ne contenir que la clé d’entité, tsp et les colonnes qui changent. Une colonne que la ligne omet conserve la valeur précédente de l’entité ; une colonne envoyée comme None / null / nil est effacée. Qu’un champ compte comme omis dépend de la façon dont vous construisez la ligne :

  • Python : une clé dont la valeur est None est envoyée comme null et efface la colonne. Retirez la clé du dict pour conserver l’ancienne valeur.
  • JavaScript : une propriété dont la valeur est undefined disparaît du payload, elle conserve donc l’ancienne valeur. Mettez-la à null pour effacer la colonne.
  • Go : un champ de struct marqué omitempty ou omitzero est omis lorsqu’il est vide, il conserve donc l’ancienne valeur. Sinon, un champ pointeur nil, comme une valeur nil dans un ironflock.Row, est envoyé comme null et efface la colonne.

Il en va de même pour les quatre méthodes d’écriture. Dans une table sans clé d’entité, une colonne que la ligne omet est stockée comme NULL.

appendToTable / append_to_table

Ajoute des données à une table de flotte via un appel de procédure distante plutôt que via pub/sub. Utilisez ceci lorsque vous avez besoin de confirmation que les données ont été persistées : une ligne refusée est une erreur.

result = await ironflock.append_to_table("sensordata", { "tsp": datetime.now(timezone.utc).isoformat(), "temperature": 22.5, "humidity": 60 })

La ligne a besoin d’un tsp, comme pour publishToTable. Le backend de données refuse une ligne sans tsp avec wamp.error.runtime_error et sans raison ; l’erreur du SDK ajoute alors “the data backend refused the row without a reason — a missing tsp is the usual cause; see the error-logs table”. Les refus typés, comme un disque plein sur une appliance, sont listés sous Refus du backend de données.

publishRowsToTable / publish_rows_to_table

Publie plusieurs lignes dans un seul message (insertion en masse) dans une table de flotte. La plateforme insère l’intégralité du lot de manière atomique (tout ou rien) en une seule opération. Utilisez ceci pour les données à haute fréquence, où un aller-retour par ligne serait trop coûteux. Comme publishToTable, ceci fonctionne en mode « fire-and-forget » — l’accusé de réception confirme la livraison au routeur, et non l’insertion en base de données.

await ironflock.publish_rows_to_table("sensordata", [ {"tsp": "2024-01-15T10:30:00.000Z", "temperature": 22.5}, {"tsp": "2024-01-15T10:30:01.000Z", "temperature": 22.7}, ])

Le second argument est une liste non vide d’objets de lignes à insérer, chacun avec son propre tsp — en Go, une slice de lignes ([]ironflock.Row, []map[string]any ou une slice de structs). Un lot que le backend de données refuse — parce qu’une ligne n’a pas de tsp, par exemple — est abandonné sans que l’émetteur en soit informé, comme avec publishToTable.

appendRowsToTable / append_rows_to_table

Ajoute plusieurs lignes via un seul appel de procédure distante (insertion en masse) dans une table de flotte. La plateforme insère l’intégralité du lot de manière atomique (tout ou rien) : si une ligne est invalide, le lot entier est rejeté et rien n’est persisté. Préférez ceci à publishRowsToTable / publish_rows_to_table lorsque vous avez besoin de connaître le résultat de l’insertion. Chaque ligne a besoin de son propre tsp ; les refus sont ceux d’appendToTable, y compris l’indication pour un lot refusé sans raison.

result = await ironflock.append_rows_to_table("sensordata", [ {"tsp": "2024-01-15T10:30:00.000Z", "temperature": 22.5}, {"tsp": "2024-01-15T10:30:01.000Z", "temperature": 22.7}, ]) # result -> {"success": True, "count": 2}

reportError / report_error

Signale une erreur applicative dans la table error-logs de votre flotte. Il s’agit d’un wrapper pratique au-dessus de publishToTable / appendToTable : il estampille la ligne avec source: "app", un niveau de gravité level et un horodatage, puis l’écrit comme n’importe quelle ligne de table normale. L’erreur arrive dans la même table error-logs que celle utilisée par les erreurs système de fleetdb (étiquetées source: "system"), elle est donc interrogeable avec getHistory, diffusable en flux avec subscribeToTable / subscribe_to_table, utilisable dans les modèles de tableaux et livrée en temps réel sur transformed.error-logs — sans déclencher la notification d’erreur système de la plateforme.

# Fire-and-forget (default): publishes to the error-logs table await ironflock.report_error("Sensor read timed out", level="warn") # Pass an exception to capture its traceback (falls back to the message) try: risky_operation() except Exception as err: await ironflock.report_error(err) # Use the append RPC when you want to await the insert outcome, and show the # operator a friendlier line than the raw error await ironflock.report_error( "Calibration failed: offset 0.42 out of range", append=True, user_message="Please recalibrate the sensor", )

La ligne est { tsp, msg, user_message, source: "app", level } : msg contient le texte technique (une trace ou une pile d’appels), user_message la ligne qu’affiche la notification d’erreur du board — le message utilisateur que vous passez ou, à défaut, le message lui-même.

Paramètres :

ParamètreTypeDescription
errorstr / string ou exception / ErrorLe message d’erreur, ou une exception dont la trace/pile (ou le message) est enregistrée
levelstr / string, optionnelGravité : "error", "warn", "info" ou "debug". Par défaut "error"
appendbool / boolean, optionnelLorsque true, utilise la procédure distante d’ajout (renvoie le résultat de l’insertion). Par défaut false (publication en mode « fire-and-forget »)
tspstr / string, optionnelHorodatage ISO-8601 de remplacement. Par défaut, l’heure actuelle
user_message / userMessagestr / string, optionnelTexte destiné à l’opérateur, stocké dans user_message. Par défaut, le message enregistré

En Python, les options sont des arguments nommés (report_error(error, level=..., append=..., tsp=..., user_message=...)) ; en JavaScript, elles sont passées via un objet d’options (reportError(error, { level, append, tsp, userMessage })) ; en Go, via une struct (ReportError(ctx, errOrMsg, ironflock.ReportErrorOptions{Level, Append, Tsp, UserMessage})), où errOrMsg est une error ou une chaîne, Level l’une des valeurs ironflock.LevelError, LevelWarn, LevelInfo, LevelDebug, et où le *Result renvoyé vaut nil sauf si Append est activé.

publish

Publie un message sur un topic WAMP qui vous est propre. Utilisez ceci pour la messagerie personnalisée ou les événements qui ne correspondent pas à une table de base de données. Le routeur réserve certains noms à la plateforme — voir URI autorisées pour une app.

await ironflock.publish("com.myapp.alerts", { "level": "warning", "message": "Temperature threshold exceeded" })

Interroger les données historiques

getHistory

Récupère des données historiques depuis une table de flotte ou un transform : les lignes correspondantes les plus récentes, par ordre croissant de tsp. Supporte le filtrage, les plages temporelles et la pagination.

# Simple query data = await ironflock.getHistory("sensordata", {"limit": 100}) # Query with time range and filters data = await ironflock.getHistory("sensordata", { "limit": 500, "offset": 0, "timeRange": ["2026-01-01T00:00:00Z", "2026-03-01T00:00:00Z"], "filterAnd": [ {"column": "temperature", "operator": ">", "value": 20}, {"column": "humidity", "operator": "<=", "value": 80} ] }) # Current value(s) only: the "latest" marker returns the newest row per entity current = await ironflock.getHistory("sensordata", { "limit": 100, "filterAnd": [{"latest": True}] }) # One field of a large json column: the column comes back pruned to its paths readings = await ironflock.getHistory("sensordata", { "limit": 100, "columns": ["json_data"], "columnPaths": ["json_data.sensor.temp"], })

Paramètres de requête :

ChampTypeDescription
limitint / numberNombre maximum de lignes à renvoyer (1–10 000, obligatoire ; 1–3 000 pour un transform)
offsetint / numberDécalage pour la pagination
timeRangelist / array[start, end] — chaînes date-heure ISO ou nombres en millisecondes epoch, jamais mélangés ; None / null pour une borne ouverte. Un ancien objet {"start": ..., "end": ...} est converti en paire
filterAndlist / arrayEntrées de filtre combinées par AND : conditions, groupes et/ou le marqueur latest (voir ci-dessous)
columnslist / arrayColonnes à renvoyer (optionnel). tsp, device_key et authid sont toujours inclus ; omettez ce champ pour obtenir toutes les colonnes
columnPathslist / arrayÉlague les colonnes json (optionnel) : chemins JSON dans la notation qu’utilisent les filtres ("json_data.a.b", "json_data['key']"). Une colonne json listée dans columns revient en ne contenant que l’union de ses chemins ; une colonne sans chemin ici revient entière. Ignoré par les transforms et par les backends de données antérieurs à fleetdb v1.0.52

En Go, ce sont les champs Limit, Offset, TimeRange, FilterAnd, Columns et ColumnPaths de ironflock.TableQueryParams. TimeRange est un *ironflock.TimeRange{Start, End} : chaque borne est un time.Time, une chaîne ISO 8601, un nombre en millisecondes epoch, ou nil pour une borne ouverte (ironflock.Between, Since et Until en construisent un à partir de valeurs time.Time). FilterAnd est un []ironflock.Filter. Il n’existe pas d’ancienne forme objet.

Opérateurs de filtre : =, !=, <>, >, <, >=, <=, LIKE, ILIKE, NOT LIKE, NOT ILIKE, IN, NOT IN, IS NULL, IS NOT NULL

Chaque condition de filtre est un objet avec les clés column, operator et value — en Go, construit avec ironflock.Where(column, operator, value), ironflock.IsNull(column) et ironflock.IsNotNull(column). Les opérateurs sont reconnus quelle que soit la casse, les espaces qui les entourent sont ignorés, et ils sont envoyés sous la forme listée ci-dessus — la seule que le backend de données accepte —, de sorte que "like" part sous la forme "LIKE", y compris à l’intérieur des groupes. Un opérateur hors de cette liste est envoyé tel quel, avec un avertissement. IS NULL et IS NOT NULL ne prennent pas de value et sont le seul moyen de tester NULL : tout autre opérateur exclut une ligne dont la colonne est NULL, exactement comme en SQL. IN et NOT IN prennent une liste (une slice en Go). LIKE est sensible à la casse ; utilisez ILIKE pour une correspondance insensible à la casse.

Combiner avec OR. Les entrées de filterAnd sont combinées par AND. Une entrée de groupe combine ses propres entrées — conditions, marqueur latest ou autres groupes — avec un seul opérateur, "AND" ou "OR" :

# Rows that are not soft-deleted: deleted IS NULL OR deleted = false "filterAnd": [ { "combinator": "OR", "filters": [ {"column": "deleted", "operator": "IS NULL"}, {"column": "deleted", "operator": "=", "value": False}, ], } ]

Un backend de données antérieur aux groupes (fleetdb avant v1.2.0) rejette cette forme au lieu d’en appliquer une partie : l’appel échoue donc de manière explicite au lieu de renvoyer silencieusement des lignes que le filtre exclut.

Lire les valeurs actuelles. Une entrée {"latest": true} dans filterAnd (ironflock.Latest() en Go) n’est pas une condition de filtre mais un changement de mode : le backend de données ne renvoie que la ligne la plus récente de chaque entité, dérivée en SQL à partir de la clé d’entité que la table déclare avec maintainLatestFlagFor. Une table sans clé d’entité renvoie son unique ligne la plus récente.

Les autres conditions se combinent au marqueur comme on peut s’y attendre : les conditions portant sur les colonnes de la clé d’entité restreignent quelles entités sont renvoyées, tandis que toutes les autres conditions et timeRange s’appliquent aux lignes les plus récentes obtenues. Ainsi, combiner {"latest": true} avec un filtre sur deleted masque les entités supprimées au lieu de faire remonter leur ligne précédente.

Les versions précédentes d’IronFlock stockaient une colonne latest_flag physique. Elle n’existe plus — un filtre hérité latest_flag = true est toujours accepté et traité comme le marqueur, mais le nouveau code doit utiliser {"latest": true}. Le marqueur latest n’est pas disponible dans getSeriesHistory.

Plages temporelles. Le backend de données lit une borne fournie sous forme de chaîne avec le Date de JavaScript ; écrivez donc une date ou une date-heure ISO 8601 (2026-07-01, 2026-07-01T12:30:15.250Z) et séparez la date et l’heure par T. Les SDK refusent un espace entre la date et l’heure pour les années 0 à 99, que Date interprète mal : "0001-01-01 00:00:00" — ce que donne str(datetime.min) en Python — serait lu comme 2001. Une heure sans fuseau est lue comme UTC. Une borne numérique est exprimée en millisecondes epoch et doit être finie : NaN et ±Infinity arriveraient au backend de données comme une borne ouverte et supprimeraient silencieusement le filtre temporel ; les SDK les refusent donc avant l’envoi. En Go, une borne time.Time est envoyée en RFC 3339 en UTC, et une borne sous forme de chaîne doit être l’une des formes ISO 8601 que Date lit correctement — une date, ou une date et une heure du jour avec un Z ou un décalage UTC facultatif (+02:00 ou +0200) ; les autres chaînes, comme le format de base 20260701T123000Z, un décalage en heures seulement ou un jour que le mois ne compte pas (2026-02-30), échouent avec ironflock.ErrInvalidArgument avant tout envoi.

Transforms. Un transform (une vue SQL d’un data template, voir Tables de transformation) se lit différemment d’une table. Le backend de données n’applique que limit — 3000 au maximum, une valeur supérieure est refusée avec URI_INVALID_LIMIT —, offset et filterAnd. Il ignore timeRange, columns, columnPaths et le marqueur latest, et abandonne un filtre portant sur une colonne que le transform ne déclare pas (un transform n’a pas de colonnes implicites tsp, device_key ou authid), ce qui élargit le résultat. Ses lignes arrivent dans l’ordre propre de la vue, inversé : vous n’obtenez les lignes les plus récentes, les plus anciennes en premier, que lorsque la vue trie les plus récentes en premier. Bornez la plage temporelle d’un transform dans son SQL.

Résultats volumineux. Un résultat trop volumineux pour un seul message du backend de données (8 Mio de JSON) arrive en fragments, que le SDK réassemble en mémoire : vous obtenez les lignes d’une seule lecture, dans le même ordre. Des fragments qui ne concordent pas — un fragment manquant ou dupliqué, ou un nombre de lignes différent du récapitulatif du backend de données — font échouer la lecture au lieu de renvoyer une partie des lignes. Une ligne unique de plus de 8 Mio, ou un résultat dépassant le garde-fou de 1 Gio du backend de données, est refusé avec URI_RESULT_TOO_LARGE : réduisez limit, resserrez timeRange, sélectionnez moins de columns ou élaguez avec columnPaths.

getSeriesHistory / get_series_history

Récupère des données de séries temporelles sous-échantillonnées d’une table de flotte : des colonnes agrégées par intervalle de temps (par ex. des moyennes horaires). Idéal pour les graphiques couvrant de longues périodes. Disponible pour les tables (pas pour les transforms), sur les backends de données à partir de fleetdb v1.0.58.

from datetime import datetime, timedelta, timezone from ironflock import SeriesMetric now = datetime.now(timezone.utc) avg = SeriesMetric(ref="temperature", method="AVG") # AVG, SUM, COUNT, MIN, MAX, FIRST, LAST series = await ironflock.get_series_history("sensordata", { "metrics": [avg, {"ref": "humidity", "method": "MAX"}], "bucketMs": 3_600_000, # one-hour buckets; omit to divide the range into `limit` buckets "limit": 24, # the bucket budget, 1–10000 "timeRange": [(now - timedelta(hours=24)).isoformat(), now.isoformat()], # required, with a start "groupBy": ["device_key"], }) for row in series: print(row["tsp"], row["device_key"], row[avg.column]) # avg.column is "AVG:temperature"

Paramètres de requête :

ChampTypeDescription
metricslist / arrayLes agrégats calculés par intervalle, au moins un : {"ref": ..., "method": ...}. ref est une colonne, ou un chemin JSON dans une colonne json ("json_data.temp") ; method vaut "AVG", "SUM", "COUNT", "MIN", "MAX", "FIRST" ou "LAST". Chaque métrique revient sous "<METHOD>:<ref>", qui doit tenir en 63 octets
bucketMsint / numberLargeur d’un intervalle en millisecondes entières, au moins 1000 (optionnel). Omettez-le pour diviser la plage temporelle en limit intervalles
limitint / numberLe budget d’intervalles (1–10 000) — pas une limite de lignes
timeRangelist / array[start, end] — chaînes date-heure ISO ou nombres epoch-ms. Obligatoire, avec un début ; None / null comme fin signifie « maintenant ». La fin doit être postérieure au début
groupBylist / arrayColonnes qui divisent chaque intervalle en une ligne par groupe (optionnel)
filterAndlist / arrayConditions et groupes combinés par AND, comme pour getHistory (optionnel). Le marqueur latest est refusé ici, y compris à l’intérieur d’un groupe ; utilisez getHistory pour lire les valeurs actuelles

Lignes. Chaque ligne correspond à un intervalle — d’un groupe, avec groupBy. tsp est le début de l’intervalle en millisecondes epoch, chaque colonne de groupBy revient sous son propre nom, et chaque métrique sous "<METHOD>:<ref>" (SeriesMetric.column en Python, seriesColumn(metric) en JavaScript, SeriesMetric.Column() en Go), par ex. row["AVG:temperature"]. Les lignes sont triées par tsp croissant ; un intervalle sans lignes est absent (pas de comblement des trous). AVG et SUM acceptent des colonnes numériques, MIN et MAX aussi des chaînes et des horodatages ; FIRST et LAST donnent la valeur de la ligne la plus ancienne et de la plus récente de l’intervalle ; COUNT compte les valeurs non nulles (pour tsp : les lignes). Un résultat volumineux est réassemblé à partir de fragments, comme pour getHistory.

Intervalles et fin ouverte. limit compte des intervalles, pas des lignes. Sans bucketMs, la plage temporelle est divisée en limit intervalles. Avec bucketMs, le backend de données élargit les intervalles à un multiple de bucketMs lorsque la plage en contient plus que limit — et une fin ouverte s’étend jusqu’au « maintenant » du backend de données, un peu après le vôtre. Les dernières 24 heures avec une fin ouverte contiennent 25 intervalles d’une heure, si bien que limit: 24 donne des intervalles de deux heures. Bornez la fin, comme dans l’exemple, ou prévoyez un intervalle de plus : avec bucketMs et une fin ouverte, limit + 1 intervalles peuvent revenir.

Les SDK vérifient avant l’envoi ce que le backend de données refuserait : au moins une métrique, des refs non vides, des méthodes connues, des noms de colonnes de 63 octets au plus, un début, une fin postérieure au début, et bucketMs sous forme d’entier d’au moins 1000 (en Go, un Bucket d’au moins 1 seconde, en millisecondes entières). Le backend de données refuse ce à quoi il ne peut pas répondre avec URI_INVALID_TIME_RANGE, URI_INVALID_METRIC, URI_INVALID_GROUP_BY ou URI_SERIES_TOO_MANY_GROUPS (plus de 50 000 lignes) — voir Refus du backend de données.

Forme obsolète. Les versions du SDK jusqu’à 1.9.0 envoyaient metrics sous forme de noms de colonnes plus un method de premier niveau ({"metrics": ["temperature"], "method": "AVG", ...}), ce que les backends de données refusent depuis fleetdb v1.0.58. Cette forme fonctionne toujours : le SDK envoie une métrique {ref, method} par colonne, émet un avertissement (un DeprecationWarning en Python, un avertissement console une fois par processus en JavaScript) et renomme les colonnes du résultat d’après les noms de colonnes, de sorte que l’ancien code continue de lire row["temperature"] — sauf si la ligne possède déjà une colonne de ce nom, comme tsp pour un COUNT de tsp. Passez aux métriques {ref, method} et lisez row["AVG:temperature"]. Le SDK Go ne connaît que la forme actuelle : une méthode par métrique (SeriesMetric).

S’abonner aux données

subscribeToTable / subscribe_to_table

S’abonne aux mises à jour en temps réel d’une table de flotte. Le gestionnaire est appelé chaque fois que le backend de données stocke une nouvelle ligne, avec comme premier argument la ligne telle qu’elle a été stockée — valeurs typées selon les colonnes du data template, colonnes secrètes masquées. Les lignes écrites via le chemin d’insertion en masse (publishRowsToTable / appendRowsToTable) sont transmises à votre gestionnaire une à la fois, de sorte que le code du gestionnaire reste identique quelle que soit la manière dont les données ont été écrites.

def on_sensor_data(*args, **kwargs): row = args[0] print("New reading:", row["tsp"], row["temperature"]) await ironflock.subscribe_to_table("sensordata", on_sensor_data)

Un transform n’a pas de lignes propres : à chaque déclenchement de sa planification, le backend de données publie la vue entière — 3000 lignes au maximum, dans l’ordre inverse de la vue — sous la forme d’un seul événement dont le premier argument est la liste des lignes. event_rows(args[0]) en Python, eventRows(args) en JavaScript et ev.Rows() en Go lisent les deux formes : la ligne d’une table donne [row], la liste d’un transform la liste elle-même ([] pour une vue vide), et tout le reste None / undefined / nil. En Go, ev.Row() vaut nil pour l’événement d’un transform.

from ironflock import event_rows def on_hourly(*args, **kwargs): for row in event_rows(args[0]) or []: print(row) await ironflock.subscribe_to_table("hourly_averages", on_hourly)

subscribe

S’abonne à un topic WAMP qui vous est propre pour de la messagerie en temps réel personnalisée. Pour les lignes de table, utilisez subscribeToTable / subscribe_to_table : le routeur refuse les abonnements aux topics bruts d’écriture des tables (voir URI autorisées pour une app).

def on_alert(*args, **kwargs): print("Alert received:", args, kwargs) await ironflock.subscribe("com.myapp.alerts", on_alert)

Colonnes secrètes

Une colonne déclarée secret: true dans le data-template de l’application est chiffrée au repos. Aucun chemin de lecture ordinaire ne renvoie la valeur en clair, pas même à l’application qui l’a écrite :

Chemin de lectureÀ quoi ressemble une colonne secrète
getHistory, subscribeToTable / subscribe_to_table, et tout le reste via le routeur de messagesl’espace réservé __secret__, exporté sous le nom SECRET_PLACEHOLDER (ironflock.SecretPlaceholder en Go)
L’accès SQL (l’identifiant Postgres FleetDB Access)le texte chiffré stocké, ifsec:1:…
revealSecrets / reveal_secretsla valeur en clair déchiffrée

NULL est transmis tel quel — NULL — sur tous les chemins, de sorte que « défini mais masqué » reste distinguable de « jamais écrit ».

Sur une table d’entités, le cycle lecture-modification-écriture est sûr : republier une ligne qui porte encore l’espace réservé __secret__ — ou omettre complètement la colonne secrète — conserve le secret stocké au lieu de l’écraser, et un null explicite l’efface. Voir Colonnes secrètes dans le guide du backend de données pour les règles exactes.

from datetime import datetime, timezone # Written like any other column — the data backend encrypts it on insert await ironflock.publish_to_table("credentials", { "tsp": datetime.now(timezone.utc).isoformat(), "device_id": "d-1", "api_token": "hunter2", }) # A normal read redacts it rows = await ironflock.getHistory("credentials", {"limit": 1}) # [{"tsp": "...", "device_id": "d-1", "api_token": "__secret__"}]

Les deux fonctions ci-dessous ne sont appelables que depuis les conteneurs de l’application elle-même. Le routeur de messages les refuse à tous les rôles navigateur ainsi qu’aux apps consommatrices, ce qui explique qu’un handle ConsumedApp n’ait pas d’équivalent : les colonnes secrètes d’une autre app arrivent masquées et le restent.

revealSecrets / reveal_secrets

Lit les lignes de l’une de vos propres tables avec ses colonnes secrètes déchiffrées.

rows = await ironflock.reveal_secrets("credentials", { "limit": 1, "filterAnd": [{"column": "device_id", "operator": "=", "value": "d-1"}], }) # [{"tsp": "...", "device_id": "d-1", "api_token": "hunter2"}]

Les paramètres de requête sont les mêmes que ceux de getHistory, à une différence près : limit ne peut pas dépasser 100. Ce plafond existe pour empêcher qu’un seul appel déchiffre une table entière, et le backend de données rejette une valeur supérieure au lieu de la ramener silencieusement à la limite.

Le backend de données répond au plus à 30 révélations par minute par identifiant d’app sur un appareil ; au-delà, l’appel échoue avec URI_RATE_LIMITED. Révélez un secret une fois et conservez-le, plutôt qu’à chaque utilisation. Une table sans colonnes secrètes est refusée avec URI_NOT_A_SECRET_COLUMN, un filtre sur une colonne secrète avec URI_SECRET_COLUMN.

verifySecret / verify_secret

Vérifie une valeur candidate par rapport à une colonne secrète sans la révéler — l’équivalent « vérification de mot de passe » de la fonction de révélation. La comparaison s’exécute à l’intérieur du backend de données, en temps constant, et seule la réponse est renvoyée.

ok = await ironflock.verify_secret( "credentials", "api_token", submitted_token, {"limit": 1, "filterAnd": [{"column": "device_id", "operator": "=", "value": "d-1"}]}, ) if ok: ...

Paramètres :

ParamètreTypeDescription
tablenamestr / stringLa table sur laquelle effectuer la vérification
columnstr / stringLa colonne secrète à comparer
candidatestr / stringLa valeur en clair testée. Comparée côté serveur et jamais écrite dans la table
query_params / queryParamsSecretQueryParamsQuelles lignes participent. Par défaut la ligne la plus récente ; limit ne peut pas dépasser 100

En Go, les paramètres sont des chaînes positionnelles placées après le contexte, et la requête est un *ironflock.TableQueryParams (nil pour la ligne la plus récente).

JavaScript renvoie { match, checked } et Go un *ironflock.SecretVerifyResult avec Match et Checked ; Python renvoie un simple bool. Lorsque vous disposez de checked, il vaut la peine de l’utiliser : il distingue une valeur candidate erronée (checked > 0) d’un sélecteur qui n’a trouvé aucune ligne (checked === 0), deux défaillances très différentes à signaler à un utilisateur.

Comme chaque ligne est chiffrée avec sa propre valeur aléatoire, deux lignes contenant le même secret stockent un texte chiffré différent. C’est pourquoi un filtre = sur une colonne secrète ne peut pas fonctionner — le backend de données refuse tout filtre sur une telle colonne avec URI_SECRET_COLUMN — et que cette fonction existe à la place.

Le backend de données répond au plus à 120 vérifications par minute par identifiant d’app sur un appareil ; au-delà, l’appel échoue avec URI_RATE_LIMITED. Pour une vérification à chaque requête (une clé d’API, par exemple), mettez brièvement en cache un résultat positif. Une colonne qui n’est pas une colonne secrète est refusée avec URI_NOT_A_SECRET_COLUMN.

Accès aux données inter-apps

Lisez les données de flotte d’une autre app depuis votre propre app, au sein du même projet. Votre app doit déclarer l’app fournisseur dans la section consumes: de son propre data-template.yml (ou détenir l’autorisation wildcard, voir plus bas), et l’utilisateur du projet doit accorder l’accès. L’accès est en lecture seule : vous pouvez interroger l’historique et vous abonner en temps réel aux lignes des tables et des transforms que le fournisseur partage, mais vous ne pouvez pas y écrire. Les connexions aux apps consommées sont mises en cache par app et fermées automatiquement lorsque votre instance s’arrête.

La connexion nécessite les identifiants propres à l’app que l’agent de l’appareil injecte (APP_AUTH_ID, APP_AUTH_SECRET). La plateforme refuse les anciens identifiants d’appareil — le repli sur le numéro de série d’une app qui n’a pas reçu d’identifiants propres, par ex. sous un agent de l’appareil plus ancien — sur le realm d’une autre app (NOT_AUTHORIZED, avec un message qui le précise), sauf si l’app fournisseur s’exécute sur le même appareil : la connexion rejoint alors son realm avec les pleins droits de cette app, et non en lecture seule.

Si votre app détient l’autorisation wildcard (consumes: [{ app: "*" }]), vous pouvez découvrir et ouvrir dynamiquement les fournisseurs avec listConsumableApps / list_consumable_apps et connectToAllApps / connect_to_all_apps (ci-dessous).

connectToApp / connect_to_app

Ouvre une connexion en lecture seule au backend de données d’une autre app et renvoie un handle. Le handle expose getHistory / get_history, subscribeToTable / subscribe_to_table et getSeriesHistory / get_series_history (tables uniquement) — les mêmes interrogations et abonnements que vous utilisez sur vos propres tables, avec les mêmes paramètres et résultats — ainsi que close et les catalogues partagés tables / transforms.

# Open a read-only handle on another app's data backend weather = await ironflock.connect_to_app("weather-app") # Inspect what the provider shares print([t["tablename"] for t in weather.tables]) # Query history and subscribe, just like your own tables rows = await weather.get_history("forecasts", {"limit": 100}) def on_forecast(*args, **kwargs): print("New forecast:", args) await weather.subscribe_to_table("forecasts", on_forecast)

Paramètres :

ParamètreTypeDescription
app_name / appNamestr / stringNom de l’app fournisseur, tel que déclaré dans votre section consumes:
stagestr / string, optionnelStage du fournisseur : "dev" ou "prod". Par défaut, le stage de votre propre app
on_error / onErrorcallable, optionnelAppelé avec un CrossAppAccessError lorsque l’accès est refusé définitivement à la connexion ouverte (par ex. si l’autorisation est révoquée ultérieurement — voir ci-dessous quand un refus est considéré comme définitif)

Si l’accès est refusé ou mal utilisé, un CrossAppAccessError est levé (Python) / lancé (JavaScript) / renvoyé (Go, un *ironflock.CrossAppAccessError à tester avec errors.As) avec un champ code (Code en Go, avec des constantes comme ironflock.CodeNoGrant) : NO_GRANT, PROVIDER_NOT_INSTALLED, UNKNOWN_APP, PRIVATE_TABLE, SECRET_COLUMN (une requête qui filtre — y compris à l’intérieur d’un groupe — ou sélectionne une colonne que le fournisseur marque comme secrète) ou NOT_AUTHORIZED (l’autorisation a été révoquée ou n’a pas pu être vérifiée à ce moment-là — ou votre app a présenté les anciens identifiants d’appareil, auquel cas le message le précise).

Un refus pendant que connectToApp / connect_to_app attend fait échouer l’appel immédiatement. Une fois un handle ouvert, un refus n’est considéré comme définitif que s’il persiste — 3 refus ou plus depuis la dernière fois que la connexion était établie (la fermeture de la session par le routeur pour une raison d’authentification compte pour un, de même que chaque reconnexion refusée), le premier remontant à au moins 60 secondes —, car la plateforme refuse de la même manière lorsqu’elle ne peut brièvement pas vérifier l’accès. D’ici là, le handle continue de se reconnecter et se rétablit si une reconnexion réussit. Une fois le refus définitif, on_error / onError / OnError est appelé et le handle est évincé : le prochain connectToApp / connect_to_app / ConnectToApp ouvre une nouvelle connexion. À partir de là, les lectures et les abonnements du handle échouent immédiatement, avec l’erreur transmise à on_error / onError — en Go, avec une erreur qui enveloppe wamp.ErrStopped.

En Python, stage et on_error sont des arguments nommés ; en JavaScript, ils sont passés via un objet d’options (connectToApp(appName, { stage, onError })) ; en Go, via une struct (ConnectToApp(ctx, appName, ironflock.ConnectToAppOptions{Stage, OnError})), où OnError est une func(*ironflock.CrossAppAccessError). En Go, ctx ne borne que l’attente de l’appelant : les appels simultanés pour la même app partagent une seule tentative, et Close(ctx) ferme un handle.

listConsumableApps / list_consumable_apps

Liste chaque fournisseur non privé du projet — la primitive de découverte pour les apps qui détiennent l’autorisation de consommation wildcard (consumes: [{ app: "*" }] dans votre data-template.yml, accordée par l’utilisateur du projet). Elle effectue un seul appel et n’ouvre aucune connexion : affichez les catalogues renvoyés dans un sélecteur, puis appelez connectToApp / connect_to_app pour ceux que vous souhaitez — ou connectToAllApps / connect_to_all_apps pour tous les ouvrir en une seule fois.

Remarque : Déclarez l’autorisation dans le data-template.yml de votre app, et mettez le * entre guillemets — un * nu est un alias YAML et ne sera pas analysé :

consumes: - app: "*"
providers = await ironflock.list_consumable_apps() for p in providers: print(p["app"], list(p["stages"].keys())) # e.g. "weather-app" ["dev", "prod"]

Chaque entrée décrit un fournisseur :

ChampTypeDescription
appstr / stringNom de l’app fournisseur
provider_app_keyint / numberLa clé d’app du fournisseur
stagesdict / objectCatalogue par stage { dev?, prod? } ; un stage n’est présent que si le fournisseur dispose d’un backend de données pour celui-ci. Chaque catalogue contient les tables et transforms non privés qu’il partage

En Go, chaque entrée est un ironflock.ConsumedAppInfo avec App, ProviderAppKey et Stages.Dev / Stages.Prod (un *StageCatalog avec Tables et Transforms, nil pour un stage absent) ; Catalog(stage) en renvoie un par son nom.

Lève (Python) / lance (JavaScript) / renvoie (Go) un CrossAppAccessError avec code: NO_GRANT si votre app ne détient aucune autorisation wildcard.

connectToAllApps / connect_to_all_apps

Ouvre des handles en lecture seule vers chaque fournisseur non privé du projet en un seul appel (consommateurs wildcard uniquement). Énumère les fournisseurs via listConsumableApps / list_consumable_apps et ouvre chacun d’eux, en ignorant ceux qui n’ont pas de backend de données pour le stage demandé. Chaque handle est mis en cache sous la même clé que connectToApp / connect_to_app, de sorte qu’un connectToApp(name) ultérieur renvoie le handle déjà préchauffé. Les handles renvoyés sont fermés ensemble lorsque votre instance s’arrête.

apps = await ironflock.connect_to_all_apps( on_error=lambda err: print("Provider skipped:", err) ) for app in apps: rows = await app.get_history(app.tables[0]["tablename"], {"limit": 10}) print(app.app, rows)

Paramètres :

ParamètreTypeDescription
stagestr / string, optionnelStage du fournisseur : "dev" ou "prod". Par défaut, le stage de votre propre app
continue_on_error / continueOnErrorbool / boolean, optionnelLorsque true (la valeur par défaut), un fournisseur qui échoue à s’ouvrir est signalé à on_error / onError et omis du résultat. Lorsque false, le premier échec est levé/lancé
on_error / onErrorcallable, optionnelAppelé avec chaque fournisseur qui n’a pas pu être ouvert (tant que continue_on_error / continueOnError vaut true), et avec un CrossAppAccessError si l’accès à une connexion déjà établie est ultérieurement refusé définitivement (par ex. si l’autorisation est révoquée)

Renvoie les handles de fournisseurs ouverts avec succès (même type de handle que connectToApp / connect_to_app). Lève (Python) / lance (JavaScript) / renvoie (Go) un CrossAppAccessError avec code: NO_GRANT si votre app ne détient aucune autorisation wildcard.

En Python, stage, on_error et continue_on_error sont des arguments nommés ; en JavaScript, ils sont passés via un objet d’options (connectToAllApps({ stage, onError, continueOnError })) ; en Go, via une struct (ConnectToAllApps(ctx, ironflock.ConnectToAllAppsOptions{Stage, OnError, StopOnError})), où OnError est une func(error) et où StopOnError: true correspond à continueOnError: false.

Stockage de fichiers managé

Chaque backend de données d’app dispose, à côté de ses tables, d’un stockage d’objets privé accessible via la propriété files (ifl.Files() en Go, un *filestore.FileStore qui peut être obtenu avant Start). Utilisez-le pour des images, des PDF, des images de caméra, des blobs de firmware — tout ce qui n’a pas sa place dans une ligne de table. Aucune configuration n’est nécessaire : une app dépourvue de section files: dans son data-template obtient malgré tout un espace de noms nommé default (un template qui déclare ses propres espaces de noms le remplace — voir Espaces de noms).

Cette section est la référence côté client. Pour le côté plateforme — déclaration des espaces de noms, quotas et rétention, partage de fichiers entre apps, identifiants S3 par app et comportement sur les appliances sur site — voir Stockage de fichiers.

L’idée clé est que stocker un objet vous renvoie une URL permanente que vous pouvez écrire directement dans une colonne de table, de sorte qu’un widget de tableau de bord puisse l’afficher sans travail supplémentaire :

from datetime import datetime, timezone # Store an object and get a permanent URL back in the same call info = await ironflock.files.put("part-1.jpg", jpeg_bytes, content_type="image/jpeg") # The URL is safe to store in a table column — a dashboard widget can then # render <img src="{{photo_url}}"> without any extra round trip await ironflock.publish_to_table("inspections", { "tsp": datetime.now(timezone.utc).isoformat(), "part_id": "1", "photo_url": info.url, }) # Read it back data = await ironflock.files.get("part-1.jpg") # Every object in the folder 2026/, in its subfolders too (pages are fetched for you) async for obj in ironflock.files.iter(prefix="2026/"): print(obj.key, obj.size)

Cette URL n’expire jamais, mais ce n’est pas un lien public : elle ne reste lisible que par un demandeur authentifié disposant de l’accès READ sur ce backend de données, et un proxy d’authentification le revérifie à chaque requête. Elle peut donc être stockée sans risque dans la base de données.

Espaces de noms

Un espace de noms est un préfixe de clé porteur de règles — rétention, règles de partage, types de contenu autorisés. Ce n’est pas un bucket distinct ; tous les espaces de noms d’une app vivent dans l’unique zone de stockage de cette app. N’en déclarez un que lorsqu’un ensemble d’objets a besoin de règles différentes ; sinon, restez dans default et organisez vos objets avec des chemins de clés comme 2026/03/part-1.jpg.

Déclarez les espaces de noms dans data-template.yml. Dès que vous en déclarez, ils remplacent le default implicite : listez aussi un espace de noms nommé default, sinon chaque appel sans espace de noms échoue avec NO_SUCH_NAMESPACE.

files: # Storage budget the app suggests for itself. The project user can change it, # and their setting is the one that gets enforced. quotaBytes: 5368709120 namespaces: # Keep default: declaring namespaces replaces the implicit one. - name: default - name: frames description: Raw camera frames, one JPEG per inspected part. contentTypes: ["image/jpeg"] maxObjectBytes: 20971520 retention: { deleteAfter: 30 days }

Notez que le budget est déclaré une seule fois pour toute l’app, et non par espace de noms. Un espace de noms n’est qu’un préfixe de clé à l’intérieur de l’unique zone de stockage de l’app : il n’y a donc rien contre quoi un budget par préfixe pourrait être appliqué. maxObjectBytes, lui, est défini par espace de noms — il plafonne un objet unique, pas un total : 100 Mio s’il n’est pas défini, 5 Gio au maximum (OBJECT_TOO_LARGE au-delà).

Chaque méthode ci-dessous accepte l’espace de noms comme argument optionnel et utilise default par défaut ; un espace de noms vide ("") signifie aussi default.

Stocker et lire des objets

# Bytes in, bytes out info = await ironflock.files.put("reports/march.pdf", pdf_bytes, content_type="application/pdf") data = await ironflock.files.get("reports/march.pdf") # Or straight from/to a local file — these stream on the large-object path, # so a multi-gigabyte file never has to fit in memory await ironflock.files.put_file("firmware/v2.bin", "/data/build/v2.bin") await ironflock.files.get_to_file("firmware/v2.bin", "/tmp/v2.bin")
MéthodeDescription
put(key, data, …)Stocke un objet (bytes en Python, Uint8Array en JavaScript, []byte en Go). Renvoie les métadonnées de l’objet, dont son url
get(key, namespace?)Renvoie le contenu de l’objet. Un objet dépassant la limite inline provient directement du stockage d’objets ou, lorsque ce n’est pas possible, par plages d’octets via le routeur (voir Objets volumineux)
put_file(key, path, …) / get_to_file(key, path, …)Python et Go (PutFile / GetToFile ; Go transfère aussi en flux depuis un io.Reader avec PutReader et vers un io.Writer avec GetTo). Stocke depuis un fichier local, ou écrit dans un fichier local. Transfère en flux sur le chemin des objets volumineux
delete(key, namespace?)Supprime un objet
copy(key, to, …)Copie un objet, éventuellement vers un autre espace de noms
move(key, to, …)Copie puis suppression. Non atomique — le service ne dispose d’aucun verbe move, une suppression échouée laisse donc les deux copies en place

JavaScript n’offre pas d’assistants de chemins de fichiers, car le paquet est livré en un seul build pour Node comme pour le navigateur — lisez et écrivez vous-même les fichiers locaux avec fs.

En Python, le travail sur les fichiers de put_file et get_to_file s’exécute hors de la boucle d’événements. get_to_file crée les répertoires parents manquants et écrit le fichier sur place dès que les premiers octets de l’objet sont disponibles : une lecture refusée (objet inexistant, non autorisé) laisse le chemin intact, tandis qu’une lecture qui échoue en cours de route laisse ce qui a été écrit jusque-là. Un FIFO est écrit au fil de l’arrivée de l’objet, et son ouverture attend un lecteur : un FIFO que personne ne lit bloque donc l’appel (pas la boucle d’événements) ; asyncio.wait_for permet d’y renoncer. put_file lit un FIFO jusqu’au bout avant de le stocker.

En Go, GetToFile crée lui aussi les répertoires parents manquants, mais télécharge dans un fichier temporaire à côté de la cible et le renomme à sa place une fois le téléchargement terminé : un téléchargement échoué ou annulé ne laisse jamais de fichier partiel et conserve le fichier précédent. Les liens symboliques sont suivis (le fichier vers lequel ils pointent est écrit, et les liens restent) ; le fichier de remplacement reçoit, dans la mesure où le processus le peut, le propriétaire, le groupe et les bits de permission de l’ancien fichier. Une FIFO ou un périphérique est écrit sur place au fil du flux de l’objet. Pour une cible à côté de laquelle aucun nouveau fichier ne peut être créé, par exemple sur un système de fichiers en lecture seule ou un pseudo-système de fichiers, utilisez GetTo avec un fichier que vous ouvrez vous-même.

put accepte : content_type / contentType (le type MIME ; l’espace de noms peut restreindre ceux qui sont autorisés) et namespace. En Python, ce sont des arguments nommés ; en JavaScript, ils sont passés dans un objet d’options ; en Go, ce sont des options (filestore.ContentType(…), filestore.Namespace(…)).

Lister et inspecter

MéthodeDescription
list(…)Une page du listage du service de fichiers, façon dossiers par défaut : objects contient les objets dont les clés prolongent le préfixe sans /, prefixes les dossiers en dessous. Un delimiter égal à "" liste à plat — chaque objet sous le préfixe, y compris dans les sous-dossiers. Renvoie objects, prefixes, is_truncated / isTruncated / IsTruncated ainsi qu’un cursor à repasser pour la page suivante
iter(…) / iterate(…)Itérateur asynchrone sur chaque objet situé sous un préfixe, y compris dans les sous-dossiers, avec pagination automatique. Nommé iter en Python, iterate en JavaScript, Iter en Go (un iter.Seq2[*ObjectInfo, error] pour une boucle range)
stat(key, namespace?)Métadonnées d’un objet sans transférer son contenu
exists(key, namespace?)Indique si un objet existe
namespaces()Les espaces de noms que cette app peut utiliser
usage(…)Quelle quantité de stockage l’app utilise — voir ci-dessous
catalog()Espaces de noms, ainsi que les limites et quotas émis par le serveur. Récupéré une fois puis mis en cache : catalog(refresh=True) / catalog(true) / RefreshCatalog(ctx) le récupère à nouveau

Dossiers. Un préfixe qui se termine par / désigne un dossier : iter(prefix="2026/") / iterate({ prefix: "2026/" }) / Iter(ctx, filestore.Prefix("2026/")) renvoie 2026/a.txt et 2026/sub/c.txt, mais pas 2026-notes.txt. list ne peut pas ouvrir un dossier par son nom, car le service de fichiers refuse un préfixe de listage qui se termine par / (FleetFiles v0.2 répond INTERNAL) : list avec le préfixe 2026 renvoie 2026-notes.txt parmi les objets et le dossier 2026/ parmi les préfixes. Le service stocke les clés en forme de normalisation Unicode C ; donnez donc aussi un préfixe de dossier en NFC. Une itération qui rencontre une clé sous une autre forme du préfixe échoue au lieu d’ignorer toutes les clés.

Les objets sont décrits par les mêmes champs dans les trois SDK, selon le style de nommage propre à chaque langage : namespace, key, size, etag, content_type / contentType, last_modified / lastModified, checksum_sha256 / checksumSha256 et url — en Go, les champs Namespace, Key, Size, ETag, ContentType, LastModified, ChecksumSHA256 et URL de filestore.ObjectInfo.

Utilisation du stockage et quota

usage obtient sa réponse du magasin d’objets en un seul appel : les totaux sont donc exacts, et non additionnés par le SDK :

u = await ironflock.files.usage() print(u.size_bytes, u.object_count, u.quota_bytes, u.free_bytes) # Break the total down per namespace (costs one listing per namespace) detailed = await ironflock.files.usage(detail=True) print(detailed.per_namespace) # {"default": 1048576, "frames": 73400320}

En Go, les champs sont SizeBytes, ObjectCount, QuotaBytes, FreeBytes et PerNamespace de filestore.StorageUsage.

ChampSignification
size_bytes / sizeBytesOctets actuellement stockés
object_count / objectCountNombre d’objets stockés
quota_bytes / quotaBytesLe budget appliqué. 0 signifie illimité
free_bytes / freeBytesOctets restants. -1 signifie illimité — indiquer 0 ici se lirait comme « plein »
per_namespace / perNamespaceOctets par espace de noms. Présent uniquement lorsque vous demandez le détail

La ventilation par espace de noms est désactivée par défaut, car le magasin ne sait pas y répondre directement : il comptabilise par zone de stockage, et un espace de noms n’est qu’un préfixe ; le SDK doit donc lister chaque espace de noms pour en additionner les tailles. Demandez-la quand vous en avez besoin, pas sur un chemin critique.

Deux quotas différents apparaissent, et il vaut la peine de ne pas les confondre. catalog() renvoie les deux :

ChampSignification
quota_bytes / quotaBytesCe qui est réellement appliqué, lu depuis le magasin d’objets — le réglage de l’utilisateur du projet
suggested_quota_bytes / suggestedQuotaBytesCe que le data template de l’app a demandé. 0 s’il n’a rien demandé

En Go, Catalog(ctx) renvoie un *filestore.Catalog avec QuotaBytes et SuggestedQuotaBytes.

Ils diffèrent dès qu’un utilisateur a augmenté ou réduit le budget de l’app, et c’est précisément pourquoi la valeur appliquée est lue depuis le magasin plutôt que depuis le template — redéployer l’app ne doit pas réinitialiser silencieusement le choix d’un utilisateur. Une interface peut afficher les deux (« l’app suggère X, vous avez réglé Y »). L’application du quota s’appuie toujours sur la première valeur. L’utilisateur peut modifier le budget à tout moment, et catalog() le rapporte tel qu’il était lors de sa dernière récupération : lisez la valeur actuelle avec usage().

Partager des objets

Il existe deux types de liens, et la différence a son importance :

MéthodeDurée de vieQui peut le lire
url(key, …)PermanenteUniquement un demandeur authentifié disposant de READ sur ce backend de données — revérifié à chaque requête. Peut être stockée sans risque dans une colonne de table
share_url / shareUrl / ShareURLExpirante (15 min par défaut, plafonnée par le serveur)Quiconque détient le lien. Rien ne revérifie l’autorisation au moment de son utilisation

share_url / shareUrl est une capacité au porteur : remettez-la à une personne qui a besoin d’un accès temporaire, et ne la stockez pas dans la base de données. Utilisez url pour tout ce qu’affiche un tableau de bord.

url renvoie None / undefined / "" (Go) lorsque le déploiement n’a pas de frontal HTTP (par exemple une appliance en HTTP simple) — c’est le signal qu’il faut se rabattre sur get. Passer l’etag d’un objet comme argument version (filestore.Version(etag) en Go) permet aux navigateurs de mettre la réponse en cache de façon immuable.

En Python et en Go, cloud_url(key, …) / CloudURL(ctx, key, …) renvoie l’URL permanente via le tunnel de fichiers cloud d’une appliance (https://i<instanceKey>-files.<cloud-edge>) : joignable depuis Internet tant que l’appliance est en ligne, avec la même autorisation que url. Elle renvoie None ("" en Go) sur les déploiements hébergés dans le cloud (où url est déjà joignable depuis Internet), là où il n’y a pas de frontal HTTP, et tant que le partage cloud du backend de données est désactivé. L’utilisateur peut le basculer à tout moment ; cloud_url / CloudURL relit donc le catalogue lorsque celui en cache date de plus de 30 secondes.

upload_url / uploadUrl / UploadURL génère une URL expirante qui accepte un envoi direct, et renvoie url, method, headers et expires_in / expiresIn (en Go, un *filestore.UploadTarget avec URL, Method, Headers et ExpiresIn). Envoyez les en-têtes tels qu’ils sont renvoyés, afin que l’objet soit stocké avec le type de contenu fixé lors de la génération. Les types de contenu et la taille maximale de l’espace de noms, une size déclarée et le quota sont vérifiés au moment où l’URL est générée, et non lorsqu’elle est utilisée : l’URL n’est signée que pour l’hôte, de sorte que quiconque la détient peut écrire n’importe quel contenu sous sa clé jusqu’à son expiration. Comme share_url / shareUrl, c’est une capacité au porteur — une capacité d’écriture.

Objets volumineux

Le SDK choisit le transport en fonction de la taille, automatiquement — il n’y a rien à configurer :

Taille de l’objetComment il transite
Jusqu’à la limite inline (actuellement 6 Mio)Un seul appel via le routeur de messages
Au-delàDirectement vers le stockage d’objets en HTTPS, en contournant le routeur
Au-delà, en lecture là où il n’y a pas de chemin directPar plages d’octets via le routeur de messages

La limite exacte est indiquée à l’exécution par le serveur sous la forme inline_max_bytes / inlineMaxBytes / InlineMaxBytes dans catalog() ; elle peut donc être relevée sans nouvelle version du SDK.

Le chemin direct exige un déploiement capable de pré-signer des URL et un appareil capable de joindre l’hôte du stockage d’objets, et pas seulement le routeur. Lorsque l’un des deux manque — le catalogue indique que le déploiement ne peut pas pré-signer (une appliance sur site, par défaut), la génération de l’URL de lecture répond NOT_SUPPORTED, ou le stockage d’objets est injoignable avant l’arrivée du moindre octet —, get (et get_to_file en Python, GetTo et GetToFile en Go) lisent l’objet par plages d’octets via le routeur à la place (FleetFiles v0.1.33 et ultérieur), chacune d’au plus la limite inline. Chaque plage est une lecture à part entière : un objet remplacé pendant une telle lecture la fait échouer avec INTERNAL (“object changed while it was read”). Les écritures n’ont pas de tel repli.

Deux plafonds subsistent pour les écritures, et tous deux signalent TOO_LARGE avec une raison précisant lequel a été atteint :

  • 5 Gio — la limite d’envoi en une seule fois du stockage d’objets. L’envoi multipart n’est pas encore implémenté.
  • La limite inline, lorsqu’il n’existe pas de point de terminaison direct — un déploiement qui ne peut pas pré-signer (une appliance, par défaut) ne peut tout simplement pas stocker un objet plus volumineux. Aucun nouvel essai ni découpage plus fin n’y changera quoi que ce soit, et le message le précise.

Deux défaillances du chemin direct courantes sur le terrain reçoivent leurs propres codes au lieu de ressembler à des problèmes d’autorisation : PRESIGN_UNREACHABLE (un proxy qui n’autorise que le routeur — une lecture se rabat sur les plages d’octets, une écriture échoue) et CLOCK_SKEW (le stockage d’objets rejette les requêtes décalées de plus de 15 minutes — vérifiez NTP sur l’appareil).

Erreurs de stockage de fichiers

Une défaillance du service de fichiers ou d’un transfert direct lève (Python) / lance (JavaScript) / renvoie (Go) une FileStoreError portant un code stable et une reason lisible par un humain. Faites vos branchements sur code, jamais sur reason. Les erreurs de fichiers locaux, un appel annulé et les erreurs WAMP du routeur autres que celles associées ci-dessous sont levées telles quelles.

from ironflock.filestore import FileStoreError try: await ironflock.files.put("huge.bin", payload) except FileStoreError as e: if e.code == "QUOTA_EXCEEDED": print("Filestore is full:", e.reason) else: raise
CodeSignification
NOT_AUTHORIZEDL’appelant n’a pas le droit d’effectuer cette opération
NO_SUCH_NAMESPACEL’espace de noms n’est pas déclaré dans le data-template
NO_SUCH_OBJECTLa clé n’existe pas
TOO_LARGEDépasse la limite de transfert en un seul appel
OBJECT_TOO_LARGEDépasse le maxObjectBytes propre à l’espace de noms
QUOTA_EXCEEDEDLe filestore est plein
CONTENT_TYPE_NOT_ALLOWEDL’espace de noms restreint les contentTypes
INVALID_KEYUne clé ou un préfixe mal formé (voir ci-dessous ; FleetFiles v0.2 le signale encore comme INTERNAL)
INVALID_RANGEUne plage d’octets en dehors de l’objet ; les SDK n’en demandent jamais
NOT_SUPPORTEDLe backend ne sait pas faire cela
NOT_AVAILABLECe déploiement n’a pas de service de fichiers — ou pas encore : après un redémarrage de la plateforme, ou pendant que le service de fichiers enregistre ses procédures. L’appel n’a été exécuté nulle part et peut être répété
PRESIGN_UNREACHABLELe stockage d’objets n’est pas joignable directement (un proxy ?)
CLOCK_SKEWL’horloge de l’appareil est trop décalée
INTERNALTout le reste — y compris un objet modifié pendant une lecture par plages

Un serveur plus récent peut introduire des codes que cette version du SDK ne connaît pas. Ils sont transmis tels quels dans code plutôt que ramenés à une valeur générique : traitez donc une valeur non reconnue comme un échec générique.

Les appels de fichiers attendent la connexion (jusqu’à 10 secondes en Python et en Go, 6 en JavaScript) et ne sont pas retentés : contrairement aux opérations sur les tables, ils ne survivent pas à un redémarrage de la plateforme, si bien qu’un appel peut alors échouer avec NOT_AVAILABLE jusqu’à ce que le service de fichiers se soit de nouveau enregistré. Un tel appel n’a été exécuté nulle part ; votre app peut donc le répéter.

Clés. Le service de fichiers fait autorité sur les clés ; les SDK envoient les clés et les préfixes tels quels. Une clé ou un préfixe mal formé — vide ; avec un / initial ou final (hormis le préfixe de dossier d’iter / iterate) ; avec un segment vide, . ou .., ou un segment se terminant par . ou commençant ou se terminant par une espace ; avec l’un des caractères \ : * ? " < > | ^ ~, un caractère de contrôle ou un caractère de format Unicode ; de plus de 900 octets, 20 segments ou 255 octets par segment ; ou sous le préfixe réservé _sys/ — est refusé avec INVALID_KEY par un service de fichiers qui classe les erreurs de clé, et avec INTERNAL par FleetFiles v0.2. Attention aux horodatages : 2026-10-09T12:00:00Z n’est pas un segment de clé valide, à cause du :.

En Python, FileStoreError est importée depuis ironflock.filestore ; en JavaScript, elle est exportée depuis la racine du paquet (import { FileStoreError } from "ironflock") ; en Go, c’est *filestore.Error (aussi ironflock.FileStoreError), avec les codes sous forme de constantes comme filestore.CodeQuotaExceeded.

Communication inter-appareils

registerDeviceFunction / register_device_function

Enregistre une procédure que d’autres appareils exécutant la même app (dans le même stage) peuvent appeler, tout comme les actions des widgets de tableau de bord. Le SDK construit pour vous l’URI complète <SWARM_KEY>.<DEVICE_KEY>.<APP_KEY>.<STAGE>.<topic>, où STAGE vaut DEV ou PROD. Le routeur n’accepte aucun autre enregistrement de la part d’une app, et uniquement des enregistrements simples (voir URI autorisées pour une app). L’enregistrement est restauré après chaque reconnexion.

def add(a, b): return a + b await ironflock.register_device_function("com.myapp.add", add)

register() est un alias de register_device_function().

callDeviceFunction / call_device_function

Appelle une procédure enregistrée par un autre appareil de la même app. Le SDK assemble automatiquement le topic WAMP complet en utilisant la clé de l’appareil cible : <SWARM_KEY>.<device_key>.<APP_KEY>.<STAGE>.<topic>, la même URI que celle que registerDeviceFunction / register_device_function enregistre sur l’appareil cible.

result = await ironflock.call_device_function( 42, # target device key "com.myapp.add", # procedure name args=[3, 5] # arguments ) print(result) # 8

call

Appelle une procédure distante en utilisant une URI WAMP complète. Une app ne peut pas enregistrer un nom comme some.full.wamp.topic ; un tel nom n’atteint donc que les procédures que les services de la plateforme enregistrent sur le realm de données de l’app, comme celles du backend de données et du stockage de fichiers. Pour appeler une fonction qu’un autre appareil de votre app a enregistrée, utilisez callDeviceFunction / call_device_function.

result = await ironflock.call("some.full.wamp.topic", args=[42])

URI autorisées pour une app

Le conteneur de votre app rejoint son propre realm de données, realm-<SWARM_KEY>-<APP_KEY>-<dev|prod>, avec le rôle de routeur app. Le routeur y vérifie chaque publication, abonnement, appel et enregistrement par rapport à la liste d’autorisations de ce rôle. Les méthodes propres au SDK restent dans ce cadre ; les règles comptent lorsque vous passez vos propres URI à publish, subscribe ou call, ou que vous utilisez directement la connection (Connection() en Go). Une action refusée échoue avec wamp.error.not_authorized (en Go, ironflock.WampURI(err) la renvoie).

URI sur votre propre realmAutoriséUtilisé par
Vos propres noms, par ex. com.myapp.statuspublier, s’abonner (y compris par préfixe), appeler — jamais enregistrerpublish, subscribe, call
<SWARM_KEY>.<APP_KEY>.<table>, bulk.<SWARM_KEY>.<APP_KEY>.<table>publier — jamais s’abonnerpublishToTable / publish_to_table, reportError / report_error, publishRowsToTable / publish_rows_to_table
transformed.<table>, transformed.bulk.<table>s’abonner uniquementsubscribeToTable / subscribe_to_table
databackend.errors.…, files.events…s’abonner uniquementLes événements d’erreur et de fichiers du backend de données
<SWARM_KEY>.<DEVICE_KEY>.<APP_KEY>.<STAGE>.<name>enregistrer (simple), appeler, publier — jamais s’abonnerregisterDeviceFunction / register_device_function, callDeviceFunction / call_device_function
append.…, appendBulk.…, history.transformed.…, secret.reveal.…, secret.verify.…, files.read.…, files.write.…appeler (le SDK les appelle)Méthodes de table, d’historique, de secrets et de fichiers
sys.appaccess.list, sys.appaccess.resolveappelerlistConsumableApps / list_consumable_apps, connectToApp / connect_to_app, connectToAllApps / connect_to_all_apps
wamp.session.getappelerLe heartbeat du SDK JavaScript

Noms soumis au contrôle d’identité. Les noms qui commencent par un chiffre — les topics d’écriture des tables et les URI des fonctions d’appareil — passent en outre le contrôle d’identité de la plateforme, qui les lie à la clé de swarm de votre realm. Un enregistrement n’est accepté que pour une URI de fonction de votre propre appareil avec la clé de swarm, la clé d’app et le stage de votre realm ; un enregistrement par préfixe ou par wildcard, uniquement s’il fixe ces quatre segments. La publication et l’appel n’exigent que la clé de swarm de votre realm : une ligne publiée sur un topic de table avec une autre clé d’app est acceptée, mais n’atteint aucun backend de données et est perdue. Laissez le SDK construire ces URI.

Noms réservés. Le routeur refuse sys., hormis les deux procédures inter-apps ci-dessus, et wamp., hormis wamp.session.get. Les autres noms du tableau appartiennent à la plateforme : ne commencez pas vos propres noms par un chiffre, bulk., transformed., databackend.errors., files., append., appendBulk., history. ou secret.. Tout autre premier segment constitue l’espace de topics propre à votre app.

Personne ne s’abonne aux topics bruts des tables. Ils transportent la ligne exactement telle qu’elle a été envoyée, y compris le texte en clair des colonnes secrètes, que le backend de données ne chiffre qu’au moment où il stocke la ligne. subscribeToTable / subscribe_to_table lit plutôt les lignes stockées depuis transformed.<table>, typées et avec les colonnes secrètes masquées.

Les données des autres apps sont en lecture seule. Sur le realm d’un fournisseur, votre app (rôle de routeur app_reader) peut uniquement s’abonner à transformed.… et files.events… et appeler history.transformed.… et files.read.…, en plus du heartbeat wamp.session.get — pas de publication, pas d’enregistrement, pas d’écritures et pas de procédures de secrets.

Métadonnées de l’appareil

setDeviceLocation / set_device_location

Demande à la plateforme de mettre à jour la localisation GPS de l’appareil, que les cartes IronFlock affichent en temps réel.

Indisponible pour le moment : aucun service de la plateforme ne répond encore à cet appel sur le realm de données de l’app ; il échoue donc avec wamp.error.no_such_procedure. Conservez les localisations dans une table qui vous est propre en attendant.

await ironflock.set_device_location(long=8.6821, lat=50.1109)
ParamètrePlage
long-180 à 180
lat-90 à 90

L’historique de localisation n’est pas stocké. Pour suivre la localisation dans le temps, créez une table dédiée et utilisez publish_to_table / publishToTable.

getRemoteAccessUrlForPort

Renvoie l’URL sous laquelle un port déclaré dans le port-template.yml de l’app est joignable une fois son tunnel actif.

url = ironflock.getRemoteAccessUrlForPort(8080) # "https://<device_key>-<app_name>-8080.app.ironflock.com" vnc = ironflock.getRemoteAccessUrlForPort(5900, protocol="tcp") # "tcp://app.ironflock.com:<the tunnel's remote port>"
ParamètreTypeDescription
portint / numberLe port, tel que déclaré dans port-template.yml
protocolstr / string, optionnelLe protocole du port : "http" (par défaut), "https", "tcp" ou "udp", quelle que soit la casse ; les espaces qui l’entourent sont ignorés, et un protocole vide vaut "http"
remote_port_environment / remotePortEnvironmentstr / string, optionneltcp/udp uniquement : la variable qui contient le port distant, au lieu de REMOTE_PORT_FOR_<port> — le remote_port_environment propre au port dans le template, dont les agents d’appareil antérieurs à 0.19.8 ont besoin

Les ports http et https sont servis sous le label de tunnel <device_key>-<app name, lower case>-<port> (les ports https avec le préfixe secure- de la plateforme) sur le domaine de tunnel que l’agent de l’appareil injecte (TUNNEL_DOMAIN), toujours sous forme d’URL https://. Sur un appareil qui appartient à une appliance sur site (l’agent injecte INSTANCE_KEY), c’est la route relayée par le cloud https://i<INSTANCE_KEY>-<label>.<CLOUD_TUNNEL_DOMAIN> qui est renvoyée.

Une URL tcp ou udp contient le port public attribué par le tunnel, que l’agent de l’appareil annonce sous REMOTE_PORT_FOR_<port> (agent 0.19.8 et ultérieur) : <protocol>://<TUNNEL_DOMAIN>:<remote port>. Sur l’appareil d’une appliance, le port exposé sur Internet, REMOTE_PORT_FOR_<port>_CLOUD, est préféré tant que l’agent l’annonce ; avant qu’il n’apparaisse, puis de nouveau une fois que l’agent l’a supprimé (relais cloud désactivé), c’est l’URL locale à l’appliance qui est renvoyée. Un port 0 n’a pas encore été attribué.

Les valeurs sont lues en direct depuis /data/env ; rappelez donc la méthode plutôt que de mettre le résultat en cache. Certains cas ne peuvent pas être servis par l’URL :

  • L’agent de l’appareil ne crée des tunnels que pour les ports des installations de production (PROD). Dans un conteneur de développement (DEV), l’URL http(s) est celle de l’installation PROD de la même app sur cet appareil, et tcp/udp n’en donnent aucune ; joignez plutôt une app DEV via l’adresse LAN de l’appareil (DEVICE_LAN_IP, DEVICE_PORT_FOR_<port>).
  • Sur un appareil edge d’une appliance installée sans domaine (mode HTTP en clair), l’agent définit TUNNEL_DOMAIN sur localhost, de sorte que les URL composées sur cet appareil pointent vers localhost : une telle appliance sert les ports des apps sur ses ports hôtes, ce qu’une URL de tunnel ne peut pas exprimer.
  • Les domaines de tunnel non définis ou vides valent par défaut app.ironflock.com. La plateforme ne définit pas CLOUD_TUNNEL_DOMAIN aujourd’hui : les URL d’une appliance utilisent app.ironflock.com, sauf si l’app définit elle-même cette variable dans ses paramètres d’environnement.
  • Dans un conteneur qui s’exécute sous un utilisateur autre que root, les valeurs sont celles avec lesquelles le conteneur a démarré (voir Variables d’environnement).

La méthode renvoie None / null (en Go "" et false) lorsque l’URL ne peut pas être composée : la clé de l’appareil ou le nom de l’app est manquant, aucun port distant n’a encore été annoncé pour un port tcp/udp (ou il vaut 0), le protocole est inconnu, ou le label n’est pas un label DNS valide (plus de 63 caractères, ou un nom d’app contenant un point). Dans un navigateur, il n’y a pas d’environnement : l’URL http(s) est composée sur app.ironflock.com à partir des options deviceKey et appName.

Propriétés & cycle de vie de la connexion

PropriétéTypeDescription
is_connectedboolIndique si la connexion à la plateforme est active
connectionCrossbarConnectionL’instance de connexion sous-jacente (usage avancé)
MéthodeDescription
run()Démarre la connexion et exécute mainFunc (bloquant)
await start()Démarre la connexion de manière asynchrone
await stop()Arrête la connexion et les connexions aux apps consommées, et annule les tâches en cours. Les connexions quittent leurs realms et ne se reconnectent plus ensuite ; start() se connecte à nouveau. Un start() ou un connect_to_app encore en attente échoue (voir Fiabilité de la connexion)
await run_async()Démarre et maintient la connexion en cours d’exécution de manière asynchrone

Fiabilité de la connexion

Le SDK se reconnecte tout seul. Lorsque la connexion tombe, il réessaie jusqu’au retour du routeur, puis restaure chaque abonnement et chaque fonction d’appareil enregistrée — vous n’avez pas à vous réabonner après une reconnexion. Si l’un d’eux ne peut pas être restauré, les autres le sont quand même, et celui qui a échoué est retenté tant que la session dure — après 1 seconde, en doublant jusqu’à 30 secondes, chaque attente variant de 15 % au plus — puis de nouveau après chaque reconnexion : le routeur l’avait accepté auparavant, son refus est donc passager, comme lorsque le contrôle d’identité d’une fonction d’appareil par la plateforme échoue brièvement en mode fermé. Le premier échec est journalisé comme avertissement, et la restauration est journalisée dès qu’elle réussit.

Les opérations sur les tables — écritures, lectures d’historique, de séries et de secrets, ainsi que les lectures d’historique d’une app consommée — survivent aussi à un redémarrage de la plateforme, comme une mise à jour du routeur ou de la plateforme. Elles attendent jusqu’à reconnect_window (Python, en secondes) / reconnectWindowMs (JavaScript) / WithReconnectWindow (Go, un time.Duration), 60 secondes par défaut, le retour de la connexion, et réessaient tant que le backend de données ne sert pas encore ses procédures : le routeur répond wamp.error.no_such_procedure, ou le backend de données refuse un appel arrivé avant son enregistrement avec “client has no handler for registration”. Un tel appel n’a jamais atteint de gestionnaire, de sorte qu’une nouvelle tentative n’écrit jamais une ligne deux fois ; toute autre erreur est levée telle quelle. Les appels de fichiers ne sont pas retentés (voir Erreurs de stockage de fichiers). Une requête déjà envoyée lorsque sa session se ferme (le routeur la ferme, le lien tombe ou stop()) échoue immédiatement au lieu d’attendre une réponse, et n’est pas retentée : elle a peut-être atteint son gestionnaire. En Go, une telle requête — et toute opération qui ne trouve pas de connexion pendant son attente — échoue avec une erreur qui enveloppe wamp.ErrNotConnected (wamp.ErrStopped après Stop). Les autres opérations de Go (Publish, Subscribe, Call, les fonctions d’appareil, la découverte inter-apps) attendent une connexion jusqu’à 10 secondes et ne sont pas retentées.

Un cas particulier est un routeur joignable qui n’a pas encore de realm pour l’app. Lors d’une installation concurrente, le conteneur de l’app démarre avant que le backend de données ait provisionné son realm, si bien que les premières tentatives de connexion sont refusées avec wamp.error.no_such_realm ; le SDK réessaie toutes les quelques secondes et l’app démarre dès que le realm existe. Un realm toujours absent après une minute n’apparaîtra très probablement jamais — l’app a été supprimée ou n’a pas de backend de données pour ce stage —, le SDK ralentit donc à partir de là à une tentative toutes les deux minutes au lieu de solliciter le routeur chaque seconde indéfiniment. La cadence rapide revient dès que le realm apparaît, ou dès que le routeur lui-même disparaît puis revient.

stop() met fin à toute attente d’une connexion. Un start() qui attend encore de rejoindre son realm pour la première fois — un realm qui n’existe pas encore, par exemple — échoue avec Connection to realm <realm> stopped before it opened. Un connect_to_app / connectToApp qui ouvre encore la connexion à un fournisseur échoue avec Connection to app '<app>' (<stage>) aborted by stop() ou, si la connexion rejoint quand même le realm, celle-ci est fermée et l’appel échoue avec Connection to app '<app>' (<stage>) closed by stop(). Un refus qui survient en même temps que stop() conserve son CrossAppAccessError. Lorsque stop() rend la main, plus rien n’est en cours de connexion ; un routeur qui ne répond pas quand la connexion quitte le realm est déconnecté après 5 secondes.

En Go, Stop(ctx) met lui aussi fin à ces attentes, avec des erreurs qui enveloppent wamp.ErrStopped : un Start encore en attente la renvoie, et un ConnectToApp encore en cours d’ouverture échoue avec ironflock: connection to app '<app>' (<stage>) aborted by Stop (ou closed by Stop, si la connexion a tout de même rejoint son realm). Stop attend la sortie du realm dans les limites de ctx, puis ferme le socket ; Run lui accorde 10 secondes.

Le cas le plus délicat est une connexion qui meurt silencieusement : un NAT ou un proxy la coupe après une période d’inactivité sans prévenir ni l’une ni l’autre extrémité, ou un routeur redémarre derrière un répartiteur de charge. Rien n’arrive pour signaler la perte. Une application qui ne fait que publier s’en aperçoit à sa prochaine écriture en échec, mais une application qui ne fait que s’abonner n’écrit jamais : sans aide, elle reste morte jusqu’au redémarrage du conteneur.

Les trois SDK détectent cette situation. La détection est active par défaut et ne demande aucune configuration.

Le SDK Python utilise le keepalive WebSocket : il envoie un ping au routeur toutes les 30 secondes tant que le lien est inactif et attend un pong dans les 10 secondes. En l’absence de réponse, la connexion est abandonnée puis rétablie. Le trafic entrant vaut preuve de vie, si bien qu’une connexion active n’est jamais sondée — le ping ne part que lorsque le lien est réellement devenu silencieux.

Les connexions inter-applications ouvertes avec connectToApp / connect_to_app ne font généralement que s’abonner, ce qui est précisément le cas que cela protège. Elles sont couvertes automatiquement, et elles se reconnectent aussi après un refus du routeur, jusqu’à ce que le refus persiste (voir connectToApp).

Gestion des erreurs

Chaque méthode du SDK échoue de manière explicite : en cas d’arguments invalides, de connexion perdue ou de rejet par la plateforme, elle lève une exception (Python), rejette (JavaScript) ou renvoie une erreur (Go) avec un message indiquant l’opération, le topic et la raison. Rien n’est ignoré silencieusement : encadrez donc d’un bloc try les appels auxquels votre code doit survivre — en Go, vérifiez l’erreur renvoyée.

try: rows = await ironflock.getHistory("sensordata", {"limit": 100}) except ValueError as e: # Invalid parameters — e.g. limit out of range, or a malformed filter print(f"Bad query: {e}") except RuntimeError as e: # Not connected, table not in the data-template, or the platform rejected the call print(f"Query failed: {e}")

En JavaScript, les échecs provenant de la plateforme sont des instances de WampError — une sous-classe normale d’Error qui transporte en plus l’URI d’erreur WAMP dans error et la charge utile de l’erreur dans args / kwargs. Tout le reste (paramètres invalides, absence de connexion) est une Error ordinaire.

En Go, une opération qui échoue renvoie un *ironflock.OperationError dont le message indique l’opération et la cause, qu’il enveloppe ; errors.Is et errors.As voient à travers. Un refus du routeur ou d’un appelé est un *ironflock.WampError (*wamp.Error) avec URI, Args et Kwargs ; ironflock.WampURI(err) renvoie l’URI, ou "". Les autres erreurs sentinelles sont ironflock.ErrMissingConfig (une identité dont l’opération a besoin manque), ironflock.ErrAlreadyStarted et wamp.ErrStopped (après Stop).

Migration : les anciennes versions des SDK Python et JavaScript journalisaient un message et renvoyaient None / null lorsqu’un appel échouait. Elles lèvent désormais une erreur : un code de la forme if result is None: ne détecte donc plus les échecs — utilisez try / except (ou try / catch).

Refus du backend de données

Une opération que le backend de données refuse échoue avec l’une des URI d’erreur WAMP ci-dessous, la raison étant son premier argument. Les trois SDK les exportent sous forme de constantes ; Go les nomme dans le style Go (ironflock.URIRateLimited, ironflock.URIStorageFull, …). En Python, les méthodes d’IronFlock lèvent une RuntimeError dont la cause est l’ApplicationError d’autobahn (les lectures d’une app consommée lèvent directement l’ApplicationError), et wamp_error_uri(err) lit l’URI dans l’un comme dans l’autre cas. En JavaScript, l’erreur est une WampError dont error est l’URI. En Go, c’est un *ironflock.OperationError autour d’un *ironflock.WampError, et ironflock.WampURI(err) lit l’URI. Les SDK ne retentent aucun de ces refus. Une ligne publiée que le backend de données refuse est abandonnée sans erreur — seules les méthodes d’ajout (append) le signalent.

from datetime import datetime, timezone from ironflock import URI_STORAGE_FULL, wamp_error_uri row = {"tsp": datetime.now(timezone.utc).isoformat(), "temperature": 22.5} try: await ironflock.append_to_table("sensordata", row) except RuntimeError as err: if wamp_error_uri(err) == URI_STORAGE_FULL: pending.append(row) # the appliance's disk is nearly full: append the row later else: raise
ConstanteRefusé
URI_RATE_LIMITEDrevealSecrets / reveal_secrets au-delà de 30, verifySecret / verify_secret au-delà de 120 appels par minute par identifiant d’app sur un appareil
URI_RESULT_TOO_LARGEUne lecture avec une seule ligne dépassant le budget de message de 8 Mio, ou un résultat dépassant le garde-fou de 1 Gio (les autres résultats volumineux arrivent en fragments, que le SDK réassemble). Ses arguments nommés indiquent les plus grosses colonnes : réduisez limit, resserrez timeRange, sélectionnez moins de columns ou élaguez avec columnPaths
URI_INVALID_LIMITUne lecture d’un transform avec un limit supérieur à 3000
URI_INVALID_TIME_RANGE, URI_INVALID_METRIC, URI_INVALID_GROUP_BY, URI_SERIES_TOO_MANY_GROUPSRequêtes de séries auxquelles la table ne peut pas répondre : pas de début, ou une fin qui ne lui est pas postérieure ; une métrique que la table ne peut pas agréger (colonne inexistante, colonne secrète, méthode que le type de la colonne n’accepte pas, tsp avec une méthode autre que COUNT) ; une colonne groupBy selon laquelle elle ne peut pas grouper ; plus de 50 000 lignes
URI_SECRET_COLUMN, URI_NOT_A_SECRET_COLUMNUn filtre sur une colonne secrète ; verifySecret / verify_secret sur une colonne qui n’est pas secrète, ou revealSecrets / reveal_secrets d’une table sans colonnes secrètes
URI_STORAGE_FULLUn ajout alors que le disque de l’appliance est presque plein (moins d’environ 3 Gio libres ; de nouveau accepté à partir de 4 Gio) — temporaire : ajoutez les lignes plus tard
URI_STORAGE_OVERUSAGEUn ajout au-delà de l’allocation de stockage du compte ; réessayer peu après n’aide pas
URI_ENTITY_KEY_CONFLICTUne ligne dont la clé d’entité (maintainLatestFlagFor) et le tsp appartiennent déjà à une autre ligne, dans le lot ou parmi les lignes stockées : donnez à chaque ligne son propre tsp
URI_SECRET_SENTINEL_UNRESOLVABLE, URI_SECRET_CIPHERTEXT_REJECTEDUne écriture qui conserve la valeur précédente d’un secret (__secret__) sans clé d’entité permettant de la retrouver ; une écriture d’une valeur déjà chiffrée

Un ajout que le backend de données refuse sans raison — wamp.error.runtime_error, sa réponse à une ligne sans tsp — n’est pas un refus typé : le message d’erreur du SDK pointe vers le tsp manquant et vers la table error-logs de l’app, où le backend de données consigne la cause.

Utilisation dans le navigateur (JavaScript uniquement)

Le SDK JavaScript fonctionne dans les navigateurs modernes. Comme les navigateurs n’ont pas de variables d’environnement, passez toute la configuration via le constructeur :

import { IronFlock } from "ironflock"; const ironflock = new IronFlock({ serialNumber: "device-serial-from-server", deviceKey: "12", appName: "MyWebApp", swarmKey: 10, appKey: 20, env: "PROD", }); await ironflock.start(); await ironflock.publishToTable("sensordata", [{ tsp: new Date().toISOString(), temperature: 22 }]);

Utilisez IronFlock.fromServer() pour récupérer la configuration depuis votre backend au lieu de coder en dur les identifiants :

const ironflock = await IronFlock.fromServer("/api/ironflock-config"); await ironflock.start();

Votre point de terminaison backend doit renvoyer un objet JSON contenant les options de connexion (serialNumber, deviceKey, appName, swarmKey, appKey, env).

Enregistrement de fonctions pour agents IA

Le SDK peut enregistrer des fonctions appelables par des agents IA. Enregistrez une procédure et référencez son topic dans votre ai-template.yml :

def get_sensor_reading(sensor_id): """Returns the latest reading from a sensor.""" reading = read_from_hardware(sensor_id) return { "sensor_id": sensor_id, "temperature": reading.temp, "humidity": reading.hum, "timestamp": reading.ts } await ironflock.register_device_function("sensors.get_latest", get_sensor_reading)

L’agent IA peut alors appeler cette fonction lorsqu’un utilisateur pose une question nécessitant des données de capteur en temps réel.

Pour relier le topic WAMP enregistré à un agent IA, référencez-le dans le .ironflock/ai-template.yml de votre app :

sensor_agent: tool_description: | Delegate to this agent when the user asks about sensor readings, live device data, or current environmental conditions. system_prompt: | You are a sensor data specialist. Use get_current to retrieve the latest reading from any sensor. Always include the unit in your response. main: true max_context_tokens: 30000 max_iterations: 5 tools: get_current: description: Returns the latest reading from a sensor. topic: sensors.get_latest parameters: sensor_id: type: string description: The sensor identifier to query. required: true

La valeur topic (sensors.get_latest) doit correspondre au nom passé à register_device_function / registerDeviceFunction / RegisterDeviceFunction dans votre code edge. IronFlock achemine automatiquement l’appel vers l’appareil où la fonction est enregistrée.

Pour la référence complète de ai-template.yml, consultez Définir des agents et des outils.

Variables d’environnement

Ces variables sont définies automatiquement par l’environnement d’exécution IronFlock dans les conteneurs d’applications :

VariableDescription
DEVICE_NAMENom d’affichage de l’appareil
DEVICE_SERIAL_NUMBERIdentifiant unique et immuable de l’appareil
DEVICE_KEYClé de l’appareil pour l’authentification
SWARM_KEYIdentifiant du projet
APP_KEYIdentifiant de l’application
APP_NAMENom de l’application
ENVEnvironnement : DEV ou PROD
APP_AUTH_ID, APP_AUTH_SECRETLes identifiants propres à l’app, que le SDK présente au routeur. L’accès inter-apps les exige ; sans eux (agent de l’appareil plus ancien), le SDK se rabat sur le numéro de série de l’appareil
import os device_name = os.environ.get("DEVICE_NAME") serial = os.environ.get("DEVICE_SERIAL_NUMBER") project_key = os.environ.get("SWARM_KEY")

Les variables d’accès à distance (TUNNEL_DOMAIN, DEVICE_LAN_IP, DEVICE_PORT_FOR_<port>, REMOTE_PORT_FOR_<port>, …) sont listées dans Configuration des ports.

L’agent de l’appareil reflète aussi ces valeurs dans /data/env, un fichier <NAME>.txt par valeur, et maintient ce miroir à jour pendant que l’app s’exécute ; les SDK le lisent en premier, de sorte que les valeurs qui changent à l’exécution, comme un port d’accès à distance, parviennent à une app en cours d’exécution. Les identifiants propres à l’app sont relus à chaque tentative de connexion, de sorte que des identifiants dont l’agent effectue la rotation pendant que l’app s’exécute sont présentés à la reconnexion suivante. L’agent rend /data/env lisible par root uniquement : dans un conteneur qui s’exécute sous un autre utilisateur, le SDK voit les valeurs avec lesquelles le conteneur a démarré et journalise un avertissement une seule fois.

Last updated on