Why This Library Exists
Interacting with GraphQL APIs typically requires:- Manually constructing query and mutation strings - Error-prone string building with no compile-time validation
- Building complex nested filter objects - GraphQL filter syntax can be verbose and difficult to construct
- Tracking which fields have been fetched - No automatic way to know which fields are available vs which need to be queried
- Managing object identity across responses - The same entity ID may appear in different responses as different object instances, leading to stale data
- Implementing change tracking - No built-in way to avoid overwriting unmodified fields in mutations
- SQLAlchemy - Identity map pattern, session management, relationship tracking
- Django ORM - Filter syntax (
field__modifier), model validation, query building - Rails ActiveRecord - Entity-centric CRUD operations (
.save(),.delete()), relationship helpers
Core Architectural Patterns
Identity Map Pattern
Same entity ID always returns the same Python object reference across your entire application. This prevents stale data and eliminates the need for manual cache synchronization.UNSET Sentinel Pattern
Three-state field system distinguishes between:- Value - Field has been set to an actual value
- None - Field has been explicitly set to null
- UNSET - Field has never been queried or touched
- Partial queries - Load only needed fields, leave rest as UNSET
- Partial updates - Modify specific fields without affecting others
- Avoiding race conditions - Update one field without overwriting concurrent changes to other fields
- Sparse field selection - Match GraphQL’s sparse field semantics in Python objects
Architecture
This library follows a three-layer architecture. For a detailed technical overview, see the Architecture Overview. Quick summary:- Layer 1: StashClient - GraphQL transport layer with HTTP/WebSocket support
- Layer 2: Pydantic Types - Schema/ORM layer with validation and change tracking
- Layer 3: StashEntityStore - Identity map and caching layer
Type Safety with Pydantic v2
All GraphQL types are PydanticBaseModel subclasses with full runtime validation:
- Catch errors at development time (not runtime)
- IDE autocomplete for all fields
- Runtime validation ensures data integrity
- Alias mapping handles GraphQL naming conventions (camelCase ↔ snake_case)
Relationship Metadata
Relationships are documented viaRelationshipMetadata objects that specify:
- target_field - Field name in mutation input (e.g.,
"studio_id") - query_field - Field name in query response (e.g.,
"studio") - inverse_type - Related entity type name
- inverse_query_field - Inverse field name for bidirectional sync
- query_strategy - How to query the relationship (
"direct_field","filter_query", or"complex_object")
Field-Aware Population
Track which fields have been fetched and load missing fields on demand:- Load expensive fields (large lists) only when needed
- Avoid re-fetching data already in memory
- Progressive data loading
- Optimize network usage for large result sets
When to Use Each Layer
Use StashClient when:
- ✅ Making one-off queries
- ✅ Need direct control over GraphQL queries
- ✅ Working with entity types that don’t need caching
- ✅ Executing mutations that return complex results
Use Pydantic Types when:
- ✅ Creating new entities with validation
- ✅ Need ORM-like
.save()/.delete()methods - ✅ Working with entity relationships
- ✅ Need change tracking for partial updates
Use StashEntityStore when:
- ✅ Need object identity across queries
- ✅ Making repeated queries for same entities
- ✅ Using Django-style filtering
- ✅ Need field-aware population
- ✅ Processing large result sets with pagination
Design Philosophy
Principle 1: Explicit Over Implicit
Operations like.save() and .delete() require passing the client explicitly:
Principle 2: Type Safety by Default
All GraphQL types are Pydantic models with runtime validation. Field typos and type mismatches are caught immediately:Principle 3: Progressive Enhancement
Start with simple client methods, add caching when needed, use advanced features as required:Principle 4: Match GraphQL Semantics
The UNSET pattern matches GraphQL’s sparse field selection. Partial updates work exactly like GraphQL input objects:Next Steps
- Getting Started Guide - Step-by-step tutorials
- Usage Patterns - Common recipes and best practices
- Identity Map Architecture - Implementation deep dive
- Library Comparisons - How this compares to alternatives
- API Reference - Complete API documentation