feat: add workflow node registry phase 2
This commit is contained in:
@@ -4,6 +4,7 @@ from copy import deepcopy
|
||||
from typing import Any
|
||||
|
||||
from app.core.process_steps import StepName
|
||||
from app.domains.rendering.workflow_node_registry import get_node_type_for_step
|
||||
|
||||
|
||||
WorkflowPresetType = str
|
||||
@@ -16,18 +17,10 @@ _PRESET_TYPES = {
|
||||
"custom",
|
||||
}
|
||||
|
||||
_STEP_TO_NODE_TYPE: dict[str, str] = {
|
||||
StepName.RESOLVE_STEP_PATH.value: "inputNode",
|
||||
StepName.STL_CACHE_GENERATE.value: "convertNode",
|
||||
StepName.BLENDER_STILL.value: "renderNode",
|
||||
StepName.BLENDER_TURNTABLE.value: "renderFramesNode",
|
||||
StepName.OUTPUT_SAVE.value: "outputNode",
|
||||
StepName.EXPORT_BLEND.value: "outputNode",
|
||||
}
|
||||
|
||||
_NODE_TYPE_TO_STEP: dict[str, str] = {
|
||||
"inputNode": StepName.RESOLVE_STEP_PATH.value,
|
||||
"convertNode": StepName.STL_CACHE_GENERATE.value,
|
||||
"processNode": StepName.ORDER_LINE_SETUP.value,
|
||||
"renderNode": StepName.BLENDER_STILL.value,
|
||||
"renderFramesNode": StepName.BLENDER_TURNTABLE.value,
|
||||
"ffmpegNode": StepName.OUTPUT_SAVE.value,
|
||||
@@ -50,7 +43,7 @@ def _make_node(
|
||||
"step": step.value,
|
||||
"params": deepcopy(params or {}),
|
||||
"ui": {
|
||||
"type": node_type or _STEP_TO_NODE_TYPE.get(step.value),
|
||||
"type": node_type or get_node_type_for_step(step.value),
|
||||
"position": {"x": x, "y": y},
|
||||
"label": label,
|
||||
},
|
||||
|
||||
@@ -0,0 +1,417 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Literal
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from app.core.process_steps import StepName
|
||||
|
||||
|
||||
StepCategory = Literal["input", "processing", "rendering", "output"]
|
||||
FieldType = Literal["number", "select", "boolean"]
|
||||
ExecutionKind = Literal["native", "bridge"]
|
||||
|
||||
|
||||
class WorkflowNodeFieldOption(BaseModel):
|
||||
value: str | int | float | bool
|
||||
label: str
|
||||
|
||||
|
||||
class WorkflowNodeFieldDefinition(BaseModel):
|
||||
key: str
|
||||
label: str
|
||||
type: FieldType
|
||||
description: str = ""
|
||||
section: str = "General"
|
||||
default: Any = None
|
||||
min: float | None = None
|
||||
max: float | None = None
|
||||
step: float | None = None
|
||||
unit: str | None = None
|
||||
options: list[WorkflowNodeFieldOption] = []
|
||||
|
||||
|
||||
class WorkflowNodeDefinition(BaseModel):
|
||||
step: str
|
||||
label: str
|
||||
category: StepCategory
|
||||
description: str
|
||||
node_type: str
|
||||
icon: str
|
||||
defaults: dict[str, Any] = {}
|
||||
fields: list[WorkflowNodeFieldDefinition] = []
|
||||
execution_kind: ExecutionKind = "native"
|
||||
legacy_compatible: bool = True
|
||||
|
||||
|
||||
def _field(
|
||||
key: str,
|
||||
label: str,
|
||||
field_type: FieldType,
|
||||
*,
|
||||
description: str = "",
|
||||
section: str = "General",
|
||||
default: Any = None,
|
||||
min: float | None = None,
|
||||
max: float | None = None,
|
||||
step: float | None = None,
|
||||
unit: str | None = None,
|
||||
options: list[tuple[str | int | float | bool, str]] | None = None,
|
||||
) -> WorkflowNodeFieldDefinition:
|
||||
return WorkflowNodeFieldDefinition(
|
||||
key=key,
|
||||
label=label,
|
||||
type=field_type,
|
||||
description=description,
|
||||
section=section,
|
||||
default=default,
|
||||
min=min,
|
||||
max=max,
|
||||
step=step,
|
||||
unit=unit,
|
||||
options=[
|
||||
WorkflowNodeFieldOption(value=value, label=option_label)
|
||||
for value, option_label in (options or [])
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
def _definition(
|
||||
step: StepName,
|
||||
label: str,
|
||||
category: StepCategory,
|
||||
description: str,
|
||||
*,
|
||||
node_type: str,
|
||||
icon: str,
|
||||
defaults: dict[str, Any] | None = None,
|
||||
fields: list[WorkflowNodeFieldDefinition] | None = None,
|
||||
execution_kind: ExecutionKind = "native",
|
||||
) -> WorkflowNodeDefinition:
|
||||
return WorkflowNodeDefinition(
|
||||
step=step.value,
|
||||
label=label,
|
||||
category=category,
|
||||
description=description,
|
||||
node_type=node_type,
|
||||
icon=icon,
|
||||
defaults=defaults or {},
|
||||
fields=fields or [],
|
||||
execution_kind=execution_kind,
|
||||
)
|
||||
|
||||
|
||||
_NODE_DEFINITIONS: list[WorkflowNodeDefinition] = [
|
||||
_definition(
|
||||
StepName.RESOLVE_STEP_PATH,
|
||||
"Resolve STEP Path",
|
||||
"input",
|
||||
"Locate the STEP file on disk from the CAD file record.",
|
||||
node_type="inputNode",
|
||||
icon="file-up",
|
||||
execution_kind="bridge",
|
||||
),
|
||||
_definition(
|
||||
StepName.OCC_OBJECT_EXTRACT,
|
||||
"Extract STEP Objects",
|
||||
"processing",
|
||||
"Extract part objects and metadata from the STEP file via OCC/cadquery.",
|
||||
node_type="processNode",
|
||||
icon="layers",
|
||||
execution_kind="bridge",
|
||||
),
|
||||
_definition(
|
||||
StepName.OCC_GLB_EXPORT,
|
||||
"Export GLB",
|
||||
"processing",
|
||||
"Convert STEP geometry into GLB for previews and downstream rendering.",
|
||||
node_type="processNode",
|
||||
icon="refresh-cw",
|
||||
execution_kind="bridge",
|
||||
),
|
||||
_definition(
|
||||
StepName.GLB_BBOX,
|
||||
"Compute Bounding Box",
|
||||
"processing",
|
||||
"Compute the model bounding box from the exported GLB for framing decisions.",
|
||||
node_type="processNode",
|
||||
icon="layers",
|
||||
execution_kind="bridge",
|
||||
),
|
||||
_definition(
|
||||
StepName.MATERIAL_MAP_RESOLVE,
|
||||
"Resolve Material Map",
|
||||
"processing",
|
||||
"Map raw part material names to HartOMat material records via aliases.",
|
||||
node_type="processNode",
|
||||
icon="layers",
|
||||
execution_kind="bridge",
|
||||
),
|
||||
_definition(
|
||||
StepName.AUTO_POPULATE_MATERIALS,
|
||||
"Auto Populate Materials",
|
||||
"processing",
|
||||
"Create missing material records for newly discovered part materials.",
|
||||
node_type="processNode",
|
||||
icon="layers",
|
||||
execution_kind="bridge",
|
||||
),
|
||||
_definition(
|
||||
StepName.BLENDER_RENDER,
|
||||
"Render Thumbnail (Blender)",
|
||||
"rendering",
|
||||
"Render a thumbnail image with Blender.",
|
||||
node_type="renderNode",
|
||||
icon="camera",
|
||||
defaults={"render_engine": "cycles", "samples": 64, "width": 512, "height": 512},
|
||||
fields=[
|
||||
_field(
|
||||
"render_engine",
|
||||
"Render Engine",
|
||||
"select",
|
||||
description="Renderer backend for this Blender job.",
|
||||
section="Render",
|
||||
default="cycles",
|
||||
options=[("cycles", "Cycles"), ("eevee", "EEVEE")],
|
||||
),
|
||||
_field(
|
||||
"samples",
|
||||
"Samples",
|
||||
"number",
|
||||
description="Quality samples for the render.",
|
||||
section="Render",
|
||||
default=64,
|
||||
min=1,
|
||||
max=4096,
|
||||
step=1,
|
||||
),
|
||||
_field("width", "Width", "number", section="Output", default=512, min=64, max=8192, step=1, unit="px"),
|
||||
_field("height", "Height", "number", section="Output", default=512, min=64, max=8192, step=1, unit="px"),
|
||||
],
|
||||
),
|
||||
_definition(
|
||||
StepName.THREEJS_RENDER,
|
||||
"Render Thumbnail (Three.js)",
|
||||
"rendering",
|
||||
"Render a thumbnail image with the headless Three.js renderer.",
|
||||
node_type="renderNode",
|
||||
icon="camera",
|
||||
defaults={"width": 512, "height": 512, "transparent_bg": True},
|
||||
fields=[
|
||||
_field("width", "Width", "number", section="Output", default=512, min=64, max=8192, step=1, unit="px"),
|
||||
_field("height", "Height", "number", section="Output", default=512, min=64, max=8192, step=1, unit="px"),
|
||||
_field(
|
||||
"transparent_bg",
|
||||
"Transparent Background",
|
||||
"boolean",
|
||||
description="Render with alpha channel instead of an opaque background.",
|
||||
section="Output",
|
||||
default=True,
|
||||
),
|
||||
],
|
||||
execution_kind="bridge",
|
||||
),
|
||||
_definition(
|
||||
StepName.THUMBNAIL_SAVE,
|
||||
"Save Thumbnail",
|
||||
"output",
|
||||
"Persist the generated thumbnail back onto the CAD file record.",
|
||||
node_type="outputNode",
|
||||
icon="download",
|
||||
execution_kind="bridge",
|
||||
),
|
||||
_definition(
|
||||
StepName.ORDER_LINE_SETUP,
|
||||
"Order Line Setup",
|
||||
"processing",
|
||||
"Validate order-line inputs and prepare the render job context.",
|
||||
node_type="processNode",
|
||||
icon="layers",
|
||||
execution_kind="bridge",
|
||||
),
|
||||
_definition(
|
||||
StepName.RESOLVE_TEMPLATE,
|
||||
"Resolve Template",
|
||||
"processing",
|
||||
"Resolve the render template for the order line and output type.",
|
||||
node_type="processNode",
|
||||
icon="layers",
|
||||
execution_kind="bridge",
|
||||
),
|
||||
_definition(
|
||||
StepName.BLENDER_STILL,
|
||||
"Render Still",
|
||||
"rendering",
|
||||
"Render a production still image with Blender.",
|
||||
node_type="renderNode",
|
||||
icon="camera",
|
||||
defaults={"render_engine": "cycles", "samples": 256, "width": 2048, "height": 2048, "rotation_z": 0},
|
||||
fields=[
|
||||
_field(
|
||||
"render_engine",
|
||||
"Render Engine",
|
||||
"select",
|
||||
description="Renderer backend for the still render.",
|
||||
section="Render",
|
||||
default="cycles",
|
||||
options=[("cycles", "Cycles"), ("eevee", "EEVEE")],
|
||||
),
|
||||
_field(
|
||||
"samples",
|
||||
"Samples",
|
||||
"number",
|
||||
description="Quality samples for the still render.",
|
||||
section="Render",
|
||||
default=256,
|
||||
min=1,
|
||||
max=4096,
|
||||
step=1,
|
||||
),
|
||||
_field("width", "Width", "number", section="Output", default=2048, min=64, max=8192, step=1, unit="px"),
|
||||
_field("height", "Height", "number", section="Output", default=2048, min=64, max=8192, step=1, unit="px"),
|
||||
_field(
|
||||
"rotation_z",
|
||||
"Rotation Z",
|
||||
"number",
|
||||
description="Additional Z-axis rotation in degrees.",
|
||||
section="Camera",
|
||||
default=0,
|
||||
min=-360,
|
||||
max=360,
|
||||
step=1,
|
||||
unit="deg",
|
||||
),
|
||||
],
|
||||
),
|
||||
_definition(
|
||||
StepName.BLENDER_TURNTABLE,
|
||||
"Render Turntable",
|
||||
"rendering",
|
||||
"Render an animated turntable sequence with Blender.",
|
||||
node_type="renderFramesNode",
|
||||
icon="film",
|
||||
defaults={
|
||||
"render_engine": "cycles",
|
||||
"samples": 64,
|
||||
"width": 2048,
|
||||
"height": 2048,
|
||||
"fps": 24,
|
||||
"duration_s": 5,
|
||||
"rotation_z": 0,
|
||||
},
|
||||
fields=[
|
||||
_field(
|
||||
"render_engine",
|
||||
"Render Engine",
|
||||
"select",
|
||||
description="Renderer backend for the turntable job.",
|
||||
section="Render",
|
||||
default="cycles",
|
||||
options=[("cycles", "Cycles"), ("eevee", "EEVEE")],
|
||||
),
|
||||
_field(
|
||||
"samples",
|
||||
"Samples",
|
||||
"number",
|
||||
description="Quality samples for each frame.",
|
||||
section="Render",
|
||||
default=64,
|
||||
min=1,
|
||||
max=4096,
|
||||
step=1,
|
||||
),
|
||||
_field("width", "Width", "number", section="Output", default=2048, min=64, max=8192, step=1, unit="px"),
|
||||
_field("height", "Height", "number", section="Output", default=2048, min=64, max=8192, step=1, unit="px"),
|
||||
_field("fps", "FPS", "number", section="Animation", default=24, min=1, max=120, step=1),
|
||||
_field(
|
||||
"duration_s",
|
||||
"Duration",
|
||||
"number",
|
||||
description="Length of the animation clip.",
|
||||
section="Animation",
|
||||
default=5,
|
||||
min=1,
|
||||
max=120,
|
||||
step=1,
|
||||
unit="s",
|
||||
),
|
||||
_field(
|
||||
"rotation_z",
|
||||
"Rotation Z",
|
||||
"number",
|
||||
description="Base Z-axis rotation before the turntable motion is applied.",
|
||||
section="Camera",
|
||||
default=0,
|
||||
min=-360,
|
||||
max=360,
|
||||
step=1,
|
||||
unit="deg",
|
||||
),
|
||||
],
|
||||
),
|
||||
_definition(
|
||||
StepName.OUTPUT_SAVE,
|
||||
"Save Output",
|
||||
"output",
|
||||
"Persist the rendered output file and create the media record.",
|
||||
node_type="outputNode",
|
||||
icon="download",
|
||||
execution_kind="bridge",
|
||||
),
|
||||
_definition(
|
||||
StepName.EXPORT_BLEND,
|
||||
"Export Blend",
|
||||
"output",
|
||||
"Persist the generated .blend file as a downloadable media asset.",
|
||||
node_type="outputNode",
|
||||
icon="download",
|
||||
execution_kind="bridge",
|
||||
),
|
||||
_definition(
|
||||
StepName.STL_CACHE_GENERATE,
|
||||
"Generate STL Cache",
|
||||
"processing",
|
||||
"Generate and cache STL derivatives next to the STEP source.",
|
||||
node_type="convertNode",
|
||||
icon="refresh-cw",
|
||||
execution_kind="bridge",
|
||||
),
|
||||
_definition(
|
||||
StepName.NOTIFY,
|
||||
"Notify",
|
||||
"output",
|
||||
"Emit a user-visible notification for workflow completion or failure.",
|
||||
node_type="outputNode",
|
||||
icon="bell",
|
||||
defaults={"channel": "audit_log"},
|
||||
fields=[
|
||||
_field(
|
||||
"channel",
|
||||
"Channel",
|
||||
"select",
|
||||
description="Notification target channel.",
|
||||
section="Notification",
|
||||
default="audit_log",
|
||||
options=[("audit_log", "Audit Log")],
|
||||
),
|
||||
],
|
||||
execution_kind="bridge",
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
_NODE_DEFINITION_BY_STEP = {definition.step: definition for definition in _NODE_DEFINITIONS}
|
||||
|
||||
|
||||
def list_node_definitions() -> list[WorkflowNodeDefinition]:
|
||||
return list(_NODE_DEFINITIONS)
|
||||
|
||||
|
||||
def get_node_definition(step: StepName | str) -> WorkflowNodeDefinition | None:
|
||||
step_value = step.value if isinstance(step, StepName) else step
|
||||
return _NODE_DEFINITION_BY_STEP.get(step_value)
|
||||
|
||||
|
||||
def get_node_type_for_step(step: StepName | str) -> str | None:
|
||||
definition = get_node_definition(step)
|
||||
return definition.node_type if definition else None
|
||||
@@ -1,16 +1,5 @@
|
||||
"""Workflow definition CRUD API.
|
||||
|
||||
Endpoints:
|
||||
GET /api/workflows/ — list all workflow definitions (admin/PM)
|
||||
GET /api/workflows/pipeline-steps — list available pipeline step definitions
|
||||
GET /api/workflows/{id} — get single definition (admin/PM)
|
||||
POST /api/workflows/ — create definition (admin only)
|
||||
PUT /api/workflows/{id} — update definition (admin only)
|
||||
DELETE /api/workflows/{id} — delete definition (admin only)
|
||||
GET /api/workflows/{id}/runs — list runs for a definition (admin/PM)
|
||||
"""
|
||||
"""Workflow definition CRUD API."""
|
||||
import uuid
|
||||
from typing import Literal
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from pydantic import BaseModel, ValidationError
|
||||
@@ -29,53 +18,12 @@ from app.domains.rendering.schemas import (
|
||||
WorkflowRunOut,
|
||||
)
|
||||
from app.domains.rendering.workflow_config_utils import canonicalize_workflow_config
|
||||
from app.domains.rendering.workflow_node_registry import (
|
||||
StepCategory,
|
||||
WorkflowNodeDefinition,
|
||||
list_node_definitions,
|
||||
)
|
||||
from app.domains.rendering.workflow_schema import WorkflowConfig
|
||||
from app.core.process_steps import StepName
|
||||
|
||||
|
||||
# ── Pipeline-step metadata helpers ──────────────────────────────────────────
|
||||
|
||||
StepCategory = Literal["input", "processing", "rendering", "output"]
|
||||
|
||||
_STEP_CATEGORIES: dict[StepName, StepCategory] = {
|
||||
StepName.RESOLVE_STEP_PATH: "input",
|
||||
StepName.OCC_OBJECT_EXTRACT: "processing",
|
||||
StepName.OCC_GLB_EXPORT: "processing",
|
||||
StepName.GLB_BBOX: "processing",
|
||||
StepName.MATERIAL_MAP_RESOLVE: "processing",
|
||||
StepName.AUTO_POPULATE_MATERIALS: "processing",
|
||||
StepName.BLENDER_RENDER: "rendering",
|
||||
StepName.THREEJS_RENDER: "rendering",
|
||||
StepName.THUMBNAIL_SAVE: "output",
|
||||
StepName.ORDER_LINE_SETUP: "processing",
|
||||
StepName.RESOLVE_TEMPLATE: "processing",
|
||||
StepName.BLENDER_STILL: "rendering",
|
||||
StepName.BLENDER_TURNTABLE: "rendering",
|
||||
StepName.OUTPUT_SAVE: "output",
|
||||
StepName.EXPORT_BLEND: "output",
|
||||
StepName.STL_CACHE_GENERATE: "processing",
|
||||
StepName.NOTIFY: "output",
|
||||
}
|
||||
|
||||
_STEP_DESCRIPTIONS: dict[StepName, str] = {
|
||||
StepName.RESOLVE_STEP_PATH: "Locate the STEP file on disk from the CadFile record",
|
||||
StepName.OCC_OBJECT_EXTRACT: "Extract part objects and metadata from the STEP file using cadquery/OCC",
|
||||
StepName.OCC_GLB_EXPORT: "Convert STEP geometry to glTF/GLB via cadquery",
|
||||
StepName.GLB_BBOX: "Compute bounding-box from the exported GLB for camera framing",
|
||||
StepName.MATERIAL_MAP_RESOLVE: "Resolve raw part-material names to HARTOMAT library materials via alias table",
|
||||
StepName.AUTO_POPULATE_MATERIALS: "Auto-create Material records for any newly discovered part names",
|
||||
StepName.BLENDER_RENDER: "Render a thumbnail PNG using Blender (Cycles or EEVEE)",
|
||||
StepName.THREEJS_RENDER: "Render a thumbnail PNG using Three.js / Playwright headless browser",
|
||||
StepName.THUMBNAIL_SAVE: "Persist the rendered thumbnail bytes to the CadFile record",
|
||||
StepName.ORDER_LINE_SETUP: "Validate and prepare an order line for rendering (check STEP path, output type)",
|
||||
StepName.RESOLVE_TEMPLATE: "Look up the matching RenderTemplate for the order line's category + output type",
|
||||
StepName.BLENDER_STILL: "Render a production still image (PNG) via Blender HTTP micro-service",
|
||||
StepName.BLENDER_TURNTABLE: "Render all turntable animation frames via Blender HTTP micro-service",
|
||||
StepName.OUTPUT_SAVE: "Upload the rendered output file to storage and create a MediaAsset record",
|
||||
StepName.EXPORT_BLEND: "Save the production .blend file as a downloadable MediaAsset",
|
||||
StepName.STL_CACHE_GENERATE: "Convert STEP → STL (low + high quality) and cache next to the STEP file",
|
||||
StepName.NOTIFY: "Emit a user notification via the audit-log notification channel",
|
||||
}
|
||||
|
||||
|
||||
class PipelineStepOut(BaseModel):
|
||||
@@ -88,6 +36,11 @@ class PipelineStepOut(BaseModel):
|
||||
class PipelineStepsResponse(BaseModel):
|
||||
steps: list[PipelineStepOut]
|
||||
|
||||
|
||||
class NodeDefinitionsResponse(BaseModel):
|
||||
definitions: list[WorkflowNodeDefinition]
|
||||
|
||||
|
||||
router = APIRouter(prefix="/api/workflows", tags=["workflows"])
|
||||
|
||||
|
||||
@@ -102,19 +55,25 @@ def _workflow_to_out(wf: WorkflowDefinition) -> WorkflowDefinitionOut:
|
||||
)
|
||||
|
||||
|
||||
@router.get("/node-definitions", response_model=NodeDefinitionsResponse)
|
||||
async def get_node_definitions(
|
||||
_user: User = Depends(require_admin_or_pm),
|
||||
):
|
||||
return NodeDefinitionsResponse(definitions=list_node_definitions())
|
||||
|
||||
|
||||
@router.get("/pipeline-steps", response_model=PipelineStepsResponse)
|
||||
async def get_pipeline_steps(
|
||||
_user: User = Depends(require_admin_or_pm),
|
||||
):
|
||||
"""Return all available pipeline step definitions for the workflow editor."""
|
||||
steps = [
|
||||
PipelineStepOut(
|
||||
name=step.value,
|
||||
label=step.value.replace("_", " ").title(),
|
||||
category=_STEP_CATEGORIES.get(step, "processing"),
|
||||
description=_STEP_DESCRIPTIONS.get(step, ""),
|
||||
name=definition.step,
|
||||
label=definition.label,
|
||||
category=definition.category,
|
||||
description=definition.description,
|
||||
)
|
||||
for step in StepName
|
||||
for definition in list_node_definitions()
|
||||
]
|
||||
return PipelineStepsResponse(steps=steps)
|
||||
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import pytest
|
||||
|
||||
from app.core.process_steps import StepName
|
||||
from app.domains.rendering.workflow_node_registry import (
|
||||
get_node_definition,
|
||||
list_node_definitions,
|
||||
)
|
||||
|
||||
|
||||
def test_node_registry_covers_all_step_names():
|
||||
registered_steps = {definition.step for definition in list_node_definitions()}
|
||||
expected_steps = {step.value for step in StepName}
|
||||
|
||||
assert registered_steps == expected_steps
|
||||
|
||||
|
||||
def test_turntable_node_definition_exposes_expected_schema():
|
||||
definition = get_node_definition(StepName.BLENDER_TURNTABLE)
|
||||
|
||||
assert definition is not None
|
||||
assert definition.node_type == "renderFramesNode"
|
||||
assert definition.defaults["fps"] == 24
|
||||
assert definition.defaults["duration_s"] == 5
|
||||
assert {field.key for field in definition.fields} >= {
|
||||
"render_engine",
|
||||
"samples",
|
||||
"width",
|
||||
"height",
|
||||
"fps",
|
||||
"duration_s",
|
||||
"rotation_z",
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_node_definitions_endpoint_returns_registry(client, auth_headers):
|
||||
response = await client.get("/api/workflows/node-definitions", headers=auth_headers)
|
||||
|
||||
assert response.status_code == 200
|
||||
body = response.json()
|
||||
assert len(body["definitions"]) == len(StepName)
|
||||
|
||||
blender_still = next(
|
||||
definition for definition in body["definitions"] if definition["step"] == StepName.BLENDER_STILL.value
|
||||
)
|
||||
assert blender_still["node_type"] == "renderNode"
|
||||
assert blender_still["defaults"]["render_engine"] == "cycles"
|
||||
Reference in New Issue
Block a user