8e6ee31596
ci / test (push) Successful in 6m18s
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.
132 lines
5.4 KiB
Python
132 lines
5.4 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_ids():
|
|
"""Every plant folder under the root that carries a knowledge.yaml,
|
|
sorted -- the candidates a job can be resolved to. A root that does not
|
|
exist yields []. Never raises."""
|
|
try:
|
|
root = plant_root()
|
|
out = []
|
|
for name in os.listdir(root):
|
|
if not valid_plant_id(name):
|
|
continue
|
|
if os.path.isfile(os.path.join(root, name, 'knowledge.yaml')):
|
|
out.append(name)
|
|
return sorted(out)
|
|
except Exception:
|
|
return []
|
|
|
|
|
|
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')
|
|
|
|
|