WillTools 2026-09-09 -- clean snapshot of the development repo for review
ci / test (push) Successful in 6m14s
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.
This commit is contained in:
+516
@@ -0,0 +1,516 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Beam Analysis's local server -- the CPython half of the HTML front/back end.
|
||||
|
||||
python tools/ba_web.py [--reads reads.json] [--job-root DIR]
|
||||
[--port N|0] [--open] [--title T]
|
||||
|
||||
`tools/pa_web.py` with the tool swapped (docs/ba-web-spec.md section 5): same
|
||||
argv, same first stdout line, same 127.0.0.1 bind, same
|
||||
`http.server.ThreadingHTTPServer`, same reads/job-root/mark resolution. The
|
||||
page the server feeds is built by `tools/ba_pagespec.py` (the job <-> values
|
||||
half, `build_pagespec` / `geometry`), and `POST /api/run` runs the beamcalc
|
||||
engine IN-PROCESS -- `beamcalc.model.from_job` -> `beamcalc.solve.analyse` --
|
||||
the way PA's server calls `panelanalysis.build_page`, never a subprocess: one
|
||||
interpreter, one traceback (spec section 5). Results are `<job-root>/
|
||||
beamanalysis/<mark>.job.json` (the job actually run), `<mark>.ba.json`
|
||||
(`beamcalc.cli.summary(a)`), `<mark>.html` (`beamcalc.report.render(a)`).
|
||||
|
||||
NO REVIT ANYWHERE ON THIS PATH. Like pa_web.py, this module registers the
|
||||
bare `vibe` package stub at import (`vibe/__init__.py` imports the Revit
|
||||
API, which is fatal off Revit), then imports `tools/pa_web.py` for its
|
||||
job/reads plumbing and the shared static handler, and `tools/ba_pagespec.py`
|
||||
for the pagespec and the job conversion. `beamcalc` is plain CPython
|
||||
(imported by ba_pagespec the ordinary way, falling back to
|
||||
`beamcalc/src` on `sys.path`); `beamcalc.report` pulls `panellift.report`,
|
||||
which is CPython too.
|
||||
|
||||
Routes (shared handler and payload shapes inherited from pa_web.py -- the
|
||||
PA `app.js` front end is reused for this tool, spec section 7):
|
||||
|
||||
GET / tools/ba_web/static/index.html when it exists,
|
||||
else tools/pa_web/static/index.html (app.js,
|
||||
draw.js, units.js and willtools.css are PA's
|
||||
single copies -- ba_web's static dir only
|
||||
overrides them).
|
||||
GET /api/spec the pagespec JSON: `ba_pagespec.
|
||||
build_pagespec(values=..., reads=...)` with the
|
||||
values file a previous /api/save left for this
|
||||
mark merged over the `--reads` prefill. reads
|
||||
is a PARTIAL job (spec section 6): only what
|
||||
the model could supply, plus `basis`.
|
||||
POST /api/geometry body {"values": {...}} -- {"geometry":
|
||||
ba_pagespec.geometry(ba_pagespec.job_from_values(
|
||||
values))}, the SAME dict /api/spec computes off
|
||||
the values the client currently holds (not a
|
||||
save, not the prefill). A ValueError parsing the
|
||||
values -- job_from_values raises one whose
|
||||
message starts with the values key -- or any
|
||||
other exception comes back {"geometry": {},
|
||||
"error": text}, never a dead page.
|
||||
POST /api/save body {"values": {...}} -- persists that dict as
|
||||
this mark's saved edits
|
||||
(`<job-root>/beamanalysis/<mark>.values.json`),
|
||||
read back by the next /api/spec.
|
||||
POST /api/run body {"values": {...}} -- `ba_pagespec.
|
||||
job_from_values(values)`, `beamcalc.model.
|
||||
from_job(job)` and `beamcalc.solve.analyse` in
|
||||
process. On a ValueError from the conversion/
|
||||
read (a value the engine refuses), {"ok": false,
|
||||
"kind": "input", "problems": [{"key": <the
|
||||
values key the message starts with, or ''>,
|
||||
"text": ...}]}; on any other exception,
|
||||
{"ok": false, "kind": "error", "problems":
|
||||
[...]} with the exception text and the
|
||||
traceback to stderr only. `kind` is what the
|
||||
shared page renders the refusal by -- "input"
|
||||
is the user's to fix, "error" is a bug (see
|
||||
pa_web.run_analysis). On success it
|
||||
writes `<mark>.job.json`, `<mark>.ba.json`
|
||||
(`cli.summary(a)`) and `<mark>.html`
|
||||
(`report.render(a)`) into the same
|
||||
`beamanalysis/` folder and answers {"ok": true,
|
||||
"result_url": "/results/<mark>.html"}.
|
||||
GET /results/<file> serves a written results HTML (or anything else
|
||||
landed in the same folder) by filename.
|
||||
POST /api/close acknowledges, then shuts the server down (used
|
||||
by a page's own Close button, and by the Revit
|
||||
button once its WebView2 window closes).
|
||||
POST /api/log body {"text": ...} (anything JSON) -- appends
|
||||
one line, `<ISO timestamp>\t<body compact>`, to
|
||||
`<job-root>/beamanalysis/<mark>.client.log` and
|
||||
answers {"ok": true} -- the client-side error
|
||||
sink PA's `app.js` posts to (ba-web-spec.md
|
||||
section 5: the route PA's server already has
|
||||
for Beam Analysis to reuse).
|
||||
|
||||
Static assets are served from `tools/ba_web/static/` first and
|
||||
`tools/pa_web/static/` second (spec section 5): ba_web/static holds only
|
||||
`index.html` and `ba_drawings.js`; `app.js`, `units.js`, `willtools.css`
|
||||
and `draw.js` are PA's single copies, never duplicated. A static path in
|
||||
neither directory is a 404.
|
||||
|
||||
Only binds 127.0.0.1. Threaded (`http.server.ThreadingHTTPServer`) so a slow
|
||||
solve never blocks a concurrent static request. Every request and every solve
|
||||
exception is logged to stderr; nothing here ever prints a plant, job or
|
||||
client name (`--reads` / `--job-root` are data, supplied by the caller, never
|
||||
this file's own business -- same rule `tools/pa_pagespec.py` follows; the
|
||||
`mark`, the `job_root` and every path to a written file stay OFF stdout).
|
||||
The one place a client-supplied value is written anywhere is `POST
|
||||
/api/log`, into `<mark>.client.log` -- into the FILE, never stdout.
|
||||
|
||||
JOB ROOT resolution (where `<mark>.values.json` / `<mark>.job.json` / the
|
||||
results HTML all live, one folder per mark under `<job-root>/beamanalysis/`):
|
||||
identical to pa_web.py -- the `--reads` file's own `"job_root"` field wins
|
||||
when present; else `--job-root`; else `%LOCALAPPDATA%\\WillTools\\panelanalysis`
|
||||
(pa_web.py's own default; Beam Analysis deliberately keeps the same fallback
|
||||
folder rather than invent a second). The MARK itself is `--reads`' own
|
||||
`"mark"` field, slugged, or the literal string `'blank'` when there is no
|
||||
mark at all -- so two blank-path runs never collide with a real beam's saved
|
||||
files, and a re-run of the same mark picks its saved edits back up.
|
||||
|
||||
`--port 0` (the default) lets the OS pick a free port; the actual URL is
|
||||
printed as `BA_WEB_URL=http://127.0.0.1:<port>/` -- the FIRST line on stdout,
|
||||
flushed immediately, so a launcher (the Revit button, a test) can read it off
|
||||
a pipe before the server does anything else. `--open` additionally opens it
|
||||
in the default browser.
|
||||
"""
|
||||
from __future__ import print_function
|
||||
|
||||
import argparse
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import threading
|
||||
import traceback
|
||||
import types
|
||||
import webbrowser
|
||||
|
||||
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/pa_web.py` / `tools/deckcheck.py` 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
|
||||
# import normally and `__init__.py` never runs. pa_web.py does the same at
|
||||
# its own import, so whichever module registers first wins -- identical stub.
|
||||
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)
|
||||
|
||||
# `beamcalc` is plain CPython; ba_pagespec imports it the ordinary way and
|
||||
# falls back to the in-repo source tree (<repo>/beamcalc/src on sys.path) when
|
||||
# it is not installed -- importing ba_pagespec performs that fallback.
|
||||
import pa_web # noqa: E402 -- job/reads plumbing + the shared handler
|
||||
import ba_pagespec # noqa: E402 -- pagespec builder, job <-> values
|
||||
import beamcalc.model as BM # noqa: E402
|
||||
from beamcalc import cli as BC_CLI # noqa: E402
|
||||
from beamcalc import report as BC_REPORT # noqa: E402
|
||||
from beamcalc import solve as BC_SOLVE # noqa: E402
|
||||
|
||||
# Re-exported rather than re-derived (docs/ba-web-spec.md section 5: "same
|
||||
# argv ... same job_root resolution ... files live under
|
||||
# <job_root>/beamanalysis/"): these ARE pa_web.py's own functions, working on
|
||||
# a Context that differs only in which folder it points at.
|
||||
_load_reads = pa_web._load_reads
|
||||
_slug = pa_web._slug
|
||||
_ensure_dir = pa_web._ensure_dir
|
||||
_default_job_root = pa_web._default_job_root
|
||||
_send_file = pa_web.Handler._send_file
|
||||
_serve_from = pa_web.Handler._serve_from
|
||||
_read_json_body = pa_web.Handler._read_json_body
|
||||
|
||||
BA_DIR_NAME = 'beamanalysis'
|
||||
STATIC_DIRS = (os.path.join(HERE, 'ba_web', 'static'),
|
||||
os.path.join(HERE, 'pa_web', 'static'))
|
||||
DEFAULT_JOB_ROOT = _default_job_root() # %LOCALAPPDATA%\WillTools\panelanalysis
|
||||
DEFAULT_TITLE = 'Beam Analysis'
|
||||
|
||||
|
||||
class Context(object):
|
||||
"""Everything one server process needs, resolved once at startup -- the
|
||||
beam analogue of `pa_web.Context`, with the folder swapped."""
|
||||
|
||||
def __init__(self, reads, job_root, ba_dir, mark):
|
||||
self.reads = reads
|
||||
self.job_root = job_root
|
||||
self.ba_dir = ba_dir
|
||||
self.mark = mark
|
||||
|
||||
|
||||
def build_context(reads_path=None, job_root_override=None):
|
||||
"""`(reads, job_root, ba_dir, mark)` resolved exactly the way pa_web.py
|
||||
resolves its own: `--reads`' `job_root` field, else `job_root_override`,
|
||||
else `pa_web._default_job_root()`; mark slugged off `--reads`' `mark`
|
||||
field, else `'blank'`. The folder is `<job_root>/beamanalysis/`."""
|
||||
reads, reads_err = _load_reads(reads_path)
|
||||
if reads_err:
|
||||
sys.stderr.write('ba_web: ' + reads_err + '\n')
|
||||
|
||||
job_root = reads.get('job_root') or job_root_override or DEFAULT_JOB_ROOT
|
||||
ba_dir = os.path.join(job_root, BA_DIR_NAME)
|
||||
_ensure_dir(ba_dir)
|
||||
|
||||
mark_raw = (reads.get('mark') or '').strip()
|
||||
mark = _slug(mark_raw) if mark_raw else 'blank'
|
||||
|
||||
return Context(reads=reads, job_root=job_root, ba_dir=ba_dir, mark=mark)
|
||||
|
||||
|
||||
# ------------------------------------------------------------- the pagespec
|
||||
|
||||
def _values_path(ctx):
|
||||
return os.path.join(ctx.ba_dir, ctx.mark + '.values.json')
|
||||
|
||||
|
||||
def _load_saved_values(ctx):
|
||||
"""The `{key: value}` dict a previous `/api/save` for this mark left --
|
||||
`{}` for anything else (no file yet, a file that will not parse, a file
|
||||
that is not shaped `{"values": {...}}`). Never raises."""
|
||||
path = _values_path(ctx)
|
||||
if not os.path.isfile(path):
|
||||
return {}
|
||||
try:
|
||||
with io.open(path, 'r', encoding='utf-8') as fh:
|
||||
data = json.load(fh)
|
||||
except Exception:
|
||||
return {}
|
||||
if isinstance(data, dict):
|
||||
values = data.get('values')
|
||||
if isinstance(values, dict):
|
||||
return values
|
||||
return {}
|
||||
|
||||
|
||||
def _save_values(ctx, values):
|
||||
_ensure_dir(ctx.ba_dir)
|
||||
blob = json.dumps({'values': values}, indent=1, sort_keys=True,
|
||||
ensure_ascii=False)
|
||||
with io.open(_values_path(ctx), 'w', encoding='utf-8') as fh:
|
||||
fh.write(blob)
|
||||
|
||||
|
||||
def build_spec_json(ctx):
|
||||
"""The pagespec dict for GET /api/spec: prefill off `ctx.reads` -- a
|
||||
PARTIAL job (ba-web-spec.md section 6: absent means unread) -- flattened
|
||||
to values by `ba_pagespec.build_pagespec`, with any saved edits for this
|
||||
mark folded over the result before the geometry is computed, so a saved
|
||||
length/width edit also redraws the geometry, not just the raw number."""
|
||||
try:
|
||||
saved = _load_saved_values(ctx)
|
||||
except Exception:
|
||||
saved = {}
|
||||
values = None
|
||||
if isinstance(saved, dict) and saved:
|
||||
# The saved dict is a complete values set from the last /api/save;
|
||||
# build_pagespec copies it and fills geometry from it.
|
||||
values = saved
|
||||
return ba_pagespec.build_pagespec(values=values, reads=ctx.reads)
|
||||
|
||||
|
||||
def geometry_for(ctx, values):
|
||||
"""`{'geometry': {...}}` for `POST /api/geometry` -- the page's own
|
||||
`values` dict, whatever the client currently holds, run back through
|
||||
`ba_pagespec.geometry(ba_pagespec.job_from_values(values))` so the dict
|
||||
reflects an edit the client made seconds ago and has not (or will not)
|
||||
have saved yet. Never raises -- a `ValueError` from `job_from_values`
|
||||
(its messages name the values key that will not parse) and any other
|
||||
exception come back as an empty geometry dict plus an `error` string, so
|
||||
a client-side redraw degrades to 'nothing to draw' rather than a dead
|
||||
page."""
|
||||
try:
|
||||
geo = ba_pagespec.geometry(ba_pagespec.job_from_values(values))
|
||||
return {'geometry': geo if isinstance(geo, dict) else {}}
|
||||
except Exception as exc:
|
||||
sys.stderr.write('ba_web: geometry rebuild raised:\n' +
|
||||
traceback.format_exc())
|
||||
return {'geometry': {},
|
||||
'error': '{0}: {1}'.format(type(exc).__name__, exc)}
|
||||
|
||||
|
||||
# ------------------------------------------------------------- run / solve
|
||||
|
||||
def _key_for_problem(message):
|
||||
"""The values key a `ValueError` message is about, or `''` when none.
|
||||
`ba_pagespec.job_from_values` and `beamcalc.model.from_job` both raise
|
||||
`ValueError`s whose text names the key or job path that was refused
|
||||
(`"extents.length_ft: not a number: 'abc'"`; `JobError('missing required
|
||||
value: extents.section.width_in')`); the message may quote the key
|
||||
(`'unknown section type: ...'` names no key at all). Best-effort: a
|
||||
dotted key at the START of the message is the values key itself; a
|
||||
dotted key inside the message maps to the values key when it is one the
|
||||
spec renders; anything else is `''`."""
|
||||
if not message:
|
||||
return ''
|
||||
head = message.split(':', 1)[0].strip()
|
||||
if head and all(part.replace('_', '').isalnum() for part in head.split('.')):
|
||||
return head
|
||||
return ''
|
||||
|
||||
|
||||
def run_analysis(ctx, values):
|
||||
"""`{"ok": True, "result_url": ...}` or `{"ok": False, "kind": ...,
|
||||
"problems": [...]}` -- `kind` as in `pa_web.run_analysis`: `'input'`
|
||||
for a value the engine refuses, `'error'` for a solve/write that raised.
|
||||
|
||||
Runs `job_from_values` -> `model.from_job` -> `solve.analyse` IN-PROCESS
|
||||
(the way PA's server calls `panelanalysis.build_page`), writes
|
||||
`<mark>.job.json` (the job actually run), `<mark>.ba.json`
|
||||
(`cli.summary(a)`) and `<mark>.html` (`report.render(a)`) into
|
||||
`<ctx.ba_dir>`, and answers the result URL. Never raises: a `ValueError`
|
||||
from the conversion/read is reported as a problem keyed to the values
|
||||
key whose value caused it where that can be told (spec section 5); any
|
||||
other exception is reported with the exception text and the traceback to
|
||||
stderr only."""
|
||||
job = None
|
||||
try:
|
||||
job = ba_pagespec.job_from_values(values)
|
||||
except ValueError as exc:
|
||||
return {'ok': False, 'kind': 'input', 'problems': [
|
||||
{'key': _key_for_problem(str(exc)), 'text': str(exc)}]}
|
||||
|
||||
try:
|
||||
beam = BM.from_job(job)
|
||||
except ValueError as exc:
|
||||
return {'ok': False, 'kind': 'input', 'problems': [
|
||||
{'key': _key_for_problem(str(exc)), 'text': str(exc)}]}
|
||||
|
||||
try:
|
||||
analysis = BC_SOLVE.analyse(beam)
|
||||
except Exception as exc:
|
||||
sys.stderr.write('ba_web: beamcalc.solve.analyse raised:\n' +
|
||||
traceback.format_exc())
|
||||
return {'ok': False, 'kind': 'error', 'problems': [
|
||||
{'key': '', 'text': '{0}: {1}'.format(type(exc).__name__, exc)}]}
|
||||
|
||||
_ensure_dir(ctx.ba_dir)
|
||||
mark = ctx.mark
|
||||
paths = {ext: os.path.join(ctx.ba_dir, mark + ext)
|
||||
for ext in ('.job.json', '.ba.json', '.html')}
|
||||
try:
|
||||
with io.open(paths['.job.json'], 'w', encoding='utf-8') as fh:
|
||||
json.dump(job, fh, indent=1, sort_keys=True, ensure_ascii=False)
|
||||
with io.open(paths['.ba.json'], 'w', encoding='utf-8') as fh:
|
||||
json.dump(BC_CLI.summary(analysis), fh, indent=1, sort_keys=True,
|
||||
ensure_ascii=False)
|
||||
with io.open(paths['.html'], 'w', encoding='utf-8') as fh:
|
||||
fh.write(BC_REPORT.render(analysis))
|
||||
except Exception as exc:
|
||||
sys.stderr.write('ba_web: could not write results:\n' +
|
||||
traceback.format_exc())
|
||||
return {'ok': False, 'kind': 'error', 'problems': [
|
||||
{'key': '', 'text': '{0}: {1}'.format(type(exc).__name__, exc)}]}
|
||||
|
||||
return {'ok': True, 'result_url': '/results/' + mark + '.html'}
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ server
|
||||
|
||||
def make_server(ctx, port=0):
|
||||
"""Bind 127.0.0.1 ONLY (never a routable interface), on `port` (0 = let
|
||||
the OS pick). Threaded so a slow solve never blocks a concurrent static
|
||||
request -- the same server shape as pa_web."""
|
||||
server = pa_web.ThreadingHTTPServer(('127.0.0.1', port), Handler)
|
||||
server.ctx = ctx
|
||||
return server
|
||||
|
||||
|
||||
class Handler(pa_web.Handler):
|
||||
"""The route table, inherited plumbing from `pa_web.Handler` (the JSON
|
||||
body reader, `_send_file`, `_serve_from`, the 404/403 handling, the
|
||||
stderr request log). The static search walks ba_web/static FIRST and
|
||||
pa_web/static SECOND, so `index.html` and `ba_drawings.js` come from the
|
||||
beam folder and `app.js` / `draw.js` / `units.js` / `willtools.css` from
|
||||
PA's single copies."""
|
||||
|
||||
server_version = 'BAWeb/1'
|
||||
|
||||
def log_message(self, fmt, *args):
|
||||
try:
|
||||
sys.stderr.write('ba_web: {0} - {1}\n'.format(
|
||||
self.address_string(), fmt % args))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _static_candidates(self, path):
|
||||
rel = path.lstrip('/') or 'index.html'
|
||||
for root in STATIC_DIRS:
|
||||
yield root, rel
|
||||
|
||||
# -------------------------------------------------------------- GET
|
||||
|
||||
def do_GET(self):
|
||||
ctx = self.server.ctx
|
||||
path = pa_web.unquote(pa_web.urlparse(self.path).path)
|
||||
try:
|
||||
if path == '/api/spec':
|
||||
self._send_json(build_spec_json(ctx))
|
||||
return
|
||||
if path.startswith('/results/'):
|
||||
self._serve_from(ctx.ba_dir, path[len('/results/'):],
|
||||
'text/html')
|
||||
return
|
||||
if path.startswith('/api/'):
|
||||
self._send_json({'ok': False, 'problems': [
|
||||
{'key': '', 'text': 'unknown endpoint'}]}, status=404)
|
||||
return
|
||||
for root, rel in self._static_candidates(path):
|
||||
full = os.path.normpath(os.path.join(root, rel))
|
||||
root_norm = os.path.normpath(root)
|
||||
if not (full == root_norm or
|
||||
full.startswith(root_norm + os.sep)):
|
||||
self.send_error(403, 'forbidden')
|
||||
return
|
||||
if os.path.isfile(full):
|
||||
self._send_file(full,
|
||||
'text/html' if rel == 'index.html'
|
||||
else 'application/octet-stream')
|
||||
return
|
||||
self.send_error(404, 'not found')
|
||||
except Exception as exc:
|
||||
self._send_error_json(exc)
|
||||
|
||||
# ------------------------------------------------------------- POST
|
||||
|
||||
def do_POST(self):
|
||||
ctx = self.server.ctx
|
||||
path = pa_web.urlparse(self.path).path
|
||||
try:
|
||||
if path == '/api/save':
|
||||
values = self._read_json_body().get('values') or {}
|
||||
_save_values(ctx, values)
|
||||
self._send_json({'ok': True})
|
||||
return
|
||||
if path == '/api/run':
|
||||
values = self._read_json_body().get('values') or {}
|
||||
self._send_json(run_analysis(ctx, values))
|
||||
return
|
||||
if path == '/api/geometry':
|
||||
values = self._read_json_body().get('values') or {}
|
||||
self._send_json(geometry_for(ctx, values))
|
||||
return
|
||||
if path == '/api/log':
|
||||
body = self._read_json_body()
|
||||
try:
|
||||
log_ctx = pa_web.Context(
|
||||
reads=ctx.reads, know={}, job_root=ctx.job_root,
|
||||
pa_dir=ctx.ba_dir, mark=ctx.mark,
|
||||
static_dir=STATIC_DIRS[0])
|
||||
pa_web._append_client_log(log_ctx, body)
|
||||
except Exception as e: # the docstring's promise: never raises
|
||||
sys.stderr.write('client.log append failed: %r\n' % (e,))
|
||||
self._send_json({'ok': True})
|
||||
return
|
||||
if path == '/api/close':
|
||||
self._send_json({'ok': True})
|
||||
threading.Thread(target=self.server.shutdown).start()
|
||||
return
|
||||
self._send_json({'ok': False, 'problems': [
|
||||
{'key': '', 'text': 'unknown endpoint'}]}, status=404)
|
||||
except Exception as exc:
|
||||
self._send_error_json(exc)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------- CLI
|
||||
|
||||
def main(argv=None):
|
||||
ap = argparse.ArgumentParser(
|
||||
description='Serve the Beam Analysis HTML front/back end.')
|
||||
ap.add_argument('--reads', default=None,
|
||||
help='JSON file of the model reads dict -- a PARTIAL job '
|
||||
'plus "basis" (the Revit gatherer writes one; omit '
|
||||
'for a blank, still-complete form)')
|
||||
ap.add_argument('--job-root', dest='job_root', default=None,
|
||||
help='folder to store this run under (overridden by '
|
||||
"--reads' own job_root field, if present)")
|
||||
ap.add_argument('--port', type=int, default=0,
|
||||
help='0 (default) picks a free port')
|
||||
ap.add_argument('--open', action='store_true',
|
||||
help='open the page in the default browser')
|
||||
ap.add_argument('--title', default=DEFAULT_TITLE,
|
||||
help='cosmetic only -- logged at startup')
|
||||
args = ap.parse_args(argv)
|
||||
|
||||
ctx = build_context(args.reads, args.job_root)
|
||||
|
||||
try:
|
||||
server = make_server(ctx, args.port or 0)
|
||||
except OSError as exc:
|
||||
sys.stderr.write('ba_web: could not bind 127.0.0.1:{0}: {1}\n'.format(
|
||||
args.port, exc))
|
||||
return 1
|
||||
|
||||
url = 'http://127.0.0.1:{0}/'.format(server.server_address[1])
|
||||
# FIRST line on stdout, flushed immediately -- a launcher (the Revit
|
||||
# button, a test) reads this off a pipe before anything else happens.
|
||||
# The mark / job_root / folder paths are written to STDERR only.
|
||||
print('BA_WEB_URL={0}'.format(url))
|
||||
sys.stdout.flush()
|
||||
sys.stderr.write('ba_web: serving {0} for mark={1} (job_root={2}, '
|
||||
'title={3})\n'.format(url, ctx.mark, ctx.job_root,
|
||||
args.title))
|
||||
|
||||
if args.open:
|
||||
try:
|
||||
webbrowser.open(url)
|
||||
except Exception as exc:
|
||||
sys.stderr.write('ba_web: could not open a browser: {0}\n'
|
||||
.format(exc))
|
||||
|
||||
try:
|
||||
server.serve_forever()
|
||||
finally:
|
||||
server.server_close()
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user