Skip to main content
Identity map and caching for Stash entities. Same-ID objects returned from any query share a single Python reference, so updates made anywhere propagate everywhere. See the Architecture Overview for design rationale. The store also provides batched and bulk operations:
  • save_batch(entities) — persist many entities in a single GraphQL round-trip, respecting creates-before-updates ordering and side-mutations.
  • save_all() — flush every dirty or new entity the store is tracking.
  • filter_and_populate(...) — query with filter + batched relationship population in one call, with nested field specs (e.g. files__path).

StashEntityStore

In-memory identity map with read-through caching for Stash entities. Provides caching, selective field loading, and query capabilities for Stash GraphQL entities. All fetched entities are cached, and subsequent requests for the same entity return the cached version (if not expired). All entities can be treated as “stubs” that may have incomplete data. Use populate() to selectively load additional fields as needed, avoiding expensive queries for data you don’t need.
Example
Initialize entity store. Parameters:

Attributes

DEFAULT_QUERY_BATCH

DEFAULT_TTL

FIND_LIMIT

STUB_QUERY_BATCH

cache_size

Get number of entities in cache (deprecated, use cache_stats) (thread-safe). Returns:

Functions

get_cached

Get entity from cache only. No network call (thread-safe). Returns the cached entity if present and not expired, None otherwise. This is the sync counterpart to get, following the Django dual sync/async pattern. Parameters: Returns:

get

Get entity by ID. Checks cache first, fetches if missing/expired (thread-safe). Parameters: Returns:

get_many

Batch get entities. Returns cached + fetches missing in single query (thread-safe). Parameters: Returns:

find

Search using Stash filters. Results cached. Max 1000 results. Parameters: Returns: Raises: Filter syntax
Django-style kwargs
find(Scene, title=“exact”) # EQUALS find(Scene, title__contains=“partial”) # INCLUDES find(Scene, title__regex=r”S\d+”) # MATCHES_REGEX find(Scene, rating100__gte=80) # GREATER_THAN find(Scene, rating100__between=(60,90)) # BETWEEN find(Scene, studio__null=True) # IS_NULL
Raw dict for complex cases
find(Scene, title={“value”: “x”, “modifier”: “NOT_EQUALS”})
Nested filters
find(Scene, performers_filter={“name”: {“value”: “Jane”, “modifier”: “EQUALS”}})

find_one

Search returning first match. Result cached. Parameters: Returns:

find_iter

Lazy search yielding individual items. Batches queries internally. Parameters: Yields:
Exampleasync for scene in store.find_iter(Scene, path__contains=“/media/”): process(scene) if done: break # Won’t fetch remaining batches

filter

Filter cached objects with Python lambda. No network call (thread-safe). Uses the type index for O(k) iteration over only the requested type, instead of snapshotting the entire cache. Parameters: Returns:

all_cached

Get all cached objects of a type. Parameters: Returns:

filter_strict

Filter cached objects, raising error if required fields are missing. This is a fail-fast version of filter() that ensures all cached objects have the required fields populated before applying the predicate. If any cached object is missing required fields, raises ValueError immediately. Supports nested field specifications using Django-style double-underscore syntax: - 'files__path': Validates that files relationship exists AND path is populated on each File - 'studio__parent__name': Validates full nested path is populated Parameters: Returns: Raises: Examples:
This will raise if any performer has rating100=UNSET
high_rated = store.filter_strict( Performer, required_fields=[‘rating100’, ‘favorite’], predicate=lambda p: p.rating100 >= 80 and p.favorite )
Validate nested fields are populated
large_images = store.filter_strict( Image, required_fields=[‘files__path’, ‘files__size’], predicate=lambda i: any( f.size > 10_000_000 for f in i.files if f.size is not None ) )
↑ Raises ValueError if any Image has files=UNSET or any File has path/size=UNSET

filter_and_populate

Filter cached objects, auto-populating missing fields as needed. This is a smart hybrid between find() and filter(): - Gets all cached objects of the type - Identifies which ones have UNSET values for required_fields - Fetches only the missing fields for incomplete objects (in batches) - Applies the predicate to all objects (now with complete data) Supports nested field specifications using Django-style double-underscore syntax: - 'files__path': Ensures files relationship is populated, then path on each File - 'studio__parent__name': Ensures studio, parent, and name are all populated This is much faster than find() when most data is already cached, since it only fetches the specific missing fields rather than re-fetching entire entities. Parameters: Returns: Examples:
Cache has 1000 performers, but only 500 have rating100 loaded
high_rated = await store.filter_and_populate( Performer, required_fields=[‘rating100’, ‘favorite’], predicate=lambda p: p.rating100 >= 80 and p.favorite )
↓ Fetches rating100+favorite for the 500 that don’t have it
↓ Then filters all 1000 with complete data
↓ Network calls: Only for missing data (much faster than find())
Filter images by nested file properties
large_images = await store.filter_and_populate( Image, required_fields=[‘files__path’, ‘files__size’], predicate=lambda i: any( f.size > 10_000_000 for f in i.files if f.size is not None ) )
↓ Fetches files relationship + path/size fields on each File object

filter_and_populate_with_stats

Filter and populate with debug statistics. Same as filter_and_populate() but returns detailed statistics about what was fetched and filtered. Useful for debugging and optimization. Supports nested field specifications using Django-style double-underscore syntax. Parameters: Returns:
TypeDescription
list[T]

Tuple of (matching_entities, stats_dict) where stats contains:

dict[str, Any]
  • total_cached: Total objects in cache
tuple[list[T], dict[str, Any]]
  • needed_population: How many needed fields fetched
tuple[list[T], dict[str, Any]]
  • populated_fields: Which fields were fetched
tuple[list[T], dict[str, Any]]
  • matches: How many matched the predicate
tuple[list[T], dict[str, Any]]
  • cache_hit_rate: Percentage with complete data
Examples: results, stats = await store.filter_and_populate_with_stats( Performer, required_fields=[‘rating100’], predicate=lambda p: p.rating100 >= 80 ) print(f”Populated {stats[‘needed_population’]} of {stats[‘total_cached’]}”) print(f”Cache hit rate: {stats[‘cache_hit_rate’]:.1%}”) print(f”Found {stats[‘matches’]} matches”)
With nested fields
results, stats = await store.filter_and_populate_with_stats( Image, required_fields=[‘files__path’, ‘files__size’], predicate=lambda i: any(f.size > 10_000_000 for f in i.files) )

populated_filter_iter

Lazy filter with auto-population, yielding results incrementally. Like filter_and_populate() but yields results as they become available instead of waiting for all entities to be processed. Useful for large datasets where you want to start processing matches immediately. Supports nested field specifications using Django-style double-underscore syntax. Parameters: Yields: Examples:
Process large dataset incrementally
async for performer in store.populated_filter_iter( Performer, required_fields=[‘rating100’, ‘scenes’], predicate=lambda p: p.rating100 >= 90 and len(p.scenes) > 100 ): # Start processing immediately as matches are found await expensive_operation(performer) if should_stop: break # Can stop early without processing all
With nested fields
async for image in store.populated_filter_iter( Image, required_fields=[‘files__path’, ‘files__size’], predicate=lambda i: any(f.size > 10_000_000 for f in i.files) ): await process_large_image(image)

missing_fields_nested

Check which nested field specifications are missing. Supports both simple and nested (Django-style) field specifications: - Simple: ‘rating100’, ‘favorite’ - Nested: ‘files__path’, ‘studio__parent__name’ Parameters: Returns:
Example
Check if image has files loaded with path field
missing = store.missing_fields_nested( image, ‘title’, # Simple field ‘files__path’ # Nested field )
Returns {‘files__path’} if files.path is not loaded

populate

Populate specific fields on an entity using field-aware fetching. This method uses _received_fields tracking to determine which fields are genuinely missing and need to be fetched. All entities are treated as potentially incomplete. Supports nested field specifications using Django-style double-underscore syntax: - 'files__path': Populate files relationship, then path on each File - 'studio__parent__name': Populate studio, then parent, then name - Can be mixed with regular fields: ['rating100', 'files__path'] Parameters: Returns: Examples:
Populate specific fields on a scene
scene = await store.populate(scene, fields=[“studio”, “performers”])
Populate nested fields using __ syntax
image = await store.populate(image, fields=[“files__path”, “files__size”])
Mix regular and nested fields
scene = await store.populate( scene, fields=[“rating100”, “studio__parent__name”] )
Populate nested object directly (identity map pattern)
scene.studio = await store.populate(scene.studio, fields=[“urls”, “details”])
Force refresh from server (invalidates cache first)
scene = await store.populate(scene, fields=[“studio”], force_refetch=True)
Populate performer from a list
performer = await store.populate( scene.performers[0], fields=[“scenes”, “images”] )
Check what’s missing before populating
missing = store.missing_fields(scene.studio, “urls”, “details”, “aliases”) if missing: scene.studio = await store.populate(scene.studio, fields=list(missing))

has_fields

Check if an object has specific fields populated. Uses _received_fields tracking when available. Parameters: Returns:

missing_fields

Get which of the specified fields are missing from an object. Parameters: Returns:

invalidate

Remove specific object from cache (thread-safe). Can be called with either a type + ID pair, or a StashObject instance directly. Parameters: Examples:

invalidate_type

Remove all objects of a type from cache (thread-safe). Parameters:

invalidate_all

Clear entire cache (thread-safe).

set_ttl

Set TTL for a type. None = use default (or never expire if no default). Parameters: Raises:

add

Add object to cache (for new objects with temp UUIDs). This is typically used with objects created via ClassName.new() that have temporary UUID IDs. After calling obj.save() or store.save(), the cache entry will be updated with the real ID from Stash. Parameters:
Example

save

Save object to Stash and update cache. Handles both new objects (create) and existing objects (update). For new objects, updates cache key from temp UUID to real Stash ID. Automatically cascades saves for unsaved related objects (with warning). Preferred pattern: explicitly save related objects before parent. Parameters: Raises:
Example

save_batch

Batch-save multiple dirty/new objects in a single HTTP request. Groups creates before updates in the alias order so that GraphQL’s sequential execution assigns server IDs to new objects before any updates that might reference them. Side mutations and queued operations are fired sequentially per-entity after the main batch succeeds (same as save). Parameters: Returns: Raises:

save_all

Find all dirty/new entities in cache and batch-save them. Partitions entities into new objects first, then updates, to ensure creates get server IDs before updates that might reference them. Returns: Raises:

delete

Delete object from Stash and remove from cache. Delegates to obj.delete(client) then invalidates the cache entry. Parameters: Returns: Raises: Examples:

get_or_create

Get entity by search criteria, optionally create if not found. Searches for an entity matching the provided criteria. If found, returns the existing entity (from cache or fetched). If not found and create_if_missing is True, creates a new entity with the search params as initial data. Note: New entities are created with UUID IDs and are NOT automatically saved. Call store.save() or entity.save() to persist to Stash. Parameters: Returns: Raises:
Example

is_cached

Check if object is in cache and not expired (thread-safe). Parameters: Returns:

cache_stats

Get cache statistics (thread-safe). Returns: