{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# rvt-rs — Python quickstart\n",
"\n",
"Walk through the most common uses of the `rvt` Python bindings: open a Revit file, inspect metadata, read the root document via the schema-directed walker, and export a document-level IFC4 scaffold.\n",
"\n",
"**Install**: `pip install rvt` once the wheel is published; from a source checkout run `maturin build --release --features python && pip install target/wheels/rvt-*.whl`.\n",
"\n",
"**Sample file**: this notebook uses `racbasicsamplefamily-2024.rfa` from the public `rac_basic_sample_family` corpus Autodesk ships with every Revit release. The repo's `samples/` directory (and the `RVT_SAMPLES_DIR` env var honoured below) is where the integration tests look for it. Any `.rvt`/`.rfa`/`.rte`/`.rft` file works — swap the path in cell 1."
],
"id": "cell-00"
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import os\n",
"import pathlib\n",
"\n",
"import rvt\n",
"\n",
"print(f\"rvt version: {rvt.__version__}\")\n",
"\n",
"# Sample-path resolution mirrors tests/python/test_rvt.py:\n",
"# 1. honour the RVT_SAMPLES_DIR env var if set\n",
"# 2. otherwise try a handful of sensible paths relative to the\n",
"# notebook's working directory\n",
"#\n",
"# The in-repo `samples/` directory lives at the monorepo root in the\n",
"# canonical checkout (../../../samples when this notebook is run from\n",
"# <rvt-rs>/docs/). Point RVT_SAMPLES_DIR at any directory holding\n",
"# `racbasicsamplefamily-*.rfa` to use your own corpus.\n",
"def resolve_samples_dir() -> pathlib.Path:\n",
" env_dir = os.environ.get(\"RVT_SAMPLES_DIR\")\n",
" if env_dir:\n",
" return pathlib.Path(env_dir).expanduser().resolve()\n",
" here = pathlib.Path.cwd().resolve()\n",
" candidates = [\n",
" here / \"samples\",\n",
" here.parent / \"samples\",\n",
" here.parent.parent / \"samples\",\n",
" here.parent.parent.parent / \"samples\",\n",
" ]\n",
" for c in candidates:\n",
" if c.exists():\n",
" return c\n",
" return candidates[-1]\n",
"\n",
"samples_dir = resolve_samples_dir()\n",
"SAMPLE = samples_dir / \"racbasicsamplefamily-2024.rfa\"\n",
"print(f\"Samples dir : {samples_dir}\")\n",
"print(f\"Sample file : {SAMPLE.name}\")\n",
"print(f\"Exists : {SAMPLE.exists()}\")\n",
"if not SAMPLE.exists():\n",
" print()\n",
" print(\"To run this notebook, set RVT_SAMPLES_DIR to a directory\")\n",
" print(\"containing Autodesk's public `rac_basic_sample_family` corpus,\")\n",
" print(\"or point SAMPLE at any .rvt/.rfa/.rte/.rft file you have locally.\")"
],
"id": "cell-01"
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 1. Open the file and inspect metadata\n",
"\n",
"Constructing `RevitFile(path)` opens the MS-CFB container and keeps the bytes cached in memory. The metadata getters (`version`, `build`, `guid`, `part_atom_title`, `original_path`) parse on-demand — nothing heavy runs until you call a method that actually touches a stream. Size limits default to 2 GiB file / 256 MiB stream / 256 MiB inflate; override via `max_file_bytes=`, `max_stream_bytes=`, `max_inflate_bytes=` keyword arguments on the constructor if you need to cap the reader tighter for hostile input."
],
"id": "cell-02"
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"f = rvt.RevitFile(str(SAMPLE))\n",
"\n",
"print(f\"Revit release : {f.version}\")\n",
"print(f\"Build tag : {f.build}\")\n",
"print(f\"Document title : {f.part_atom_title}\")\n",
"print(f\"Document GUID : {f.guid}\")\n",
"print(f\"Original path : {f.original_path}\")\n",
"print(f\"repr() : {f!r}\")"
],
"id": "cell-03"
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 2. Full `BasicFileInfo` as JSON\n",
"\n",
"`basic_file_info_json()` returns the entire `BasicFileInfo` record as a JSON string. It is the single-call equivalent of the four getters above (`version`, `build`, `guid`, `original_path`) plus any future fields the Rust `BasicFileInfo` struct grows — `json.loads()` gives you a plain dict. Returns `None` if the `BasicFileInfo` stream can't be parsed."
],
"id": "cell-04"
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import json\n",
"\n",
"bfi_json = f.basic_file_info_json()\n",
"if bfi_json is None:\n",
" print(\"BasicFileInfo stream unreadable; metadata getters will also return None\")\n",
"else:\n",
" bfi = json.loads(bfi_json)\n",
" print(json.dumps(bfi, indent=2))"
],
"id": "cell-05"
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 3. List the OLE streams\n",
"\n",
"A Revit file is an MS-CFB container holding a set of named streams. The 2016–2026 releases all carry the same 12 invariant streams plus a version-specific `Partitions/NN` marker. `missing_required_streams()` is the cheapest way to pre-validate an input before running heavy extractors — an empty list means the file at least looks structurally like Revit."
],
"id": "cell-06"
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"for s in f.stream_names():\n",
" print(f\" {s}\")\n",
"\n",
"missing = f.missing_required_streams()\n",
"if missing:\n",
" print(f\"\\nmissing required streams: {missing}\")\n",
"else:\n",
" print(\"\\nall required streams present\")"
],
"id": "cell-07"
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 4. Schema inventory\n",
"\n",
"The `Formats/Latest` stream carries Revit's complete serialisation schema: every class name, every field, every C++ type signature. `schema_summary()` is cheap (counts only); `schema_json()` is the full dump and runs around 1–2 MB for a typical family. We print the summary plus the first few class names from the full dump to keep the output readable."
],
"id": "cell-08"
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"summary = f.schema_summary()\n",
"print(f\"Classes in this file : {summary['classes']}\")\n",
"print(f\"Total declared fields : {summary['fields']}\")\n",
"print(f\"Distinct C++ type sigs : {summary['cpp_types']}\")\n",
"\n",
"schema = json.loads(f.schema_json())\n",
"print(f\"\\nFirst 10 class names (of {len(schema['classes'])}):\")\n",
"for cls in schema['classes'][:10]:\n",
" print(f\" tag={cls['tag']:>5} fields={len(cls['fields']):>3} {cls['name']}\")\n",
"\n",
"# Confirm ADocument is present — it is the root class the Layer-5a\n",
"# walker targets in the next cell.\n",
"adoc = next((c for c in schema['classes'] if c['name'] == 'ADocument'), None)\n",
"print(f\"\\nADocument found: {adoc is not None} \"\n",
" f\"(fields={len(adoc['fields']) if adoc else 'n/a'})\")"
],
"id": "cell-09"
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 5. Read the root `ADocument` record\n",
"\n",
"`read_adocument()` runs the Layer-5a walker and returns `ADocument`'s declared instance fields as a Python dict, or `None` if the entry-point detector can't confidently locate the record. Releases 2024–2026 are cross-version validated: the last three `ElementId` fields (`m_ownerFamilyId`, `m_ownerFamilyContainingGroupId`, `m_devBranchInfo`) are byte-identical across those releases on the reference corpus. Releases 2016–2023 decode the schema in full but the walker currently reports `partial` on them — see `docs/compatibility.md` for the matrix.\n",
"\n",
"Per-field decoding beyond `ADocument` (walls, doors, levels, families, etc.) is implemented on the Rust side; exposing it to Python is tracked separately (see task #147 / `ADocumentInstance` completeness markers)."
],
"id": "cell-10"
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"doc = f.read_adocument()\n",
"if doc is None:\n",
" print(\"walker couldn't locate ADocument in this file\")\n",
"else:\n",
" print(f\"ADocument at offset 0x{doc['entry_offset']:06x} (Revit {doc['version']})\")\n",
" print()\n",
" for i, field in enumerate(doc['fields']):\n",
" kind = field['kind']\n",
" name = field['name']\n",
" detail = ''\n",
" if kind == 'pointer':\n",
" detail = f\"slots [{field['slot_a']:#x}, {field['slot_b']:#x}]\"\n",
" elif kind == 'element_id':\n",
" detail = f\"tag={field['tag']}, id={field['id']}\"\n",
" elif kind == 'ref_container':\n",
" detail = f\"count={field['count']}\"\n",
" elif kind == 'bytes':\n",
" detail = f\"len={field['len']}\"\n",
" print(f\" [{i:>2}] {name:<36} {kind:<14} {detail}\")"
],
"id": "cell-11"
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 6. Export to IFC4 (document-level)\n",
"\n",
"`write_ifc()` produces a spec-valid IFC4 STEP text: the ISO-10303-21 envelope, the required framework entities (`IfcPerson` / `IfcOrganization` / `IfcApplication` / `IfcOwnerHistory` / `IfcSIUnit` / `IfcUnitAssignment` / `IfcGeometricRepresentationContext`), and an `IfcProject` populated from the document metadata.\n",
"\n",
"**Coverage is document-level only.** Building-element geometry (walls, doors, floors, roofs, etc.) is not yet emitted — that pipeline depends on per-class element decoders currently exposed only through the Rust API. See `docs/compatibility.md` §IFC for the current scope and `CONTRIBUTING.md` → *Where help is most wanted* if you want to extend coverage."
],
"id": "cell-12"
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"ifc_text = f.write_ifc()\n",
"print(f\"IFC output length : {len(ifc_text):,} bytes\")\n",
"print(f\"IFC4 schema : {'FILE_SCHEMA(' in ifc_text and 'IFC4' in ifc_text}\")\n",
"print(f\"IfcProject count : {ifc_text.count('IFCPROJECT(')}\")\n",
"\n",
"# Write the STEP text next to the input so you can inspect it with\n",
"# any IFC viewer / validator (e.g. IfcOpenShell's `IfcConvert`).\n",
"out_path = pathlib.Path(str(SAMPLE).replace('.rfa', '.ifc'))\n",
"out_path.write_text(ifc_text)\n",
"print(f\"Wrote : {out_path}\")\n",
"\n",
"print()\n",
"print(\"First 20 lines of the STEP file:\")\n",
"for line in ifc_text.splitlines()[:20]:\n",
" print(f\" {line}\")"
],
"id": "cell-13"
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 7. Cross-version sanity check\n",
"\n",
"If you have multiple `racbasicsamplefamily-*.rfa` releases on disk, this cell opens each and prints the reported Revit release year. The 2016–2019 files use the older `rac_basic_sample_family-<year>.rfa` naming; 2020+ use the compact `racbasicsamplefamily-<year>.rfa` form. Skip this cell if you only have the one sample."
],
"id": "cell-14"
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"candidates = sorted(samples_dir.glob('rac*sample*family*.rfa')) if samples_dir.exists() else []\n",
"\n",
"if not candidates:\n",
" print(f\"No corpus files found under {samples_dir}\")\n",
"else:\n",
" for path in candidates:\n",
" try:\n",
" g = rvt.RevitFile(str(path))\n",
" print(f\" {path.name:<45} Revit {g.version} title={g.part_atom_title!r}\")\n",
" except Exception as exc:\n",
" print(f\" {path.name:<45} <open failed: {exc}>\")"
],
"id": "cell-15"
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Where to next?\n",
"\n",
"- **Compatibility matrix**: [`docs/compatibility.md`](compatibility.md) — release-by-release breakdown of what parses vs. what the walker decodes vs. what the IFC exporter emits. Source-of-truth for current limitations.\n",
"- **Python API reference**: [`docs/python.md`](python.md) — full surface of the `rvt` module with return shapes.\n",
"- **Recon report**: [`docs/rvt-moat-break-reconnaissance.md`](rvt-moat-break-reconnaissance.md) — the living technical record of the Revit on-disk format as we've reverse-engineered it clean-room.\n",
"- **Contributing**: [`CONTRIBUTING.md`](../CONTRIBUTING.md) — especially the *Where help is most wanted* section if you want to extend the walker or IFC coverage.\n",
"- **Issues**: [github.com/DrunkOnJava/rvt-rs/issues](https://github.com/DrunkOnJava/rvt-rs/issues)\n",
"\n",
"### Current limitations (summary)\n",
"\n",
"- **Walker coverage**: `ADocument` root fields decoded across all 11 releases; fully cross-version-validated on 2024–2026. Per-class element decoders (walls, doors, families, etc.) exist on the Rust side but are not yet exposed through the Python bindings.\n",
"- **IFC export**: document-level scaffold only. No element geometry, no materials, no property sets yet. Output is schema-valid IFC4 STEP that `IfcOpenShell` will round-trip.\n",
"- **Write path**: the Rust crate supports a verified write-back round-trip; no Python binding yet."
],
"id": "cell-16"
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"name": "python"
}
},
"nbformat": 4,
"nbformat_minor": 5
}