Metadata-Version: 2.5
Name: rvtkit
Version: 0.2.0
Summary: Pythonic wrapper for the Autodesk Revit API (2025-2027) on CPython 3.12 + pythonnet
Author-email: Badi Hamid <badi.hamid@gmail.com>
License-Expression: MIT
License-File: LICENSE
License-File: THIRD_PARTY_NOTICES.md
Keywords: aec,autodesk,bim,pythonnet,revit
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Science/Research
Classifier: Operating System :: Microsoft :: Windows
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Scientific/Engineering
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: <3.14,>=3.12
Requires-Dist: cloudpickle>=3.0
Requires-Dist: pythonnet>=3.0; sys_platform == 'win32'
Provides-Extra: ai
Requires-Dist: mcp>=1.0; extra == 'ai'
Requires-Dist: rank-bm25>=0.2; extra == 'ai'
Provides-Extra: animation
Requires-Dist: imageio[ffmpeg]>=2.30; extra == 'animation'
Requires-Dist: pillow>=10.0; extra == 'animation'
Provides-Extra: console
Requires-Dist: ipykernel>=6.0; extra == 'console'
Requires-Dist: qtconsole>=5.5; extra == 'console'
Provides-Extra: dash
Requires-Dist: dash-bootstrap-components>=1.6; extra == 'dash'
Requires-Dist: dash>=2.18; extra == 'dash'
Requires-Dist: pandas>=2.0; extra == 'dash'
Requires-Dist: plotly>=5.20; extra == 'dash'
Provides-Extra: dev
Requires-Dist: build; extra == 'dev'
Requires-Dist: hypothesis; extra == 'dev'
Requires-Dist: openpyxl>=3.1; extra == 'dev'
Requires-Dist: pandas>=2.0; extra == 'dev'
Requires-Dist: pyright; extra == 'dev'
Requires-Dist: pytest-cov; extra == 'dev'
Requires-Dist: pytest-sugar; extra == 'dev'
Requires-Dist: pytest>=7.0; extra == 'dev'
Requires-Dist: ruff; extra == 'dev'
Provides-Extra: docs
Requires-Dist: mkdocs>=1.5; extra == 'docs'
Provides-Extra: excel
Requires-Dist: openpyxl>=3.1; extra == 'excel'
Requires-Dist: pandas>=2.0; extra == 'excel'
Provides-Extra: gltf
Requires-Dist: pygltflib>=1.16; extra == 'gltf'
Provides-Extra: pandas
Requires-Dist: pandas>=2.0; extra == 'pandas'
Description-Content-Type: text/markdown

# rvtkit — Pythonic wrapper for the Autodesk Revit API (2025 - 2027)

![pypi](https://img.shields.io/pypi/v/rvtkit)
![python](https://img.shields.io/badge/python-3.12%20%7C%203.13-blue)
![license](https://img.shields.io/badge/license-MIT-green)

A Pythonic wrapper around the Autodesk Revit API with **idiomatic ergonomics**
and a **LINQ-style query layer**. Targets **Revit 2025 / 2026 / 2027**
(.NET 8+ / CoreCLR) on **CPython 3.12** via [pythonnet](https://pythonnet.github.io/).

No external dependencies beyond `Autodesk.Revit.DB` (and `Autodesk.Revit.UI`
when UI context is explicitly requested).

```python
import rvtkit as rk

walls = rk.activeDoc.categories['Walls'].instances
```

> **Formerly `revit`.** The package was renamed `rvtkit` (October 2026) and
> `import revit` no longer works : write `import rvtkit as rk`.

> Autodesk and Revit are registered trademarks of Autodesk, Inc. rvtkit is not
> affiliated with, sponsored or endorsed by Autodesk.

---

## Table of contents

- [Compatibility](#compatibility)
- [Requirements](#requirements)
- [Installation](#installation)
- [Package layout](#package-layout)
- [Documentation](#documentation)
  - [Quick start](#quick-start)
  - [Live proxies (`activeDoc` / `activeView` / `app`)](#live-proxies)
  - [Document](#document)
  - [Categories](#categories)
  - [Families](#families)
  - [Family types](#family-types)
  - [Instances / elements](#instances--elements)
  - [LinqList — operators](#linqlist--operators)
  - [LinqMap — operators](#linqmap--operators)
  - [IDE autocomplete (typed lambdas)](#ide-autocomplete-typed-lambdas)
  - [Context managers (`selectionScope` / `isolated`)](#context-managers)
  - [Jupyter / pyRevit Output rendering](#jupyter--pyrevit-output-rendering)
  - [Diagnostics & banners](#diagnostics--banners)
  - [Smart printing](#smart-printing)
  - [Full examples](#full-examples)
- [Licence](#licence)

---

## Compatibility

### Revit versions

| Revit | Runtime           | Bootstrap call            | Status |
|-------|-------------------|---------------------------|--------|
| 2025  | CPython 3.12 + pythonnet (.NET 8) | `loadAssemblies("2025")` | ✅ runtime-validated by author |
| 2026  | CPython 3.12 + pythonnet (.NET 8) | `loadAssemblies("2026")` | ✅ primary target — 544 tests in CI |
| 2027  | CPython 3.12 + pythonnet (.NET 10) | `loadAssemblies("2027")` | ✅ runtime-validated by author |

Revit 2025+ runs on .NET 8 (2027 on .NET 10) / CoreCLR and requires
CPython 3.12 + pythonnet 3.x. See `rvtkit.bootstrap` and the embedded
runtime configs in `rvtkit/_runtimeconfigs/`.

> **Note:** Revit 2020 - 2024 (.NET Framework 4.8 / IronPython) is no longer
> supported as of `0.1.3`. Pin to `0.1.2` if you need legacy Revit support.

### Launchers

| Launcher                          | Injected variable | Status |
|-----------------------------------|-------------------|--------|
| pyRevit (CPython mode)            | `__revit__`       | ✅ |
| Custom .NET addin / shell host (CPython 3.12) | `__uiapp__` / `__revit__` | ✅ |
| RevitPythonShell                  | `__revit__`       | ❌ IronPython only — not supported since `0.1.3` |

---

## Requirements

- Revit 2025, 2026, or 2027 (.NET 8+ / CoreCLR host)
- CPython 3.12 or 3.13 (`requires-python = ">=3.12,<3.14"` enforced by pip)
- `pythonnet >= 3.0` — installed automatically by `pip install` on Windows
  (declared as `pythonnet>=3.0; sys_platform == 'win32'`, so Linux/macOS
  CI runs skip it)
- Install via `pip install` — see [Installation](#installation)

### External dependencies (host-provided, not on PyPI)

Pulled in at runtime via `clr` / pythonnet from whichever Revit version
is currently running. **No PyPI third-party package is required
besides `pythonnet`.**

| Dependency | Source | Used for |
|---|---|---|
| `Autodesk.Revit.DB` | Revit install | Core API — required everywhere |
| `Autodesk.Revit.UI` | Revit install | Dialogs, selection helpers (lazy import) |
| `clr` | pythonnet | .NET interop (assembly loading) |
| `System.Reflection`, `System.Runtime.Loader` | .NET BCL | Bootstrap (assembly resolver) |
| `System.Collections.Generic`, `System.IO`, `System.Xml`, `System.Drawing.Imaging`, `System.Diagnostics` | .NET BCL | Various wrappers, lazy-loaded |

Pure Python standard library only otherwise: `os`, `sys`, `math`,
`pathlib`, `collections`, `enum`.

---

## Installation

```bash
pip install rvtkit
```

The distribution and the import are both named `rvtkit` (`pip install rvtkit`, `import rvtkit`).

The 3D viewer runs xeokit (AGPL-3.0), which rvtkit does not ship : the first
viewer that needs it downloads it once from jsDelivr into
`%LOCALAPPDATA%\rvtkit\xeokit` and works offline afterwards.

### Configuring the host inside Revit

In your startup script (pyRevit CPython mode, or a custom CPython addin/shell host):

```python
import rvtkit
rvtkit.init()                # walks the call stack to find __revit__ or __uiapp__
```

`init()` is also called automatically when you `import rvtkit`, so in most
hosts (RPS console, pyRevit, custom addin where `__revit__` lives in globals
or builtins) you don't need to call it explicitly. Re-call it after a
`rvtkit.reload()` or when entering a new scope where the host changed.

If no host is injected anywhere on the call stack, fall back to the explicit
form:

```python
import rvtkit
rvtkit.set_host(__revit__)   # or __uiapp__ depending on the launcher
```

---

## Package layout

```
revit/
├── __init__.py       # getApp / getDoc / getDocs / set_host / init / Print / run_test
├── _host.py          # UIApplication resolution
├── _accessors.py     # RevitContextError + import boundaries
├── _app.py           # Application (renamed from app.py in 0.1.5)
├── _print.py         # rvtkit.Print() — package banner
├── _runtest.py       # rvtkit.run_test() — in-Revit functional check
├── _wrappers.py      # ElementWrapper (base) + parameter access
├── doc.py            # Document  ← main hub (carved into _doc_* mixins)
├── category.py       # CategoryWrapper
├── family.py         # FamilyWrapper / SystemFamilyWrapper / SystemFamilyTypeWrapper
├── elements.py       # FamilyTypeWrapper / InstanceWrapper / ViewWrapper
└── linq/
    ├── __init__.py   # exposes LinqList, LinqMap (aliased as List, Map)
    ├── linqlist.py   # LinqList[T]      — chainable collection
    ├── linqmap.py    # LinqMap[K, V]    — chainable dict (returned by GroupBy)
    └── _exporters.py # JSON / XML / TOML / YAML writers
```

---

## Documentation

Most doc-level collections (`materials`, `views`, `sheets`, `families`,
`familyTypes`, `categories`, `levels`, `grids`, `phases`, `worksets`,
`viewTemplates`, `scopeBoxes`, `filters`, `titleBlocks`, `revisions`, …)
are **subscriptable by name** — prefer `doc.categories['Walls']` over
`doc.getCategory('Walls')`. Both forms exist; the bracket form is more
idiomatic and keeps queries terse.

### Quick start

```python
import rvtkit

app = rvtkit.getApp()
doc = rvtkit.getDoc()
```

### Live proxies

Three module-level attributes track the current host state, so user code
doesn't have to re-call `getApp()` / `getDoc()` on every line:

```python
rvtkit.app          # cached Application (single instance per process)
rvtkit.activeDoc    # live proxy that always forwards to the active Document
rvtkit.activeView   # live proxy that always forwards to the active view
```

`rvtkit.app` is resolved on first access (needs the .NET assemblies loaded)
and memoised. `rvtkit.activeDoc` / `rvtkit.activeView` are *live forwarders*
— they re-resolve the active doc/view on every attribute access, so they
remain correct after the user switches documents in Revit:

```python
rvtkit.activeDoc.title                   # current document title
rvtkit.activeDoc.materials['Aluminium']  # current document materials
rvtkit.activeView.name                   # current view name
rvtkit.app.version_name                  # 'Autodesk Revit 2026'
```

> **Migration (0.1.4):** the old `rvtkit.doc` / `rvtkit.view` proxies were
> renamed to `rvtkit.activeDoc` / `rvtkit.activeView` — the old names were
> shadowed by the `rvtkit.doc` / `rvtkit.view` submodules at first lazy
> import. Update any pre-0.1.4 user code accordingly.

### Document

```python
doc = rvtkit.getDoc()

doc.title                          # file name
doc.path                           # full path
doc.is_workshared                  # True/False
doc.view                           # active view (alias for activeView)

doc.categories['Walls']            # CategoryWrapper
doc.families['Basic Wall']         # SystemFamilyWrapper
doc.families['M_Fixed']            # FamilyWrapper (loadable family)
doc.familyTypes['Generic - 200mm'] # FamilyType wrapper (loadable + system)
doc.materials['Aluminium']         # MaterialWrapper
doc.levels['Level 1']              # Level wrapper
doc.getElementById(123456)         # wrapper from an integer id
```

`doc.<collection>` returns a `LinqList` you can iterate, filter, or look up
by name. See `doc.py` for the full list.

### Categories

```python
cat = doc.categories['Walls']     # or 'Doors', 'Floors', etc.

cat.name                          # 'Walls'
cat.getFamilies()                 # LinqList[FamilyWrapper | SystemFamilyWrapper]
cat.getFamilyTypes()              # LinqList[FamilyTypeWrapper | SystemFamilyTypeWrapper]
cat.getInstances()                # LinqList[ElementWrapper | InstanceWrapper]
```

Loadable / system detection is **automatic and dynamic** — no hard-coded list.
Works for every Revit category (architecture, MEP, structure, etc.).

### Families

```python
# Loadable
fam = doc.families['M_Fixed']
fam.name, fam.getFamilyTypes(), fam.getInstances()

# System (walls, floors, roofs…)
fam = doc.families['Basic Wall']
fam.getFamilyTypes()[0]           # first type via index
```

### Family types

```python
ft = doc.familyTypes['Generic - 200mm']

ft.name, ft.family_name, ft.id

# Parameter access — by name, with unit conversion helpers
ft['Width']                       # raw value (Revit internal feet)
ft.mm('Width')                    # in millimetres
ft.m('Width')                     # in metres
ft.params                         # LinqList of ParamWrapper, subscriptable
ft.params['Width'].value          # explicit form

ft.getInstances()                 # LinqList[ElementWrapper] of this type
```

### Instances / elements

```python
inst = doc.categories['Doors'].getInstances().First()

inst.id, inst.name, inst.category, inst.family_name
inst.family_type                  # FamilyTypeWrapper (navigable)
inst.family                       # FamilyWrapper

# Parameters — subscriptable, both read and write
inst['Mark']                      # read
inst.params['Mark'] = 'W1'        # write (transaction implicit when none active)
inst.mm('Height')
inst.location                     # (x, y, z) or LocationCurve
```

### LinqList — operators

`LinqList` is returned by every collection method. Intermediate operations
are **lazy** (evaluated on demand).

| Category | Operators |
|----------|-----------|
| Filter / project | `Where` `Select` `SelectMany` `Distinct` `Partition` |
| Order | `OrderBy` `OrderByDescending` `Reverse` |
| Slice | `Take` `Skip` `TakeWhile` `SkipWhile` |
| Set algebra | `Union` `Intersect` `Except` `Concat` `Zip` `Aggregate` (also `\|` `&` `-` `+`) |
| Itertools | `Pairwise` `Window(n)` `Chunk(n)` |
| Group | `GroupBy` → `LinqMap` ; `CountBy` → `LinqMap[K, int]` |
| Joining | `Join` (INNER) `GroupJoin` `DefaultIfEmpty` |
| Terminals | `First` `FirstOrDefault` `Last` `Single` `Any` `All` `Count` `Sum` `Min` `Max` `Average` `MinBy` `MaxBy` `Contains` |
| Convert | `ToList` `ToDict` |
| Side effects | `ForEach` `Tap` `Print` |
| Revit shortcut | `Instances` (= `SelectMany(t => t.getInstances())`) |
| **Pandas (QA / reporting)** | `to_df` `query` `pivot_table` `value_counts` `describe` |

```python
# Typical chain
doc.categories['Walls'].getFamilyTypes() \
   .Where(lambda t: (t.mm('Width') or 0) > 200) \
   .OrderByDescending(lambda t: t.mm('Width')) \
   .Take(5) \
   .Select(lambda t: t.name)
```

#### QA / reporting via pandas

For BIM QA audits and reporting deliverables, `LinqList` exposes a
pandas-borrowed shortcut layer. `query()` returns a
`LinqList[Wrapper]` (not a DataFrame), so chaining stays natural;
`pivot_table` / `value_counts` / `describe` produce ready-to-export
analytical outputs.

```python
walls = doc.categories['Walls'].instances

# SQL-like filter, round-trip to wrappers
walls.query("Area > 50 and not `Fire Rating`") \
     .to_df(['Mark', 'Level', 'Area']) \
     .to_excel('audit_CF.xlsx')

# Pivot — area per level × wall type
walls.pivot_table('Area', index='Level',
                  columns='Type', aggfunc='sum')
```

Requires `pip install rvtkit[pandas]`.

### LinqMap — operators

`LinqMap` is returned by `GroupBy()`. Each value is a `LinqList`.

```python
groups = doc.categories['Walls'].getFamilyTypes() \
            .GroupBy(lambda t: t.family_name)

groups['Basic Wall']                           # LinqList of Basic Wall types
groups.Keys()                                  # LinqList of keys
groups.Where(lambda k, v: v.Count() > 2)       # filter the groups
groups.Select(lambda k, v: (k, v.Count()))     # project → LinqList of tuples
groups.Print(lambda k, v: '{}: {}'.format(k, v.Count()))
groups.Flatten()                               # flat LinqList of every element
```

`LinqMap` is `Generic[K, V]`, so the `k` and `v` lambda parameters carry
the right types end-to-end — see the next section.

### IDE autocomplete (typed lambdas)

`LinqList` and `LinqMap` are generic (`LinqList[T]` / `LinqMap[K, V]`),
and the doc-level collections expose typed return signatures, so Pylance,
Pyright, Jedi, and PyCharm propose wrapper members on lambda parameters:

```python
rvtkit.activeDoc.materials.Where(lambda m: m.<TAB>)         # → MaterialWrapper members
rvtkit.activeDoc.getElements().Select(lambda e: e.<TAB>)    # → ElementWrapper members

# Group + filter — k is str, v is LinqList[ElementWrapper]
rvtkit.activeDoc.getElements() \
    .GroupBy(lambda e: e.category.name) \
    .Where(lambda k, v: k.<TAB>)                           # → str members
```

Subscript chains preserve the wrapper type too:

```python
rvtkit.activeDoc.materials['Aluminium'].thermal.<TAB>       # → ThermalWrapper members
rvtkit.activeDoc.familyTypes['Generic - 200mm'].mm('<TAB>') # parameter helpers
```

### Context managers

Two `with`-blocks save/restore Revit UI state automatically — even when the
body raises:

```python
# Save the current selection, set a new one, restore on exit
with rvtkit.activeDoc.selectionScope(walls):
    ...   # selection restored, even on exception

# Temporarily isolate elements in a view, restore full visibility on exit
with rvtkit.activeView.isolated(walls):
    rvtkit.activeView.Print()
    ...
```

`isolated()` uses `IsolateElementsTemporary` (session-only, not persisted on
save) but is still wrapped in a SubTransaction so it composes with any
outer `TransactionWrapper`.

### Jupyter / pyRevit Output rendering

`LinqList` and `LinqMap` both implement `_repr_html_`, so they render as
proper tables in Jupyter notebooks **and** in the pyRevit Output window.
No explicit call needed — just leave the value as the last expression of a
cell:

```python
rvtkit.activeDoc.materials \
    .Where(lambda m: m.thermal is not None) \
    .Select(lambda m: (m.name, m.thermal.thermalConductivity))
# → HTML table in Jupyter / pyRevit Output, plain repr in a terminal
```

### Diagnostics & banners

Three `Print()` surfaces describe what the package sees, at three
different scopes:

```python
rvtkit.Print()              # the package itself — version, install path,
                           # Python runtime, host status, loaded submodules.
                           # Works *before* a host is bound (handy for
                           # diagnosing import problems in plain CPython).

rvtkit.app.Print()          # the running Revit host — VersionName, build,
                           # user, # documents open, active doc, memory,
                           # PID, uptime.

rvtkit.activeDoc.Print()    # the active document — title, path, workshared
                           # status, # elements, # families, etc.
```

Other diagnostics:

```python
rvtkit.submodules           # sorted LinqList[str] of loaded rvtkit.* modules
rvtkit.reload()             # force-reload every rvtkit.* submodule
                           # (returns LinqList[str] of reloaded names)
rvtkit.run_test()           # in-Revit functional check — PASS/FAIL table
                           # over import / host / doc / proxies / linq
                           # / params / transaction / Print
```

Errors raised when the package is exercised outside a Revit process
(no `Autodesk.Revit.DB` assemblies, no host bound) come out as a single
`RevitContextError(RuntimeError)` with a two-frame traceback — no
seven-deep `ModuleNotFoundError` chain to wade through:

```python
from rvtkit import RevitContextError
try:
    rvtkit.getDoc()
except RevitContextError as e:
    print('not running inside Revit:', e)
```

### Smart printing

Every wrapper and collection defines its own multi-line `__str__` — just use `print()`:

```python
print(doc.families['Basic Wall'])
print(doc.categories['Walls'].getFamilyTypes())

doc.categories['Windows'].getFamilies().Print(lambda f: f.name)
```

### Full examples

```python
import rvtkit
doc = rvtkit.getDoc()

# Walls thicker than 200 mm, sorted desc
doc.categories['Walls'].getFamilyTypes() \
   .Where(lambda t: (t.mm('Width') or 0) > 200) \
   .OrderByDescending(lambda t: t.mm('Width')) \
   .Print(lambda t: '{:40s}  {:>8.1f} mm'.format(t.name, t.mm('Width')))

# Instance count per window family
doc.categories['Windows'].getFamilies() \
   .GroupBy(lambda f: f.name) \
   .Select(lambda k, v: (k, v.Flatten().Count())) \
   .ForEach(lambda pair: print('{}: {} instance(s)'.format(*pair)))

# Navigation from an instance
inst = doc.categories['Doors'].getInstances().First()
print(inst.family_name, inst.family_type.name, inst.family_type.mm('Height'))
```

---

## Licence

MIT (see `LICENSE`). The wheel also ships a few third-party files (AvalonDock,
ModernWpf, FilterDataGrid, PyQtDarkTheme, Material Symbols, the WebView2 SDK),
each under its own licence : see `THIRD_PARTY_NOTICES.md`, installed with the
package's metadata. xeokit (AGPL-3.0), which the 3D viewer runs, is not part of
rvtkit : it is downloaded on first use (see [Installation](#installation)).
