25592fa6c2
ci / test (push) Successful in 6m14s
History-free snapshot of the private development repository at 77a82eb. Code, docs and the public catalogue only: no plant or job knowledge layers, no client, job or producer names (tools/scrub.py clean against the widened term list; a second word-level grep clean). The intro deck is left out because its slide images cannot be checked for job drawings.
186 lines
8.8 KiB
Python
186 lines
8.8 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""Which job's knowledge layer a HOST-SIDE tool should read, with no doc open.
|
|
|
|
Inside Revit, `designspec.root_dir(doc)` finds a job's `_ticketqc` folder by
|
|
`doc.Title` -- WILLTOOLS_JOBS wins when it is set, APPDATA is the fallback for
|
|
a model with no path yet. That lookup needs a live document and cannot be
|
|
ported here: a terminal running `tools/snapshot_cycle.py` or
|
|
`tools/knowledge_drift.py` has no `doc.Title` to pick a folder by, and
|
|
guessing one from several candidates is a worse failure than asking, because a
|
|
wrong guess runs silently on the wrong job's settings and looks completely
|
|
normal -- the exact failure mode this whole codebase is built against
|
|
(see CLAUDE.md's "could not judge" discipline).
|
|
|
|
THE RESOLUTION ORDER, deliberately, and in this order only:
|
|
|
|
1. an EXPLICIT file (a `--knowledge` flag, or an env var that names a
|
|
FILE) wins outright. The caller already knows which file; nothing
|
|
here second-guesses it, and a bad path is reported the ordinary way a
|
|
missing file is reported (by whoever tries to read it), not specially
|
|
here.
|
|
2. WILLTOOLS_JOBS names a folder holding EXACTLY ONE job subfolder (one
|
|
that has a `knowledge.yaml` in it) -- unambiguous, so it is used with
|
|
no further input.
|
|
3. a NAMED job (`--job "sample_job"`) picks one out of several by
|
|
name.
|
|
4. otherwise: REFUSE, and name every candidate folder found. This is the
|
|
correct behaviour when several real job folders exist and none was
|
|
named -- not a hole to fill in later. Silently picking "the newest
|
|
one" or "the one that happens to set the key being asked for" is a
|
|
guess wearing the clothes of a rule, and the day it guesses wrong the
|
|
run looks completely normal while reading another job's tolerances.
|
|
|
|
This is intentionally the only place this order is written down. Two tools
|
|
reading job knowledge off disk had two different half-answers to "where is
|
|
it" before this module existed -- `tools/snapshot_cycle.py` required an
|
|
explicit `--knowledge` and reported the resulting absence as if the setting
|
|
itself were missing, and `tools/knowledge_drift.py` walked a path hardcoded
|
|
to this checkout and ignored WILLTOOLS_JOBS entirely. Both import this now.
|
|
|
|
`plantpath.py` is this file's sibling for the PLANT layer -- one FILE, chosen
|
|
by an env var that may name either a file or its folder. This is a different
|
|
shape (WILLTOOLS_JOBS names a folder that may hold several jobs, and there is
|
|
no `doc.Title` here to break the tie) so it earns its own module rather than
|
|
being bent to fit plantpath's shape, but the discipline is the same one:
|
|
never fall back to a plausible-looking wrong answer in silence, and say
|
|
which of "not configured", "not found" and "found but ambiguous" happened,
|
|
because they are different facts and read as different sentences.
|
|
|
|
IronPython 2.7: no f-strings, no pathlib. (This module has no Revit import at
|
|
module scope -- or at all -- so it is also safe to load from host CPython the
|
|
same way `plantpath.py` is: `tools/snapshot_cycle.py` and
|
|
`tools/knowledge_drift.py` both do, via the `vibe` package stub documented in
|
|
`WillTools.extension/tests/conftest.py`.)
|
|
"""
|
|
# pylint: disable=invalid-name
|
|
import os
|
|
|
|
JOBS_ENV = 'WILLTOOLS_JOBS' # same spelling as designspec.JOBS_ENV
|
|
KNOWLEDGE_FILENAME = 'knowledge.yaml'
|
|
|
|
|
|
class JobLocateResult(object):
|
|
"""What `resolve_job_knowledge()` found, or precisely why it did not.
|
|
|
|
`path` is set only on success -- callers should treat `bool(result)` (or
|
|
`result.path`) as the found/not-found test. `blocking` marks the
|
|
not-found cases that are a genuine misconfiguration worth stopping a run
|
|
over (an ambiguous WILLTOOLS_JOBS, a `--job` that names nothing, a
|
|
WILLTOOLS_JOBS that is not even a folder) as opposed to the ordinary
|
|
"nothing configured yet" case, which a caller may reasonably continue
|
|
past on DEFAULTS alone -- exactly as it always could when no `--knowledge`
|
|
was given.
|
|
"""
|
|
__slots__ = ('path', 'how', 'reason', 'candidates', 'root', 'blocking')
|
|
|
|
def __init__(self, path=None, how=None, reason=None, candidates=None,
|
|
root=None, blocking=False):
|
|
self.path = path
|
|
self.how = how
|
|
self.reason = reason
|
|
self.candidates = list(candidates or [])
|
|
self.root = root
|
|
self.blocking = bool(blocking)
|
|
|
|
def __bool__(self):
|
|
return self.path is not None
|
|
__nonzero__ = __bool__ # IronPython 2.7
|
|
|
|
def __repr__(self):
|
|
if self.path:
|
|
return 'JobLocateResult(path={0!r}, how={1!r})'.format(
|
|
self.path, self.how)
|
|
return 'JobLocateResult(reason={0!r})'.format(self.reason)
|
|
|
|
|
|
def job_candidates(jobs_root):
|
|
"""Every subfolder of `jobs_root` that carries a `knowledge.yaml`.
|
|
|
|
Sorted, so a refusal that lists them is stable across runs and a "single
|
|
unambiguous" check does not depend on directory-walk order. Nothing here
|
|
tries to tell a "real" job folder from a stray one (a leftover detach
|
|
copy, an old bootstrap) -- that would be exactly the judgement call this
|
|
module exists to refuse to make silently. Every folder with a
|
|
`knowledge.yaml` in it is a candidate; a human names the right one.
|
|
"""
|
|
out = []
|
|
if not jobs_root or not os.path.isdir(jobs_root):
|
|
return out
|
|
for name in sorted(os.listdir(jobs_root)):
|
|
folder = os.path.join(jobs_root, name)
|
|
if not os.path.isdir(folder):
|
|
continue
|
|
candidate = os.path.join(folder, KNOWLEDGE_FILENAME)
|
|
if os.path.isfile(candidate):
|
|
out.append(candidate)
|
|
return out
|
|
|
|
|
|
def _job_name_of(knowledge_path):
|
|
return os.path.basename(os.path.dirname(knowledge_path))
|
|
|
|
|
|
def resolve_job_knowledge(explicit=None, jobs_root=None, job_name=None,
|
|
env=None):
|
|
"""Which job knowledge.yaml a host-side tool should read. A JobLocateResult.
|
|
|
|
explicit a path stated outright -- a `--knowledge` flag, or an env var
|
|
that already names a FILE (`WILLTOOLS_JOB_KNOWLEDGE`, as
|
|
`tools/snapshot_cycle.py`'s argparse default already reads).
|
|
Wins over everything below. Not checked for existence here:
|
|
whoever reads the file reports a missing one the ordinary
|
|
way, and duplicating that check here would be a second,
|
|
possibly-drifted answer to "does this file exist".
|
|
jobs_root WILLTOOLS_JOBS, already resolved by the caller. Pass `None`
|
|
to have it read from `env` here instead.
|
|
job_name a folder name under `jobs_root`, stated explicitly (a `--job`
|
|
flag) to pick one out of several.
|
|
env `os.environ` by default; overridable so tests never touch the
|
|
real environment.
|
|
"""
|
|
env = os.environ if env is None else env
|
|
if explicit:
|
|
return JobLocateResult(path=explicit, how='explicit')
|
|
|
|
root = jobs_root if jobs_root is not None else env.get(JOBS_ENV)
|
|
if not root:
|
|
return JobLocateResult(
|
|
reason='no job knowledge file was named (--knowledge) and {0} '
|
|
'is not set, so there is no folder to look for a job '
|
|
'under.'.format(JOBS_ENV))
|
|
if not os.path.isdir(root):
|
|
return JobLocateResult(
|
|
root=root, blocking=True,
|
|
reason='{0} is set to {1}, but that is not a folder.'
|
|
.format(JOBS_ENV, root))
|
|
|
|
if job_name:
|
|
candidate = os.path.join(root, job_name, KNOWLEDGE_FILENAME)
|
|
if os.path.isfile(candidate):
|
|
return JobLocateResult(path=candidate, how='named', root=root)
|
|
candidates = job_candidates(root)
|
|
names = [_job_name_of(c) for c in candidates]
|
|
return JobLocateResult(
|
|
root=root, candidates=candidates, blocking=True,
|
|
reason='no job named {0!r} under {1} (looked for {2}). Job '
|
|
'folders found there: {3}'
|
|
.format(job_name, root, candidate,
|
|
', '.join(names) or '(none)'))
|
|
|
|
candidates = job_candidates(root)
|
|
if len(candidates) == 1:
|
|
return JobLocateResult(path=candidates[0], how='single', root=root)
|
|
if not candidates:
|
|
return JobLocateResult(
|
|
root=root,
|
|
reason='{0} is set to {1}, but no job folder under it has a '
|
|
'{2}.'.format(JOBS_ENV, root, KNOWLEDGE_FILENAME))
|
|
names = [_job_name_of(c) for c in candidates]
|
|
return JobLocateResult(
|
|
root=root, candidates=candidates, blocking=True,
|
|
reason='{0} is {1}, which holds {2} job folders ({3}) -- there is '
|
|
'no doc.Title here to pick one by, so nothing here guesses. '
|
|
'Say which with --job, or point --knowledge at the file '
|
|
'directly.'.format(JOBS_ENV, root, len(candidates),
|
|
', '.join(names)))
|