Skip to content

Entity Wrappers for Idempotent Creation

This module provides type-safe wrapper classes for creating new entities (devices, interfaces, groups) with skip-if-identifier-exists semantics, enabling idempotent entity creation in function blocks.

Overview

When creating entities in a function block, you often want to avoid duplicates if an entity with the same identifying fields already exists. The entity wrappers solve this by:

  1. Automatic ID Generation: IDs are generated at instantiation, immediately available for cross-entity references
  2. Mandatory Identifier Fields: Specify which fields identify an entity as "existing"
  3. Type-Safe Field Selection: Enum-based field selectors prevent typos and provide IDE autocomplete
  4. Backend-Handled Deduplication: The backend checks for existing entities based on your identifier fields

Quick Start

from neops_worker_sdk.workflow import WorkflowContext
from neops_worker_sdk.workflow.entities import Device, Interface, DeviceField, InterfaceField
from neops_workflow_engine_client import BaseEntityDto

def execute(context: WorkflowContext):
    # Create a new device - ID is available immediately
    device = Device(
        hostname="router1",
        ip="192.168.1.1",
        skip_if_identifier_exists=True,
        identifier_fields=[DeviceField.hostname, DeviceField.ip],
    )

    # Create interfaces referencing the device
    interface = Interface(
        name="eth0",
        device=BaseEntityDto(id=device.id),  # Cross-reference works!
        skip_if_identifier_exists=True,
        identifier_fields=[InterfaceField.name],
    )

    # Add to context
    context.devices.append(device)
    context.interfaces.append(interface)

Entity Wrapper Classes

Device

Creates a new device with skip-if-identifier-exists support.

from neops_worker_sdk.workflow.entities import Device, DeviceField

device = Device(
    # Standard DeviceTypeDto fields
    hostname="router1",
    ip="192.168.1.1",
    vendor="Cisco",
    model="ISR4451",

    # Required creation options
    skip_if_identifier_exists=True,
    identifier_fields=[DeviceField.hostname, DeviceField.ip],
)

Allowed identifier fields: DeviceField.hostname, DeviceField.ip

Interface

Creates a new interface with skip-if-identifier-exists support.

from neops_worker_sdk.workflow.entities import Interface, InterfaceField
from neops_workflow_engine_client import BaseEntityDto

interface = Interface(
    name="eth0",
    device=BaseEntityDto(id=device.id),
    status="UP",

    skip_if_identifier_exists=True,
    identifier_fields=[InterfaceField.name],
)

Allowed identifier fields: InterfaceField.name

DeviceGroup

Creates a new device group with skip-if-identifier-exists support.

from neops_worker_sdk.workflow.entities import DeviceGroup, GroupField

group = DeviceGroup(
    name="datacenter-1",
    description="Primary datacenter",

    skip_if_identifier_exists=True,
    identifier_fields=[GroupField.name],
)

Allowed identifier fields: GroupField.name

Field Selector Enums

Field selectors provide type-safe specification of which fields to use for uniqueness checks:

Entity Enum Available Fields
Device DeviceField hostname, ip
Interface InterfaceField name
Group GroupField name

Why Enums?

Using enums instead of strings provides:

  • IDE Autocomplete: Your IDE suggests available fields
  • Compile-Time Safety: Type checkers flag invalid field names
  • Runtime Validation: Enums are validated against actual DTO fields at import time
# Good - IDE autocomplete, type-safe
identifier_fields=[DeviceField.hostname]

# Would fail type checking
identifier_fields=["hostnam"]  # Typo not caught at edit time with strings

Identifier Fields Order

The order of identifier_fields matters! Fields are checked in sequence when the backend determines if an entity already exists.

# Checks hostname first, then ip
device = Device(
    hostname="router1",
    ip="192.168.1.1",
    skip_if_identifier_exists=True,
    identifier_fields=[DeviceField.hostname, DeviceField.ip],
)

Automatic ID Generation

IDs are generated automatically when you create a wrapper instance:

device = Device(
    hostname="router1",
    skip_if_identifier_exists=True,
    identifier_fields=[DeviceField.hostname],
)

# ID is immediately available
print(device.id)  # e.g., 12345678

# Use it in related entities
interface = Interface(
    name="eth0",
    device=BaseEntityDto(id=device.id),  # Reference works immediately
    skip_if_identifier_exists=True,
    identifier_fields=[InterfaceField.name],
)

Generated IDs are: - Large integers (≥ 10,000,000) to avoid collisions with existing DB IDs - Cryptographically random for uniqueness - Temporary - the backend may assign a different permanent ID

Mandatory Fields

Both skip_if_identifier_exists and identifier_fields are mandatory with no defaults:

# This fails - IDE shows error, runtime validation fails
device = Device(hostname="router1")  # Missing skip_if_identifier_exists and identifier_fields

# This also fails - they must be used together
device = Device(
    hostname="router1",
    skip_if_identifier_exists=True,
    # Missing identifier_fields
)

# Correct usage
device = Device(
    hostname="router1",
    skip_if_identifier_exists=True,
    identifier_fields=[DeviceField.hostname],
)

Using Lists or Tuples

Both lists and tuples work for identifier_fields:

# List (recommended for readability)
identifier_fields=[DeviceField.hostname, DeviceField.ip]

# Tuple also works
identifier_fields=(DeviceField.hostname, DeviceField.ip)

Integration with WorkflowContext

Entity wrappers integrate seamlessly with the existing WorkflowContext:

def execute(context: WorkflowContext):
    # Create new entities
    device = Device(
        hostname="router1",
        skip_if_identifier_exists=True,
        identifier_fields=[DeviceField.hostname],
    )

    # Add to context lists (wrappers inherit from DTOs)
    context.devices.append(device)

    # On completion, compute_db_updates() generates the appropriate DTOs
    # with skipIfIdentifierExists and identifierFields for the backend

Backend API Contract

When entities are serialized for the backend, the create operation includes:

{
  "operation": "create",
  "entity": "device",
  "id": 12345678,
  "data": {
    "hostname": "router1",
    "ip": "192.168.1.1"
  },
  "skipIfIdentifierExists": true,
  "identifierFields": ["hostname", "ip"]
}

The backend uses identifierFields (in order) to check for existing entities and skips creation if a match is found when skipIfIdentifierExists is true.