Skip to Content
IoT 앱 개발IronFlock SDKIronFlock SDK

IronFlock SDK

IronFlock SDK는 엣지 애플리케이션이 IronFlock 플랫폼과 상호작용할 수 있게 합니다. Python, JavaScript, Go의 세 가지 언어로 제공되며 기능은 동일하고, 아래에서 나란히 설명합니다. 등록된 디바이스에서 실행될 때 자동으로 인증을 처리하고 데이터 발행, 이력 조회, 디바이스 간 원격 프로시저 호출, 디바이스 메타데이터 업데이트 기능을 제공합니다.

SDK패키지요구 사항
Pythonironflock on PyPIPython 3.8+
JavaScriptironflock on npmNode.js 18+ 또는 최신 브라우저
Gogithub.com/RecordEvolution/ironflock-goGo 1.27.2+

설치

pip install ironflock

또는 앱의 requirements.txt에 ironflock을 추가하십시오.

빠른 시작

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()

IronFlock 앱 컨테이너 내에서 사용할 때 SDK는 환경에서 자동으로 연결 자격 증명을 읽습니다 — 수동 구성이 필요 없습니다. 테이블에 쓰는 모든 행에는 해당 행의 타임스탬프인 tsp가 포함됩니다(publishToTable 참조).

생성자 옵션

ironflock = IronFlock( mainFunc=main, # 연결 후 실행할 비동기 함수 serial_number="abc123", # 디바이스 시리얼 재정의 (선택 사항) reconnect_window=60, # 테이블 작업이 플랫폼 재시작을 견디는 시간(초) (선택 사항) )
파라미터설명
mainFunc연결이 설정된 후 실행되는 비동기 함수
serial_number발행 및 테이블 쓰기와 함께 전송되는 디바이스 시리얼 번호 재정의. 기본값은 DEVICE_SERIAL_NUMBER 환경 변수. 디바이스 에이전트가 앱별 자격 증명을 주입하는 동안에는 연결이 인증에 사용하는 신원을 바꾸지 않습니다
reconnect_window테이블 작업이 재시작 후 플랫폼이 돌아오기를 기다렸다가 예외를 발생시키기까지의 시간(초). 기본값은 60이며, 0이면 꺼집니다(연결 안정성 참조)

데이터 발행

publishToTable / publish_to_table

플릿 테이블에 데이터 레코드를 발행합니다. 테이블 이름은 앱의 data-template.yml에 정의된 테이블과 일치해야 합니다. SDK는 자동으로 데이터를 올바른 프로젝트 데이터베이스로 라우팅합니다.

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" })

데이터 백엔드는 첫 번째 인수에서 행을 읽습니다: Python에서는 dict, JavaScript에서는 배열의 첫 번째 요소, Go에서는 첫 번째 위치 인수(map 또는 구조체)입니다. 추가 키워드 인수 — Go에서는 인수 중 ironflock.Kwargs 값 — 는 데이터 템플릿이 이를 매핑한 컬럼(kwargs.<name>)에 대해서만 읽힙니다.

모든 행에는 tsp가 필요합니다. 모든 테이블에는 필수 tsp 컬럼, 즉 행의 타임스탬프(예: ISO 8601 문자열, 또는 Go에서는 UTC의 RFC 3339로 전송되는 time.Time)가 있으며, 데이터 템플릿이 다른 곳에 매핑하지 않는 한 데이터 백엔드는 이를 행에서 읽습니다. tsp가 없는 발행된 행은 버려지며, 남는 흔적은 앱의 error-logs 테이블에 기록된 항목뿐입니다 — 확인 응답을 받은 발행은 성공한 것처럼 보입니다. 저장 공간 부족으로 거부된 행은 그 항목조차 남기지 않고 버려집니다. 행이 저장되었는지 알아야 하는 경우 appendToTable / append_to_table을 사용하십시오.

엔티티 테이블의 부분 행. 엔티티 키(maintainLatestFlagFor)가 있는 테이블에서는 행에 엔티티 키, tsp, 그리고 변경되는 컬럼만 담으면 됩니다. 행이 생략한 컬럼은 엔티티의 이전 값을 유지하고, None / null / nil로 보낸 컬럼은 비워집니다. 필드가 생략된 것으로 간주되는지는 행을 만드는 방식에 따라 달라집니다:

  • Python: 값이 None인 키는 null로 전송되어 컬럼을 비웁니다. 이전 값을 유지하려면 dict에서 키를 제거하십시오.
  • JavaScript: 값이 undefined인 속성은 페이로드에서 빠지므로 이전 값이 유지됩니다. 컬럼을 비우려면 null로 설정하십시오.
  • Go: omitempty 또는 omitzero 태그가 붙은 구조체 필드는 비어 있으면 생략되므로 이전 값이 유지됩니다. 그 밖의 경우 nil 포인터 필드는 ironflock.Row의 nil 값과 마찬가지로 null로 전송되어 컬럼을 비웁니다.

네 가지 쓰기 메서드 모두에 동일하게 적용됩니다. 엔티티 키가 없는 테이블에서는 행이 생략한 컬럼이 NULL로 저장됩니다.

appendToTable / append_to_table

pub/sub 대신 원격 프로시저 호출을 사용하여 플릿 테이블에 데이터를 추가합니다. 데이터가 저장되었음을 확인이 필요한 경우 이 방법을 사용하십시오: 거부된 행은 오류가 됩니다.

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

publishToTable과 마찬가지로 행에는 tsp가 필요합니다. 데이터 백엔드는 tsp가 없는 행을 이유 없이 wamp.error.runtime_error로 거부하며, 이때 SDK의 오류에는 “the data backend refused the row without a reason — a missing tsp is the usual cause; see the error-logs table” 라는 문구가 덧붙습니다. 어플라이언스의 디스크가 가득 찬 경우와 같은 유형별 거부는 데이터 백엔드 거부에 나열되어 있습니다.

publishRowsToTable / publish_rows_to_table

플릿 테이블에 단일 메시지로 여러 행(대량 삽입)을 발행합니다. 플랫폼은 전체 배치를 단일 작업으로 원자적으로(전부 아니면 전무) 삽입합니다. 행마다 한 번의 왕복이 너무 비용이 큰 고빈도 데이터에 이 방법을 사용하십시오. publishToTable과 마찬가지로 이는 fire-and-forget 방식입니다 — 확인 응답은 라우터로의 전달을 보장하는 것이지 데이터베이스 삽입을 보장하는 것은 아닙니다.

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}, ])

두 번째 인수는 삽입할 행 객체의 비어 있지 않은 목록이며, 각 행은 자체 tsp를 가집니다 — Go에서는 행의 슬라이스([]ironflock.Row, []map[string]any 또는 구조체 슬라이스)입니다. 데이터 백엔드가 거부한 배치(예: 어떤 행에 tsp가 없는 경우)는 publishToTable과 마찬가지로 발행자에게 아무 알림 없이 버려집니다.

appendRowsToTable / append_rows_to_table

플릿 테이블에 단일 원격 프로시저 호출로 여러 행(대량 삽입)을 추가합니다. 플랫폼은 전체 배치를 원자적으로(전부 아니면 전무) 삽입합니다. 하나의 행이라도 유효하지 않으면 전체 배치가 거부되며 아무것도 저장되지 않습니다. 삽입 결과가 필요한 경우 publishRowsToTable / publish_rows_to_table보다 이 방법을 사용하십시오. 모든 행에는 각자의 tsp가 필요하며, 거부는 appendToTable과 동일하고 이유 없이 거부된 배치에 대한 힌트도 포함됩니다.

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

애플리케이션 오류를 플릿의 error-logs 테이블에 기록합니다. 이는 publishToTable / appendToTable을 감싼 편의 래퍼입니다. 행에 source: "app", 심각도 level, 타임스탬프를 찍은 다음 일반 테이블 행과 동일하게 작성합니다. 이 오류는 fleetdb 시스템 오류가 사용하는 것과 동일한 error-logs 테이블(source: "system"으로 태그됨)에 기록되므로, getHistory로 조회할 수 있고, subscribeToTable / subscribe_to_table로 스트리밍할 수 있으며, 보드 템플릿에서 사용할 수 있고, transformed.error-logs로 실시간 전달됩니다. 이때 플랫폼의 시스템 오류 토스트는 발생시키지 않습니다.

# 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", )

행은 { tsp, msg, user_message, source: "app", level }입니다: msg에는 기술적인 텍스트(트레이스백 또는 스택)가, user_message에는 보드의 오류 토스트가 표시하는 문구가 담깁니다 — 전달한 사용자 메시지, 또는 이를 전달하지 않은 경우 메시지 자체입니다.

파라미터:

파라미터유형설명
errorstr / string 또는 예외 / Error오류 메시지 또는 트레이스백/스택(또는 메시지)이 기록되는 예외
levelstr / string, 선택심각도: "error", "warn", "info" 또는 "debug". 기본값은 "error"
appendbool / boolean, 선택true인 경우 append RPC를 사용합니다(삽입 결과를 반환). 기본값은 false(fire-and-forget 발행)
tspstr / string, 선택ISO-8601 타임스탬프 재정의. 기본값은 현재 시간
user_message / userMessagestr / string, 선택운영자에게 보여 줄 텍스트로, user_message로 저장됩니다. 기본값은 기록되는 메시지

Python에서는 옵션이 키워드 인자입니다(report_error(error, level=..., append=..., tsp=..., user_message=...)). JavaScript에서는 옵션 객체를 통해 전달됩니다(reportError(error, { level, append, tsp, userMessage })). Go에서는 구조체를 통해 전달되며(ReportError(ctx, errOrMsg, ironflock.ReportErrorOptions{Level, Append, Tsp, UserMessage})), 여기서 errOrMsg는 error 또는 문자열이고, Level은 ironflock.LevelError, LevelWarn, LevelInfo, LevelDebug 중 하나이며, 반환되는 *Result는 Append가 설정되지 않으면 nil입니다.

publish

자체 WAMP 토픽에 메시지를 발행합니다. 데이터베이스 테이블에 매핑되지 않는 커스텀 메시징 또는 이벤트에 이 방법을 사용하십시오. 라우터는 일부 이름을 플랫폼용으로 예약합니다 — 앱이 사용할 수 있는 URI를 참조하십시오.

await ironflock.publish("com.myapp.alerts", { "level": "warning", "message": "온도 임계값 초과" })

이력 데이터 조회

getHistory

플릿 테이블 또는 transform에서 이력 데이터를 가져옵니다: 조건에 맞는 최신 행을 tsp 오름차순으로 반환합니다. 필터링, 시간 범위, 페이지네이션을 지원합니다.

# 단순 조회 data = await ironflock.getHistory("sensordata", {"limit": 100}) # 시간 범위 및 필터를 사용한 조회 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} ] }) # 현재 값만 조회: "latest" 마커는 엔티티별 최신 행을 반환합니다 current = await ironflock.getHistory("sensordata", { "limit": 100, "filterAnd": [{"latest": True}] }) # 큰 json 컬럼의 필드 하나만 조회: 컬럼은 지정한 경로만 남도록 잘려서 반환됩니다 readings = await ironflock.getHistory("sensordata", { "limit": 100, "columns": ["json_data"], "columnPaths": ["json_data.sensor.temp"], })

쿼리 파라미터:

필드유형설명
limitint / number반환할 최대 행 수 (1–10,000, 필수. transform의 경우 1–3,000)
offsetint / number페이지네이션 오프셋
timeRangelist / array[start, end] — ISO 날짜/시간 문자열 또는 epoch 밀리초 숫자이며, 섞어 쓸 수 없습니다. 열린 끝에는 None / null. 레거시 {"start": ..., "end": ...} 객체는 이 쌍으로 변환됩니다
filterAndlist / arrayAND로 결합되는 필터 항목: 조건, 그룹 및/또는 latest 마커 (아래 참조)
columnslist / array반환할 컬럼 (선택). tsp, device_key, authid는 항상 포함됩니다. 생략하면 모든 컬럼을 반환합니다
columnPathslist / arrayjson 컬럼을 잘라냅니다 (선택): 필터와 같은 표기법의 JSON 경로("json_data.a.b", "json_data['key']"). columns에 나열된 json 컬럼은 해당 경로들의 합집합만 담은 채 반환되고, 여기에 경로가 없는 컬럼은 전체가 반환됩니다. transform과 fleetdb v1.0.52보다 오래된 데이터 백엔드에서는 무시됩니다

Go에서는 이들이 ironflock.TableQueryParams의 Limit, Offset, TimeRange, FilterAnd, Columns, ColumnPaths 필드입니다. TimeRange는 *ironflock.TimeRange{Start, End}이며, 각 경계는 time.Time, ISO 8601 문자열, epoch 밀리초 숫자, 또는 열린 끝을 뜻하는 nil입니다(ironflock.Between, Since, Until은 time.Time 값으로 이를 만듭니다). FilterAnd는 []ironflock.Filter입니다. 레거시 객체 형식은 없습니다.

필터 연산자: =, !=, <>, >, <, >=, <=, LIKE, ILIKE, NOT LIKE, NOT ILIKE, IN, NOT IN, IS NULL, IS NOT NULL

각 필터 조건은 column, operator, value 키가 있는 객체입니다 — Go에서는 ironflock.Where(column, operator, value), ironflock.IsNull(column), ironflock.IsNotNull(column)으로 만듭니다. 연산자는 대소문자를 구분하지 않고 앞뒤 공백을 무시하여 매칭되며, 위에 나열된 형태 — 데이터 백엔드가 받아들이는 유일한 형태 — 로 전송됩니다. 따라서 "like"는 그룹 안에서도 "LIKE"로 전송됩니다. 목록에 없는 연산자는 경고와 함께 주어진 그대로 전송됩니다. IS NULL과 IS NOT NULL은 value를 받지 않으며 NULL 여부를 묻는 유일한 방법입니다: 그 밖의 모든 연산자는 SQL과 똑같이 해당 컬럼이 NULL인 행을 제외합니다. IN과 NOT IN은 목록(Go에서는 슬라이스)을 받습니다. LIKE는 대소문자를 구분하며, 대소문자 구분 없이 매칭하려면 ILIKE를 사용하십시오.

OR로 조합하기. filterAnd의 항목들은 AND로 결합됩니다. 그룹 항목은 자신의 항목 — 조건, latest 마커 또는 하위 그룹 — 을 하나의 연산자 "AND" 또는 "OR"로 조합합니다:

# 소프트 삭제되지 않은 행: deleted IS NULL OR deleted = false "filterAnd": [ { "combinator": "OR", "filters": [ {"column": "deleted", "operator": "IS NULL"}, {"column": "deleted", "operator": "=", "value": False}, ], } ]

그룹을 지원하기 이전의 데이터 백엔드(v1.2.0 이전의 fleetdb)는 이 형태의 일부만 적용하는 대신 이를 거부합니다. 따라서 필터가 제외해야 할 행을 조용히 반환하는 대신 명확하게 실패합니다.

현재 값 조회. filterAnd의 {"latest": true} 항목(Go에서는 ironflock.Latest())은 필터 조건이 아니라 모드 스위치입니다. 데이터 백엔드는 테이블이 maintainLatestFlagFor로 선언한 엔티티 키를 기준으로 SQL에서 도출한, 엔티티별 최신 행만 반환합니다. 엔티티 키가 없는 테이블은 가장 최근 행 하나를 반환합니다.

다른 조건은 예상대로 이 마커와 조합됩니다. 엔티티 키 컬럼에 대한 조건은 어떤 엔티티가 반환되는지를 좁히고, 그 외의 모든 조건과 timeRange는 그 결과로 얻은 최신 행에 적용됩니다. 따라서 {"latest": true}를 deleted 필터와 조합하면 삭제된 엔티티의 이전 행이 다시 나타나는 대신 해당 엔티티가 숨겨집니다.

이전 버전의 IronFlock은 물리적 latest_flag 컬럼을 저장했습니다. 이 컬럼은 더 이상 존재하지 않습니다 — 레거시 latest_flag = true 필터는 여전히 허용되며 이 마커와 동일하게 처리되지만, 새 코드에서는 {"latest": true}를 사용해야 합니다. latest 마커는 getSeriesHistory에서는 사용할 수 없습니다.

시간 범위. 데이터 백엔드는 문자열 경계값을 JavaScript의 Date로 읽으므로, ISO 8601 날짜 또는 날짜/시간(2026-07-01, 2026-07-01T12:30:15.250Z)으로 작성하고 날짜와 시간은 T로 구분하십시오. SDK는 0–99년에서 날짜와 시간 사이에 공백이 있는 값을 거부합니다. Date가 이를 잘못 읽기 때문입니다: "0001-01-01 00:00:00" — Python의 str(datetime.min)이 반환하는 값 — 은 2001년으로 읽힙니다. 시간대가 없는 시각은 UTC로 읽힙니다. 숫자 경계값은 epoch 밀리초이며 유한해야 합니다: NaN과 ±Infinity는 데이터 백엔드에 열린 경계로 도달하여 시간 필터를 조용히 무효화하므로, SDK는 전송 전에 이를 거부합니다. Go에서 time.Time 경계값은 UTC의 RFC 3339로 전송되며, 문자열 경계값은 Date가 올바르게 읽는 ISO 8601 형식 중 하나여야 합니다 — 날짜, 또는 날짜와 시각에 선택적으로 Z나 UTC 오프셋(+02:00 또는 +0200)이 붙은 형식입니다. 그 밖의 문자열, 예를 들어 기본 형식 20260701T123000Z, 시간만 있는 오프셋, 해당 월에 없는 날짜(2026-02-30)는 아무것도 전송되기 전에 ironflock.ErrInvalidArgument로 실패합니다.

transform 조회. transform(데이터 템플릿의 SQL 뷰, 변환 테이블 참조)은 테이블과 다르게 읽힙니다. 데이터 백엔드는 limit(최대 3000이며, 그보다 크면 URI_INVALID_LIMIT로 거부됩니다), offset, filterAnd만 적용합니다. timeRange, columns, columnPaths, latest 마커는 무시하며, transform이 선언하지 않은 컬럼에 대한 필터는 버리므로(transform에는 암묵적인 tsp, device_key, authid가 없습니다) 결과가 넓어집니다. 행은 뷰 자체의 순서를 뒤집은 순서로 반환됩니다: 뷰가 최신 순으로 정렬할 때에만 최신 행을 오래된 것부터 받게 됩니다. transform의 시간 범위는 해당 SQL에서 제한하십시오.

대용량 결과. 데이터 백엔드의 메시지 하나(JSON 8 MiB)에 담기에는 너무 큰 결과는 청크로 나뉘어 도착하며, SDK가 이를 메모리에서 다시 조립합니다: 한 번의 읽기에 해당하는 행을 같은 순서로 받게 됩니다. 청크가 맞지 않으면 — 하나가 누락되거나 중복되었거나, 행 수가 데이터 백엔드의 요약과 다르면 — 행의 일부를 반환하는 대신 읽기가 실패합니다. 8 MiB를 넘는 단일 행이나 데이터 백엔드의 1 GiB 보호 한도를 넘는 결과는 URI_RESULT_TOO_LARGE로 거부됩니다: limit을 낮추거나, timeRange를 좁히거나, columns를 더 적게 선택하거나, columnPaths로 잘라내십시오.

getSeriesHistory / get_series_history

플릿 테이블에서 다운샘플링된 시계열 데이터를 가져옵니다. 컬럼을 시간 버킷별로 집계합니다(예: 시간별 평균). 긴 기간에 걸친 차트에 이상적입니다. 테이블에서 사용할 수 있으며(transform에서는 불가), 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, # 1시간 버킷. 생략하면 범위를 `limit`개의 버킷으로 나눕니다 "limit": 24, # 버킷 예산, 1–10000 "timeRange": [(now - timedelta(hours=24)).isoformat(), now.isoformat()], # 필수이며 시작값이 있어야 함 "groupBy": ["device_key"], }) for row in series: print(row["tsp"], row["device_key"], row[avg.column]) # avg.column은 "AVG:temperature"

쿼리 파라미터:

필드유형설명
metricslist / array버킷별로 계산되는 집계이며, 최소 하나가 필요합니다: {"ref": ..., "method": ...}. ref는 컬럼이거나 json 컬럼 안의 JSON 경로("json_data.temp")이고, method는 "AVG", "SUM", "COUNT", "MIN", "MAX", "FIRST" 또는 "LAST"입니다. 각 메트릭은 "<METHOD>:<ref>"라는 이름으로 반환되며, 이 이름은 63바이트 이내여야 합니다
bucketMsint / number버킷 너비(정수 밀리초, 최소 1000, 선택). 생략하면 시간 범위를 limit개의 버킷으로 나눕니다
limitint / number버킷 예산(1–10,000) — 행 수 제한이 아닙니다
timeRangelist / array[start, end] — ISO 날짜/시간 문자열 또는 epoch-ms 숫자. 필수이며 시작값이 있어야 합니다. 끝에 None / null을 지정하면 현재 시각을 의미합니다. 끝은 시작보다 뒤여야 합니다
groupBylist / array각 버킷을 그룹별 행으로 나누는 컬럼(선택)
filterAndlist / arraygetHistory와 마찬가지로 AND로 결합되는 조건과 그룹(선택). 여기서는 latest 마커가 그룹 안에서도 거부되며, 현재 값을 조회하려면 getHistory를 사용하십시오

행. 각 행은 하나의 버킷입니다 — groupBy를 사용하면 한 그룹의 버킷입니다. tsp는 epoch 밀리초로 나타낸 버킷의 시작 시각이며, 각 groupBy 컬럼은 자신의 이름으로, 각 메트릭은 "<METHOD>:<ref>"(Python에서는 SeriesMetric.column, JavaScript에서는 seriesColumn(metric), Go에서는 SeriesMetric.Column())라는 이름으로 반환됩니다. 예: row["AVG:temperature"]. 행은 tsp 오름차순이며, 행이 없는 버킷은 나타나지 않습니다(빈 구간 채우기 없음). AVG와 SUM은 숫자 컬럼을 받고, MIN과 MAX는 문자열과 타임스탬프도 받습니다. FIRST와 LAST는 버킷에서 가장 이른 행과 가장 늦은 행의 값을 제공하고, COUNT는 null이 아닌 값의 개수를 셉니다(tsp의 경우 행 수). 대용량 결과는 getHistory와 마찬가지로 청크에서 다시 조립됩니다.

버킷과 열린 끝. limit은 행이 아니라 버킷을 셉니다. bucketMs가 없으면 시간 범위가 limit개의 버킷으로 나뉩니다. bucketMs가 있으면, 범위에 버킷이 limit개보다 많을 때 데이터 백엔드는 버킷을 bucketMs의 배수로 넓힙니다 — 그리고 열린 끝은 데이터 백엔드 자신의 현재 시각, 즉 여러분의 현재 시각보다 조금 뒤까지 닿습니다. 끝이 열린 최근 24시간에는 1시간 버킷이 25개 들어가므로, limit: 24이면 2시간 버킷이 됩니다. 예시처럼 끝을 지정하거나 버킷 하나를 더 허용하십시오: bucketMs와 열린 끝을 사용하면 limit + 1개의 버킷이 반환될 수 있습니다.

SDK는 데이터 백엔드가 거부할 내용을 전송 전에 확인합니다: 최소 하나의 메트릭, 비어 있지 않은 ref, 알려진 메서드, 63바이트 이내의 컬럼 이름, 시작값, 시작보다 뒤인 끝값, 그리고 1000 이상의 정수인 bucketMs(Go에서는 1초 이상이며 밀리초 단위로 나누어떨어지는 Bucket). 데이터 백엔드는 답할 수 없는 쿼리를 URI_INVALID_TIME_RANGE, URI_INVALID_METRIC, URI_INVALID_GROUP_BY 또는 URI_SERIES_TOO_MANY_GROUPS(50,000행 초과)로 거부합니다 — 데이터 백엔드 거부를 참조하십시오.

사용 중단된 형식. 1.9.0까지의 SDK 릴리스는 metrics를 컬럼 이름으로, 최상위 method와 함께 전송했습니다({"metrics": ["temperature"], "method": "AVG", ...}). 데이터 백엔드는 fleetdb v1.0.58부터 이 형식을 거부합니다. 그래도 이 형식은 여전히 작동합니다: SDK가 컬럼마다 {ref, method} 메트릭 하나를 전송하고, 경고를 내며(Python에서는 DeprecationWarning, JavaScript에서는 프로세스당 한 번의 콘솔 경고), 결과 컬럼의 이름을 다시 컬럼 이름으로 바꾸므로 기존 코드는 계속 row["temperature"]를 읽을 수 있습니다 — 단, 행에 이미 같은 이름의 컬럼이 있는 경우(예: tsp에 대한 COUNT의 tsp)는 예외입니다. {ref, method} 메트릭으로 옮기고 row["AVG:temperature"]를 읽으십시오. Go SDK에는 현재 형식, 즉 메트릭마다 메서드를 지정하는 형식(SeriesMetric)만 있습니다.

데이터 구독

subscribeToTable / subscribe_to_table

플릿 테이블의 실시간 업데이트를 구독합니다. 데이터 백엔드가 새 행을 저장할 때마다 핸들러가 호출되며, 저장된 그대로의 행 — 값은 데이터 템플릿의 컬럼 유형으로 변환되고 비밀 컬럼은 가려진 상태 — 이 첫 번째 인수로 전달됩니다. 대량 삽입 경로(publishRowsToTable / appendRowsToTable)를 통해 작성된 행은 한 번에 하나씩 핸들러로 전달되므로, 데이터가 어떻게 작성되었는지와 관계없이 핸들러 코드는 동일하게 유지됩니다.

def on_sensor_data(*args, **kwargs): row = args[0] print("새 판독값:", row["tsp"], row["temperature"]) await ironflock.subscribe_to_table("sensordata", on_sensor_data)

transform에는 자체 행이 없습니다: 스케줄의 매 틱마다 데이터 백엔드는 뷰 전체 — 최대 3000행, 뷰의 순서를 뒤집은 순서 — 를, 첫 번째 인수가 행 목록인 하나의 이벤트로 발행합니다. Python의 event_rows(args[0]), JavaScript의 eventRows(args), Go의 ev.Rows()는 두 형태를 모두 읽습니다: 테이블의 행은 [row]를, transform의 목록은 그 목록(빈 뷰라면 [])을, 그 밖의 것은 None / undefined / nil을 돌려줍니다. Go에서 transform의 이벤트에 대한 ev.Row()는 nil입니다.

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

커스텀 실시간 메시징을 위해 자체 WAMP 토픽을 구독합니다. 테이블 행에는 subscribeToTable / subscribe_to_table을 사용하십시오: 라우터는 원시 테이블 쓰기 토픽에 대한 구독을 거부합니다(앱이 사용할 수 있는 URI 참조).

def on_alert(*args, **kwargs): print("알람 수신:", args, kwargs) await ironflock.subscribe("com.myapp.alerts", on_alert)

비밀 컬럼

앱의 데이터 템플릿에서 secret: true로 선언된 컬럼은 저장 시 암호화됩니다. 어떤 일반 읽기 경로도 평문을 반환하지 않으며, 그 값을 작성한 앱에게조차 반환하지 않습니다:

읽기 경로비밀 컬럼이 어떻게 보이는지
getHistory, subscribeToTable / subscribe_to_table, 그리고 메시지 라우터를 거치는 그 밖의 모든 것플레이스홀더 __secret__. SECRET_PLACEHOLDER(Go에서는 ironflock.SecretPlaceholder)로 export됩니다
SQL 접근 (FleetDB Access의 Postgres 로그인)저장된 암호문, ifsec:1:…
revealSecrets / reveal_secrets복호화된 평문

NULL은 모든 경로에서 NULL 그대로 전달되므로, “설정되었지만 숨겨짐”과 “한 번도 작성되지 않음”은 계속 구분됩니다.

엔티티 테이블에서는 읽기-수정-쓰기가 안전합니다. __secret__ 플레이스홀더를 그대로 담고 있는 행을 다시 발행하거나 비밀 컬럼을 아예 생략하면 저장된 비밀 값을 덮어쓰는 대신 유지하고, 명시적인 null은 이를 비웁니다. 정확한 규칙은 데이터 백엔드 가이드의 비밀 컬럼을 참조하십시오.

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__"}]

아래 두 함수는 오직 앱 자신의 컨테이너에서만 호출할 수 있습니다. 메시지 라우터는 모든 브라우저 역할과 소비 대상 앱에 대해 이들을 거부하며, 그렇기 때문에 ConsumedApp 핸들에는 이에 해당하는 기능이 없습니다. 다른 앱의 비밀 컬럼은 가려진 채로 도착하고 계속 그 상태로 남습니다.

revealSecrets / reveal_secrets

자신의 테이블 중 하나의 행을 비밀 컬럼이 복호화된 상태로 읽습니다.

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

쿼리 파라미터는 getHistory와 동일하되 한 가지가 다릅니다. limit은 100을 초과할 수 없습니다. 이 상한은 한 번의 호출로 테이블 전체가 복호화되는 것을 막기 위한 것이며, 데이터 백엔드는 더 큰 값을 조용히 잘라내지 않고 거부합니다.

데이터 백엔드는 디바이스의 앱 자격 증명당 분당 최대 30회의 reveal에 응답하며, 이를 넘으면 호출이 URI_RATE_LIMITED로 실패합니다. 비밀 값은 사용할 때마다 복호화하지 말고 한 번 복호화해 보관하십시오. 비밀 컬럼이 없는 테이블은 URI_NOT_A_SECRET_COLUMN으로, 비밀 컬럼에 대한 필터는 URI_SECRET_COLUMN으로 거부됩니다.

verifySecret / verify_secret

비밀 컬럼을 드러내지 않은 채 후보 값을 대조합니다 — 평문 조회 함수에 대응하는 비밀번호 확인용 함수입니다. 비교는 데이터 백엔드 내부에서 상수 시간으로 수행되며, 결과만 돌아옵니다.

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

파라미터:

파라미터유형설명
tablenamestr / string대조할 테이블
columnstr / string비교할 비밀 컬럼
candidatestr / string검사 대상 평문. 서버 측에서 비교되며 테이블에 절대 기록되지 않습니다
query_params / queryParamsSecretQueryParams어떤 행이 대조에 참여할지. 기본값은 가장 최근 행 하나이며, limit은 100을 초과할 수 없습니다

Go에서는 파라미터가 컨텍스트 뒤에 오는 위치 문자열 인수이며, 쿼리는 *ironflock.TableQueryParams입니다(가장 최근 행이면 nil).

JavaScript는 { match, checked }를, Go는 Match와 Checked를 가진 *ironflock.SecretVerifyResult를 반환하고, Python은 단순한 bool을 반환합니다. checked를 쓸 수 있는 곳에서는 활용할 가치가 있습니다. 잘못된 후보 값(checked > 0)과 선택 조건에 아무 행도 걸리지 않은 경우(checked === 0)를 구분해 주며, 이 둘은 사용자에게 알려야 할 내용이 전혀 다른 실패이기 때문입니다.

모든 행이 각자의 무작위 값으로 암호화되므로, 동일한 비밀 값을 담은 두 행도 서로 다른 암호문으로 저장됩니다. 비밀 컬럼에 대한 = 필터가 동작할 수 없고 — 데이터 백엔드는 비밀 컬럼에 대한 모든 필터를 URI_SECRET_COLUMN으로 거부합니다 — 대신 이 함수가 존재하는 이유가 바로 그것입니다.

데이터 백엔드는 디바이스의 앱 자격 증명당 분당 최대 120회의 확인에 응답하며, 이를 넘으면 호출이 URI_RATE_LIMITED로 실패합니다. 매 요청마다 확인하는 경우(예: API 키)에는 긍정적인 결과를 잠시 캐시하십시오. 비밀 컬럼이 아닌 컬럼은 URI_NOT_A_SECRET_COLUMN으로 거부됩니다.

앱 간 데이터 접근

같은 프로젝트 내에서 다른 앱의 플릿 데이터를 자신의 앱에서 읽습니다. 당신의 앱은 자신의 data-template.yml의 consumes: 섹션에 제공자 앱을 선언해야 하며(또는 아래의 와일드카드 권한을 가지고 있어야 하며), 프로젝트 사용자가 접근 권한을 부여해야 합니다. 접근은 읽기 전용입니다. 제공자가 공유하는 테이블과 transform의 이력을 조회하고 행을 실시간으로 구독할 수 있지만, 여기에 쓸 수는 없습니다. 소비 대상 앱에 대한 연결은 앱별로 캐시되며 인스턴스가 중지되면 자동으로 닫힙니다.

연결에는 디바이스 에이전트가 주입하는 앱별 자격 증명(APP_AUTH_ID, APP_AUTH_SECRET)이 필요합니다. 플랫폼은 다른 앱의 realm에서 레거시 디바이스 자격 증명 — 앱별 자격 증명을 받지 못한 앱(예: 이전 디바이스 에이전트에서 실행되는 앱)이 대신 사용하는 시리얼 번호 — 을 거부합니다(NOT_AUTHORIZED, 그 사실을 알리는 메시지와 함께). 단, 제공자 앱이 같은 디바이스에서 실행되는 경우는 예외이며, 이때는 읽기 전용이 아니라 그 앱 자신의 전체 권한으로 해당 realm에 참여합니다.

앱이 와일드카드 권한(consumes: [{ app: "*" }])을 가지고 있으면, listConsumableApps / list_consumable_apps와 connectToAllApps / connect_to_all_apps(아래)로 제공자를 동적으로 발견하고 열 수 있습니다.

connectToApp / connect_to_app

다른 앱의 데이터 백엔드에 대한 읽기 전용 연결을 열고 핸들을 반환합니다. 핸들은 getHistory / get_history, subscribeToTable / subscribe_to_table, getSeriesHistory / get_series_history(테이블 전용)를 제공합니다 — 자신의 테이블에서 사용하는 것과 동일한 조회 및 구독이며, 파라미터와 결과도 동일합니다 — 여기에 더해 close와 공유된 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)

파라미터:

파라미터유형설명
app_name / appNamestr / stringconsumes: 섹션에 선언된 제공자 앱 이름
stagestr / string, 선택제공자 스테이지: "dev" 또는 "prod". 기본값은 자신의 앱 스테이지
on_error / onErrorcallable, 선택열린 연결이 최종적으로 거부될 경우(예: 권한이 나중에 취소된 경우 — 거부가 언제 최종으로 간주되는지는 아래 참조) CrossAppAccessError와 함께 호출됩니다

접근이 거부되거나 잘못 사용되면 code 필드(Go에서는 ironflock.CodeNoGrant 같은 상수를 가진 Code)를 가진 CrossAppAccessError가 발생(Python)/throw(JavaScript)/반환(Go, errors.As로 검사하는 *ironflock.CrossAppAccessError)됩니다: NO_GRANT, PROVIDER_NOT_INSTALLED, UNKNOWN_APP, PRIVATE_TABLE, SECRET_COLUMN(제공자가 비밀로 표시한 컬럼을 — 그룹 안에서도 — 필터링하거나 선택하는 쿼리) 또는 NOT_AUTHORIZED(권한이 취소되었거나 그 시점에 확인할 수 없었던 경우 — 또는 앱이 레거시 디바이스 자격 증명을 제시한 경우로, 이때는 메시지가 그 사실을 알려줍니다).

connectToApp / connect_to_app이 대기하는 동안 거부되면 호출은 즉시 실패합니다. 핸들이 열린 뒤에는 거부가 지속될 때에만 최종으로 간주됩니다 — 연결이 마지막으로 유지된 이후 3회 이상의 거부가 있고(라우터가 인증 사유로 세션을 닫는 것도 1회로, 거부된 재연결도 각각 1회로 셉니다), 그중 첫 번째가 최소 60초 전인 경우입니다. 플랫폼이 잠시 접근 권한을 확인할 수 없는 동안에도 같은 방식으로 거부하기 때문입니다. 그때까지 핸들은 계속 재연결을 시도하며, 재연결에 성공하면 복구됩니다. 거부가 최종이 되면 on_error / onError / OnError가 호출되고 핸들은 캐시에서 제거됩니다: 다음 connectToApp / connect_to_app / ConnectToApp은 새 연결을 엽니다. 그 이후로 이 핸들의 읽기와 구독은 on_error / onError가 받은 오류로 — Go에서는 wamp.ErrStopped를 감싼 오류로 — 즉시 실패합니다.

Python에서는 stage와 on_error가 키워드 인자입니다. JavaScript에서는 옵션 객체를 통해 전달됩니다(connectToApp(appName, { stage, onError })). Go에서는 구조체를 통해 전달되며(ConnectToApp(ctx, appName, ironflock.ConnectToAppOptions{Stage, OnError})), 여기서 OnError는 func(*ironflock.CrossAppAccessError)입니다. Go에서 ctx는 호출자의 대기만 제한합니다: 같은 앱에 대한 동시 호출은 하나의 시도를 공유하며, Close(ctx)는 핸들을 닫습니다.

listConsumableApps / list_consumable_apps

프로젝트 내의 모든 비공개가 아닌 제공자를 나열합니다 — 와일드카드 소비 권한(data-template.yml의 consumes: [{ app: "*" }], 프로젝트 사용자가 부여)을 가진 앱을 위한 발견 기본 요소입니다. 단일 호출을 수행하며 연결은 전혀 열지 않습니다: 반환된 카탈로그를 선택기에 렌더링한 다음, 원하는 제공자에 대해 connectToApp / connect_to_app을 호출하거나 — 한 번에 모두 열려면 connectToAllApps / connect_to_all_apps를 호출하십시오.

참고: 앱의 data-template.yml에 권한을 선언하고, *를 따옴표로 감싸십시오 — 따옴표 없는 *는 YAML 별칭이므로 파싱되지 않습니다:

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"]

각 항목은 하나의 제공자를 설명합니다:

필드유형설명
appstr / string제공자 앱 이름
provider_app_keyint / number제공자의 앱 키
stagesdict / object스테이지별 카탈로그 { dev?, prod? }; 스테이지는 제공자가 해당 스테이지에 대한 데이터 백엔드를 가지고 있는 경우에만 존재합니다. 각 카탈로그는 제공자가 공유하는 비공개가 아닌 tables와 transforms를 담고 있습니다

Go에서 각 항목은 App, ProviderAppKey, Stages.Dev / Stages.Prod(Tables와 Transforms를 가진 *StageCatalog이며, 없는 스테이지는 nil)를 가진 ironflock.ConsumedAppInfo입니다. Catalog(stage)는 이름으로 스테이지 카탈로그 하나를 반환합니다.

앱이 와일드카드 권한을 가지고 있지 않으면 code: NO_GRANT를 가진 CrossAppAccessError가 발생(Python)/throw(JavaScript)/반환(Go)됩니다.

connectToAllApps / connect_to_all_apps

프로젝트 내의 모든 비공개가 아닌 제공자에 대한 읽기 전용 핸들을 한 번의 호출로 엽니다(와일드카드 소비자 전용). listConsumableApps / list_consumable_apps를 통해 제공자를 열거하고 각각을 열며, 요청된 스테이지에 대한 데이터 백엔드가 없는 제공자는 건너뜁니다. 각 핸들은 connectToApp / connect_to_app과 동일한 키로 캐시되므로, 이후 connectToApp(name) 호출은 이미 준비된 핸들을 반환합니다. 반환된 핸들은 인스턴스가 중지될 때 함께 닫힙니다.

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)

파라미터:

파라미터유형설명
stagestr / string, 선택제공자 스테이지: "dev" 또는 "prod". 기본값은 자신의 앱 스테이지
continue_on_error / continueOnErrorbool / boolean, 선택true(기본값)인 경우, 여는 데 실패한 제공자는 on_error / onError에 보고되고 결과에서 제외됩니다. false인 경우, 첫 번째 실패가 발생/throw됩니다
on_error / onErrorcallable, 선택열 수 없었던 각 제공자와 함께 호출되며(continue_on_error / continueOnError가 true인 동안), 이미 열린 연결이 나중에 최종적으로 거부될 경우(예: 권한이 취소된 경우) CrossAppAccessError와 함께 호출됩니다

성공적으로 열린 제공자 핸들을 반환합니다(connectToApp / connect_to_app과 동일한 핸들 유형). 앱이 와일드카드 권한을 가지고 있지 않으면 code: NO_GRANT를 가진 CrossAppAccessError가 발생(Python)/throw(JavaScript)/반환(Go)됩니다.

Python에서는 stage, on_error, continue_on_error가 키워드 인자입니다. JavaScript에서는 옵션 객체를 통해 전달됩니다(connectToAllApps({ stage, onError, continueOnError })). Go에서는 구조체를 통해 전달되며(ConnectToAllApps(ctx, ironflock.ConnectToAllAppsOptions{Stage, OnError, StopOnError})), 여기서 OnError는 func(error)이고 StopOnError: true는 continueOnError: false에 해당합니다.

관리형 파일 저장소

모든 앱 데이터 백엔드는 테이블과 함께 비공개 객체 저장소를 제공받으며, files 속성(Go에서는 Start 전에도 가져올 수 있는 *filestore.FileStore인 ifl.Files())을 통해 여기에 접근합니다. 이미지, PDF, 카메라 프레임, 펌웨어 바이너리 — 테이블 행에 담을 만한 것이 아닌 모든 데이터에 사용하십시오. 별도의 설정은 필요 없습니다: 데이터 템플릿에 files: 섹션이 없는 앱도 default라는 이름의 네임스페이스를 하나 갖습니다(자체 네임스페이스를 선언하는 템플릿은 이를 대체합니다 — 네임스페이스 참조).

이 섹션은 클라이언트 측 참조입니다. 플랫폼 측 — 네임스페이스, 할당량 및 보존 기간 선언, 앱 간 파일 공유, 앱별 S3 자격 증명, 온프레미스 동작 — 은 파일 저장소를 참조하십시오.

핵심은 객체를 저장하면 그 즉시 영구 URL을 돌려받는다는 점입니다. 이 URL은 테이블 컬럼에 바로 기록할 수 있으므로, 대시보드 위젯이 추가 작업 없이 이를 렌더링할 수 있습니다:

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)

이 URL은 만료되지 않지만 공개 링크는 아닙니다: 이 데이터 백엔드에 대한 READ 접근 권한을 가진 인증된 요청자에게만 읽을 수 있는 상태로 유지되며, 인증 프록시가 매 요청마다 이를 다시 확인합니다. 따라서 데이터베이스에 저장해도 안전합니다.

네임스페이스

네임스페이스는 정책 — 보존 기간, 공유 규칙, 허용되는 콘텐츠 유형 — 을 담고 있는 키 접두사입니다. 별도의 버킷이 아닙니다. 앱의 모든 네임스페이스는 해당 앱의 단일 저장 영역 안에 존재합니다. 어떤 객체 집합에 다른 규칙이 필요할 때만 네임스페이스를 선언하고, 그렇지 않다면 default에 머무르면서 2026/03/part-1.jpg와 같은 키 경로로 객체를 정리하십시오.

네임스페이스는 data-template.yml에 선언합니다. 하나라도 선언하면 암묵적인 default가 대체됩니다: default라는 이름의 네임스페이스도 함께 나열하십시오. 그렇지 않으면 네임스페이스를 지정하지 않은 모든 호출이 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 }

이 예산은 네임스페이스별이 아니라 앱 전체에 대해 한 번만 선언된다는 점에 유의하십시오. 네임스페이스는 앱의 단일 저장 영역 안에 있는 키 접두사일 뿐이므로, 접두사별 예산을 강제할 대상 자체가 존재하지 않습니다. 반면 maxObjectBytes는 네임스페이스별로 적용됩니다 — 총량이 아니라 개별 객체 하나의 크기를 제한합니다: 설정하지 않으면 100 MiB, 최대 5 GiB입니다(이를 넘으면 OBJECT_TOO_LARGE).

아래의 모든 메서드는 네임스페이스를 선택적 인자로 받으며, 기본값은 default입니다. 빈 네임스페이스("")도 default를 의미합니다.

객체 저장 및 읽기

# 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")
메서드설명
put(key, data, …)객체를 저장합니다(Python에서는 bytes, JavaScript에서는 Uint8Array, Go에서는 []byte). url을 포함한 객체의 메타데이터를 반환합니다
get(key, namespace?)객체의 내용을 반환합니다. 인라인 제한을 넘는 객체는 객체 저장소에서 직접 가져오며, 그것이 불가능한 경우에는 라우터를 통해 범위 단위로 가져옵니다(대용량 객체 참조)
put_file(key, path, …) / get_to_file(key, path, …)Python 및 Go(PutFile / GetToFile. Go는 PutReader로 io.Reader에서, GetTo로 io.Writer로 스트리밍할 수도 있습니다). 로컬 파일에서 저장하거나 로컬 파일로 씁니다. 대용량 객체 경로에서는 스트리밍합니다
delete(key, namespace?)객체를 삭제합니다
copy(key, to, …)객체를 복사하며, 선택적으로 다른 네임스페이스로 복사할 수 있습니다
move(key, to, …)복사 후 삭제입니다. 원자적이지 않습니다 — 서비스에 move 동사가 없으므로, 삭제가 실패하면 사본이 양쪽에 모두 남습니다

JavaScript에는 파일 경로 헬퍼가 없습니다. 패키지가 Node와 브라우저 양쪽을 위해 단일 빌드를 배포하기 때문입니다 — 로컬 파일은 fs로 직접 읽고 쓰십시오.

Python에서 put_file과 get_to_file의 파일 작업은 이벤트 루프 밖에서 실행됩니다. get_to_file은 없는 상위 디렉터리를 만들고, 객체의 첫 바이트를 받은 시점에 파일을 제자리에 씁니다: 거부된 읽기(객체 없음, 권한 없음)는 경로를 건드리지 않지만, 도중에 실패한 읽기는 그때까지 쓴 내용을 남깁니다. FIFO는 객체가 스트리밍되는 대로 쓰이며, FIFO를 여는 동작은 읽는 쪽을 기다리므로 아무도 읽지 않는 FIFO는 호출을 블로킹합니다(이벤트 루프는 블로킹하지 않습니다). asyncio.wait_for로 이를 포기할 수 있습니다. put_file은 FIFO를 끝까지 읽은 뒤에 저장합니다.

Go에서도 GetToFile은 없는 상위 디렉터리를 만들지만, 대상 옆의 임시 파일로 다운로드한 뒤 완료되면 제자리로 이름을 바꿉니다: 실패하거나 취소된 다운로드는 부분 파일을 남기지 않고 이전 파일을 유지합니다. 심볼릭 링크는 따라가며(링크가 가리키는 파일이 쓰이고 링크는 그대로 남습니다), 교체된 파일은 프로세스가 허용하는 한 기존 파일의 소유자, 그룹, 권한 비트를 이어받습니다. FIFO나 디바이스는 객체가 스트리밍되는 대로 제자리에 쓰입니다. 읽기 전용 파일 시스템이나 의사 파일 시스템처럼 옆에 새 파일을 만들 수 없는 대상에는 직접 연 파일과 함께 GetTo를 사용하십시오.

put이 받는 인자: content_type / contentType(MIME 유형. 네임스페이스가 어떤 유형을 허용할지 제한할 수 있습니다)과 namespace. Python에서는 키워드 인자이며, JavaScript에서는 옵션 객체에 담아 전달하며, Go에서는 옵션입니다(filestore.ContentType(…), filestore.Namespace(…)).

목록 조회 및 검사

메서드설명
list(…)파일 서비스 목록의 한 페이지이며, 기본적으로 폴더 방식입니다: objects에는 키가 접두사 뒤에 / 없이 이어지는 객체가, prefixes에는 그 아래의 폴더가 담깁니다. delimiter를 ""로 지정하면 평면적으로 나열합니다 — 하위 폴더를 포함해 접두사 아래의 모든 객체를 나열합니다. objects, prefixes, is_truncated / isTruncated / IsTruncated, 그리고 다음 페이지를 위해 다시 전달할 cursor를 반환합니다
iter(…) / iterate(…)하위 폴더를 포함해 접두사 아래의 모든 객체를 순회하는 비동기 이터레이터이며, 페이지네이션은 자동으로 처리됩니다. Python에서는 iter, JavaScript에서는 iterate, Go에서는 Iter(range 루프용 iter.Seq2[*ObjectInfo, error])라는 이름입니다
stat(key, namespace?)내용을 전송하지 않고 객체 하나의 메타데이터를 조회합니다
exists(key, namespace?)객체의 존재 여부
namespaces()이 앱이 사용할 수 있는 네임스페이스
usage(…)앱이 사용 중인 저장 용량 — 아래 참조
catalog()네임스페이스와 서버가 발급한 제한값 및 할당량. 한 번 가져온 뒤 캐시됩니다: catalog(refresh=True) / catalog(true) / RefreshCatalog(ctx)로 다시 가져옵니다

폴더. /로 끝나는 접두사는 폴더를 나타냅니다: iter(prefix="2026/") / iterate({ prefix: "2026/" }) / Iter(ctx, filestore.Prefix("2026/"))는 2026/a.txt와 2026/sub/c.txt를 내놓지만 2026-notes.txt는 내놓지 않습니다. 파일 서비스가 /로 끝나는 목록 접두사를 거부하기 때문에(FleetFiles v0.2는 INTERNAL로 응답) list는 폴더를 이름으로 열 수 없습니다: 접두사 2026으로 list를 호출하면 객체 중에 2026-notes.txt가, 접두사 중에 폴더 2026/이 반환됩니다. 서비스는 키를 유니코드 정규화 형식 C로 저장하므로 폴더 접두사도 NFC로 지정하십시오. 접두사의 다른 정규화 형식 아래에 있는 키를 만난 순회는 모든 키를 건너뛰는 대신 실패합니다.

객체는 세 SDK 모두에서 동일한 필드로 기술되며, 각 언어의 명명 방식을 따릅니다: namespace, key, size, etag, content_type / contentType, last_modified / lastModified, checksum_sha256 / checksumSha256, url — Go에서는 filestore.ObjectInfo의 필드 Namespace, Key, Size, ETag, ContentType, LastModified, ChecksumSHA256, URL입니다.

저장소 사용량 및 할당량

usage는 객체 저장소에서 단 한 번의 호출로 답을 받아오므로, 합계는 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}

Go에서 필드는 filestore.StorageUsage의 SizeBytes, ObjectCount, QuotaBytes, FreeBytes, PerNamespace입니다.

필드의미
size_bytes / sizeBytes현재 저장되어 있는 바이트 수
object_count / objectCount저장된 객체의 개수
quota_bytes / quotaBytes강제되는 예산. 0은 무제한을 의미합니다
free_bytes / freeBytes남은 바이트 수. -1은 무제한을 의미합니다 — 여기에 0을 보고하면 “가득 참”으로 읽히기 때문입니다
per_namespace / perNamespace네임스페이스별 바이트 수. 상세 분석을 요청한 경우에만 존재합니다

네임스페이스별 분석은 기본적으로 꺼져 있습니다. 저장소가 이를 직접 답해줄 수 없기 때문입니다: 저장소는 저장 영역 단위로 집계하고 네임스페이스는 접두사일 뿐이므로, SDK가 각 네임스페이스를 나열해 크기를 더해야 합니다. 필요할 때만 요청하고, 자주 실행되는 경로에서는 사용하지 마십시오.

서로 다른 두 가지 할당량이 등장하며, 이 둘을 구분해 두는 것이 좋습니다. catalog()는 두 값을 모두 보고합니다:

필드의미
quota_bytes / quotaBytes객체 저장소에서 읽어온, 실제로 강제되는 값 — 프로젝트 사용자의 설정
suggested_quota_bytes / suggestedQuotaBytes앱의 데이터 템플릿이 요청한 값. 아무것도 요청하지 않았다면 0

Go에서 Catalog(ctx)는 QuotaBytes와 SuggestedQuotaBytes를 가진 *filestore.Catalog를 반환합니다.

이 두 값은 사용자가 앱의 예산을 올리거나 내린 경우 언제나 달라집니다. 강제되는 값을 템플릿이 아니라 저장소에서 읽어오는 이유가 바로 이것입니다 — 앱을 재배포한다고 해서 사용자의 선택이 조용히 초기화되어서는 안 되기 때문입니다. UI는 두 값을 모두 보여줄 수 있습니다(“앱은 X를 제안하며, 사용자는 Y로 설정했습니다”). 강제 적용에는 언제나 첫 번째 값이 사용됩니다. 사용자는 언제든 예산을 변경할 수 있으며, catalog()는 마지막으로 가져온 시점의 값을 보고합니다: 현재 값은 usage()에서 읽으십시오.

객체 공유

링크에는 두 종류가 있으며, 그 차이가 중요합니다:

메서드수명읽을 수 있는 대상
url(key, …)영구이 데이터 백엔드에 대한 READ 권한을 가진 인증된 요청자만 — 매 요청마다 다시 확인됩니다. 테이블 컬럼에 저장해도 안전합니다
share_url / shareUrl / ShareURL만료됨(기본 15분, 서버가 상한을 강제)링크를 가진 사람은 누구나. 링크가 사용될 때 권한을 다시 확인하는 것은 아무것도 없습니다

share_url / shareUrl은 소지자 권한(bearer capability)입니다: 임시 접근이 필요한 사람에게 건네주되, 데이터베이스에는 저장하지 마십시오. 대시보드가 렌더링하는 대상에는 url을 사용하십시오.

배포 환경에 HTTP 엣지가 없는 경우(예: 순수 HTTP 어플라이언스) url은 None / undefined / ""(Go)를 반환합니다 — 이는 get으로 대체하라는 신호입니다. 객체의 etag를 version 인자(Go에서는 filestore.Version(etag))로 전달하면 브라우저가 응답을 불변으로 캐시할 수 있습니다.

Python과 Go에서 cloud_url(key, …) / CloudURL(ctx, key, …)은 어플라이언스의 클라우드 파일 터널을 거치는 영구 URL(https://i<instanceKey>-files.<cloud-edge>)을 반환합니다: 어플라이언스가 온라인인 동안 인터넷에서 접근할 수 있으며, 권한 확인은 url과 동일합니다. 클라우드 호스팅 배포(url이 이미 인터넷에서 접근 가능한 경우), HTTP 엣지가 없는 경우, 그리고 데이터 백엔드의 클라우드 공유가 꺼져 있는 동안에는 None(Go에서는 "")을 반환합니다. 사용자가 클라우드 공유를 언제든 전환할 수 있으므로, cloud_url / CloudURL은 캐시된 카탈로그가 30초보다 오래되면 카탈로그를 다시 읽습니다.

upload_url / uploadUrl / UploadURL은 직접 업로드를 받아주는 만료형 URL을 발급하며, url, method, headers, expires_in / expiresIn을 반환합니다(Go에서는 URL, Method, Headers, ExpiresIn을 가진 *filestore.UploadTarget). 객체가 발급 시의 콘텐츠 유형으로 저장되도록 헤더는 주어진 그대로 보내십시오. 네임스페이스의 콘텐츠 유형과 크기 상한, 선언된 size, 할당량은 URL을 사용할 때가 아니라 발급할 때 확인됩니다: URL은 호스트에 대해서만 서명되므로, 이를 가진 사람은 누구나 만료될 때까지 해당 키에 어떤 내용이든 쓸 수 있습니다. share_url / shareUrl과 마찬가지로 이것은 소지자 권한(bearer capability)이며 — 쓰기 권한입니다.

대용량 객체

SDK는 크기에 따라 전송 방식을 자동으로 선택합니다 — 설정할 것은 없습니다:

객체 크기전송 방식
인라인 제한까지(현재 6 MiB)메시지 라우터를 통한 단일 호출
그보다 큰 경우라우터를 우회하여 HTTPS로 객체 저장소에 직접 전송
그보다 크고, 직접 경로가 없는 환경에서 읽는 경우메시지 라우터를 통해 범위 단위로 전송

정확한 제한값은 런타임에 서버가 catalog()의 inline_max_bytes / inlineMaxBytes / InlineMaxBytes로 알려주므로, SDK 릴리스 없이도 상향할 수 있습니다.

직접 경로에는 URL을 사전 서명할 수 있는 배포 환경과, 라우터뿐 아니라 객체 저장소 호스트에도 도달할 수 있는 디바이스가 필요합니다. 둘 중 하나라도 없으면 — 카탈로그상 배포 환경이 사전 서명을 할 수 없거나(온프레미스 어플라이언스는 기본적으로 그렇습니다), 읽기 URL 발급이 NOT_SUPPORTED로 응답하거나, 바이트가 하나도 도착하기 전에 객체 저장소에 도달할 수 없는 경우 — get(및 Python의 get_to_file, Go의 GetTo와 GetToFile)은 대신 라우터를 통해 객체를 범위 단위로 읽습니다(FleetFiles v0.1.33 이상). 각 범위는 최대 인라인 제한 크기입니다. 각 범위는 별개의 읽기입니다: 이런 읽기 도중 객체가 교체되면 읽기는 INTERNAL(“object changed while it was read”)로 실패합니다. 쓰기에는 이런 대체 경로가 없습니다.

쓰기에는 두 가지 상한이 그대로 남아 있으며, 둘 다 어느 쪽에 걸렸는지를 명시한 reason과 함께 TOO_LARGE를 보고합니다:

  • 5 GiB — 객체 저장소의 단일 업로드 제한입니다. 멀티파트 업로드는 아직 구현되지 않았습니다.
  • 직접 엔드포인트가 없는 환경에서의 인라인 제한 — 사전 서명을 할 수 없는 배포 환경(어플라이언스는 기본적으로 그렇습니다)은 그보다 큰 객체를 아예 저장할 수 없습니다. 재시도하거나 청크를 더 작게 해도 해결되지 않으며, 메시지가 그 점을 알려줍니다.

직접 경로에서 현장에서 흔한 두 가지 실패는 권한 문제처럼 보이지 않도록 각자의 코드를 갖습니다: PRESIGN_UNREACHABLE(라우터만 허용하는 프록시 — 읽기는 범위 단위 전송으로 대체되고, 쓰기는 실패합니다)와 CLOCK_SKEW(객체 저장소는 15분 넘게 어긋난 요청을 거부합니다 — 디바이스의 NTP를 확인하십시오).

파일 저장소 오류

파일 서비스 또는 직접 전송의 실패는 안정적인 code와 사람이 읽을 수 있는 reason을 담은 FileStoreError를 발생(Python)/throw(JavaScript)/반환(Go)시킵니다. 분기는 항상 code로 하고, 절대 reason으로 하지 마십시오. 로컬 파일 오류, 취소된 호출, 그리고 아래에 매핑된 것 이외의 라우터 수준 WAMP 오류는 그대로 발생합니다.

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
코드의미
NOT_AUTHORIZED호출자가 이 작업을 수행할 수 없습니다
NO_SUCH_NAMESPACE네임스페이스가 데이터 템플릿에 선언되어 있지 않습니다
NO_SUCH_OBJECT키가 존재하지 않습니다
TOO_LARGE단일 호출 전송 제한을 초과했습니다
OBJECT_TOO_LARGE네임스페이스 자체의 maxObjectBytes를 초과했습니다
QUOTA_EXCEEDED파일 저장소가 가득 찼습니다
CONTENT_TYPE_NOT_ALLOWED네임스페이스가 contentTypes를 제한합니다
INVALID_KEY형식이 잘못된 키 또는 접두사(아래 참조. FleetFiles v0.2는 아직 INTERNAL로 보고합니다)
INVALID_RANGE객체 범위를 벗어난 바이트 범위. SDK는 이를 요청하지 않습니다
NOT_SUPPORTED백엔드가 이를 수행할 수 없습니다
NOT_AVAILABLE이 배포 환경에는 파일 서비스가 없습니다 — 또는 아직 없습니다: 플랫폼 재시작 후, 또는 파일 서비스가 프로시저를 등록하는 동안입니다. 호출은 어디에서도 실행되지 않았으므로 다시 시도해도 됩니다
PRESIGN_UNREACHABLE객체 저장소에 직접 도달할 수 없습니다(프록시 때문일 수 있습니다)
CLOCK_SKEW디바이스 시계가 너무 많이 어긋나 있습니다
INTERNAL그 외 모든 경우 — 범위 단위 읽기 도중 변경된 객체도 포함합니다

더 새로운 서버는 이 SDK 릴리스가 알지 못하는 코드를 도입할 수 있습니다. 이런 코드는 하나로 뭉뚱그려지지 않고 code에 그대로 전달되므로, 인식할 수 없는 값은 일반적인 실패로 처리하십시오.

파일 호출은 연결을 기다리지만(Python과 Go에서는 최대 10초, JavaScript에서는 6초) 재시도되지 않습니다: 테이블 작업과 달리 플랫폼 재시작을 견디지 못하므로, 재시작 중에는 파일 서비스가 다시 등록될 때까지 호출이 NOT_AVAILABLE로 실패할 수 있습니다. 그런 호출은 어디에서도 실행되지 않았으므로 앱에서 다시 시도해도 됩니다.

키. 키의 유효성은 파일 서비스가 판단하며, SDK는 키와 접두사를 주어진 그대로 전송합니다. 형식이 잘못된 키 또는 접두사 — 비어 있는 것, 앞이나 뒤에 /가 있는 것(iter / iterate의 폴더 접두사는 제외), 빈 세그먼트나 . 또는 .. 세그먼트, .으로 끝나거나 공백으로 시작하거나 끝나는 세그먼트가 있는 것, \ : * ? " < > | ^ ~ 중 하나나 제어 문자 또는 유니코드 서식 문자가 있는 것, 900바이트, 20개 세그먼트 또는 세그먼트당 255바이트를 넘는 것, 또는 예약된 _sys/ 접두사 아래에 있는 것 — 은 키 오류를 분류하는 파일 서비스에서는 INVALID_KEY로, FleetFiles v0.2에서는 INTERNAL로 거부됩니다. 타임스탬프에 주의하십시오: 2026-10-09T12:00:00Z는 : 때문에 유효한 키 세그먼트가 아닙니다.

Python에서는 FileStoreError를 ironflock.filestore에서 임포트합니다. JavaScript에서는 패키지 루트에서 익스포트됩니다(import { FileStoreError } from "ironflock"). Go에서는 *filestore.Error(또는 ironflock.FileStoreError)이며, 코드는 filestore.CodeQuotaExceeded와 같은 상수로 제공됩니다.

디바이스 간 통신

registerDeviceFunction / register_device_function

같은 앱을 (같은 스테이지에서) 실행하는 다른 디바이스가 호출할 수 있는 프로시저를 등록하며, 대시보드 위젯 액션에서도 호출할 수 있습니다. SDK가 전체 URI <SWARM_KEY>.<DEVICE_KEY>.<APP_KEY>.<STAGE>.<topic>을 자동으로 구성하며, 여기서 STAGE는 DEV 또는 PROD입니다. 라우터는 앱의 다른 등록은 받아들이지 않으며, 단일 등록만 허용합니다(앱이 사용할 수 있는 URI 참조). 등록은 재연결할 때마다 복원됩니다.

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

register()는 register_device_function()의 별칭입니다.

callDeviceFunction / call_device_function

같은 앱의 다른 디바이스가 등록한 프로시저를 호출합니다. SDK가 타겟 디바이스의 키를 사용하여 전체 WAMP 토픽을 자동으로 조합합니다: <SWARM_KEY>.<device_key>.<APP_KEY>.<STAGE>.<topic>이며, 이는 registerDeviceFunction / register_device_function이 타겟 디바이스에서 등록하는 것과 같은 URI입니다.

result = await ironflock.call_device_function( 42, # 타겟 디바이스 키 "com.myapp.add", # 프로시저 이름 args=[3, 5] # 인수 ) print(result) # 8

call

전체 WAMP URI를 사용하여 원격 프로시저를 호출합니다. 앱은 some.full.wamp.topic과 같은 이름을 등록할 수 없으므로, 이런 이름으로는 플랫폼 서비스가 앱의 데이터 realm에 등록한 프로시저(데이터 백엔드나 파일 저장소의 프로시저 등)에만 도달합니다. 앱의 다른 디바이스가 등록한 함수를 호출하려면 callDeviceFunction / call_device_function을 사용하십시오.

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

앱이 사용할 수 있는 URI

앱 컨테이너는 라우터 역할 app으로 자신의 데이터 realm인 realm-<SWARM_KEY>-<APP_KEY>-<dev|prod>에 참여합니다. 라우터는 그 안에서 일어나는 모든 발행, 구독, 호출, 등록을 해당 역할의 허용 목록과 대조합니다. SDK 자체의 메서드는 그 범위 안에 머무릅니다. 이 규칙이 중요해지는 것은 publish, subscribe, call에 자신의 URI를 전달하거나 connection(Go에서는 Connection())을 직접 사용할 때입니다. 거부된 작업은 wamp.error.not_authorized로 실패합니다(Go에서는 ironflock.WampURI(err)가 이를 반환합니다).

자신의 realm에 있는 URI허용되는 작업사용하는 곳
자체 이름(예: com.myapp.status)발행, 구독(접두사 구독 포함), 호출 — 등록은 불가publish, subscribe, call
<SWARM_KEY>.<APP_KEY>.<table>, bulk.<SWARM_KEY>.<APP_KEY>.<table>발행 — 구독은 불가publishToTable / publish_to_table, reportError / report_error, publishRowsToTable / publish_rows_to_table
transformed.<table>, transformed.bulk.<table>구독만subscribeToTable / subscribe_to_table
databackend.errors.…, files.events…구독만데이터 백엔드의 오류 이벤트와 파일 이벤트
<SWARM_KEY>.<DEVICE_KEY>.<APP_KEY>.<STAGE>.<name>등록(단일), 호출, 발행 — 구독은 불가registerDeviceFunction / register_device_function, callDeviceFunction / call_device_function
append.…, appendBulk.…, history.transformed.…, secret.reveal.…, secret.verify.…, files.read.…, files.write.…호출(SDK가 호출합니다)테이블, 이력, 비밀, 파일 메서드
sys.appaccess.list, sys.appaccess.resolve호출listConsumableApps / list_consumable_apps, connectToApp / connect_to_app, connectToAllApps / connect_to_all_apps
wamp.session.get호출JavaScript SDK의 하트비트

신원 확인을 거치는 이름. 숫자로 시작하는 이름 — 테이블 쓰기 토픽과 디바이스 함수 URI — 은 플랫폼의 신원 확인도 통과해야 하며, 이 확인은 해당 이름을 realm의 프로젝트 키(SWARM_KEY)에 묶습니다. 등록은 realm의 프로젝트 키, 앱 키, 스테이지를 가진 자기 디바이스의 함수 URI에 대해서만 허용되며, 접두사 등록이나 와일드카드 등록은 이 네 세그먼트를 고정한 경우에만 허용됩니다. 발행과 호출에는 realm의 프로젝트 키만 있으면 됩니다: 다른 앱 키를 가진 테이블 토픽에 발행된 행은 받아들여지지만, 어떤 데이터 백엔드에도 도달하지 않고 사라집니다. 이 URI들은 SDK가 구성하도록 맡기십시오.

예약된 이름. 라우터는 위의 두 앱 간 프로시저를 제외한 sys.와, wamp.session.get을 제외한 wamp.를 거부합니다. 표의 나머지 이름은 플랫폼에 속합니다: 자신의 이름을 숫자, bulk., transformed., databackend.errors., files., append., appendBulk., history. 또는 secret.으로 시작하지 마십시오. 그 밖의 모든 첫 세그먼트는 앱 자신의 토픽 공간입니다.

원시 테이블 토픽은 아무도 구독하지 않습니다. 이 토픽은 보낸 그대로의 행을 전달하며, 여기에는 비밀 컬럼의 평문도 포함됩니다. 데이터 백엔드는 행을 저장할 때에만 비밀 컬럼을 암호화하기 때문입니다. subscribeToTable / subscribe_to_table은 대신 transformed.<table>에서 저장된 행을 읽으며, 이 행은 유형이 지정되고 비밀 컬럼이 가려져 있습니다.

다른 앱의 데이터는 읽기 전용입니다. 제공자의 realm에서 앱(라우터 역할 app_reader)은 하트비트 wamp.session.get 외에는 transformed.…와 files.events…를 구독하고 history.transformed.…와 files.read.…를 호출하는 것만 할 수 있습니다 — 발행, 등록, 쓰기, 비밀 프로시저는 모두 불가합니다.

디바이스 메타데이터

setDeviceLocation / set_device_location

플랫폼에 디바이스의 GPS 위치 업데이트를 요청하며, IronFlock 맵은 이 위치를 실시간으로 표시합니다.

현재 사용할 수 없음: 앱의 데이터 realm에서 이 호출에 응답하는 플랫폼 서비스가 아직 없으므로, 호출은 wamp.error.no_such_procedure로 실패합니다. 사용할 수 있게 될 때까지는 위치를 자체 테이블에 보관하십시오.

await ironflock.set_device_location(long=8.6821, lat=50.1109)
파라미터범위
long-180 ~ 180
lat-90 ~ 90

위치 이력은 저장되지 않습니다. 시간에 따른 위치를 추적하려면 전용 테이블을 생성하고 publish_to_table / publishToTable을 사용하십시오.

getRemoteAccessUrlForPort

앱의 port-template.yml에 선언된 포트가 터널이 활성화되면 접근 가능한 URL을 반환합니다.

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>"
파라미터유형설명
portint / numberport-template.yml에 선언된 포트
protocolstr / string, 선택포트의 프로토콜: "http"(기본값), "https", "tcp" 또는 "udp". 대소문자를 구분하지 않으며, 앞뒤 공백은 무시되고, 빈 값은 "http"로 처리됩니다
remote_port_environment / remotePortEnvironmentstr / string, 선택tcp/udp 전용: REMOTE_PORT_FOR_<port> 대신 원격 포트를 담고 있는 변수 — 템플릿에 있는 해당 포트 자체의 remote_port_environment로, 0.19.8 이전의 디바이스 에이전트에서는 이것이 필요합니다

http 및 https 포트는 디바이스 에이전트가 주입하는 터널 도메인(TUNNEL_DOMAIN)에서 터널 레이블 <device_key>-<app name, lower case>-<port>(https 포트에는 플랫폼의 secure- 접두사가 붙음)로 제공되며, 항상 https:// URL입니다. 온프레미스 어플라이언스에 속한 디바이스(에이전트가 INSTANCE_KEY를 주입함)에서는 대신 클라우드를 통해 전달되는 경로 https://i<INSTANCE_KEY>-<label>.<CLOUD_TUNNEL_DOMAIN>이 반환됩니다.

tcp 또는 udp URL에는 터널이 할당한 공개 포트가 담기며, 디바이스 에이전트는 이를 REMOTE_PORT_FOR_<port>로 알립니다(에이전트 0.19.8 이상): <protocol>://<TUNNEL_DOMAIN>:<remote port>. 어플라이언스의 디바이스에서는 에이전트가 인터넷 연결 포트인 REMOTE_PORT_FOR_<port>_CLOUD를 알리는 동안 이를 우선합니다. 이 값이 나타나기 전과, 에이전트가 이를 제거한 뒤(클라우드 전달이 꺼진 경우)에는 어플라이언스 로컬 URL이 반환됩니다. 포트가 0이면 아직 할당되지 않은 것입니다.

값은 /data/env에서 실시간으로 읽으므로, 결과를 캐시하지 말고 메서드를 다시 호출하십시오. URL로는 처리할 수 없는 경우도 있습니다:

  • 디바이스 에이전트는 프로덕션(PROD) 설치의 포트만 터널링합니다. 개발(DEV) 컨테이너에서 http(s) URL은 이 디바이스에 있는 같은 앱의 PROD 설치의 URL이며, tcp/udp는 URL을 제공하지 않습니다. DEV 앱에는 대신 디바이스의 LAN 주소(DEVICE_LAN_IP, DEVICE_PORT_FOR_<port>)로 접근하십시오.
  • 도메인 없이 설치된 어플라이언스(일반 모드)의 엣지 디바이스에서는 에이전트가 TUNNEL_DOMAIN을 localhost로 설정하므로, 그곳에서 구성된 URL은 localhost를 가리킵니다: 이런 어플라이언스는 앱 포트를 자신의 호스트 포트로 제공하며, 이는 터널 URL로 표현할 수 없습니다.
  • 설정되지 않았거나 비어 있는 터널 도메인은 기본값 app.ironflock.com이 됩니다. 현재 플랫폼은 CLOUD_TUNNEL_DOMAIN을 설정하지 않습니다: 앱이 환경 설정에서 이 변수를 직접 설정하지 않는 한, 어플라이언스의 URL은 app.ironflock.com을 사용합니다.
  • root가 아닌 사용자로 실행되는 컨테이너에서는 값이 컨테이너가 시작될 때의 값입니다(환경 변수 참조).

URL을 구성할 수 없으면 메서드는 None / null(Go에서는 ""와 false)을 반환합니다: 디바이스 키나 앱 이름이 없는 경우, tcp/udp 포트에 아직 원격 포트가 알려지지 않은 경우(또는 0인 경우), 프로토콜을 알 수 없는 경우, 또는 레이블이 유효한 DNS 레이블이 아닌 경우(63자 초과, 또는 앱 이름에 점이 포함된 경우)입니다. 브라우저에는 환경이 없으므로, http(s) URL은 deviceKey와 appName 옵션으로 app.ironflock.com 위에 구성됩니다.

연결 속성 및 생명주기

속성유형설명
is_connectedbool플랫폼 연결이 활성화되어 있는지 여부
connectionCrossbarConnection내부 연결 인스턴스 (고급 사용)
메서드설명
run()연결을 시작하고 mainFunc 실행 (블로킹)
await start()비동기적으로 연결 시작
await stop()연결 및 소비 대상 앱 연결 중지, 실행 중인 작업 취소. 연결은 각자의 realm을 떠나고 이후 재연결하지 않으며, start()로 새로 연결합니다. 아직 대기 중인 start() 또는 connect_to_app은 실패합니다(연결 안정성 참조)
await run_async()비동기적으로 연결을 시작하고 유지

연결 안정성

SDK는 스스로 재연결합니다. 소켓이 끊기면 라우터가 복구될 때까지 재시도한 뒤, 모든 구독과 등록된 모든 디바이스 함수를 복원합니다. 재연결 후 다시 구독할 필요는 없습니다. 어느 하나를 복원하지 못하더라도 나머지는 그대로 복원되며, 실패한 항목은 세션이 유지되는 동안 재시도됩니다 — 1초 후에 시작해 대기 시간이 두 배씩 최대 30초까지 늘어나고, 각 대기 시간은 최대 15%까지 변동됩니다 — 그리고 재연결할 때마다 다시 시도됩니다: 라우터가 이전에 이를 받아들였으므로 그 거부는 일시적인 것입니다. 예를 들어 디바이스 함수에 대한 플랫폼의 신원 확인이 잠시 fail-closed로 실패하는 경우입니다. 첫 번째 실패는 경고로 기록되며, 복원에 성공하면 그 사실도 기록됩니다.

테이블 작업 — 쓰기, 이력·시리즈·비밀 읽기, 그리고 소비 대상 앱의 이력 읽기 — 도 라우터나 플랫폼 업데이트와 같은 플랫폼 재시작을 견뎌 냅니다. 이 작업들은 연결이 돌아오기를 최대 reconnect_window(Python, 초 단위) / reconnectWindowMs(JavaScript) / WithReconnectWindow(Go, time.Duration), 기본 60초 동안 기다리며, 데이터 백엔드가 아직 프로시저를 제공하지 않는 동안에는 재시도합니다: 라우터가 wamp.error.no_such_procedure로 응답하거나, 데이터 백엔드가 자신의 등록보다 먼저 도착한 호출을 “client has no handler for registration”으로 거부하는 경우입니다. 이런 호출은 핸들러에 도달한 적이 없으므로 재시도해도 행이 두 번 쓰이는 일은 없으며, 그 밖의 오류는 발생한 그대로 전달됩니다. 파일 호출은 재시도되지 않습니다(파일 저장소 오류 참조). 이미 보낸 요청은 해당 세션이 닫히면(라우터가 세션을 닫거나, 링크가 끊기거나, stop()이 호출되는 경우) 응답을 기다리지 않고 즉시 실패하며, 재시도되지 않습니다: 핸들러에 도달했을 수도 있기 때문입니다. Go에서는 이런 요청 — 그리고 대기 시간 안에 연결을 찾지 못한 모든 작업 — 이 wamp.ErrNotConnected(Stop 이후에는 wamp.ErrStopped)를 감싼 오류로 실패합니다. Go의 그 밖의 작업(Publish, Subscribe, Call, 디바이스 함수, 앱 간 발견)은 연결을 최대 10초 기다리며 재시도되지 않습니다.

특수한 경우는 라우터에는 접근할 수 있지만 앱용 realm이 아직 없는 상황입니다. 동시 설치 경쟁 상황에서는 데이터 백엔드가 realm을 프로비저닝하기 전에 앱 컨테이너가 먼저 시작되므로, 처음 몇 번의 접속 시도는 wamp.error.no_such_realm으로 거부됩니다. SDK는 몇 초마다 재시도하며 realm이 생기는 즉시 앱이 올라옵니다. 1분이 지나도 realm이 없다면 앞으로도 생기지 않을 가능성이 높으므로(앱이 삭제되었거나 이 스테이지용 데이터 백엔드가 없는 경우), SDK는 그때부터 라우터를 영원히 매초 두드리는 대신 2분에 한 번 시도하도록 속도를 늦춥니다. realm이 나타나거나 라우터 자체가 사라졌다가 돌아오면 즉시 빠른 주기로 돌아갑니다.

stop()은 모든 연결 대기를 끝냅니다. realm에 처음 참여하기를 아직 기다리고 있는 start()(예를 들어 realm이 아직 없는 경우)는 Connection to realm <realm> stopped before it opened로 실패합니다. 아직 제공자 연결을 여는 중인 connect_to_app / connectToApp은 Connection to app '<app>' (<stage>) aborted by stop()으로 실패하며, 그럼에도 연결이 realm에 참여하면 그 연결을 닫고 Connection to app '<app>' (<stage>) closed by stop()으로 실패합니다. stop()과 경쟁하는 거부는 그 CrossAppAccessError를 유지합니다. stop()이 반환될 때에는 연결을 시도하고 있는 것이 아무것도 남아 있지 않습니다. realm을 떠날 때 라우터가 응답하지 않으면 5초 후에 연결을 끊습니다.

Go에서도 Stop(ctx)가 이런 대기를 끝내며, 오류는 wamp.ErrStopped를 감쌉니다: 아직 대기 중인 Start는 이를 반환하고, 아직 여는 중인 ConnectToApp은 ironflock: connection to app '<app>' (<stage>) aborted by Stop(그럼에도 연결이 realm에 참여한 경우에는 closed by Stop)으로 실패합니다. Stop은 ctx 안에서 realm 떠나기를 기다린 뒤 소켓을 닫으며, Run은 여기에 10초를 줍니다.

더 까다로운 경우는 연결이 조용히 끊기는 상황입니다. 유휴 상태에서 NAT나 프록시가 양쪽 모두에 알리지 않고 연결을 끊거나, 로드 밸런서 뒤의 라우터가 재시작되는 경우입니다. 손실을 알리는 신호는 아무것도 도착하지 않습니다. 발행만 하는 앱은 다음 쓰기가 실패할 때 알아차리지만, 구독만 하는 앱은 아예 쓰기를 하지 않으므로 별도의 장치가 없으면 컨테이너가 재시작될 때까지 죽은 채로 남습니다.

세 SDK 모두 이를 감지합니다. 감지는 기본적으로 켜져 있으며 별도의 설정이 필요 없습니다.

Python SDK는 WebSocket 킵얼라이브를 사용합니다. 링크가 유휴 상태인 동안 30초마다 라우터에 ping을 보내고 10초 안에 pong을 기대합니다. 응답이 없으면 연결을 끊고 다시 연결합니다. 수신 트래픽도 생존의 증거로 간주되므로 활발한 연결은 탐침되지 않습니다. ping은 링크가 실제로 조용해졌을 때만 나갑니다.

connectToApp / connect_to_app으로 연 앱 간 연결은 보통 구독만 하며, 이것이 바로 이 장치가 보호하는 경우입니다. 자동으로 적용되며, 라우터가 연결을 거부한 뒤에도 거부가 지속될 때까지는 재연결합니다(connectToApp 참조).

오류 처리

모든 SDK 메서드는 실패를 명확하게 알립니다. 잘못된 인자, 연결 끊김, 플랫폼의 거부가 발생하면 작업, 토픽, 원인을 명시한 메시지와 함께 예외를 발생시키거나(Python), reject하거나(JavaScript), 오류를 반환합니다(Go). 조용히 무시되는 오류는 없으므로, 실패해도 계속 진행되어야 하는 호출은 try 블록으로 감싸십시오 — Go에서는 반환된 오류를 확인하십시오.

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}")

JavaScript에서 플랫폼으로부터 발생한 실패는 WampError 인스턴스입니다 — 일반적인 Error 하위 클래스이며, 추가로 WAMP 오류 URI를 error에, 오류 페이로드를 args / kwargs에 담고 있습니다. 그 외의 모든 경우(잘못된 파라미터, 연결 없음)는 일반 Error입니다.

Go에서 실패한 작업은 작업과 원인을 명시하는 메시지를 가진 *ironflock.OperationError를 반환하며, 이 오류는 원인을 감쌉니다. errors.Is와 errors.As는 이를 꿰뚫어 봅니다. 라우터나 피호출자의 거부는 URI, Args, Kwargs를 가진 *ironflock.WampError(*wamp.Error)이며, ironflock.WampURI(err)는 그 URI 또는 ""를 반환합니다. 그 밖의 센티널 오류로는 ironflock.ErrMissingConfig(작업에 필요한 신원 정보가 없음), ironflock.ErrAlreadyStarted, wamp.ErrStopped(Stop 이후)가 있습니다.

업그레이드 안내: 이전 Python 및 JavaScript SDK 버전은 호출이 실패하면 메시지를 로그에 남기고 None / null을 반환했습니다. 이제는 대신 예외를 발생시키므로 if result is None: 형태의 코드로는 더 이상 실패를 감지할 수 없습니다 — try / except(또는 try / catch)를 사용하십시오.

데이터 백엔드 거부

데이터 백엔드가 거부한 작업은 아래 WAMP 오류 URI 중 하나로 실패하며, 첫 번째 인수가 그 이유입니다. 세 SDK 모두 이를 상수로 export하며, Go는 Go 스타일의 이름을 씁니다(ironflock.URIRateLimited, ironflock.URIStorageFull, …). Python에서 IronFlock 메서드는 autobahn의 ApplicationError를 원인(cause)으로 하는 RuntimeError를 발생시키며(소비 대상 앱의 읽기는 ApplicationError 자체를 발생시킵니다), wamp_error_uri(err)는 둘 중 어느 것에서든 URI를 읽습니다. JavaScript에서 오류는 WampError이며, 그 error가 URI입니다. Go에서는 *ironflock.WampError를 감싼 *ironflock.OperationError이며, ironflock.WampURI(err)가 URI를 읽습니다. SDK는 이 중 어느 것도 재시도하지 않습니다. 데이터 백엔드가 거부한 발행된 행은 오류 없이 버려집니다 — 이를 보고하는 것은 append 메서드뿐입니다.

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
상수거부되는 경우
URI_RATE_LIMITED디바이스의 앱 자격 증명당 분당 30회를 넘는 revealSecrets / reveal_secrets 호출, 120회를 넘는 verifySecret / verify_secret 호출
URI_RESULT_TOO_LARGE8 MiB 메시지 예산을 넘는 단일 행이 있는 읽기, 또는 1 GiB 보호 한도를 넘는 결과(그 밖의 대용량 결과는 청크로 도착하며 SDK가 다시 조립합니다). 키워드 인수가 가장 큰 컬럼을 알려줍니다: limit을 낮추거나, timeRange를 좁히거나, columns를 더 적게 선택하거나, columnPaths로 잘라내십시오
URI_INVALID_LIMITlimit이 3000을 넘는 transform 읽기
URI_INVALID_TIME_RANGE, URI_INVALID_METRIC, URI_INVALID_GROUP_BY, URI_SERIES_TOO_MANY_GROUPS테이블이 답할 수 없는 시리즈 쿼리: 시작값이 없거나 끝값이 시작값보다 뒤가 아닌 경우; 테이블이 집계할 수 없는 메트릭(존재하지 않는 컬럼, 비밀 컬럼, 컬럼 유형이 받지 않는 메서드, COUNT 이외의 메서드를 지정한 tsp); 그룹화할 수 없는 groupBy 컬럼; 50,000행 초과
URI_SECRET_COLUMN, URI_NOT_A_SECRET_COLUMN비밀 컬럼에 대한 필터; 비밀이 아닌 컬럼에 대한 verifySecret / verify_secret, 또는 비밀 컬럼이 없는 테이블에 대한 revealSecrets / reveal_secrets
URI_STORAGE_FULL어플라이언스의 디스크가 거의 가득 찬 상태에서의 추가(여유 공간 약 3 GiB 미만, 4 GiB부터 다시 허용) — 일시적입니다: 행은 나중에 추가하십시오
URI_STORAGE_OVERUSAGE계정의 저장 허용량을 넘는 추가. 곧바로 재시도해도 소용없습니다
URI_ENTITY_KEY_CONFLICT엔티티 키(maintainLatestFlagFor)와 tsp가 배치 안이나 저장된 다른 행과 이미 같은 행: 모든 행에 각자의 tsp를 부여하십시오
URI_SECRET_SENTINEL_UNRESOLVABLE, URI_SECRET_CIPHERTEXT_REJECTED비밀의 이전 값을 유지하는(__secret__) 쓰기인데 이를 찾을 엔티티 키가 없는 경우; 이미 암호화된 값의 쓰기

데이터 백엔드가 이유 없이 거부한 추가 — tsp가 없는 행에 대한 데이터 백엔드의 응답인 wamp.error.runtime_error — 는 유형별 거부가 아닙니다: SDK의 오류 메시지는 누락된 tsp와, 데이터 백엔드가 원인을 기록하는 앱의 error-logs 테이블을 가리킵니다.

브라우저 사용 (JavaScript 전용)

JavaScript SDK는 최신 브라우저에서 작동합니다. 브라우저에는 환경 변수가 없으므로 생성자를 통해 모든 구성을 전달하십시오:

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 }]);

자격 증명을 하드코딩하는 대신 IronFlock.fromServer()를 사용하여 백엔드에서 구성을 가져오십시오:

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

백엔드 엔드포인트는 연결 옵션(serialNumber, deviceKey, appName, swarmKey, appKey, env)이 포함된 JSON 객체를 반환해야 합니다.

AI 에이전트 함수 등록

SDK는 AI 에이전트가 호출할 수 있는 함수를 등록할 수 있습니다. 프로시저를 등록하고 ai-template.yml에서 해당 토픽을 참조하십시오:

def get_sensor_reading(sensor_id): """센서에서 최신 판독값을 반환합니다.""" 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)

그런 다음 AI 에이전트는 사용자가 라이브 센서 데이터가 필요한 질문을 할 때 이 함수를 호출할 수 있습니다.

등록된 WAMP 토픽을 AI 에이전트에 연결하려면 앱의 .ironflock/ai-template.yml에서 참조하십시오:

sensor_agent: tool_description: | 사용자가 센서 판독값, 라이브 디바이스 데이터, 또는 현재 환경 조건에 대해 질문할 때 이 에이전트에 위임하세요. system_prompt: | 당신은 센서 데이터 전문가입니다. get_current를 사용하여 모든 센서에서 최신 판독값을 가져오세요. 응답에 항상 단위를 포함하세요. main: true max_context_tokens: 30000 max_iterations: 5 tools: get_current: description: 센서에서 최신 판독값을 반환합니다. topic: sensors.get_latest parameters: sensor_id: type: string description: 조회할 센서 식별자. required: true

topic 값(sensors.get_latest)은 엣지 코드에서 register_device_function / registerDeviceFunction / RegisterDeviceFunction에 전달된 이름과 일치해야 합니다. IronFlock이 자동으로 함수가 등록된 디바이스로 호출을 라우팅합니다.

전체 ai-template.yml 참조는 에이전트 및 도구 정의를 참조하십시오.

환경 변수

이 변수들은 앱 컨테이너 내에서 IronFlock 런타임이 자동으로 설정합니다:

변수설명
DEVICE_NAME디바이스 표시 이름
DEVICE_SERIAL_NUMBER고유하고 변경 불가능한 디바이스 식별자
DEVICE_KEY인증용 디바이스 키
SWARM_KEY프로젝트 식별자
APP_KEY앱 식별자
APP_NAME앱 이름
ENV환경: DEV 또는 PROD
APP_AUTH_ID, APP_AUTH_SECRETSDK가 라우터에 제시하는 앱 자체의 자격 증명. 앱 간 접근에는 이것이 필요하며, 없으면(이전 디바이스 에이전트) SDK는 디바이스 시리얼 번호로 폴백합니다
import os device_name = os.environ.get("DEVICE_NAME") serial = os.environ.get("DEVICE_SERIAL_NUMBER") project_key = os.environ.get("SWARM_KEY")

원격 접근 변수(TUNNEL_DOMAIN, DEVICE_LAN_IP, DEVICE_PORT_FOR_<port>, REMOTE_PORT_FOR_<port>, …)는 포트 구성에 나와 있습니다.

디바이스 에이전트는 이 값들을 /data/env에도 값마다 <NAME>.txt 파일 하나씩으로 미러링하고, 앱이 실행되는 동안 이 미러를 최신 상태로 유지합니다. SDK는 이를 먼저 읽으므로 원격 접근 포트처럼 런타임에 바뀌는 값도 실행 중인 앱에 전달됩니다. 앱별 자격 증명은 연결을 시도할 때마다 다시 읽히므로, 앱이 실행되는 동안 에이전트가 교체한 자격 증명은 다음 재연결 때 제시됩니다. 에이전트는 /data/env를 root만 읽을 수 있게 합니다: 다른 사용자로 실행되는 컨테이너에서는 SDK가 컨테이너 시작 시의 값을 보게 되며, 경고를 한 번 기록합니다.

Last updated on