Keyboard shortcuts

Press ← or → to navigate between chapters

Press ? to show this help

Press Esc to hide this help

함수에서 첨부 파일 사용

첨부 파일(Attachment)은 객체 속성에 연결할 수 있는 파일입니다. 업로드한 파일은 먼저 임시 상태이며 액션으로 객체에 연결하면 영구 보관합니다. 함수는 첨부 파일을 입력받거나 객체 속성에서 읽고, 지원 언어에서는 새 파일을 만들어 반환하거나 객체에 연결할 수 있습니다.

언어와 메모리

언어타입내용 읽기생성
TSv1@foundry/functions-api의 AttachmentreadAsync(): Promise<Blob>Attachments.uploadFile(filename, blob)
TSv2@osdk/functions의 AttachmentfetchContents(): Promise<Response>함수에서 첨부 파일 생성은 지원하지 않습니다.
Pythonfunctions.api가 다시 내보내는 OSDK Attachmentread(): BytesIOclient.ontology.attachments.upload(file_path, attachment_name)

TSv1의 엄격한 메모리 한도 때문에 20 MB 미만의 첨부 파일을 처리하는 것이 권장됩니다. 이는 첨부 파일 자체의 최대 업로드 크기와 다른 권장 조건입니다. TSv2와 Python 서버리스 함수의 기본 메모리는 1 GiB이며 Ontology Manager에서 조정할 수 있습니다. 배포형을 포함한 메모리 설정을 따릅니다.

PDF나 Excel의 내부 구조는 별도 파서가 필요합니다. 한 번에 큰 파일을 메모리로 읽으면 원본 바이트 외에 파서가 만든 구조도 메모리를 사용합니다.

Excel에서 객체 생성

다음 예제는 첫 시트의 id, column_a, column_b를 읽습니다. 가상의 MyObjectType은 String 기본 키 id, String propertyA와 Double propertyB를 가진다고 가정합니다. Python 속성 이름은 property_a와 property_b입니다.

id와 column_a는 비어 있지 않은 실제 문자열 셀, column_b는 유한한 숫자 셀이어야 합니다. ID의 앞자리 0을 보존하려면 Excel에서 처음부터 텍스트로 저장해야 합니다. 숫자 셀에 표시 형식만 적용한 ID는 문자열로 자동 복원하지 않습니다.

이 예제는 새 객체를 생성합니다. 기존 객체를 갱신하려면 해당 객체를 조회하고 편집 컨테이너의 edit 또는 TSv1 속성 수정 경로를 사용하도록 별도 업무 로직을 정해야 합니다.

TypeScript v1과 SheetJS

xlsx 패키지를 npm 의존성으로 설치합니다. 아래 클래스는 시트의 첫 행을 헤더로 사용합니다. sheet_to_json의 제네릭은 런타임 필드 검증을 하지 않으므로 실제 값 타입을 확인합니다.

import { Attachment, Edits, OntologyEditFunction, UserFacingError } from "@foundry/functions-api";
import { MyObjectType, Objects } from "@foundry/ontology-api";
import * as XLSX from "xlsx";

export class ExcelImportFunctions {
    @Edits(MyObjectType)
    @OntologyEditFunction()
    public async processExcelUpload(file: Attachment): Promise<void> {
        const blob = await file.readAsync();
        const workbook = XLSX.read(await blob.arrayBuffer(), { type: "array" });
        const sheetName = workbook.SheetNames[0];
        const worksheet = sheetName === undefined ? undefined : workbook.Sheets[sheetName];
        if (worksheet === undefined) throw new UserFacingError("첫 번째 시트가 없습니다.");
        const rows = XLSX.utils.sheet_to_json<Record<string, unknown>>(worksheet, { defval: null, raw: true });
        if (rows.length === 0) throw new UserFacingError("가져올 데이터 행이 없습니다.");
        const seen = new Set<string>();
        for (let index = 0; index < rows.length; index++) {
            const row = rows[index]!;
            const id = row.id;
            const name = row.column_a;
            const value = row.column_b;
            if (typeof id !== "string" || id.length === 0 ||
                typeof name !== "string" || name.length === 0 ||
                typeof value !== "number" || !Number.isFinite(value)) {
                throw new UserFacingError(`데이터 항목 ${index + 1}의 id, column_a, column_b 형식이 잘못되었습니다.`);
            }
            if (seen.has(id)) throw new UserFacingError(`파일 안에 중복 ID가 있습니다: ${id}`);
            seen.add(id);
            const object = Objects.create().myObjectType(id);
            object.propertyA = name;
            object.propertyB = value;
        }
    }
}

클래스를 index.ts에서 내보내고 함수 기반 액션에 연결해야 편집을 적용합니다.

Python과 pandas

Libraries에서 pandas와 .xlsx용 openpyxl 엔진을 준비합니다. dtype=object로 저장된 셀 타입을 유지하고 keep_default_na=False로 NA 같은 문자열을 자동 결측값으로 바꾸지 않습니다.

import math
from numbers import Real
import pandas as pd
from functions.api import Attachment, OntologyEdit, UserFacingError, function
from ontology_sdk import FoundryClient
from ontology_sdk.ontology.objects import MyObjectType


@function(edits=[MyObjectType])
def process_excel_upload(excel_file: Attachment) -> list[OntologyEdit]:
    frame = pd.read_excel(excel_file.read(), sheet_name=0, dtype=object,
                          keep_default_na=False, engine="openpyxl")
    required = ["id", "column_a", "column_b"]
    if frame.empty or any(column not in frame.columns for column in required):
        raise UserFacingError("id, column_a, column_b 헤더와 데이터 행이 필요합니다.")
    positions = [frame.columns.get_loc(column) for column in required]
    edits = FoundryClient().ontology.edits()
    seen: set[str] = set()
    for index, row in enumerate(frame.itertuples(index=False, name=None), start=1):
        object_id, name, value = (row[position] for position in positions)
        if not isinstance(object_id, str) or not object_id or not isinstance(name, str) or not name:
            raise UserFacingError(f"데이터 항목 {index}의 id와 column_a는 비어 있지 않은 문자열이어야 합니다.")
        if isinstance(value, bool) or not isinstance(value, Real) or not math.isfinite(value):
            raise UserFacingError(f"데이터 항목 {index}의 column_b는 유한한 숫자여야 합니다.")
        if object_id in seen:
            raise UserFacingError(f"파일 안에 중복 ID가 있습니다: {object_id}")
        seen.add(object_id)
        created = edits.objects.MyObjectType.create(object_id)
        created.property_a = name
        created.property_b = float(value)
    return edits.get_edits()

여러 파일을 받으려면 TypeScript는 Attachment[], Python은 list[Attachment]를 사용합니다. 파일 간 기본 키 충돌도 같은 작업 범위에서 다뤄야 합니다.

TSv1의 파일시스템 의존성

TSv1에는 Node.js fs 파일시스템이 없습니다. 파일 파서가 fs에 의존하면 컴파일 또는 실행 중 모듈 오류가 날 수 있습니다. 메모리 파일시스템을 쓸 수 있는 라이브러리라면 memfs를 fs 이름으로 alias하는 방법이 있습니다.

package.json에서 fs의 의존성 별칭을 지정합니다. 기존 항목을 유지하며 함수 템플릿과 전체 의존성의 호환성을 확인해야 합니다.

{
  "dependencies": {
    "fs": "npm:memfs@4.80.0"
  }
}

이 alias는 메모리 파일시스템을 제공하며 실제 호스트 파일시스템을 여는 기능은 아닙니다. 다른 Node.js 시스템 API에도 의존하는 패키지는 fs alias만으로 동작하지 않을 수 있습니다.

텍스트 정비 로그를 새 첨부 파일로 저장

가상의 Aircraft.maintenanceLog는 UTF-8 텍스트 첨부 파일입니다. 아래 예제 업무 규칙은 기존 파일 뒤에 개행 하나와 완료 로그의 전체 내용을 덧붙입니다. 기존 첨부 파일을 변경하는 대신 새 파일을 업로드하여 속성이 가리키는 첨부 파일을 바꿉니다.

TypeScript v1

import { Attachment, Attachments, Edits, OntologyEditFunction, UserFacingError } from "@foundry/functions-api";
import { Aircraft } from "@foundry/ontology-api";

export class MaintenanceFunctions {
    @Edits(Aircraft)
    @OntologyEditFunction()
    public async updateMaintenanceLog(aircraft: Aircraft, completedMaintenanceLog: Attachment): Promise<void> {
        if (aircraft.maintenanceLog === undefined) throw new UserFacingError("기존 정비 로그가 없습니다.");
        const [current, completed] = await Promise.all([
            aircraft.maintenanceLog.readAsync(), completedMaintenanceLog.readAsync(),
        ]);
        const updated = new Blob([current, "\n", completed], { type: "text/plain;charset=utf-8" });
        aircraft.maintenanceLog = await Attachments.uploadFile("maintenance-log.txt", updated);
    }
}

Python OSDK v2

v2의 upload는 Attachment를 반환합니다. 임시 로컬 파일을 업로드한 뒤 편집 컨테이너의 속성에 연결합니다.

from pathlib import Path
from tempfile import TemporaryDirectory
from shutil import copyfileobj
from functions.api import Attachment, OntologyEdit, UserFacingError, function
from ontology_sdk import FoundryClient
from ontology_sdk.ontology.objects import Aircraft


@function(edits=[Aircraft])
def update_maintenance_log(aircraft: Aircraft, completed_maintenance_log: Attachment) -> list[OntologyEdit]:
    if aircraft.maintenance_log is None:
        raise UserFacingError("기존 정비 로그가 없습니다.")
    client = FoundryClient()
    edits = client.ontology.edits()
    with TemporaryDirectory() as directory:
        path = Path(directory) / "updated-maintenance-log.txt"
        with path.open("wb") as output:
            copyfileobj(aircraft.maintenance_log.read(), output)
            output.write(b"\n")
            copyfileobj(completed_maintenance_log.read(), output)
        uploaded = client.ontology.attachments.upload(str(path), "maintenance-log.txt")
    edits.objects.Aircraft.edit(aircraft).maintenance_log = uploaded
    return edits.get_edits()

파일 업로드 뒤 반환한 온톨로지 편집을 액션이 적용해야 영구 연결됩니다. 연결되지 않은 첨부 파일은 업로더만 볼 수 있으며 업로드 후 1시간이 지나면 삭제됩니다. 업로드와 액션 편집 성공은 같은 시점이 아니므로 편집 실패 때 임시 업로드가 잠시 남을 수 있습니다.

액션 첨부 파일 업로드는 수신 입력, 크기와 연결 한도를 설명합니다. 미디어는 미디어 집합의 파일을 처리하는 별도 API입니다.