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:
- Automatic ID Generation: IDs are generated at instantiation, immediately available for cross-entity references
- Mandatory Identifier Fields: Specify which fields identify an entity as "existing"
- Type-Safe Field Selection: Enum-based field selectors prevent typos and provide IDE autocomplete
- 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.