Files
willtools-demo/WillTools.extension/lib/vibe/plantpath.py
T
will 52d94303bc
ci / test (push) Has been cancelled
WillTools: precast shop-ticket QC and panel handling tools
Public demo snapshot of an in-house pyRevit extension for precast concrete
shop-ticket QC (37 checks / 46 checklist rows) and a standalone panel
handling solver (PCI Chapter 8).

- No client, job, plant or product data: knowledge layers ship as
  structural templates only.
- Ticket QC's model-read escalation targets a LAN model server endpoint
  (configurable), not a cloud provider.
- Built from the private development repo via tools/make_dist.py.
2026-09-03 16:33:47 -05:00

115 lines
4.8 KiB
Python

# -*- coding: utf-8 -*-
"""Where the PLANT knowledge layer lives on disk.
Split out of `designspec` for one reason: `designspec` imports the Revit API
at module scope (evidence scraping needs a live document), and choosing the
plant knowledge file is pure path arithmetic that a host CPython test can and
should exercise directly. `designspec` re-exports every name here, so nothing
that already says `designspec.plant_knowledge_path()` had to change.
THE LAYOUT, in one line:
<root>/knowledge.yaml the machine-wide plant file
<root>/<plant>/knowledge.yaml one folder per plant
A job's own knowledge file names its plant in `job.plant`; `knowledge.load()`
reads that name BEFORE it merges anything and passes it here. A job that
names no plant reads the machine-wide file, which is how every install worked
before per-plant folders existed and still does.
IronPython 2.7: no f-strings, no pathlib.
"""
# pylint: disable=invalid-name
import os
import re
from vibe.compat import string_types
# The plant-wide knowledge file: one per machine, above every project.
PLANT_FOLDER = 'WillTools'
PLANT_ENV = 'WILLTOOLS_KNOWLEDGE'
# What a plant id may be made of, because it is used as a PATH COMPONENT.
# It arrives from a YAML file, so `..`, a slash or a drive letter in it would
# be a directory traversal written by whoever edits a job file. Anything
# outside this set is refused outright rather than scrubbed into something
# else: a plant id that quietly became a different folder name is exactly the
# silence this codebase keeps paying for.
_PLANT_ID_RE = re.compile(r'^[A-Za-z0-9][A-Za-z0-9._-]*$')
def valid_plant_id(plant):
"""Is this string usable as a plant id, i.e. as one folder name?
Returns False for None, for the empty string, and for anything carrying a
separator, a drive letter or a dot-dot. See _PLANT_ID_RE.
"""
if not plant or not isinstance(plant, string_types):
return False
return bool(_PLANT_ID_RE.match(plant.strip()))
def plant_root():
"""The folder the plant knowledge lives in -- one file, or one per plant.
THE ROOT IS A FOLDER AND THE MACHINE-WIDE FILE IS `knowledge.yaml` INSIDE
IT. That relationship has always held (%APPDATA%/WillTools/knowledge.yaml)
and is what lets WILLTOOLS_KNOWLEDGE keep meaning what it always meant
while also naming a root:
set to a FOLDER that folder is the root. Nothing else to say.
set to a FILE that file is the machine-wide plant file, so its
DIRECTORY is the root, by the same relationship.
Not a guess -- the definition, read backwards.
unset %APPDATA%/WillTools, as ever.
Which is why a machine already pointing the variable at a single file
picks up per-plant folders with no change to the variable: the folders
are siblings of the file it already names.
A path that is set but exists as neither is treated as a FILE, because
that is what the variable has meant for its whole life and the operator
almost certainly meant a file that has moved -- the missing path is then
reported by name instead of a different, existing path being read in
silence.
"""
override = os.environ.get(PLANT_ENV)
if not override:
base = os.environ.get('APPDATA') or os.path.expanduser('~')
return os.path.join(base, PLANT_FOLDER)
if os.path.isdir(override):
return override
return os.path.dirname(override) or override
def plant_knowledge_path(plant=None):
"""The plant knowledge file this run should read.
No plant named -- the ordinary case, and every install that predates the
key -- returns the machine-wide file exactly as this function always has:
WILLTOOLS_KNOWLEDGE if it names one, else <root>/knowledge.yaml.
A plant named (`job.plant` in the job layer) returns
<root>/<plant>/knowledge.yaml. A SECOND PLANT IS A NEW FOLDER, never an
edit to the first one's file. See plant_root() for what <root> is and why
a machine already pointing the variable at a single file needs no change.
The per-plant FOLDER and the machine-wide FILE share a root and cannot
collide: one is a directory, the other is called `knowledge.yaml`.
An INVALID plant id -- a separator, a `..`, an empty string -- resolves to
NOTHING and returns None. load() reports it. Scrubbing a malformed name
into some nearby folder is how a run ends up on another plant's rules
while looking entirely normal.
"""
if plant is None:
override = os.environ.get(PLANT_ENV)
if override and not os.path.isdir(override):
return override # the single-file arrangement, untouched
return os.path.join(plant_root(), 'knowledge.yaml')
if not valid_plant_id(plant):
return None
return os.path.join(plant_root(), plant.strip(), 'knowledge.yaml')