Aller au contenu

Typed I/O ports

Each function exposes named input and output ports. Ports carry types from the canonical catalog; the workflow editor uses them to validate connections at draw time, and the worker uses them to route values into the right slots inside the container.

inputs: and outputs: under a function are dicts where each key is a port name and each value describes the port:

functions:
size:
handler: bolts:size
inputs:
diameter:
type: Length
description: "Bolt nominal diameter in metres."
required: true # default: true
load:
type: Force
required: false # optional input
outputs:
stress:
type: Stress
description: "Von Mises equivalent stress."
report:
type: File[pdf]
required: false # File outputs may be optional

Each entry has the same shape:

FieldTypeRequiredNotes
typestring or mappingyesType expression — see below.
descriptionstringnoFree text, surfaced in the editor and AutoForm.
requiredbooleannoDefaults to true. See semantics below.

The port name is the dict key — it must be a non-empty string, typically snake_case. It becomes the keyword argument the runner passes to your handler in mode A, and the JSON key in /workspace/in/data.json in modes B/C.

Every type: value is parsed against the type catalog. The parser accepts:

type: Length
type: Force
type: Stress
type: Integer
type: String

All canonical names from the catalog. Sous-typage applies on connection — Force → Numeric is fine, Force → Length is rejected.

type: File # any extension
type: File[pdf] # exactly .pdf
type: File[step,iges,brep] # intersection allowed

Extensions are case-insensitive and never include the leading dot (File[.pdf] is rejected — write File[pdf]). When validating an actual uploaded file, the platform checks the file’s extension against the list.

type: list[Force] # homogeneous list
type: list[Vector3]
type: list[Torsor]

Sous-typage applies element-wise: list[Force] → list[Numeric] ✅, list[Force] → list[Length] ❌.

type: dict[str, Length]
type: dict[str, Force]

Always string-keyed. The value type is the only one parsed — the key is fixed to str.

type: literal[8.8, 10.9, 12.9] # only these values are valid

Useful for enumerations like quality classes. Any value not in the list fails validation.

Either string form or YAML mapping form, both equivalent:

# string form
type: "{F: Force3, M: Moment3}"
# YAML form
type:
F: Force3
M: Moment3

Inline structs match field-by-field on connection — adding or removing a field changes the type. For repeated structs, define them in the catalog (or use the canonical Torsor for {F, M}).

The semantics of required are mode-asymmetric:

Port kindrequired: truerequired: false
InputCaller must supply the value; missing → 422 at submit time.May be omitted; absent from the kwargs / in/data.json.
Output (File)Worker validates the file is present in out/files/; missing → run failure.Worker accepts both presence and absence.
Output (non-File)The function must return the key in its output dict.n/a — non-File outputs are always required and the parser rejects required: false on them.

The default is true everywhere. Only declare required: false when the absence of the value carries real meaning (an optional diagnostic file, a fall-through input).

The Object catalog type is a one-way door. It accepts arbitrary JSON shapes on the input side, but outputs cannot use Object (directly or nested) — the parser rejects them at deploy time.

The reason is workflow typing: a function returning Object is opaque to downstream nodes. If you genuinely have arbitrary output, refactor: return a typed struct, a dict[str, T] for some known T, or split the result into multiple typed outputs.

For File inputs, the file is staged at /workspace/in/files/<port_name>.<ext> where <ext> comes from the caller’s uploaded file. Mode A’s runner passes a pathlib.Path keyed by the port name; modes B/C read it themselves.

For File outputs, the worker expects exactly one file under /workspace/out/files/ whose stem matches the port name — out/files/report.pdf for an output called report. The extension is preserved verbatim in the response.

The extension constraint declared in the manifest (File[pdf]) is checked against:

  • the actual extension of input files at submit time,
  • the actual extension written under out/files/ post-execution.

Mode A is the only mode where you can omit inputs: / outputs:. The deploy parser imports your handler, reads its signature, and infers ports from type hints:

def size(diameter: float, load: float) -> dict:
return {"stress": load / area(diameter)}

Without an inputs: block in mecapy.yml, the parser infers diameter: Float and load: Float. The output uses a permissive result: object because dict (untyped) can’t be narrowed.

For workflow-eligible functions, prefer explicit declaration — introspection falls back to permissive types when the signature is ambiguous, and the workflow editor needs precise types to validate connections. The pedagogical example below shows the recommended shape:

functions:
stress_bolt:
handler: handler:stress_bolt
inputs:
torsor: { type: Torsor, description: "Force/moment torsor at the bolt section." }
diameter: { type: Length, description: "Bolt nominal diameter (m)." }
pitch: { type: Length, description: "Thread pitch (m)." }
outputs:
sigma_eq: { type: Stress, description: "Von Mises equivalent stress (Pa)." }

In modes B and C inputs: / outputs: are mandatory because there is no Python signature to read.

Once parsed, the typed I/O is stored on FunctionVersion.io_spec and exposed via GET /function-versions/{id} as a FunctionIoSpec object. The frontend’s typed AutoForm and the workflow connection checker both consume this spec — declaring it correctly is what unlocks rich UX downstream.