Quick Reference: UNSET & UUID4 Patterns¶
One-page reference for developers using the UNSET sentinel and UUID4 auto-generation patterns.
UNSET Sentinel¶
Import¶
Three Field States¶
scene.title = "Test" # Level 1: Set to value
scene.title = None # Level 2: Set to null
scene.title = UNSET # Level 3: Never touched
Checking Field State¶
# ✅ CORRECT - Use identity comparison (is)
if scene.title is UNSET:
print("Never touched")
elif scene.title is None:
print("Explicitly null")
else:
print(f"Value: {scene.title}")
# ❌ WRONG - Don't use equality or boolean
if not scene.title: # Ambiguous! Could be UNSET, None, or empty string
pass
Field Definitions¶
class MyEntity(StashObject):
# Required field with UNSET default
name: str | UnsetType = UNSET
# Optional field (can be null)
description: str | None | UnsetType = UNSET
# Required field (no default)
id: str
to_input() Pattern¶
async def to_input(self) -> dict[str, Any]:
data = {}
# Only include non-UNSET fields
if self.name is not UNSET:
data["name"] = self.name
if self.description is not UNSET:
data["description"] = self.description # Could be None or value
return data
Common Patterns¶
# Describe field state
def describe(value):
if value is UNSET:
return "UNSET"
elif value is None:
return "NULL"
else:
return f"VALUE: {value}"
# Build GraphQL input excluding UNSET
def build_input(**fields):
return {k: v for k, v in fields.items() if v is not UNSET}
# Set field conditionally
if some_condition:
scene.rating100 = 85
# If not set, rating100 stays UNSET (no data to send)
UUID4 Auto-Generation¶
Creating New Objects¶
# UUID4 is auto-generated
scene = Scene(title="Test")
print(scene.id) # "a1b2c3d4e5f6789012345678901234ab"
print(scene.is_new()) # True
# Explicit ID (no UUID generated)
scene = Scene(id="123", title="Test")
print(scene.is_new()) # False
Checking if New¶
if scene.is_new():
print("This is a new object with temporary UUID")
else:
print("This is an existing object with server ID")
Save Workflow¶
# Create new object
scene = Scene(title="Test")
temp_id = scene.id # UUID4
# Save to server
await scene.save(client)
# ID is now server-assigned
print(scene.id) # "456" (server ID)
print(scene.is_new()) # False
print(temp_id != scene.id) # True
Manual ID Update¶
# Normally done automatically by save()
scene = Scene(title="Test")
scene.update_id("789") # Replace UUID with server ID
Detection Logic¶
# is_new() returns True if:
# - ID is 32 hex characters (UUID4)
# - ID is legacy "new" marker
# - ID is empty/None
# Examples:
Scene(title="Test").is_new() # True (UUID4)
Scene(id="new", ...).is_new() # True (legacy marker)
Scene(id="123", ...).is_new() # False (server ID)
Scene(id="", ...).is_new() # True (empty)
Combined Usage¶
Creating and Saving New Object¶
from stash_graphql_client.types import Scene, UNSET
# Create new scene with partial data
scene = Scene(
title="My Scene", # Set
rating100=None, # Explicitly null
# details = UNSET # Never touched (implicit)
)
print(scene.is_new()) # True
print(scene.id[:8]) # "a1b2c3d4" (UUID4 prefix)
# to_input() includes only set fields
input_dict = await scene.to_input()
# {"title": "My Scene", "rating100": null}
# "details" excluded because UNSET
# Save to server
await scene.save(client)
print(scene.is_new()) # False
print(scene.id) # "123" (server ID)
Partial Update¶
# Fetch existing object
scene = await client.find_scene("123")
# Update only one field
scene.title = "Updated Title"
# Other fields remain unchanged (not marked dirty)
# to_input() only includes changed fields + ID
input_dict = await scene.to_input()
# {"id": "123", "title": "Updated Title"}
# Save sends minimal update
await scene.save(client)
Explicit Null vs UNSET¶
scene = await client.find_scene("123")
# Set to null (clear server value)
scene.rating100 = None
await scene.save(client)
# Server: rating100 = null
# Leave UNSET (don't touch server value)
scene.details = UNSET
await scene.save(client)
# Server: details unchanged
Testing Patterns¶
Test UNSET¶
async def test_unset_excluded():
scene = Scene(id="123", title="Test")
# rating100 is UNSET by default (no data for this field)
input_dict = await scene.to_input()
assert "title" in input_dict
assert "rating100" not in input_dict # UNSET = no data to send
Test UUID4¶
def test_new_object_gets_uuid():
scene = Scene(title="Test")
assert scene.id is not None
assert len(scene.id) == 32
assert scene.is_new() is True
async def test_save_updates_id(client):
scene = Scene(title="Test")
original_id = scene.id
# Mock response
respx.post("http://localhost:9999/graphql").mock(
return_value=httpx.Response(200, json={"data": {"sceneCreate": {"id": "456"}}})
)
await scene.save(client)
assert scene.id == "456"
assert scene.id != original_id
assert scene.is_new() is False
Common Mistakes¶
❌ Wrong: Using == with UNSET¶
✅ Right: Using is with UNSET¶
❌ Wrong: Boolean check for UNSET¶
✅ Right: Explicit UNSET check¶
❌ Wrong: Manually setting UUID¶
✅ Right: Let it auto-generate¶
Type Annotations¶
For Fields¶
from stash_graphql_client.types.unset import UnsetType
# Required field with UNSET default
title: str | UnsetType = UNSET
# Optional field (nullable)
description: str | None | UnsetType = UNSET
# Required field (no UNSET)
id: str
For Functions¶
from typing import Any
def process_field(value: str | None | UnsetType) -> Any:
if value is UNSET:
return None
elif value is None:
return "NULL"
else:
return value.upper() # mypy knows it's str here
Performance Notes¶
- UUID4 generation: ~0.1μs per object
- UNSET check: O(1) identity comparison
- Memory: UNSET is a singleton (minimal overhead)
Summary¶
| Pattern | Use When | Example |
|---|---|---|
| Set to value | Field has a value | scene.title = "Test" |
| Set to null | Want to clear server value | scene.rating100 = None |
| UNSET | No data for this field | Default state for unloaded fields |
| Auto UUID | Creating new object | scene = Scene(title="Test") |
| is_new() | Check if saved | if scene.is_new(): ... |
| update_id() | After create (auto in save) | scene.update_id("123") |
Convenience Helper Methods¶
Some entity types provide convenience methods for relationship management.
Scene Helpers (7 methods)¶
# Add/remove entities from scene
scene.add_to_gallery(gallery) # Add scene to gallery
scene.remove_from_gallery(gallery) # Remove scene from gallery
scene.add_performer(performer) # Add performer to scene
scene.remove_performer(performer) # Remove performer from scene
scene.add_tag(tag) # Add tag to scene
scene.remove_tag(tag) # Remove tag from scene
scene.set_studio(studio) # Set scene's studio
Tag Helpers (6 methods)¶
# Parent/child relationships (bidirectional)
tag.add_parent(parent_tag) # Add parent tag (syncs both sides)
tag.remove_parent(parent_tag) # Remove parent tag (syncs both sides)
tag.add_child(child_tag) # Add child tag (syncs both sides)
tag.remove_child(child_tag) # Remove child tag (syncs both sides)
# Recursive hierarchy traversal
descendants = tag.get_all_descendants() # Get all descendant tags
ancestors = tag.get_all_ancestors() # Get all ancestor tags
Entity Mapping Helpers¶
Convert entity names to IDs with auto-creation support:
# Map tag names to IDs
tag_ids = await client.map_tag_ids(
["Action", "Drama", "NewTag"],
create=True # Auto-create missing tags
)
# Map studio names to IDs
studio_ids = await client.map_studio_ids(
["Studio A", "Studio B"],
create=True
)
# Map performer names to IDs (includes alias search)
performer_ids = await client.map_performer_ids(
["Jane Doe", "John Smith"],
create=True,
on_multiple=OnMultipleMatch.RETURN_FIRST # Handle duplicates
)
Studio Hierarchy Helpers¶
# Get full parent chain from root to studio
hierarchy = await client.find_studio_hierarchy(studio_id)
# Returns: [<Root Studio>, <Parent Studio>, <Child Studio>]
# Find the top-level parent studio
root = await client.find_studio_root(studio_id)
Notes¶
- Group convenience helpers are documented in Bidirectional Relationships but not yet implemented. Currently, manage group relationships using direct field assignment with
containing_groupsandsub_groupsfields. - Other entity types (Performer, Gallery, Image, etc.) do not have convenience helpers - use direct field assignment instead.
Additional Resources¶
- Full Guide: UNSET & UUID4 Patterns - Comprehensive guide with examples
- Usage Examples: Convenience Methods - ID mapping, hierarchy navigation
- Architecture: Bidirectional Relationships - Entity relationship patterns
- API Reference: StashEntityStore - Identity map and caching