IronFlock SDK
El SDK de IronFlock permite que tus aplicaciones de borde interactúen con la plataforma IronFlock. Está disponible en tres lenguajes — Python, JavaScript y Go — con la misma funcionalidad, documentados uno junto a otro a continuación. Gestiona la autenticación automáticamente cuando se ejecuta en un dispositivo registrado y proporciona funciones para publicar datos, consultar historial, llamar procedimientos remotos entre dispositivos y actualizar metadatos de dispositivos.
| SDK | Paquete | Requisitos |
|---|---|---|
| Python | ironflock en PyPI | Python 3.8+ |
| JavaScript | ironflock en npm | Node.js 18+ o navegador moderno |
| Go | github.com/RecordEvolution/ironflock-go | Go 1.27.2+ |
Instalación
Python
pip install ironflockO añade ironflock al archivo requirements.txt de tu app.
Inicio rápido
Python
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()Cuando se usa dentro de un contenedor de app de IronFlock, el SDK lee las credenciales de conexión del entorno automáticamente — no se necesita configuración manual. Cada fila que escribes en una tabla lleva un tsp, su marca de tiempo (consulta publishToTable).
Opciones del constructor
Python
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)
)| Parámetro | Descripción |
|---|---|
mainFunc | Una función asíncrona que se ejecuta una vez establecida la conexión |
serial_number | Sobrescribe el número de serie del dispositivo que se envía con las publicaciones y las escrituras en tablas. Por defecto usa la variable de entorno DEVICE_SERIAL_NUMBER. No cambia la identidad con la que se autentica la conexión mientras el agente del dispositivo inyecte credenciales propias de la app |
reconnect_window | Segundos que una operación de tabla espera a que la plataforma vuelva tras un reinicio antes de lanzar un error. Por defecto 60; 0 lo desactiva (consulta Fiabilidad de la conexión) |
Publicación de datos
publishToTable / publish_to_table
Publica un registro de datos en una tabla de flota. El nombre de la tabla debe coincidir con una tabla definida en el archivo data-template.yml de tu app. El SDK enruta los datos automáticamente a la base de datos del proyecto correspondiente.
Python
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"
})El backend de datos lee la fila del primer argumento: un dict en Python, el primer elemento del array en JavaScript, el primer argumento posicional en Go (un map o un struct). Los argumentos con nombre adicionales — en Go, un valor ironflock.Kwargs entre los argumentos — solo se leen para las columnas que la plantilla de datos les asigna (kwargs.<name>).
Cada fila necesita un tsp. Toda tabla tiene una columna tsp obligatoria, la marca de tiempo de la fila — p. ej., una cadena ISO 8601, o en Go un time.Time, que se envía como RFC 3339 en UTC —, que el backend de datos lee de la fila salvo que la plantilla de datos la asigne a otra fuente. Descarta una fila publicada sin tsp y solo deja constancia de ello con una entrada en la tabla error-logs de la app — la publicación confirmada parece exitosa. Una fila que rechaza por falta de almacenamiento se descarta sin siquiera eso. Usa appendToTable / append_to_table cuando necesites saber si una fila se almacenó.
Filas parciales en tablas de entidades. En una tabla con clave de entidad (maintainLatestFlagFor), una fila puede llevar solo la clave de entidad, tsp y las columnas que cambian. Una columna que la fila omite conserva el valor anterior de la entidad; una columna enviada como None / null / nil se borra. Que un campo cuente como omitido depende de cómo construyas la fila:
- Python: una clave cuyo valor es
Nonese envía como null y borra la columna. Elimina la clave del dict para conservar el valor anterior. - JavaScript: una propiedad cuyo valor es
undefineddesaparece del payload, así que conserva el valor anterior. Asígnalenullpara borrar la columna. - Go: un campo de struct con la etiqueta
omitemptyuomitzerose omite cuando está vacío, así que conserva el valor anterior. En otro caso, un campo punteronil, igual que un valornilen unironflock.Row, se envía como null y borra la columna.
Lo mismo vale para los cuatro métodos de escritura. En una tabla sin clave de entidad, una columna que la fila omite se almacena como NULL.
appendToTable / append_to_table
Añade datos a una tabla de flota usando una llamada a procedimiento remoto en lugar de pub/sub. Usa esto cuando necesites confirmación de que los datos fueron persistidos: una fila rechazada es un error.
Python
result = await ironflock.append_to_table("sensordata", {
"tsp": datetime.now(timezone.utc).isoformat(),
"temperature": 22.5,
"humidity": 60
})La fila necesita un tsp, igual que con publishToTable. El backend de datos rechaza una fila sin tsp con wamp.error.runtime_error y sin motivo; el error del SDK añade entonces “the data backend refused the row without a reason — a missing tsp is the usual cause; see the error-logs table”. Los rechazos tipados, como un disco lleno en un appliance, se enumeran en Rechazos del backend de datos.
publishRowsToTable / publish_rows_to_table
Publica muchas filas en un solo mensaje (inserción masiva) en una tabla de flota. La plataforma inserta todo el lote de forma atómica (todo o nada) en una sola operación. Usa esto para datos de alta frecuencia donde una ida y vuelta por fila sería demasiado costosa. Al igual que publishToTable, esto es fire-and-forget — la confirmación valida la entrega al router, no la inserción en la base de datos.
Python
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},
])El segundo argumento es una lista no vacía de objetos de fila a insertar, cada uno con su propio tsp — en Go, un slice de filas ([]ironflock.Row, []map[string]any o un slice de structs). Un lote que el backend de datos rechaza — porque a una fila le falta su tsp, por ejemplo — se descarta sin avisar a quien publica, igual que con publishToTable.
appendRowsToTable / append_rows_to_table
Añade muchas filas en una sola llamada a procedimiento remoto (inserción masiva) en una tabla de flota. La plataforma inserta todo el lote de forma atómica (todo o nada): si cualquier fila es inválida, se rechaza el lote completo y no se persiste nada. Prefiere esto sobre publishRowsToTable / publish_rows_to_table cuando necesites conocer el resultado de la inserción. Cada fila necesita su propio tsp; los rechazos son los de appendToTable, incluida la pista para un lote rechazado sin motivo.
Python
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
Reporta un error de aplicación en la tabla error-logs de tu flota. Esto es un envoltorio de conveniencia sobre publishToTable / appendToTable: marca la fila con source: "app", un level de severidad y una marca de tiempo, y luego la escribe como cualquier fila normal de tabla. El error llega a la misma tabla error-logs que usan los errores de sistema de fleetdb (etiquetados con source: "system"), por lo que se puede consultar con getHistory, transmitir con subscribeToTable / subscribe_to_table, usar en plantillas de tablero y entregar en tiempo real en transformed.error-logs — sin disparar el toast de error de sistema de la plataforma.
Python
# 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 fila es { tsp, msg, user_message, source: "app", level }: msg contiene el texto técnico (un traceback o un stack), user_message la línea que muestra el toast de error del tablero — el mensaje de usuario que pasas o, si no pasas ninguno, el propio mensaje.
Parámetros:
| Parámetro | Tipo | Descripción |
|---|---|---|
error | str / string o excepción / Error | El mensaje de error, o una excepción cuyo traceback/stack (o mensaje) se registra |
level | str / string, opcional | Severidad: "error", "warn", "info" o "debug". Por defecto "error" |
append | bool / boolean, opcional | Cuando es true, usa la RPC de append (devuelve el resultado de la inserción). Por defecto false (publicación fire-and-forget) |
tsp | str / string, opcional | Sobrescritura de marca de tiempo ISO-8601. Por defecto la hora actual |
user_message / userMessage | str / string, opcional | Texto para el operador, almacenado como user_message. Por defecto, el mensaje registrado |
En Python las opciones son argumentos con nombre (
report_error(error, level=..., append=..., tsp=..., user_message=...)); en JavaScript se pasan a través de un objeto de opciones (reportError(error, { level, append, tsp, userMessage })); en Go, a través de un struct (ReportError(ctx, errOrMsg, ironflock.ReportErrorOptions{Level, Append, Tsp, UserMessage})), dondeerrOrMsges unerroro una cadena,Leveluno deironflock.LevelError,LevelWarn,LevelInfo,LevelDebug, y el*Resultdevuelto esnilsalvo que se indiqueAppend.
publish
Publica un mensaje en un topic WAMP propio. Usa esto para mensajería personalizada o eventos que no se corresponden con una tabla de base de datos. El router reserva algunos nombres para la plataforma — consulta URIs que una app puede usar.
Python
await ironflock.publish("com.myapp.alerts", {
"level": "warning",
"message": "Temperature threshold exceeded"
})Consulta de datos históricos
getHistory
Recupera datos históricos de una tabla de flota o de un transform: las filas más recientes que coinciden, en orden ascendente de tsp. Soporta filtrado, rangos de tiempo y paginación.
Python
# 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"],
})Parámetros de consulta:
| Campo | Tipo | Descripción |
|---|---|---|
limit | int / number | Máximo de filas a devolver (1–10.000, obligatorio; 1–3.000 para un transform) |
offset | int / number | Desplazamiento para paginación |
timeRange | list / array | [start, end] — cadenas de fecha y hora ISO o números de milisegundos epoch, nunca mezclados; None / null para un extremo abierto. Un objeto heredado {"start": ..., "end": ...} se convierte en el par |
filterAnd | list / array | Entradas de filtro combinadas con AND: condiciones, grupos y/o el marcador latest (ver abajo) |
columns | list / array | Columnas a devolver (opcional). tsp, device_key y authid siempre se incluyen; omítelo para obtener todas las columnas |
columnPaths | list / array | Recorta columnas json (opcional): rutas JSON en la notación que usan los filtros ("json_data.a.b", "json_data['key']"). Una columna json incluida en columns vuelve conteniendo solo la unión de sus rutas; una sin ruta aquí vuelve completa. Lo ignoran los transforms y los backends de datos anteriores a fleetdb v1.0.52 |
En Go, estos son los campos Limit, Offset, TimeRange, FilterAnd, Columns y ColumnPaths de ironflock.TableQueryParams. TimeRange es un *ironflock.TimeRange{Start, End}: cada límite es un time.Time, una cadena ISO 8601, un número en milisegundos epoch o nil para un extremo abierto (ironflock.Between, Since y Until construyen uno a partir de valores time.Time). FilterAnd es un []ironflock.Filter. No existe la forma de objeto heredada.
Operadores de filtro: =, !=, <>, >, <, >=, <=, LIKE, ILIKE, NOT LIKE, NOT ILIKE, IN, NOT IN, IS NULL, IS NOT NULL
Cada condición de filtro es un objeto con las claves column, operator y value — en Go, construido con ironflock.Where(column, operator, value), ironflock.IsNull(column) e ironflock.IsNotNull(column). Los operadores se reconocen sin distinguir mayúsculas y minúsculas, ignorando los espacios alrededor, y se envían en la forma indicada arriba — la única que acepta el backend de datos —, de modo que "like" sale como "LIKE", también dentro de grupos. Un operador que no está en la lista se envía tal cual, con una advertencia. IS NULL e IS NOT NULL no llevan value y son la única forma de preguntar por NULL: cualquier otro operador excluye una fila cuya columna sea NULL, exactamente como en SQL. IN y NOT IN reciben una lista (un slice en Go). LIKE distingue mayúsculas y minúsculas; usa ILIKE para comparar sin distinguirlas.
Combinar con OR. Las entradas de filterAnd se combinan con AND. Una entrada de grupo combina sus propias entradas — condiciones, el marcador latest u otros grupos — con un único operador, "AND" u "OR":
Python
# 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 datos anterior a los grupos (fleetdb antes de v1.2.0) rechaza esta forma en lugar de aplicar solo una parte, así que falla de forma explícita en vez de devolver en silencio filas que el filtro excluye.
Leer los valores actuales. Una entrada {"latest": true} en filterAnd (ironflock.Latest() en Go) no es una condición de filtro sino un cambio de modo: el backend de datos devuelve solo la fila más reciente de cada entidad, derivada en SQL a partir de la clave de entidad que la tabla declara con maintainLatestFlagFor. Una tabla sin clave de entidad devuelve su única fila más reciente.
Las demás condiciones se combinan con el marcador como cabría esperar: las condiciones sobre columnas de la clave de entidad acotan qué entidades se devuelven, mientras que todas las demás condiciones y timeRange se aplican a las filas más recientes resultantes. Así, combinar {"latest": true} con un filtro sobre deleted oculta las entidades eliminadas en lugar de hacer reaparecer su fila anterior.
Las versiones anteriores de IronFlock almacenaban una columna física latest_flag. Ya no existe — un filtro heredado latest_flag = true se sigue aceptando y se trata como el marcador, pero el código nuevo debería usar {"latest": true}. El marcador latest no está disponible en getSeriesHistory.
Rangos de tiempo. El backend de datos lee un límite en forma de cadena con el Date de JavaScript, así que escribe una fecha o una fecha y hora ISO 8601 (2026-07-01, 2026-07-01T12:30:15.250Z) y separa la fecha y la hora con T. Los SDKs rechazan un espacio entre fecha y hora en los años 0–99, que Date interpreta mal: "0001-01-01 00:00:00" — lo que devuelve str(datetime.min) en Python — se leería como 2001. Una hora sin zona horaria se lee como UTC. Un límite numérico se expresa en milisegundos epoch y debe ser finito: NaN y ±Infinity llegarían al backend de datos como un extremo abierto y eliminarían en silencio el filtro de tiempo, así que los SDKs los rechazan antes de enviar. En Go, un límite time.Time se envía como RFC 3339 en UTC, y un límite de cadena debe ser una de las formas ISO 8601 que Date lee correctamente — una fecha, o una fecha y una hora del día con un Z o un desfase UTC opcional (+02:00 o +0200); otras cadenas, como el formato básico 20260701T123000Z, un desfase solo de horas o un día que el mes no tiene (2026-02-30), fallan con ironflock.ErrInvalidArgument antes de que se envíe nada.
Transforms. Un transform (una vista SQL de una plantilla de datos, consulta Tablas de transformación) se lee de forma distinta a una tabla. El backend de datos solo aplica limit — como máximo 3000; uno mayor se rechaza con URI_INVALID_LIMIT —, offset y filterAnd. Ignora timeRange, columns, columnPaths y el marcador latest, y descarta un filtro sobre una columna que el transform no declara (un transform no tiene columnas implícitas tsp, device_key ni authid), lo que amplía el resultado. Sus filas llegan en el orden propio de la vista, invertido: obtienes las filas más recientes, de la más antigua a la más reciente, solo cuando la vista ordena primero las más recientes. Acota el rango de tiempo de un transform en su SQL.
Resultados grandes. Un resultado demasiado grande para un único mensaje del backend de datos (8 MiB de JSON) llega en fragmentos, que el SDK vuelve a ensamblar en memoria: obtienes las filas de una única lectura, en el mismo orden. Los fragmentos que no cuadran — uno que falta o está duplicado, o un recuento de filas que difiere del resumen del backend de datos — hacen fallar la lectura en lugar de devolver parte de las filas. Una sola fila de más de 8 MiB, o un resultado que supera la protección de 1 GiB del backend de datos, se rechaza con URI_RESULT_TOO_LARGE: reduce limit, acota timeRange, selecciona menos columns o recorta con columnPaths.
getSeriesHistory / get_series_history
Recupera datos de series temporales submuestreados de una tabla de flota: columnas agregadas por intervalo de tiempo (p. ej., promedios por hora). Ideal para gráficos que abarcan rangos de tiempo largos. Disponible para tablas (no para transforms), en backends de datos a partir de fleetdb v1.0.58.
Python
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"Parámetros de consulta:
| Campo | Tipo | Descripción |
|---|---|---|
metrics | list / array | Los agregados que se calculan por intervalo, al menos uno: {"ref": ..., "method": ...}. ref es una columna o una ruta JSON dentro de una columna json ("json_data.temp"); method es "AVG", "SUM", "COUNT", "MIN", "MAX", "FIRST" o "LAST". Cada métrica vuelve bajo "<METHOD>:<ref>", que debe caber en 63 bytes |
bucketMs | int / number | Ancho del intervalo en milisegundos enteros, al menos 1000 (opcional). Omítelo para dividir el rango de tiempo en limit intervalos |
limit | int / number | El presupuesto de intervalos (1–10.000) — no un límite de filas |
timeRange | list / array | [start, end] — cadenas de fecha y hora ISO o números epoch-ms. Obligatorio, con un inicio; None / null como fin significa ahora. El fin debe ser posterior al inicio |
groupBy | list / array | Columnas que dividen cada intervalo en una fila por grupo (opcional) |
filterAnd | list / array | Condiciones y grupos combinados con AND, como en getHistory (opcional). Aquí se rechaza el marcador latest, también dentro de un grupo; usa getHistory para leer los valores actuales |
Filas. Cada fila es un intervalo — de un grupo, con groupBy. tsp es el inicio del intervalo en milisegundos epoch, cada columna de groupBy vuelve con su propio nombre y cada métrica bajo "<METHOD>:<ref>" (SeriesMetric.column en Python, seriesColumn(metric) en JavaScript, SeriesMetric.Column() en Go), p. ej. row["AVG:temperature"]. Las filas van en orden ascendente de tsp; un intervalo sin filas no aparece (no se rellenan huecos). AVG y SUM admiten columnas numéricas, MIN y MAX también cadenas y marcas de tiempo; FIRST y LAST dan el valor de la fila más antigua y de la más reciente del intervalo; COUNT cuenta los valores que no son nulos (en el caso de tsp: las filas). Un resultado grande se vuelve a ensamblar a partir de fragmentos, como en getHistory.
Intervalos y un extremo abierto. limit cuenta intervalos, no filas. Sin bucketMs, el rango de tiempo se divide en limit intervalos. Con él, el backend de datos ensancha los intervalos a un múltiplo de bucketMs cuando el rango contiene más de limit — y un fin abierto llega hasta el ahora del propio backend de datos, un poco posterior al tuyo. Las últimas 24 horas con un fin abierto contienen 25 intervalos de una hora, así que limit: 24 obtiene intervalos de dos horas. Acota el fin, como en el ejemplo, o admite un intervalo más: con bucketMs y un fin abierto pueden volver limit + 1 intervalos.
Los SDKs comprueban antes de enviar lo que el backend de datos rechazaría: al menos una métrica, refs no vacías, métodos conocidos, nombres de columna de hasta 63 bytes, un inicio, un fin posterior al inicio y bucketMs como entero de al menos 1000 (en Go, Bucket de al menos 1 segundo, en milisegundos enteros). El backend de datos rechaza lo que no puede responder con URI_INVALID_TIME_RANGE, URI_INVALID_METRIC, URI_INVALID_GROUP_BY o URI_SERIES_TOO_MANY_GROUPS (más de 50.000 filas) — consulta Rechazos del backend de datos.
Forma obsoleta. Las versiones del SDK hasta la 1.9.0 enviaban
metricscomo nombres de columna más unmethodde nivel superior ({"metrics": ["temperature"], "method": "AVG", ...}), que los backends de datos rechazan desde fleetdb v1.0.58. Esa forma sigue funcionando: el SDK envía una métrica{ref, method}por columna, emite una advertencia (unDeprecationWarningen Python, una advertencia en consola una vez por proceso en JavaScript) y vuelve a renombrar las columnas del resultado con los nombres de columna, de modo que el código antiguo sigue leyendorow["temperature"]— salvo que la fila ya tenga una columna con ese nombre, comotspparaCOUNTdetsp. Pasa a métricas{ref, method}y leerow["AVG:temperature"]. El SDK de Go solo tiene la forma actual: un método por métrica (SeriesMetric).
Suscripción a datos
subscribeToTable / subscribe_to_table
Se suscribe a actualizaciones en tiempo real de una tabla de flota. El handler se llama cada vez que el backend de datos almacena una fila nueva, con la fila tal como se almacenó — valores tipados según las columnas de la plantilla de datos, columnas secretas enmascaradas — como primer argumento. Las filas escritas a través de la ruta de inserción masiva (publishRowsToTable / appendRowsToTable) se entregan a tu handler una por una, por lo que el código del handler permanece igual sin importar cómo se escribieron los datos.
Python
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 no tiene filas propias: en cada ejecución de su programación, el backend de datos publica la vista completa — como máximo 3000 filas, en el orden inverso de la vista — como un único evento cuyo primer argumento es la lista de filas. event_rows(args[0]) en Python, eventRows(args) en JavaScript y ev.Rows() en Go leen ambas formas: la fila de una tabla da [row], la lista de un transform la propia lista ([] para una vista vacía), y cualquier otra cosa None / undefined / nil. En Go, ev.Row() es nil para el evento de un transform.
Python
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
Se suscribe a un topic WAMP propio para mensajería personalizada en tiempo real. Para filas de tablas usa subscribeToTable / subscribe_to_table: el router rechaza las suscripciones a los topics de escritura en bruto de las tablas (consulta URIs que una app puede usar).
Python
def on_alert(*args, **kwargs):
print("Alert received:", args, kwargs)
await ironflock.subscribe("com.myapp.alerts", on_alert)Columnas secretas
Una columna declarada secret: true en la plantilla de datos de la app se almacena cifrada. Ninguna ruta de lectura ordinaria devuelve el texto en claro, ni siquiera a la app que lo escribió:
| Ruta de lectura | Qué aspecto tiene una columna secreta |
|---|---|
getHistory, subscribeToTable / subscribe_to_table y todo lo demás que pasa por el router de mensajes | el marcador de posición __secret__, exportado como SECRET_PLACEHOLDER (ironflock.SecretPlaceholder en Go) |
| El acceso SQL (el inicio de sesión de Postgres de FleetDB Access) | el texto cifrado almacenado, ifsec:1:… |
revealSecrets / reveal_secrets | el texto en claro descifrado |
NULL se transmite tal cual como NULL en todas las rutas, así que establecido pero oculto se sigue distinguiendo de nunca escrito.
En una tabla de entidades, el patrón leer-modificar-escribir es seguro: volver a publicar una fila que todavía lleva el marcador de posición __secret__ — u omitir por completo la columna secreta — conserva el secreto almacenado en lugar de sobrescribirlo, y un null explícito lo borra. Consulta Columnas secretas en la guía del backend de datos para conocer las reglas exactas.
Python
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__"}]Ambas funciones que siguen solo se pueden llamar desde los propios contenedores de la app. El router de mensajes se las deniega a todos los roles de navegador y a las apps consumidoras, y por eso un manejador ConsumedApp no tiene un equivalente: las columnas secretas de otra app llegan enmascaradas y así se quedan.
revealSecrets / reveal_secrets
Lee filas de una de tus propias tablas con sus columnas secretas descifradas.
Python
rows = await ironflock.reveal_secrets("credentials", {
"limit": 1,
"filterAnd": [{"column": "device_id", "operator": "=", "value": "d-1"}],
})
# [{"tsp": "...", "device_id": "d-1", "api_token": "hunter2"}]Los parámetros de consulta son los mismos que los de getHistory, con una diferencia: limit no puede superar 100. Ese techo existe para impedir que una sola llamada descifre una tabla entera, y el backend de datos rechaza un valor mayor en lugar de recortarlo en silencio.
El backend de datos responde como máximo a 30 revelaciones por minuto por credencial de app en un dispositivo; por encima de eso, la llamada falla con URI_RATE_LIMITED. Revela un secreto una vez y consérvalo, en lugar de hacerlo en cada uso. Una tabla sin columnas secretas se rechaza con URI_NOT_A_SECRET_COLUMN, y un filtro sobre una columna secreta con URI_SECRET_COLUMN.
verifySecret / verify_secret
Comprueba un valor candidato contra una columna secreta sin revelarla — la contraparte de la función de revelado para comprobar contraseñas. La comparación se ejecuta dentro del backend de datos, en tiempo constante, y solo vuelve la respuesta.
Python
ok = await ironflock.verify_secret(
"credentials", "api_token", submitted_token,
{"limit": 1, "filterAnd": [{"column": "device_id", "operator": "=", "value": "d-1"}]},
)
if ok:
...Parámetros:
| Parámetro | Tipo | Descripción |
|---|---|---|
tablename | str / string | La tabla contra la que se comprueba |
column | str / string | La columna secreta a comparar |
candidate | str / string | El texto en claro que se está probando. Se compara en el servidor y nunca se escribe en la tabla |
query_params / queryParams | SecretQueryParams | Qué filas participan. Por defecto, la fila más reciente; limit no puede superar 100 |
En Go, los parámetros son cadenas posicionales después del contexto, y la consulta es un *ironflock.TableQueryParams (nil para la fila más reciente).
JavaScript devuelve { match, checked } y Go un *ironflock.SecretVerifyResult con Match y Checked; Python devuelve un bool simple. Cuando dispones de checked, merece la pena usarlo: distingue un candidato incorrecto (checked > 0) de un selector que no ha encontrado ninguna fila (checked === 0), dos fallos muy distintos a la hora de informar al usuario.
Como cada fila se cifra con su propio valor aleatorio, dos filas que contienen el mismo secreto almacenan textos cifrados distintos. Por eso un filtro = sobre una columna secreta no puede funcionar — el backend de datos rechaza cualquier filtro sobre una de ellas con URI_SECRET_COLUMN — y existe en su lugar esta función.
El backend de datos responde como máximo a 120 comprobaciones por minuto por credencial de app en un dispositivo; por encima de eso, la llamada falla con URI_RATE_LIMITED. Para una comprobación en cada petición (una clave de API, por ejemplo), guarda en caché brevemente un resultado positivo. Una columna que no es secreta se rechaza con URI_NOT_A_SECRET_COLUMN.
Acceso a datos entre apps
Lee los datos de flota de otra app desde dentro de tu propia app, en el mismo proyecto. Tu app debe declarar la app proveedora en la sección consumes: de su propio data-template.yml (o tener la concesión comodín, ver más abajo), y el usuario del proyecto debe conceder el acceso. El acceso es de solo lectura: puedes consultar el historial y suscribirte en tiempo real a las filas de las tablas y transforms que comparte el proveedor, pero no puedes escribir en ellas. Las conexiones a apps consumidas se almacenan en caché por app y se cierran automáticamente cuando tu instancia se detiene.
La conexión necesita las credenciales propias de la app que inyecta el agente del dispositivo (APP_AUTH_ID, APP_AUTH_SECRET). La plataforma rechaza la credencial heredada del dispositivo — el recurso al número de serie de una app que no recibió credenciales propias, p. ej. con un agente del dispositivo más antiguo — en el realm de otra app (NOT_AUTHORIZED, con un mensaje que lo indica), salvo que la app proveedora se ejecute en el mismo dispositivo: entonces la conexión se une a su realm con todos los derechos de esa app en lugar de solo lectura.
Si tu app posee la concesión comodín (consumes: [{ app: "*" }]), puedes descubrir y abrir proveedores de forma dinámica con listConsumableApps / list_consumable_apps y connectToAllApps / connect_to_all_apps (más abajo).
connectToApp / connect_to_app
Abre una conexión de solo lectura al backend de datos de otra app y devuelve un manejador (handle). El manejador expone getHistory / get_history, subscribeToTable / subscribe_to_table y getSeriesHistory / get_series_history (solo tablas) — las mismas consultas y suscripciones que usas en tus propias tablas, con los mismos parámetros y resultados — además de close y los catálogos compartidos tables / transforms.
Python
# 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)Parámetros:
| Parámetro | Tipo | Descripción |
|---|---|---|
app_name / appName | str / string | Nombre de la app proveedora, tal como se declara en tu sección consumes: |
stage | str / string, opcional | Stage del proveedor: "dev" o "prod". Por defecto, el stage de tu propia app |
on_error / onError | callable, opcional | Se invoca con un CrossAppAccessError cuando a la conexión abierta se le deniega el acceso de forma definitiva (p. ej., si la concesión se revoca más tarde — más abajo se explica cuándo una denegación se considera definitiva) |
Si el acceso se deniega o se usa incorrectamente, se lanza un CrossAppAccessError (Python / JavaScript) o se devuelve (Go, un *ironflock.CrossAppAccessError que se comprueba con errors.As) con un campo code (Code en Go, con constantes como ironflock.CodeNoGrant): NO_GRANT, PROVIDER_NOT_INSTALLED, UNKNOWN_APP, PRIVATE_TABLE, SECRET_COLUMN (una consulta que filtra — también dentro de un grupo — o selecciona una columna que el proveedor marca como secreta) o NOT_AUTHORIZED (la concesión se revocó o no pudo verificarse en ese momento — o tu app presentó la credencial heredada del dispositivo, en cuyo caso el mensaje lo indica).
Una denegación mientras connectToApp / connect_to_app espera hace fallar la llamada de inmediato. Una vez abierto un manejador, una denegación solo se considera definitiva cuando persiste — 3 rechazos o más desde la última vez que la conexión estuvo activa (que el router cierre la sesión por un motivo de autenticación cuenta como uno, igual que cada reconexión rechazada), el primero hace al menos 60 segundos —, porque la plataforma rechaza de la misma forma mientras brevemente no puede verificar el acceso. Hasta entonces, el manejador sigue reconectándose y se recupera si una reconexión tiene éxito. Una vez que la denegación es definitiva, se llama a on_error / onError / OnError y el manejador se descarta: el siguiente connectToApp / connect_to_app / ConnectToApp abre una conexión nueva. A partir de entonces, las lecturas y suscripciones del manejador fallan de inmediato, con el error que recibió on_error / onError — en Go, con un error que envuelve wamp.ErrStopped.
En Python,
stageyon_errorson argumentos con nombre; en JavaScript se pasan a través de un objeto de opciones (connectToApp(appName, { stage, onError })); en Go, a través de un struct (ConnectToApp(ctx, appName, ironflock.ConnectToAppOptions{Stage, OnError})), dondeOnErrores unafunc(*ironflock.CrossAppAccessError). En Go,ctxsolo acota la espera de quien llama: las llamadas simultáneas para la misma app comparten un intento, yClose(ctx)cierra un manejador.
listConsumableApps / list_consumable_apps
Lista todos los proveedores no privados del proyecto — la primitiva de descubrimiento para apps que poseen la concesión de consumo comodín (consumes: [{ app: "*" }] en tu data-template.yml, concedida por el usuario del proyecto). Realiza una única llamada y no abre ninguna conexión: renderiza los catálogos devueltos en un selector, luego llama a connectToApp / connect_to_app para los que quieras — o a connectToAllApps / connect_to_all_apps para abrirlos todos a la vez.
Nota: Declara la concesión en el
data-template.ymlde tu app, y entrecomilla el*— un*sin comillas es un alias de YAML y no se parseará:
consumes:
- app: "*"Python
providers = await ironflock.list_consumable_apps()
for p in providers:
print(p["app"], list(p["stages"].keys())) # e.g. "weather-app" ["dev", "prod"]Cada entrada describe un proveedor:
| Campo | Tipo | Descripción |
|---|---|---|
app | str / string | Nombre de la app proveedora |
provider_app_key | int / number | La clave de app del proveedor |
stages | dict / object | Catálogo por stage { dev?, prod? }; un stage está presente solo si el proveedor tiene un backend de datos para él. Cada catálogo contiene las tables y transforms no privadas que comparte |
En Go, cada entrada es un ironflock.ConsumedAppInfo con App, ProviderAppKey y Stages.Dev / Stages.Prod (un *StageCatalog con Tables y Transforms, nil para un stage que no existe); Catalog(stage) devuelve uno por su nombre.
Genera (Python) / lanza (JavaScript) / devuelve (Go) un CrossAppAccessError con code: NO_GRANT si tu app no posee ninguna concesión comodín.
connectToAllApps / connect_to_all_apps
Abre manejadores de solo lectura a todos los proveedores no privados del proyecto en una sola llamada (solo para consumidores comodín). Enumera los proveedores mediante listConsumableApps / list_consumable_apps y abre cada uno, omitiendo los que no tengan un backend de datos para el stage solicitado. Cada manejador se almacena en caché bajo la misma clave que connectToApp / connect_to_app, de modo que una llamada posterior a connectToApp(name) devuelve el manejador ya preparado. Los manejadores devueltos se cierran juntos cuando tu instancia se detiene.
Python
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)Parámetros:
| Parámetro | Tipo | Descripción |
|---|---|---|
stage | str / string, opcional | Stage del proveedor: "dev" o "prod". Por defecto, el stage de tu propia app |
continue_on_error / continueOnError | bool / boolean, opcional | Cuando es true (el valor por defecto), un proveedor que no se puede abrir se reporta a on_error / onError y se omite del resultado. Cuando es false, el primer fallo se lanza |
on_error / onError | callable, opcional | Se invoca con cada proveedor que no se pudo abrir (mientras continue_on_error / continueOnError sea true), y con un CrossAppAccessError si a una conexión ya abierta se le deniega después el acceso de forma definitiva (p. ej., si la concesión se revoca) |
Devuelve los manejadores de proveedor abiertos correctamente (el mismo tipo de manejador que connectToApp / connect_to_app). Genera (Python) / lanza (JavaScript) / devuelve (Go) un CrossAppAccessError con code: NO_GRANT si tu app no posee ninguna concesión comodín.
En Python,
stage,on_errorycontinue_on_errorson argumentos con nombre; en JavaScript se pasan a través de un objeto de opciones (connectToAllApps({ stage, onError, continueOnError })); en Go, a través de un struct (ConnectToAllApps(ctx, ironflock.ConnectToAllAppsOptions{Stage, OnError, StopOnError})), dondeOnErrores unafunc(error)yStopOnError: truecorresponde acontinueOnError: false.
Almacenamiento de archivos gestionado
Cada backend de datos de app dispone de almacenamiento de objetos privado junto a sus tablas, accesible a través de la propiedad files (ifl.Files() en Go, un *filestore.FileStore que puede obtenerse antes de Start). Úsalo para imágenes, PDF, fotogramas de cámara, blobs de firmware — cualquier cosa que no pertenezca a una fila de una tabla. No requiere configuración alguna: una app sin sección files: en su plantilla de datos también obtiene un espacio de nombres llamado default (una plantilla que declara sus propios espacios de nombres lo reemplaza — consulta Espacios de nombres).
Esta sección es la referencia del lado del cliente. Para el lado de la plataforma — declarar espacios de nombres, cuotas y retención, compartir archivos entre apps, credenciales S3 por app y el comportamiento en appliances on-premises — consulta Almacenamiento de archivos.
La idea clave es que almacenar un objeto te devuelve una URL permanente que puedes escribir directamente en una columna de tabla, de modo que un widget de tablero pueda renderizarla sin ningún trabajo adicional:
Python
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)Esa URL nunca caduca, pero no es un enlace público: solo permanece legible para un solicitante autenticado que tenga acceso READ sobre este backend de datos, y un proxy de autenticación lo vuelve a comprobar en cada petición. Por eso es seguro almacenarla en la base de datos.
Espacios de nombres
Un espacio de nombres es un prefijo de clave que lleva asociada una política — retención, reglas de compartición, tipos de contenido permitidos. No es un bucket aparte; todos los espacios de nombres de una app viven dentro de la única área de almacenamiento de esa app. Declara uno solo cuando un conjunto de objetos necesite reglas distintas; en caso contrario quédate en default y organiza tus objetos con rutas de clave como 2026/03/part-1.jpg.
Declara los espacios de nombres en data-template.yml. Declarar cualquiera reemplaza el default implícito: incluye también uno llamado default, o toda llamada sin espacio de nombres fallará con 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 }Ten en cuenta que el presupuesto se declara una sola vez para toda la app, no por espacio de nombres. Un espacio de nombres es solo un prefijo de clave dentro de la única área de almacenamiento de la app, así que no hay nada contra lo que aplicar un presupuesto por prefijo. maxObjectBytes sí es por espacio de nombres — limita un objeto individual, no un total: 100 MiB si no se establece, como máximo 5 GiB (OBJECT_TOO_LARGE por encima).
Todos los métodos siguientes aceptan el espacio de nombres como argumento opcional y, por defecto, usan default; un espacio de nombres vacío ("") también significa default.
Almacenar y leer objetos
Python
# 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étodo | Descripción |
|---|---|
put(key, data, …) | Almacena un objeto (bytes en Python, Uint8Array en JavaScript, []byte en Go). Devuelve los metadatos del objeto, incluida su url |
get(key, namespace?) | Devuelve el contenido del objeto. Un objeto por encima del límite inline llega directamente del almacenamiento de objetos o, donde eso no es posible, por rangos de bytes a través del router (consulta Objetos grandes) |
put_file(key, path, …) / get_to_file(key, path, …) | Python y Go (PutFile / GetToFile; Go además hace streaming desde un io.Reader con PutReader y hacia un io.Writer con GetTo). Almacena desde un archivo local, o escribe en él. Usa streaming en la ruta de objetos grandes |
delete(key, namespace?) | Elimina un objeto |
copy(key, to, …) | Copia un objeto, opcionalmente a otro espacio de nombres |
move(key, to, …) | Copia y luego elimina. No es atómico — el servicio no tiene un verbo de movimiento, así que un borrado fallido deja ambas copias |
JavaScript no tiene utilidades para rutas de archivo porque el paquete se distribuye con una única compilación para Node y el navegador — lee y escribe los archivos locales tú mismo con fs.
En Python, el trabajo con archivos de put_file y get_to_file se ejecuta fuera del event loop. get_to_file crea los directorios padre que falten y escribe el archivo in situ en cuanto tiene los primeros bytes del objeto: una lectura rechazada (no existe el objeto, no autorizado) deja la ruta intacta, mientras que una lectura que falla a mitad deja lo escrito hasta entonces. Un FIFO se escribe a medida que llega el objeto, y abrirlo espera a un lector, así que un FIFO que nadie lee bloquea la llamada (no el event loop); asyncio.wait_for puede abandonarla. put_file lee un FIFO hasta el final antes de almacenarlo.
En Go, GetToFile también crea los directorios padre que falten, pero descarga en un archivo temporal junto al destino y lo renombra en su lugar una vez completo: una descarga fallida o cancelada nunca deja un archivo parcial y conserva el anterior. Los enlaces simbólicos se siguen (se escribe el archivo al que apuntan, y los enlaces se mantienen); el reemplazo recibe, en la medida en que el proceso pueda, el propietario, el grupo y los bits de permiso del archivo anterior. Un FIFO o un dispositivo se escribe en su sitio a medida que llega el objeto. Para un destino junto al que no se puede crear un archivo nuevo, como uno en un sistema de archivos de solo lectura o en un pseudosistema de archivos, usa GetTo con un archivo que abras tú mismo.
put acepta: content_type / contentType (el tipo MIME; el espacio de nombres puede restringir cuáles se permiten) y namespace. En Python son argumentos con nombre; en JavaScript van en un objeto de opciones; en Go son opciones (filestore.ContentType(…), filestore.Namespace(…)).
Listar e inspeccionar
| Método | Descripción |
|---|---|
list(…) | Una página del listado del servicio de archivos, con estilo de carpetas por defecto: objects contiene los objetos cuyas claves continúan el prefijo sin una /, prefixes las carpetas que hay debajo. Un delimiter de "" lista en plano — todos los objetos bajo el prefijo, también en subcarpetas. Devuelve objects, prefixes, is_truncated / isTruncated / IsTruncated y un cursor que puedes volver a pasar para la página siguiente |
iter(…) / iterate(…) | Iterador asíncrono sobre todos los objetos bajo un prefijo, también en subcarpetas, paginando automáticamente. Se llama iter en Python, iterate en JavaScript e Iter en Go (un iter.Seq2[*ObjectInfo, error] para un bucle range) |
stat(key, namespace?) | Metadatos de un objeto sin transferir su contenido |
exists(key, namespace?) | Si un objeto existe o no |
namespaces() | Los espacios de nombres que esta app puede usar |
usage(…) | Cuánto almacenamiento está usando la app — véase más abajo |
catalog() | Espacios de nombres más los límites y cuotas emitidos por el servidor. Se obtiene una vez y se almacena en caché: catalog(refresh=True) / catalog(true) / RefreshCatalog(ctx) lo vuelve a obtener |
Carpetas. Un prefijo que termina en / designa una carpeta: iter(prefix="2026/") / iterate({ prefix: "2026/" }) / Iter(ctx, filestore.Prefix("2026/")) devuelve 2026/a.txt y 2026/sub/c.txt, pero no 2026-notes.txt. list no puede abrir una carpeta por su nombre, porque el servicio de archivos rechaza un prefijo de listado que termine en / (FleetFiles v0.2 responde INTERNAL): list con el prefijo 2026 devuelve 2026-notes.txt entre los objetos y la carpeta 2026/ entre los prefijos. El servicio almacena las claves en la forma de normalización Unicode C, así que indica también el prefijo de carpeta en NFC; una iteración que encuentra una clave con otra forma del prefijo falla en lugar de omitir todas las claves.
Los objetos se describen con los mismos campos en los tres SDK, siguiendo el estilo de nomenclatura de cada lenguaje: namespace, key, size, etag, content_type / contentType, last_modified / lastModified, checksum_sha256 / checksumSha256 y url — en Go, los campos Namespace, Key, Size, ETag, ContentType, LastModified, ChecksumSHA256 y URL de filestore.ObjectInfo.
Uso de almacenamiento y cuota
usage responde desde el almacén de objetos en una sola llamada, así que los totales son exactos en lugar de sumados por el SDK:
Python
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, los campos son SizeBytes, ObjectCount, QuotaBytes, FreeBytes y PerNamespace de filestore.StorageUsage.
| Campo | Significado |
|---|---|
size_bytes / sizeBytes | Bytes almacenados actualmente |
object_count / objectCount | Número de objetos almacenados |
quota_bytes / quotaBytes | El presupuesto aplicado. 0 significa ilimitado |
free_bytes / freeBytes | Bytes restantes. -1 significa ilimitado — indicar 0 ahí se leería como “lleno” |
per_namespace / perNamespace | Bytes por espacio de nombres. Presente solo cuando pides el desglose detallado |
El desglose por espacio de nombres está desactivado por defecto porque el almacén no puede responderlo directamente: lleva la contabilidad por área de almacenamiento, y un espacio de nombres es solo un prefijo, así que el SDK tiene que listar cada espacio de nombres para sumar los tamaños. Pídelo cuando lo necesites, no en una ruta crítica.
Aparecen dos cuotas distintas, y conviene no confundirlas. catalog() informa de ambas:
| Campo | Significado |
|---|---|
quota_bytes / quotaBytes | Lo que realmente se aplica, leído del almacén de objetos — el ajuste del usuario del proyecto |
suggested_quota_bytes / suggestedQuotaBytes | Lo que pidió la plantilla de datos de la app. 0 si no pidió nada |
En Go, Catalog(ctx) devuelve un *filestore.Catalog con QuotaBytes y SuggestedQuotaBytes.
Difieren siempre que un usuario ha subido o bajado el presupuesto de la app, y por eso el valor aplicado se lee del almacén y no de la plantilla — volver a desplegar la app no debe restablecer en silencio la elección de un usuario. Una interfaz puede mostrar ambos (“la app sugiere X, tú has establecido Y”). La aplicación efectiva siempre usa el primero. El usuario puede cambiar el presupuesto en cualquier momento, y catalog() lo informa tal como estaba en su última obtención: lee el valor actual con usage().
Compartir objetos
Hay dos tipos de enlace, y la diferencia importa:
| Método | Vigencia | Quién puede leerlo |
|---|---|---|
url(key, …) | Permanente | Solo un solicitante autenticado con READ sobre este backend de datos — se vuelve a comprobar en cada petición. Seguro para almacenar en una columna de tabla |
share_url / shareUrl / ShareURL | Caduca (15 min por defecto, acotado por el servidor) | Cualquiera que tenga el enlace. Nada vuelve a comprobar la autorización cuando se usa |
share_url / shareUrl es una capacidad al portador: entrégalo a una persona que necesite acceso temporal, y no lo almacenes en la base de datos. Usa url para todo aquello que renderice un tablero.
url devuelve None / undefined / "" (Go) cuando el despliegue no tiene un borde HTTP (por ejemplo, un appliance de HTTP simple) — esa es la señal para recurrir a get. Pasar el etag de un objeto como argumento version (filestore.Version(etag) en Go) permite que los navegadores cacheen la respuesta de forma inmutable.
En Python y Go, cloud_url(key, …) / CloudURL(ctx, key, …) devuelve la URL permanente a través del túnel de archivos en la nube de un appliance (https://i<instanceKey>-files.<cloud-edge>): accesible desde internet mientras el appliance esté en línea, con la misma autorización que url. Devuelve None ("" en Go) en despliegues alojados en la nube (donde url ya es accesible desde internet), donde no hay borde HTTP, y mientras el uso compartido en la nube del backend de datos esté desactivado. El usuario puede cambiarlo en cualquier momento, así que cloud_url / CloudURL vuelve a leer el catálogo cuando el que tiene en caché tiene más de 30 segundos.
upload_url / uploadUrl / UploadURL emite una URL con caducidad que acepta una subida directa, y devuelve url, method, headers y expires_in / expiresIn (en Go, un *filestore.UploadTarget con URL, Method, Headers y ExpiresIn). Envía las cabeceras tal como se devuelven, para que el objeto se almacene con el tipo de contenido fijado al emitirla. Los tipos de contenido y el tamaño máximo del espacio de nombres, un size declarado y la cuota se comprueban cuando se emite la URL, no cuando se usa: la URL está firmada solo para el host, así que cualquiera que la tenga puede escribir cualquier contenido en su clave hasta que caduque. Al igual que share_url / shareUrl, es una capacidad al portador — una que escribe.
Objetos grandes
El SDK elige el transporte según el tamaño, de forma automática — no hay nada que configurar:
| Tamaño del objeto | Cómo viaja |
|---|---|
| Hasta el límite inline (actualmente 6 MiB) | Una única llamada a través del router de mensajes |
| Mayor | Directamente al almacenamiento de objetos por HTTPS, sin pasar por el router |
| Mayor, en lecturas donde no hay ruta directa | Por rangos de bytes a través del router de mensajes |
El servidor informa del límite exacto en tiempo de ejecución como inline_max_bytes / inlineMaxBytes / InlineMaxBytes en catalog(), de modo que se puede subir sin publicar una nueva versión del SDK.
La ruta directa necesita un despliegue capaz de prefirmar URLs y un dispositivo que alcance el host del almacenamiento de objetos, no solo el router. Donde falta cualquiera de los dos — el catálogo indica que el despliegue no puede prefirmar (un appliance on-premises, por defecto), la emisión de la URL de lectura responde NOT_SUPPORTED, o el almacenamiento de objetos no es accesible antes de que llegue ningún byte —, get (y get_to_file en Python, GetTo y GetToFile en Go) leen en su lugar el objeto por rangos de bytes a través del router (FleetFiles v0.1.33 y posteriores), cada uno de como máximo el límite inline. Cada rango es una lectura propia: un objeto reemplazado durante una lectura así la hace fallar con INTERNAL (“object changed while it was read”). Las escrituras no tienen esa alternativa.
Quedan dos topes para las escrituras, y ambos reportan TOO_LARGE con un motivo que indica cuál de los dos has alcanzado:
- 5 GiB — el límite de subida en una sola operación del almacén de objetos. La subida multiparte aún no está implementada.
- El límite inline, allí donde no hay endpoint directo — un despliegue que no puede prefirmar (un appliance, por defecto) no puede almacenar en absoluto un objeto más grande. Ningún reintento ni fragmento más pequeño ayudará, y el mensaje lo dice.
Dos fallos de la ruta directa habituales en campo tienen sus propios códigos en lugar de parecer problemas de autorización: PRESIGN_UNREACHABLE (un proxy que solo permite el router — una lectura recurre a los rangos de bytes, una escritura falla) y CLOCK_SKEW (el almacén de objetos rechaza las peticiones desfasadas más de 15 minutos — revisa el NTP del dispositivo).
Errores de almacenamiento de archivos
Un fallo del servicio de archivos o de una transferencia directa genera (Python) / lanza (JavaScript) / devuelve (Go) un FileStoreError que lleva un code estable y un reason legible para humanos. Ramifica según code, nunca según reason. Los errores de archivos locales, una llamada cancelada y los errores WAMP del router distintos de los asignados abajo se lanzan tal cual.
Python
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| Código | Significado |
|---|---|
NOT_AUTHORIZED | El llamador no puede realizar esta operación |
NO_SUCH_NAMESPACE | El espacio de nombres no está declarado en la plantilla de datos |
NO_SUCH_OBJECT | La clave no existe |
TOO_LARGE | Supera el límite de transferencia en una sola llamada |
OBJECT_TOO_LARGE | Supera el maxObjectBytes propio del espacio de nombres |
QUOTA_EXCEEDED | El filestore está lleno |
CONTENT_TYPE_NOT_ALLOWED | El espacio de nombres restringe los contentTypes |
INVALID_KEY | Una clave o un prefijo mal formados (ver abajo; FleetFiles v0.2 todavía lo reporta como INTERNAL) |
INVALID_RANGE | Un rango de bytes fuera del objeto; los SDKs nunca piden uno |
NOT_SUPPORTED | El backend no puede hacer esto |
NOT_AVAILABLE | Este despliegue no tiene servicio de archivos — o todavía no: tras un reinicio de la plataforma, o mientras el servicio de archivos registra sus procedimientos. La llamada no se ejecutó en ningún sitio y puede repetirse |
PRESIGN_UNREACHABLE | El almacenamiento de objetos no es accesible directamente (¿un proxy?) |
CLOCK_SKEW | El reloj del dispositivo está demasiado desfasado |
INTERNAL | Cualquier otra cosa — también un objeto que cambió durante una lectura por rangos |
Un servidor más nuevo puede introducir códigos que esta versión del SDK no conoce. Se propagan tal cual en code en lugar de colapsarse, así que trata un valor no reconocido como un fallo genérico.
Las llamadas de archivos esperan a la conexión (hasta 10 segundos en Python y Go, 6 en JavaScript) y no se reintentan: a diferencia de las operaciones de tabla, no sobreviven a un reinicio de la plataforma, así que durante uno una llamada puede fallar con NOT_AVAILABLE hasta que el servicio de archivos se haya vuelto a registrar. Una llamada así no se ejecutó en ningún sitio, por lo que tu app puede repetirla.
Claves. El servicio de archivos es la autoridad sobre las claves; los SDKs envían las claves y los prefijos tal cual. Una clave o un prefijo mal formados — vacíos; con una / inicial o final (salvo el prefijo de carpeta de iter / iterate); con un segmento vacío, . o .., o un segmento que termina en . o que empieza o termina con un espacio; con uno de \ : * ? " < > | ^ ~, un carácter de control o un carácter de formato Unicode; de más de 900 bytes, 20 segmentos o 255 bytes por segmento; o bajo el prefijo reservado _sys/ — se rechazan con INVALID_KEY en un servicio de archivos que clasifica los errores de clave, y con INTERNAL en FleetFiles v0.2. Cuidado con las marcas de tiempo: 2026-10-09T12:00:00Z no es un segmento de clave válido, por el :.
En Python,
FileStoreErrorse importa desdeironflock.filestore; en JavaScript se exporta desde la raíz del paquete (import { FileStoreError } from "ironflock"); en Go es*filestore.Error(tambiénironflock.FileStoreError), con los códigos como constantes comofilestore.CodeQuotaExceeded.
Comunicación entre dispositivos
registerDeviceFunction / register_device_function
Registra un procedimiento al que pueden llamar otros dispositivos que ejecutan la misma app (en el mismo stage), y también las acciones de los widgets del tablero. El SDK construye por ti la URI completa <SWARM_KEY>.<DEVICE_KEY>.<APP_KEY>.<STAGE>.<topic>, donde STAGE es DEV o PROD. El router no acepta ningún otro registro de una app, y solo registros individuales (consulta URIs que una app puede usar). El registro se restaura tras cada reconexión.
Python
def add(a, b):
return a + b
await ironflock.register_device_function("com.myapp.add", add)
register()es un alias deregister_device_function().
callDeviceFunction / call_device_function
Llama a un procedimiento registrado por otro dispositivo de la misma app. El SDK construye el topic WAMP completo automáticamente usando la clave del dispositivo de destino: <SWARM_KEY>.<device_key>.<APP_KEY>.<STAGE>.<topic>, la misma URI que registerDeviceFunction / register_device_function registra en el dispositivo de destino.
Python
result = await ironflock.call_device_function(
42, # target device key
"com.myapp.add", # procedure name
args=[3, 5] # arguments
)
print(result) # 8call
Llama a un procedimiento remoto usando una URI WAMP completa. Una app no puede registrar un nombre como some.full.wamp.topic, así que un nombre así solo llega a procedimientos que los servicios de la plataforma registran en el realm de datos de la app, como los del backend de datos y los del almacenamiento de archivos. Para llamar a una función que registró otro dispositivo de tu app, usa callDeviceFunction / call_device_function.
Python
result = await ironflock.call("some.full.wamp.topic", args=[42])URIs que una app puede usar
El contenedor de tu app se une a su propio realm de datos, realm-<SWARM_KEY>-<APP_KEY>-<dev|prod>, con el rol de router app. El router comprueba allí cada publicación, suscripción, llamada y registro contra la lista de permitidos de ese rol. Los métodos propios del SDK se mantienen dentro de ella; las reglas importan cuando pasas tus propias URIs a publish, subscribe o call, o usas la connection directamente (Connection() en Go). Una acción rechazada falla con wamp.error.not_authorized (en Go, ironflock.WampURI(err) la devuelve).
| URI en tu propio realm | Permitido | Usado por |
|---|---|---|
Tus propios nombres, p. ej. com.myapp.status | publicar, suscribirse (también por prefijo), llamar — nunca registrar | publish, subscribe, call |
<SWARM_KEY>.<APP_KEY>.<table>, bulk.<SWARM_KEY>.<APP_KEY>.<table> | publicar — nunca suscribirse | publishToTable / publish_to_table, reportError / report_error, publishRowsToTable / publish_rows_to_table |
transformed.<table>, transformed.bulk.<table> | solo suscribirse | subscribeToTable / subscribe_to_table |
databackend.errors.…, files.events… | solo suscribirse | Los eventos de error y de archivos del backend de datos |
<SWARM_KEY>.<DEVICE_KEY>.<APP_KEY>.<STAGE>.<name> | registrar (individual), llamar, publicar — nunca suscribirse | registerDeviceFunction / register_device_function, callDeviceFunction / call_device_function |
append.…, appendBulk.…, history.transformed.…, secret.reveal.…, secret.verify.…, files.read.…, files.write.… | llamar (el SDK los llama) | Métodos de tablas, historial, secretos y archivos |
sys.appaccess.list, sys.appaccess.resolve | llamar | listConsumableApps / list_consumable_apps, connectToApp / connect_to_app, connectToAllApps / connect_to_all_apps |
wamp.session.get | llamar | El heartbeat del SDK de JavaScript |
Nombres con comprobación de identidad. Los nombres que empiezan por un dígito — los topics de escritura de las tablas y las URIs de las funciones de dispositivo — pasan además la comprobación de identidad de la plataforma, que los vincula a la clave de swarm de tu realm. Un registro solo se acepta para una URI de función de tu propio dispositivo con la clave de swarm, la clave de app y el stage de tu realm; un registro por prefijo o comodín, solo cuando fija esos cuatro segmentos. Publicar y llamar solo requieren la clave de swarm de tu realm: una fila publicada en un topic de tabla con otra clave de app se acepta, pero no llega a ningún backend de datos y se pierde. Deja que el SDK construya estas URIs.
Nombres reservados. El router rechaza sys., salvo los dos procedimientos entre apps de arriba, y wamp., salvo wamp.session.get. Los demás nombres de la tabla pertenecen a la plataforma: no empieces tus propios nombres por un dígito, bulk., transformed., databackend.errors., files., append., appendBulk., history. ni secret.. Cualquier otro primer segmento es el espacio de topics propio de tu app.
Nadie se suscribe a los topics en bruto de las tablas. Transportan la fila exactamente como se envió, incluido el texto plano de las columnas secretas, que el backend de datos solo cifra al almacenar la fila. subscribeToTable / subscribe_to_table lee en su lugar las filas almacenadas desde transformed.<table>, tipadas y con las columnas secretas enmascaradas.
Los datos de otras apps son de solo lectura. En el realm de un proveedor, tu app (rol de router app_reader) solo puede suscribirse a transformed.… y files.events… y llamar a history.transformed.… y files.read.…, además del heartbeat wamp.session.get — sin publicar, sin registrar, sin escrituras y sin procedimientos de secretos.
Metadatos del dispositivo
setDeviceLocation / set_device_location
Pide a la plataforma que actualice la ubicación GPS del dispositivo, que los mapas de IronFlock muestran en tiempo real.
No disponible por el momento: todavía ningún servicio de la plataforma responde a esta llamada en el realm de datos de la app, así que falla con
wamp.error.no_such_procedure. Mientras tanto, guarda las ubicaciones en una tabla propia.
Python
await ironflock.set_device_location(long=8.6821, lat=50.1109)| Parámetro | Rango |
|---|---|
long | -180 a 180 |
lat | -90 a 90 |
El historial de ubicación no se almacena. Para rastrear la ubicación a lo largo del tiempo, crea una tabla dedicada y usa
publish_to_table/publishToTable.
getRemoteAccessUrlForPort
Devuelve la URL bajo la que es accesible un puerto declarado en el port-template.yml de la app una vez que su túnel está activo.
Python
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>"| Parámetro | Tipo | Descripción |
|---|---|---|
port | int / number | El puerto, tal como se declara en port-template.yml |
protocol | str / string, opcional | El protocolo del puerto: "http" (predeterminado), "https", "tcp" o "udp", sin distinguir mayúsculas y minúsculas; los espacios alrededor se ignoran, y uno vacío equivale a "http" |
remote_port_environment / remotePortEnvironment | str / string, opcional | Solo tcp/udp: la variable que contiene el puerto remoto, en lugar de REMOTE_PORT_FOR_<port> — el remote_port_environment propio del puerto en la plantilla, que necesitan los agentes de dispositivo anteriores a 0.19.8 |
Los puertos http y https se sirven bajo la etiqueta de túnel <device_key>-<app name, lower case>-<port> (los puertos https con el prefijo secure- de la plataforma) en el dominio de túnel que inyecta el agente del dispositivo (TUNNEL_DOMAIN), siempre como URL https://. En un dispositivo que pertenece a un appliance on-premises (el agente inyecta INSTANCE_KEY), se devuelve en su lugar la ruta reenviada a través de la nube https://i<INSTANCE_KEY>-<label>.<CLOUD_TUNNEL_DOMAIN>.
Una URL tcp o udp lleva el puerto público que asignó el túnel, que el agente del dispositivo anuncia como REMOTE_PORT_FOR_<port> (agente 0.19.8 y posteriores): <protocol>://<TUNNEL_DOMAIN>:<remote port>. En el dispositivo de un appliance se prefiere el puerto expuesto a internet, REMOTE_PORT_FOR_<port>_CLOUD, mientras el agente lo anuncie; antes de que aparezca, y de nuevo una vez que el agente lo ha eliminado (reenvío a través de la nube desactivado), se devuelve la URL local del appliance. Un puerto 0 todavía no se ha asignado.
Los valores se leen en vivo de /data/env, así que vuelve a llamar al método en lugar de guardar el resultado en caché. Hay casos que la URL no puede cubrir:
- El agente del dispositivo solo crea túneles para los puertos de instalaciones de producción (PROD). En un contenedor de desarrollo (DEV), la URL http(s) es la de la instalación PROD de la misma app en este dispositivo, y tcp/udp no dan ninguna; accede a una app DEV a través de la dirección LAN del dispositivo (
DEVICE_LAN_IP,DEVICE_PORT_FOR_<port>). - En un dispositivo edge de un appliance instalado sin dominio (modo HTTP simple), el agente establece
TUNNEL_DOMAINenlocalhost, así que las URLs que se componen allí apuntan a localhost: un appliance así sirve los puertos de las apps en sus puertos del host, algo que una URL de túnel no puede expresar. - Los dominios de túnel no definidos o vacíos usan por defecto
app.ironflock.com. La plataforma no estableceCLOUD_TUNNEL_DOMAINhoy en día: las URLs de un appliance usanapp.ironflock.comsalvo que la propia app establezca esa variable en su configuración de entorno. - En un contenedor que se ejecuta con un usuario distinto de root, los valores son aquellos con los que arrancó el contenedor (consulta Variables de entorno).
El método devuelve None / null (en Go, "" y false) cuando no se puede componer la URL: falta la clave del dispositivo o el nombre de la app, un puerto tcp/udp todavía no tiene puerto remoto anunciado (o es 0), el protocolo es desconocido, o la etiqueta no es una etiqueta DNS válida (más de 63 caracteres, o un nombre de app con un punto). En un navegador no hay entorno: la URL http(s) se compone sobre app.ironflock.com a partir de las opciones deviceKey y appName.
Propiedades de conexión y ciclo de vida
Python
| Propiedad | Tipo | Descripción |
|---|---|---|
is_connected | bool | Indica si la conexión con la plataforma está activa |
connection | CrossbarConnection | La instancia de conexión subyacente (uso avanzado) |
| Método | Descripción |
|---|---|
run() | Inicia la conexión y ejecuta mainFunc (bloqueante) |
await start() | Inicia la conexión de forma asíncrona |
await stop() | Detiene la conexión y las conexiones a apps consumidas, y cancela tareas en ejecución. Las conexiones abandonan sus realms y no se reconectan después; start() vuelve a conectar. Un start() o connect_to_app que siga esperando falla (consulta Fiabilidad de la conexión) |
await run_async() | Inicia y mantiene la conexión de forma asíncrona |
Fiabilidad de la conexión
El SDK se reconecta por sí solo. Cuando la conexión se cae, reintenta hasta que el router vuelve a estar disponible y entonces restaura todas las suscripciones y todas las funciones de dispositivo registradas: no necesitas volver a suscribirte tras una reconexión. Si una no se puede restaurar, las demás sí, y la que falló se reintenta mientras dure la sesión — tras 1 segundo, duplicando hasta 30 segundos, con cada espera variada hasta un 15 % — y de nuevo tras cada reconexión: el router la aceptó antes, así que su rechazo es pasajero, como cuando la comprobación de identidad de una función de dispositivo por parte de la plataforma falla cerrada por un momento. El primer fallo se registra como advertencia, y la restauración se registra en cuanto tiene éxito.
Las operaciones de tabla — escrituras, lecturas de historial, de series y de secretos, y las lecturas de historial de una app consumida — también sobreviven a un reinicio de la plataforma, como una actualización del router o de la plataforma. Esperan hasta reconnect_window (Python, en segundos) / reconnectWindowMs (JavaScript) / WithReconnectWindow (Go, un time.Duration), 60 segundos por defecto, a que vuelva la conexión, y reintentan mientras el backend de datos todavía no sirva sus procedimientos: el router responde wamp.error.no_such_procedure, o el backend de datos rechaza una llamada que se adelantó a su registro con “client has no handler for registration”. Una llamada así nunca llegó a un handler, así que un reintento nunca escribe una fila dos veces; cualquier otro error se lanza tal cual. Las llamadas de archivos no se reintentan (consulta Errores de almacenamiento de archivos). Una petición ya enviada cuando se cierra su sesión (el router la cierra, se cae el enlace o stop()) falla de inmediato en lugar de esperar una respuesta, y no se reintenta: puede que haya llegado a su handler. En Go, una petición así — y cualquier operación que no encuentre conexión dentro de su espera — falla con un error que envuelve wamp.ErrNotConnected (wamp.ErrStopped después de Stop). Las demás operaciones de Go (Publish, Subscribe, Call, las funciones de dispositivo, el descubrimiento entre apps) esperan hasta 10 segundos a una conexión y no se reintentan.
Un caso especial es un router accesible que todavía no tiene un realm para la app. En la carrera de instalación concurrente, el contenedor de la app arranca antes de que el backend de datos haya aprovisionado su realm, por lo que los primeros intentos de unión se rechazan con wamp.error.no_such_realm; el SDK sigue reintentando cada pocos segundos y la app arranca en cuanto existe el realm. Un realm que sigue faltando después de un minuto muy probablemente no aparecerá nunca — la app fue eliminada o no tiene backend de datos para este stage —, así que a partir de entonces el SDK reduce el ritmo a un intento cada dos minutos en lugar de golpear el router cada segundo para siempre. El ritmo rápido vuelve en cuanto aparece el realm, o cuando el propio router se cae y regresa.
stop() pone fin a toda espera de una conexión. Un start() que todavía espera unirse por primera vez a su realm — uno que aún no existe, por ejemplo — falla con Connection to realm <realm> stopped before it opened. Un connect_to_app / connectToApp que todavía está abriendo la conexión a una app proveedora falla con Connection to app '<app>' (<stage>) aborted by stop() o, si la conexión se une a su realm de todos modos, esta se cierra y la llamada falla con Connection to app '<app>' (<stage>) closed by stop(). Una denegación que llega a la vez que stop() conserva su CrossAppAccessError. Cuando stop() termina, ya no queda nada intentando conectarse; un router que no responde cuando la conexión abandona el realm queda desconectado tras 5 segundos.
En Go, Stop(ctx) también termina estas esperas, con errores que envuelven wamp.ErrStopped: un Start que siga esperando lo devuelve, y un ConnectToApp que siga abriendo la conexión falla con ironflock: connection to app '<app>' (<stage>) aborted by Stop (o closed by Stop, cuando la conexión se unió de todos modos). Stop espera la salida del realm dentro de ctx y luego cierra el socket; Run le da 10 segundos.
El caso más difícil es una conexión que muere en silencio: un NAT o un proxy la corta por inactividad sin avisar a ninguno de los dos extremos, o un router se reinicia detrás de un balanceador de carga. No llega nada que señale la pérdida. Una app que solo publica se entera en su siguiente escritura fallida, pero una app que solo se suscribe nunca escribe, así que sin ayuda se queda muerta hasta que el contenedor se reinicia.
Los tres SDK lo detectan. La detección está activa por defecto y no requiere configuración.
Python
El SDK de Python usa keepalive de WebSocket: envía un ping al router cada 30 segundos mientras el enlace está inactivo y espera un pong en 10 segundos. Si no llega, la conexión se descarta y se restablece. El tráfico entrante cuenta como prueba de vida, por lo que una conexión con actividad nunca se sondea: el ping solo sale cuando el enlace realmente se ha quedado en silencio.
Las conexiones entre apps abiertas con
connectToApp/connect_to_appnormalmente solo se suscriben, que es justo el caso que esto protege. Están cubiertas automáticamente, y además se reconectan después de que el router las rechace, hasta que el rechazo persiste (consultaconnectToApp).
Manejo de errores
Todos los métodos del SDK fallan de forma explícita: ante argumentos inválidos, una conexión perdida o un rechazo de la plataforma, lanzan una excepción (Python), rechazan la promesa (JavaScript) o devuelven un error (Go) con un mensaje que indica la operación, el topic y el motivo. Nada se descarta en silencio, así que envuelve en un bloque try las llamadas que quieras que sobrevivan — en Go, comprueba el error devuelto.
Python
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, los fallos que provienen de la plataforma son instancias de WampError — una subclase normal de Error que además lleva la URI de error WAMP en error y la carga útil del error en args / kwargs. Todo lo demás (parámetros inválidos, sin conexión) es un Error normal.
En Go, una operación fallida devuelve un *ironflock.OperationError cuyo mensaje indica la operación y la causa, que envuelve; errors.Is y errors.As ven a través de él. Un rechazo del router o de un destinatario de la llamada es un *ironflock.WampError (*wamp.Error) con URI, Args y Kwargs; ironflock.WampURI(err) devuelve la URI, o "". Otros errores centinela son ironflock.ErrMissingConfig (falta un dato de identidad que la operación necesita), ironflock.ErrAlreadyStarted y wamp.ErrStopped (después de Stop).
Migración: las versiones anteriores de los SDK de Python y JavaScript registraban un mensaje y devolvían
None/nullcuando una llamada fallaba. Ahora lanzan un error en su lugar, por lo que el código con la formaif result is None:ya no detecta los fallos — usatry/except(otry/catch).
Rechazos del backend de datos
Una operación que el backend de datos rechaza falla con una de las URIs de error WAMP de abajo, con el motivo como primer argumento. Los tres SDK las exportan como constantes; Go las nombra al estilo de Go (ironflock.URIRateLimited, ironflock.URIStorageFull, …). En Python, los métodos de IronFlock lanzan un RuntimeError cuya causa es el ApplicationError de autobahn (las lecturas de una app consumida lanzan directamente el ApplicationError), y wamp_error_uri(err) lee la URI de cualquiera de los dos. En JavaScript, el error es un WampError cuyo error es la URI. En Go, es un *ironflock.OperationError que envuelve un *ironflock.WampError, e ironflock.WampURI(err) lee la URI. Los SDKs no reintentan ninguno de ellos. Una fila publicada que el backend de datos rechaza se descarta sin error — solo los métodos de append lo informan.
Python
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| Constante | Rechazado |
|---|---|
URI_RATE_LIMITED | revealSecrets / reveal_secrets por encima de 30, verifySecret / verify_secret por encima de 120 llamadas por minuto por credencial de app en un dispositivo |
URI_RESULT_TOO_LARGE | Una lectura con una sola fila por encima del presupuesto de mensaje de 8 MiB, o un resultado por encima de la protección de 1 GiB (los demás resultados grandes llegan en fragmentos, que el SDK vuelve a ensamblar). Sus argumentos con nombre indican las columnas más grandes: reduce limit, acota timeRange, selecciona menos columns o recorta con columnPaths |
URI_INVALID_LIMIT | Una lectura de un transform con un limit superior a 3000 |
URI_INVALID_TIME_RANGE, URI_INVALID_METRIC, URI_INVALID_GROUP_BY, URI_SERIES_TOO_MANY_GROUPS | Consultas de series que la tabla no puede responder: sin inicio, o un fin que no es posterior a él; una métrica que la tabla no puede agregar (no existe la columna, una columna secreta, un método que el tipo de la columna no admite, tsp con un método distinto de COUNT); una columna de groupBy por la que no puede agrupar; más de 50.000 filas |
URI_SECRET_COLUMN, URI_NOT_A_SECRET_COLUMN | Un filtro sobre una columna secreta; verifySecret / verify_secret sobre una columna que no es secreta, o revealSecrets / reveal_secrets de una tabla sin columnas secretas |
URI_STORAGE_FULL | Un append mientras el disco del appliance está casi lleno (por debajo de unos 3 GiB libres; se vuelve a aceptar a partir de 4 GiB) — temporal: añade las filas más tarde |
URI_STORAGE_OVERUSAGE | Un append que supera la asignación de almacenamiento de la cuenta; reintentar pronto no ayuda |
URI_ENTITY_KEY_CONFLICT | Una fila cuya clave de entidad (maintainLatestFlagFor) y tsp ya tiene otra fila, en el lote o almacenada: da a cada fila su propio tsp |
URI_SECRET_SENTINEL_UNRESOLVABLE, URI_SECRET_CIPHERTEXT_REJECTED | Una escritura que conserva el valor anterior de un secreto (__secret__) sin una clave de entidad con la que encontrarlo; una escritura de un valor ya cifrado |
Un append que el backend de datos rechaza sin motivo — wamp.error.runtime_error, su respuesta a una fila sin tsp — no es un rechazo tipado: el mensaje de error del SDK apunta al tsp que falta y a la tabla error-logs de la app, donde el backend de datos registra la causa.
Uso en navegador (solo JavaScript)
El SDK de JavaScript funciona en navegadores modernos. Como los navegadores no tienen variables de entorno, pasa toda la configuración a través del constructor:
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 }]);Usa IronFlock.fromServer() para obtener la configuración desde tu backend en lugar de codificar las credenciales:
const ironflock = await IronFlock.fromServer("/api/ironflock-config");
await ironflock.start();Tu endpoint del backend debe devolver un objeto JSON con las opciones de conexión (serialNumber, deviceKey, appName, swarmKey, appKey, env).
Registro de funciones para agentes de IA
El SDK puede registrar funciones que son invocables por agentes de IA. Registra un procedimiento y referencia su topic en tu ai-template.yml:
Python
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)El agente de IA puede entonces llamar a esta función cuando un usuario haga una pregunta que requiera datos de sensores en tiempo real.
Para conectar el topic WAMP registrado con un agente de IA, referencíalo en el archivo .ironflock/ai-template.yml de tu app:
sensor_agent:
tool_description: |
Delega a este agente cuando el usuario pregunte sobre lecturas de sensores,
datos en vivo del dispositivo o condiciones ambientales actuales.
system_prompt: |
Eres un especialista en datos de sensores. Usa get_current para obtener
la lectura más reciente de cualquier sensor. Incluye siempre la unidad
en tu respuesta.
main: true
max_context_tokens: 30000
max_iterations: 5
tools:
get_current:
description: Devuelve la lectura más reciente de un sensor.
topic: sensors.get_latest
parameters:
sensor_id:
type: string
description: El identificador del sensor a consultar.
required: trueEl valor de topic (sensors.get_latest) debe coincidir con el nombre indicado en register_device_function / registerDeviceFunction / RegisterDeviceFunction en el código edge. IronFlock enruta automáticamente la llamada al dispositivo donde está registrada la función.
Para la referencia completa de ai-template.yml, consulta Definición de agentes y herramientas.
Variables de entorno
Estas variables son establecidas automáticamente por el runtime de IronFlock dentro de los contenedores de apps:
| Variable | Descripción |
|---|---|
DEVICE_NAME | Nombre visible del dispositivo |
DEVICE_SERIAL_NUMBER | Identificador único e inmutable del dispositivo |
DEVICE_KEY | Clave del dispositivo para autenticación |
SWARM_KEY | Identificador del proyecto |
APP_KEY | Identificador de la app |
APP_NAME | Nombre de la app |
ENV | Entorno: DEV o PROD |
APP_AUTH_ID, APP_AUTH_SECRET | La credencial propia de la app, que el SDK presenta al router. El acceso entre apps la requiere; sin ella (un agente del dispositivo más antiguo), el SDK recurre al número de serie del dispositivo |
Python
import os
device_name = os.environ.get("DEVICE_NAME")
serial = os.environ.get("DEVICE_SERIAL_NUMBER")
project_key = os.environ.get("SWARM_KEY")Las variables de acceso remoto (TUNNEL_DOMAIN, DEVICE_LAN_IP, DEVICE_PORT_FOR_<port>, REMOTE_PORT_FOR_<port>, …) se enumeran en Configuración de puertos.
El agente del dispositivo también refleja estos valores en /data/env, un archivo <NAME>.txt por cada uno, y mantiene ese reflejo actualizado mientras la app se ejecuta; los SDKs lo leen primero, de modo que los valores que cambian en tiempo de ejecución, como un puerto de acceso remoto, llegan a una app en ejecución. La credencial propia de la app se vuelve a leer en cada intento de conexión, así que una credencial que el agente rote mientras la app se ejecuta se presenta en la siguiente reconexión. El agente hace que /data/env solo sea legible por root: en un contenedor que se ejecuta con otro usuario, el SDK ve los valores con los que arrancó el contenedor y registra una advertencia una sola vez.