FireProx Phase 1 Demo - Asynchronous API¶
This notebook demonstrates all Phase 1 features of the FireProx asynchronous API.
Prerequisites: Firestore emulator must be running on port 8080.
Note: Jupyter notebooks require special handling for async code. Each async cell must use await and be preceded by cell magic if needed.
1. Setup and Initialization¶
from fire_prox import AsyncFireProx
from fire_prox.testing import async_demo_client
client = async_demo_client()
db = AsyncFireProx(client)
print("AsyncFireProx initialized successfully!")
AsyncFireProx initialized successfully!
2. Creating a New Document (DETACHED State)¶
# Get a collection reference
users = db.collection('users')
# Create a new document (not yet in Firestore)
user = users.new()
print(f"State: {user.state}")
print(f"Is detached: {user.is_detached()}")
print(f"Is dirty: {user.is_dirty()}")
State: DETACHED Is detached: True Is dirty: True
3. Setting Attributes on a DETACHED Document¶
# Set attributes using dot notation
user.name = 'Ada Lovelace'
user.year = 1815
user.occupation = 'Mathematician'
print(f"Name: {user.to_dict()['name']}")
print(f"Data: {user.to_dict()}")
print(f"Still detached: {user.is_detached()}")
Name: Ada Lovelace
Data: {'name': 'Ada Lovelace', 'year': 1815, 'occupation': 'Mathematician'}
Still detached: True
4. Saving with Custom ID (DETACHED → LOADED)¶
# Save with a custom document ID (async operation)
await user.save(doc_id='alovelace')
print(f"State after save: {user.state}")
print(f"Is loaded: {user.is_loaded()}")
print(f"Is dirty: {user.is_dirty()}")
print(f"Document ID: {user.id}")
print(f"Document path: {user.path}")
State after save: LOADED Is loaded: True Is dirty: False Document ID: alovelace Document path: users/alovelace
5. Getting a Document by Path (ATTACHED State)¶
# Get a document reference (doesn't fetch data yet)
user2 = db.doc('users/alovelace')
print(f"State: {user2.state}")
print(f"Is attached: {user2.is_attached()}")
print(f"Document ID: {user2.id}")
print(f"Document path: {user2.path}")
print("Data not fetched yet!")
State: ATTACHED Is attached: True Document ID: alovelace Document path: users/alovelace Data not fetched yet!
6. Lazy Loading (ATTACHED → LOADED)¶
# Async API now supports lazy loading!
# Accessing attributes automatically triggers fetch
name = user2.name # Automatically fetches data on first access
print(f"Name: {name}")
print(f"State after access: {user2.state}")
print(f"Is loaded: {user2.is_loaded()}")
print(f"Full data: {user2.to_dict()}")
Name: Ada Lovelace
State after access: LOADED
Is loaded: True
Full data: {'name': 'Ada Lovelace', 'occupation': 'Mathematician', 'year': 1815}
# You can also explicitly fetch if preferred
user3_explicit = db.doc('users/alovelace')
await user3_explicit.fetch()
print(f"Before fetch - State: {user3_explicit.state}")
print(f"After fetch - State: {user3_explicit.state}")
print(f"Data: {user3_explicit.to_dict()}")
Before fetch - State: LOADED
After fetch - State: LOADED
Data: {'name': 'Ada Lovelace', 'occupation': 'Mathematician', 'year': 1815}
7. Modifying a LOADED Document¶
# Modify attributes (synchronous operations)
user2.year = 1816
user2.contributions = ['Analytical Engine', 'First Algorithm']
print(f"Is dirty: {user2.is_dirty()}")
print(f"Modified year: {user2.year}")
print(f"Contributions: {user2.contributions}")
Is dirty: True Modified year: 1816 Contributions: ['Analytical Engine', 'First Algorithm']
8. Saving Updates (Async)¶
# Save the modifications (async operation)
await user2.save()
print(f"Is dirty after save: {user2.is_dirty()}")
print(f"State: {user2.state}")
print("Changes saved to Firestore!")
Is dirty after save: False State: LOADED Changes saved to Firestore!
9. Refreshing Data with force=True¶
# Fetch fresh data from Firestore (async operation)
await user2.fetch(force=True)
print(f"Refreshed data: {user2.to_dict()}")
print(f"Year after refresh: {user2.year}")
print(f"Contributions: {user2.contributions}")
Refreshed data: {'name': 'Ada Lovelace', 'occupation': 'Mathematician', 'contributions': ['Analytical Engine', 'First Algorithm'], 'year': 1816}
Year after refresh: 1816
Contributions: ['Analytical Engine', 'First Algorithm']
10. Deleting Attributes¶
# Delete an attribute (synchronous)
del user2.contributions
print(f"Is dirty: {user2.is_dirty()}")
print(f"Data after delete: {user2.to_dict()}")
print("Attribute 'contributions' removed locally")
Is dirty: True
Data after delete: {'name': 'Ada Lovelace', 'occupation': 'Mathematician', 'year': 1816}
Attribute 'contributions' removed locally
# Save to persist the deletion (async)
await user2.save()
await user2.fetch(force=True)
print(f"Data after save: {user2.to_dict()}")
print("'contributions' field removed from Firestore")
Data after save: {'name': 'Ada Lovelace', 'occupation': 'Mathematician', 'year': 1816}
'contributions' field removed from Firestore
11. Creating Document with Auto-Generated ID¶
# Create a new document without specifying ID
user3 = users.new()
user3.name = 'Grace Hopper'
user3.year = 1906
# Save without doc_id - Firestore generates ID (async)
await user3.save()
print(f"Auto-generated ID: {user3.id}")
print(f"Path: {user3.path}")
print(f"Data: {user3.to_dict()}")
Auto-generated ID: IBPAPQMfDnwDQsyWYiNO
Path: users/IBPAPQMfDnwDQsyWYiNO
Data: {'name': 'Grace Hopper', 'year': 1906}
12. Collection Properties¶
# Inspect collection properties (synchronous)
print(f"Collection ID: {users.id}")
print(f"Collection path: {users.path}")
print(f"String repr: {str(users)}")
print(f"Repr: {repr(users)}")
Collection ID: users Collection path: users String repr: AsyncFireCollection(users) Repr: <AsyncFireCollection path='users'>
13. Deleting a Document (LOADED → DELETED)¶
# Delete a document from Firestore (async operation)
await user3.delete()
print(f"State after delete: {user3.state}")
print(f"Is deleted: {user3.is_deleted()}")
print(f"ID still accessible: {user3.id}")
print(f"Path still accessible: {user3.path}")
State after delete: DELETED Is deleted: True ID still accessible: IBPAPQMfDnwDQsyWYiNO Path still accessible: users/IBPAPQMfDnwDQsyWYiNO
14. Error Handling - Invalid Operations on DELETED¶
# Attempting operations on DELETED document raises errors
try:
await user3.save()
except RuntimeError as e:
print(f"Save error: {e}")
try:
await user3.fetch()
except RuntimeError as e:
print(f"Fetch error: {e}")
Save error: Cannot save() on a DELETED FireObject Fetch error: Cannot fetch() on a DELETED FireObject
15. Hydration from Native Firestore Snapshot¶
# Use native async Firestore API to get a snapshot
from fire_prox import AsyncFireObject
doc_ref = client.collection('users').document('alovelace')
snapshot = await doc_ref.get()
# Hydrate to AsyncFireObject
user4 = AsyncFireObject.from_snapshot(snapshot)
print(f"State: {user4.state}")
print(f"Is loaded: {user4.is_loaded()}")
print(f"Data: {user4.to_dict()}")
print("Hydrated from native async snapshot!")
State: LOADED
Is loaded: True
Data: {'name': 'Ada Lovelace', 'occupation': 'Mathematician', 'year': 1816}
Hydrated from native async snapshot!
16. Working with Nested Data¶
# Create document with nested data structures
user5 = users.new()
user5.name = 'Alan Turing'
user5.address = {
'city': 'London',
'country': 'UK'
}
user5.achievements = ['Turing Machine', 'Enigma', 'Turing Test']
await user5.save(doc_id='aturing')
print(f"Nested data saved: {user5.to_dict()}")
Nested data saved: {'name': 'Alan Turing', 'address': {'city': 'London', 'country': 'UK'}, 'achievements': ['Turing Machine', 'Enigma', 'Turing Test']}
17. Accessing Nested Data¶
# Access nested fields
user6 = db.doc('users/aturing')
await user6.fetch()
print(f"City: {user6.address['city']}")
print(f"First achievement: {user6.achievements[0]}")
print(f"All achievements: {user6.achievements}")
City: London First achievement: Turing Machine All achievements: ['Turing Machine', 'Enigma', 'Turing Test']
18. Multiple Async Operations¶
# Create multiple documents with async operations
user7 = users.new()
user7.name = 'Katherine Johnson'
user7.year = 1918
await user7.save(doc_id='kjohnson')
user8 = users.new()
user8.name = 'Margaret Hamilton'
user8.year = 1936
await user8.save(doc_id='mhamilton')
print("Created multiple documents:")
print(f" - {user7.name} ({user7.path})")
print(f" - {user8.name} ({user8.path})")
Created multiple documents: - Katherine Johnson (users/kjohnson) - Margaret Hamilton (users/mhamilton)
Summary¶
This demo covered all Phase 1 features of the Async API:
✅ State Machine: DETACHED → ATTACHED → LOADED → DELETED
✅ Dynamic Attributes: Set/get/delete using dot notation (sync)
✅ Lazy Loading: ✅ Automatic fetch on attribute access (like sync API)
✅ Async Save: await save() for create/update operations
✅ Async Delete: await delete() to remove documents
✅ State Inspection: state, is_loaded(), is_dirty(), etc. (sync)
✅ Collection Interface: new(), doc(), properties (sync)
✅ Hydration: from_snapshot() for native async query results
✅ Nested Data: Dictionaries and lists as attributes
✅ Error Handling: Clear messages for invalid operations
Key Features: Async API¶
| Feature | Async API |
|---|---|
| Lazy Loading | ✅ Automatic (uses sync client internally) |
| Fetch | await user.fetch() (explicit) OR user.name (lazy) |
| Save | await user.save() |
| Delete | await user.delete() |
| Attribute access | Triggers sync fetch if ATTACHED, then instant dict lookup |
Implementation Note: Lazy loading in async uses a companion sync Firestore client internally to perform a synchronous fetch when needed. This happens transparently and only once per object.
Next: See Phase 2 features (subcollections, queries, partial updates)