ORM
RevitPy includes an ORM (Object-Relational Mapping) layer that provides change tracking, intelligent caching, relationship management, and both synchronous and asynchronous query execution. The central class is RevitContext.
RevitContext
RevitContext is the main ORM context. It coordinates querying, change tracking, caching, and relationships.
Creating a Context
from revitpy.orm.context import RevitContext, ContextConfiguration, create_context
# With defaults
context = create_context(provider)
# With custom configuration
config = ContextConfiguration(
auto_track_changes=True,
cache_policy=CachePolicy.MEMORY,
cache_max_size=10000,
cache_max_memory_mb=500,
lazy_loading_enabled=True,
batch_size=100,
thread_safe=True,
validation_enabled=True,
performance_monitoring=True,
)
context = RevitContext(provider, config=config)
RevitContext is a context manager. When the with block exits, the context is disposed:
with RevitContext(provider) as ctx:
# work with the context
pass # ctx.dispose() is called automatically
ContextConfiguration Fields
| Field | Type | Default | Description |
|---|---|---|---|
auto_track_changes |
bool |
True |
Automatically track changes to attached entities |
cache_policy |
CachePolicy |
CachePolicy.MEMORY |
Caching strategy |
cache_max_size |
int |
10000 |
Maximum number of cached entries |
cache_max_memory_mb |
int |
500 |
Maximum cache memory in MB |
lazy_loading_enabled |
bool |
True |
Enable lazy loading of relationships |
batch_size |
int |
100 |
Batch size for bulk operations |
thread_safe |
bool |
True |
Enable thread-safe operations |
validation_enabled |
bool |
True |
Enable entity validation |
performance_monitoring |
bool |
True |
Enable performance statistics |
Querying with the ORM QueryBuilder
The ORM has its own QueryBuilder (in revitpy.orm.query_builder) that provides lazy evaluation, query optimization, async execution, and caching.
Creating Queries
# Query all elements
results = context.query().to_list()
# Query by type
results = context.query(WallElement).to_list()
# Get all elements of a type as an ElementSet
walls = context.all(WallElement)
Fluent Query Methods
The ORM QueryBuilder uses predicate functions instead of property-name-based filters:
# Filter with a predicate
tall_walls = (
context.query(WallElement)
.where(lambda w: w.get_parameter_value("Height") > 3.0)
.to_list()
)
# Project with a selector
names = (
context.query(WallElement)
.select(lambda w: w.name)
.to_list()
)
# Sort
sorted_walls = (
context.query(WallElement)
.order_by(lambda w: w.name)
.to_list()
)
# Sort descending
sorted_desc = (
context.query(WallElement)
.order_by_descending(lambda w: w.get_parameter_value("Height"))
.to_list()
)
# Pagination
page = (
context.query(WallElement)
.skip(20)
.take(10)
.to_list()
)
# Distinct
unique = (
context.query(WallElement)
.distinct(lambda w: w.get_parameter_value("Type"))
.to_list()
)
Terminal Operations (Synchronous)
| Method | Returns | Description |
|---|---|---|
to_list() |
list[T] |
Execute query and return results as a list |
first(predicate=None) |
T |
First matching element (raises QueryError if empty) |
first_or_default(predicate=None, default=None) |
T or None |
First matching element or default |
single(predicate=None) |
T |
Single matching element (raises on 0 or >1) |
single_or_default(predicate=None, default=None) |
T or None |
Single matching element or default |
count(predicate=None) |
int |
Count of matching elements |
any(predicate=None) |
bool |
True if any elements match |
all(predicate) |
bool |
True if all elements match |
to_dict(key_selector) |
dict[Any, T] |
Results as a dictionary |
group_by(key_selector) |
dict[Any, list[T]] |
Grouped results |
Terminal Operations (Asynchronous)
| Method | Returns | Description |
|---|---|---|
to_list_async() |
list[T] |
Async execute and return list |
first_async(predicate=None) |
T |
Async first element |
first_or_default_async(predicate=None, default=None) |
T or None |
Async first or default |
single_async(predicate=None) |
T |
Async single element |
count_async(predicate=None) |
int |
Async count |
any_async(predicate=None) |
bool |
Async any check |
to_dict_async(key_selector) |
dict[Any, T] |
Async dictionary |
Streaming Queries
For large datasets, convert a query to a streaming query:
streaming = context.query(WallElement).as_streaming(batch_size=100)
async for batch in streaming:
for element in batch:
process(element)
# Or collect all into a list
all_results = await streaming.to_list_async()
Convenience Methods on RevitContext
RevitContext also provides shortcut query methods:
# Get all elements of a type
walls = context.all(WallElement)
# Filter with a predicate
tall = context.where(WallElement, lambda w: w.height > 3.0)
# Get first
wall = context.first(WallElement)
wall = context.first(WallElement, lambda w: w.name == "Wall-1")
# Get first or default
wall = context.first_or_default(WallElement, default=None)
# Get single
wall = context.single(WallElement)
# Count
total = context.count(WallElement)
filtered = context.count(WallElement, lambda w: w.height > 3.0)
# Check existence
exists = context.any(WallElement)
# Get by ID
wall = context.get_by_id(WallElement, element_id)
Change Tracking
When auto_track_changes is enabled (the default), entities retrieved through the context are automatically tracked. The context monitors which entities have been added, modified, or deleted.
Entity States
The ElementState enum defines the possible states:
| State | Description |
|---|---|
UNCHANGED |
Entity has not been modified since it was attached |
ADDED |
Entity has been marked as new |
MODIFIED |
Entity has changes pending |
DELETED |
Entity has been marked for deletion |
DETACHED |
Entity is not tracked by the context |
Tracking Operations
# Attach an entity to the context for tracking
context.attach(entity, entity_id=some_id)
# Detach from tracking
context.detach(entity)
# Mark as added (new entity)
context.add(entity)
# Mark as deleted
context.remove(entity)
# Check entity state
state = context.get_entity_state(entity)
# Accept changes (mark current state as baseline)
context.accept_changes(entity) # For one entity
context.accept_changes() # For all entities
# Reject changes (revert to baseline)
context.reject_changes(entity)
context.reject_changes()
Saving Changes
Call save_changes() to persist all pending changes:
with RevitContext(provider) as ctx:
wall = ctx.get_by_id(WallElement, wall_id)
# ... modify wall ...
ctx.add(new_element)
ctx.remove(old_element)
changes_saved = ctx.save_changes()
print(f"Saved {changes_saved} changes")
save_changes() returns the number of changes persisted. If there are no pending changes, it returns 0.
Context Properties
context.has_changes–Trueif there are pending changes.context.change_count– Number of pending changes.
Transactions
RevitContext provides a transaction context manager that integrates with change tracking:
with context.transaction(auto_commit=True):
# Perform operations
context.add(new_entity)
# If auto_commit is True and there are changes, save_changes() is called.
# On exception, reject_changes() is called automatically.
Caching
The ORM layer uses intelligent caching to reduce redundant Revit API calls.
CacheConfiguration
The CacheConfiguration dataclass controls cache behavior:
| Field | Type | Default | Description |
|---|---|---|---|
max_size |
int |
10000 |
Maximum number of cached entries |
max_memory_mb |
int |
500 |
Maximum memory in MB |
default_ttl_seconds |
int or None |
3600 |
Default time-to-live (1 hour) |
eviction_policy |
EvictionPolicy |
EvictionPolicy.LRU |
Eviction strategy |
enable_statistics |
bool |
True |
Enable hit/miss tracking |
cleanup_interval_seconds |
int |
300 |
Interval for cleanup (5 minutes) |
compression_enabled |
bool |
False |
Enable data compression |
thread_safe |
bool |
True |
Thread-safe operations |
Cache Policies
The CachePolicy enum defines caching strategies:
| Policy | Description |
|---|---|
NONE |
No caching |
MEMORY |
In-memory caching only |
PERSISTENT |
Persistent cache with invalidation |
AGGRESSIVE |
Cache everything aggressively |
Cache Management
# Clear all cached data
context.clear_cache()
# Invalidate specific entries
context.invalidate_cache(entity_type=WallElement, entity_id=some_id)
context.invalidate_cache(entity_type=WallElement) # All walls
context.invalidate_cache() # Everything
# Access cache statistics
stats = context.cache_statistics
Relationships
RevitPy supports relationship loading between entities with configurable loading strategies.
LoadStrategy Enum
| Strategy | Description |
|---|---|
LAZY |
Load on demand when the relationship is first accessed |
EAGER |
Load with the parent entity |
SELECT |
Use a separate select query |
BATCH |
Batch load multiple entities |
Loading Relationships
from revitpy.orm.types import LoadStrategy
# Load a relationship for an entity
related = context.load_relationship(entity, "rooms", strategy=LoadStrategy.LAZY)
# Configure a relationship
context.configure_relationship(WallElement, "rooms", RoomElement)
Async Operations
Convert a RevitContext to an async interface:
async_ctx = context.as_async()
# Use async query methods
results = await async_ctx.query(WallElement).to_list_async()
The create_async_context factory function creates an AsyncRevitContext directly:
from revitpy.orm.context import create_async_context
async_ctx = create_async_context(provider)