Files
willtools-demo/tools/pa_pagespec.py
T
will 8e6ee31596
ci / test (push) Successful in 6m18s
WillTools 2026-09-09 -- clean snapshot of the development repo for review
History-free snapshot of the private development repository at cbe45c5
(2026-09-09). Code, docs and public catalogue only; no plant or job
knowledge layers, no client or job names (tools/scrub.py clean). The
intro deck is left out of this snapshot because its slide images could
not be checked for job drawings.
2026-09-09 08:32:15 -05:00

142 lines
5.8 KiB
Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Build a Panel Analysis page-spec JSON with NO Revit running.
python tools/pa_pagespec.py
python tools/pa_pagespec.py --reads reads.json --out spec.json
python tools/pa_pagespec.py --reads reads.json --job-root JOBDIR
This is the CPython half of the owner's scope note on the HTML-page
rewrite: the front end (前端) must open and run before Revit ever supplies a
value, Revit only supplies PREFILL. `vibe.pa_pagespec.build_pagespec()` is
already Revit-free (see that module's own docstring); this script is what
gets `values` / `know` in front of it the same way `tools/deckcheck.py` gets
knowledge in front of its own checks -- off a live `doc`, on host CPython.
`--reads` is a JSON file shaped like whatever `PanelAnalysis.pushbutton`'s
`_reads_for()` hands `panelanalysisform.prefill()` -- the exact file a
bridge dry-run should save (see `_scratch/pa_pagespec_dump.py`, which writes
`_scratch/pa_reads_wpa655.json` for this purpose) or a hand-written stand-in
for testing the page with no model at all. Omitted, or pointed at a file
that is not there, this runs with `reads = {}` -- an EMPTY, but still
complete and defaulted, form (see `pa_pagespec.build_pagespec`'s "THE TWO
CALLING SHAPES" note): every field present, every `pa_defaults` rule
applied, every table at its minimum row count, every drawing's geometry
degraded to a placeholder rather than missing. That is the shape the HTML
page needs to be developable and testable in a browser with no Revit and no
model at all.
`--job-root` names the folder holding a job's OWN `knowledge.yaml` (see
`tools/deckcheck.py`'s `build_knowledge()` docstring for exactly what that
resolves) -- omitted, this runs with just the shipped defaults + catalogue
layers, same as `deckcheck.build_knowledge(None)`.
`--out` writes the spec there (UTF-8); omitted, it prints to stdout.
CONSTRAINTS THIS RESPECTS (same as `tools/deckcheck.py`, for the same
reason): host CPython, no Revit, standalone; prints no plant or client name
of its own -- design numbers and marks flowing through a `--reads` file are
data, not this file's business, per CLAUDE.md's "No client or job
information in the repo".
"""
from __future__ import print_function
import argparse
import io
import json
import os
import sys
import types
HERE = os.path.dirname(os.path.abspath(__file__))
REPO = os.path.abspath(os.path.join(HERE, os.pardir))
LIB = os.path.join(REPO, 'WillTools.extension', 'lib')
if LIB not in sys.path:
sys.path.insert(0, LIB)
# Same trick `tools/deckcheck.py` / `tools/ticket_delta.py` /
# `WillTools.extension/tests/conftest.py` all use: `vibe/__init__.py`
# imports the Revit API at module scope, which is fatal off Revit. Register
# a bare `vibe` package pointed at the directory so the Revit-free
# submodules (`panelanalysisform`, `pa_pagespec`, the five page modules, the
# four `pa_sketch_*` modules, `pa_defaults`) import normally and
# `__init__.py` never runs.
if 'vibe' not in sys.modules:
_stub = types.ModuleType('vibe')
_stub.__path__ = [os.path.join(LIB, 'vibe')]
sys.modules['vibe'] = _stub
if HERE not in sys.path:
sys.path.insert(0, HERE)
import deckcheck # noqa: E402 -- reuses build_knowledge(), not re-derived
from vibe import pa_pagespec # noqa: E402
from vibe import panelanalysisform as F # noqa: E402
def _load_reads(path):
"""`(reads_dict, error_or_None)`. A path that is not given, or not
there, is the ordinary "no Revit at all" case -- `{}`, no error. A path
that IS there and will not parse as a JSON object is reported, never
silently treated as empty (the same "a layer that fails to load must
never look like a layer that agrees" discipline `knowledge._merge_file`
uses)."""
if not path:
return {}, None
if not os.path.isfile(path):
return {}, "'{0}' does not exist; running with reads = {{}}.".format(
path)
try:
with io.open(path, 'r', encoding='utf-8') as fh:
data = json.load(fh)
except Exception as exc:
return {}, "'{0}' could not be read as JSON ({1!r}); running with " \
'reads = {{}}.'.format(path, exc)
if not isinstance(data, dict):
return {}, "'{0}' is not a JSON object; running with reads = {{}}." \
.format(path)
return data, None
def main(argv=None):
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
parser.add_argument('--reads', default=None,
help='JSON file of the model reads dict (omit for '
'an empty, still-complete form)')
parser.add_argument('--job-root', default=None,
help="folder holding the job's own knowledge.yaml "
'(omit for shipped defaults + catalogue only)')
parser.add_argument('--out', default=None,
help='write the spec here (UTF-8); omit to print '
'to stdout')
args = parser.parse_args(argv)
reads, reads_err = _load_reads(args.reads)
if reads_err:
sys.stderr.write(reads_err + '\n')
try:
know = deckcheck.build_knowledge(args.job_root)
except Exception as exc:
sys.stderr.write(
'knowledge could not be resolved off Revit ({0!r}); running '
'with the bare shipped defaults.\n'.format(exc))
know = {}
values = F.prefill(reads, know=know)
spec = pa_pagespec.build_pagespec(values, know=know, reads=reads)
blob = pa_pagespec.dumps(spec)
if args.out:
with io.open(args.out, 'w', encoding='utf-8') as fh:
fh.write(blob if isinstance(blob, str) else blob.decode('utf-8'))
sys.stderr.write('wrote {0} ({1} bytes)\n'.format(
args.out, len(blob)))
else:
print(blob)
return 0
if __name__ == '__main__':
raise SystemExit(main())