Sample CLI
gway web.app setup
References
['debug', 'info', 'resource', 'to_html', 'to_list', 'unwrap', 'version', 'web', 'web.app_url', 'web.redirect_error', 'web.static_url', 'web.work_url']
Full Code
def setup(*,
app=None,
project="web.site",
path=None,
static="static",
work="work",
home: str = "readme",
prefix: str = "view",
navbar: bool = True,
):
"""
Configure one or more Bottle-based apps. Use web server start-app to launch.
"""
global _version
_version = _version or gw.version()
# Normalize `project` into a list of project names
projects = gw.to_list(project, flat=True)
# Determine default `path` if not provided
if path is None:
first_proj = projects[0]
path = "gway" if first_proj == "web.site" else first_proj.replace(".", "/")
_is_new_app = not (app := gw.unwrap(app, Bottle) if (oapp := app) else None)
gw.debug(f"Unwrapped {app=} from {oapp=} ({_is_new_app=})")
if _is_new_app:
gw.info("No Bottle app found; creating a new Bottle app.")
app = Bottle()
# Define URL-building helpers
gw.web.static_url = lambda *args, **kwargs: build_url(static, *args, **kwargs)
gw.web.work_url = lambda *args, **kwargs: build_url(work, *args, **kwargs)
gw.web.app_url = lambda *args, **kwargs: build_url(path, *args, **kwargs)
gw.web.redirect_error = redirect_error
@app.route("/accept-cookies", method="POST")
def accept_cookies():
response.set_cookie("cookies_accepted", "yes")
redirect_url = request.forms.get("next", "/readme")
response.status = 303
if not redirect_url.startswith("/"):
redirect_url = f"/{redirect_url}"
response.set_header("Location", f"/{path}{redirect_url}")
return ""
if static:
@app.route(f"/{static}/<filename:path>")
def send_static(filename):
return static_file(filename, root=gw.resource("data", "static"))
if work:
@app.route(f"/{work}/<filename:path>")
def send_work(filename):
filename = filename.replace('-', '_')
return static_file(filename, root=gw.resource("work", "shared"))
# TODO: When passing a param to a view, such as from a form that was left empty by the user
# avoid passing None, instead pass "" (empty string) as it may differentiate between a
# form that has not been submitted and one submitted with empty fields.
@app.route(f"/{path}/<view:path>", method=["GET", "POST"])
def view_dispatch(view):
nonlocal navbar
segments = [s for s in view.strip("/").split("/") if s]
if not segments:
segments = [home]
view_name = segments[0].replace("-", "_")
args = segments[1:]
kwargs = dict(request.query)
if request.method == "POST":
try:
if request.json:
kwargs.update(request.json)
elif request.forms:
kwargs.update(request.forms.decode())
except Exception as e:
return redirect_error(e, note="Error loading JSON payload", broken_view_name=view_name)
sources = []
for proj_name in projects:
try:
sources.append(gw[proj_name])
except Exception:
continue
for source in sources:
view_func = getattr(source, f"{prefix}_{view_name}", None)
if callable(view_func):
break
else:
return redirect_error(
note=f"View '{prefix}_{view_name}' not found in any project: {projects}",
broken_view_name=view_name,
default=f"/{path}/{home}" if path and home else "/gway/readme"
)
try:
gw.debug(f"Dispatch to {view_func.__name__} (args={args}, kwargs={kwargs})")
content = view_func(*args, **kwargs)
if content and not isinstance(content, str):
content = gw.to_html(content)
visited = update_visited(view_name)
except HTTPResponse as resp:
return resp
except Exception as e:
return redirect_error(e, note="Error during view execution", broken_view_name=view_name)
full_url = request.fullpath
if request.query_string:
full_url += "?" + request.query_string
if navbar is True:
navbar = render_navbar(visited, path, current_url=full_url)
if not cookies_enabled():
consent_box = f"""
<div class="consent-box">
<form action="/accept-cookies" method="post">
<input type="hidden" name="next" value="/{view}" />
This site uses cookies to improve your experience.
<button type="submit">Accept</button>
</form>
</div>
"""
content = consent_box + content
style_param = request.query.get("css") or request.query.get("style")
if style_param:
if not style_param.endswith(".css"):
style_param += ".css"
response.set_cookie("css", style_param, path="/")
css_files = ["default.css", style_param]
else:
css_cookie = request.get_cookie("css", "")
css_files = ["default.css"] + [c.strip() for c in css_cookie.split(",") if c.strip()]
return render_template(
title="GWAY - " + view_name.replace("_", " ").title(),
navbar=navbar,
content=content,
static=static,
css_files=css_files
)
@app.route("/", method=["GET", "POST"])
def index():
response.status = 302
response.set_header("Location", f"/{path}/readme")
return ""
@app.error(404)
def handle_404(error):
fallback = "/gway/readme"
try:
return redirect_error(
error,
note=f"404 Not Found: {request.url}",
default=f"/{path}/{home}" if path and home else fallback
)
except Exception as e:
return redirect_error(e, note="Failed during 404 fallback", default=fallback)
if _is_new_app:
app = security_middleware(app)
return app if not oapp else (oapp, app)
return oapp
Sample CLI
gway web.site publish-parts
Full Code
def publish_parts(source, source_path=None, source_class=io.StringInput,
destination_path=None,
reader=None, reader_name='standalone',
parser=None, parser_name='restructuredtext',
writer=None, writer_name='pseudoxml',
settings=None, settings_spec=None,
settings_overrides=None, config_section=None,
enable_exit_status=False):
"""
Set up & run a `Publisher`, and return a dictionary of document parts.
Dictionary keys are the names of parts.
Dictionary values are `str` instances; encoding is up to the client,
e.g.::
parts = publish_parts(...)
body = parts['body'].encode(parts['encoding'], parts['errors'])
See the `API documentation`__ for details on the provided parts.
Parameters: see `publish_programmatically()`.
__ https://docutils.sourceforge.io/docs/api/publisher.html#publish-parts
"""
output, publisher = publish_programmatically(
source=source, source_path=source_path, source_class=source_class,
destination_class=io.StringOutput,
destination=None, destination_path=destination_path,
reader=reader, reader_name=reader_name,
parser=parser, parser_name=parser_name,
writer=writer, writer_name=writer_name,
settings=settings, settings_spec=settings_spec,
settings_overrides=settings_overrides,
config_section=config_section,
enable_exit_status=enable_exit_status)
return publisher.writer.parts
Sample CLI
gway web.site redirect
Full Code
def redirect(url, code=None):
""" Aborts execution and causes a 303 or 302 redirect, depending on
the HTTP protocol version. """
if not code:
code = 303 if request.get('SERVER_PROTOCOL') == "HTTP/1.1" else 302
res = response.copy(cls=HTTPResponse)
res.status = code
res.body = ""
res.set_header('Location', urljoin(request.url, url))
raise res
Sample CLI
gway web.site view-awg-finder
References
['awg', 'awg.find_cable']
Full Code
def view_awg_finder(
*args, meters=None, amps="40", volts="220", material="cu",
max_lines="3", phases="1", conduit=None, neutral="0", **kwargs
):
"""Page builder for AWG cable finder with HTML form and result."""
if not meters:
return '''
<h1>AWG Cable Finder</h1>
<p>Warning: This calculator may not be applicable to your use case. It may be completely wrong.
Consult a LOCAL certified electrician before making real-life cable sizing decisions.</p>
<form method="post">
<label>Meters: <input type="number" name="meters" required min="1" /></label><br/>
<label>Amps: <input type="number" name="amps" value="40" /></label><br/>
<label>Volts: <input type="number" name="volts" value="220" /></label><br/>
<label>Material:
<select name="material">
<option value="cu">Copper (cu)</option>
<option value="al">Aluminum (al)</option>
<option value="?">Don't know</option>
</select>
</label><br/>
<label>Max Lines: <input type="number" name="max_lines" value="3" /></label><br/>
<label>Phases:
<select name="phases">
<option value="1">Single Phase (1)</option>
<option value="3">Three Phase (3)</option>
</select>
</label><br/>
<label>Neutral (0 or 1): <input type="number" name="neutral" value="0" /></label><br/>
<label>Conduit (emt/true/blank): <input name="conduit" /></label><br/><br/>
<button type="submit" class="submit">Find Cable</button>
</form>
'''
try:
result = gw.awg.find_cable(
meters=meters, amps=amps, volts=volts,
material=material, max_lines=max_lines,
phases=phases, conduit=conduit, neutral=neutral
)
except Exception as e:
return f"<p class='error'>Error: {e}</p><p><a href='/awg-finder'>Try again</a></p>"
return f"""
<h1>Recommended Cable</h1>
<ul>
<li><strong>AWG Size:</strong> {result['awg']}</li>
<li><strong>Lines:</strong> {result['lines']}</li>
<li><strong>Total Cables:</strong> {result['cables']}</li>
<li><strong>Total Length (m):</strong> {result['cable_m']}</li>
<li><strong>Voltage Drop:</strong> {result['vdrop']:.2f} V ({result['vdperc']:.2f}%)</li>
<li><strong>Voltage at End:</strong> {result['vend']:.2f} V</li>
{f"<li><strong>Conduit:</strong> {result['conduit']} ({result['pipe_in']})</li>" if 'conduit' in result else ""}
</ul>
<p><a href="/awg-finder">Calculate again</a></p>
"""
Sample CLI
gway web.site view-help
Full Code
def view_help(topic="", *args, **kwargs):
"""Render dynamic help based on GWAY introspection and search-style links."""
topic = topic.replace(" ", "/").replace(".", "/").replace("-", "_") if topic else ""
parts = [p for p in topic.strip("/").split("/") if p]
if not parts:
help_info = gw.help()
title = "Available Projects"
content = "<ul>"
for project in help_info["Available Projects"]:
content += f'<li><a href="?topic={project}">{project}</a></li>'
content += "</ul>"
return f"<h1>{title}</h1>{content}"
elif len(parts) == 1:
project = parts[0]
help_info = gw.help(project)
title = f"Help Topics for <code>{project}</code>"
else:
*project_path, maybe_function = parts
obj = gw
for segment in project_path:
obj = getattr(obj, segment, None)
if obj is None:
return f"<h2>Not Found</h2><p>Project path invalid at <code>{segment}</code>.</p>"
project_str = ".".join(project_path)
if hasattr(obj, maybe_function):
function = maybe_function
help_info = gw.help(project_str, function, full=True)
full_name = f"{project_str}.{function}"
title = f"Help for <code>{full_name}</code>"
else:
# It's a project, not a function
help_info = gw.help(project_str)
full_name = f"{project_str}.{maybe_function}"
title = f"Help Topics for <code>{full_name}</code>"
if help_info is None:
return "<h2>Not Found</h2><p>No help found for the given input.</p>"
if "Matches" in help_info:
sections = [_help_section(match, use_query_links=True) for match in help_info["Matches"]]
return f"<h1>{title}</h1>{''.join(sections)}"
return f"<h1>{title}</h1>{_help_section(help_info, use_query_links=True)}"
Sample CLI
gway web.site view-qr-code
References
['qr', 'qr.generate_url', 'web', 'web.app_url']
Full Code
def view_qr_code(*args, value=None, **kwargs):
"""Generate a QR code for a given value and serve it from cache if available."""
if not value:
return '''
<h1>QR Code Generator</h1>
<form method="post">
<input type="text" name="value" placeholder="Enter text or URL" required class="main" />
<button type="submit" class="submit">Generate QR</button>
</form>
'''
qr_url = gw.qr.generate_url(value)
back_link = gw.web.app_url("qr-code")
return f"""
<h1>QR Code for:</h1>
<h2><code>{value}</code></h2>
<img src="{qr_url}" alt="QR Code" class="qr" />
<p><a href="{back_link}">Generate another</a></p>
"""
Sample CLI
gway web.site view-readme
Full Code
def view_readme(*args, **kwargs):
"""Render the README.rst file as HTML."""
readme_path = gw.resource("README.rst")
with open(readme_path, encoding="utf-8") as f:
rst_content = f.read()
html_parts = publish_parts(source=rst_content, writer_name="html")
return html_parts["html_body"]
References
['info', 'release', 'release.build_help_db', 'resource', 'sql', 'sql.open_connection', 'warning']
Full Code
def help(*args, full=False):
from gway import gw
import os, textwrap, ast, sqlite3
gw.info(f"Help on {' '.join(args)} requested")
def extract_gw_refs(source):
refs = set()
try:
tree = ast.parse(source)
except SyntaxError:
return refs
class GwVisitor(ast.NodeVisitor):
def visit_Attribute(self, node):
parts = []
cur = node
while isinstance(cur, ast.Attribute):
parts.append(cur.attr)
cur = cur.value
if isinstance(cur, ast.Name) and cur.id == "gw":
parts.append("gw")
full = ".".join(reversed(parts))[3:] # remove "gw."
refs.add(full)
self.generic_visit(node)
GwVisitor().visit(tree)
return refs
db_path = gw.resource("data", "help.sqlite")
if not os.path.isfile(db_path):
gw.release.build_help_db()
joined_args = " ".join(args).strip().replace("-", "_")
norm_args = [a.replace("-", "_").replace("/", ".") for a in args]
with gw.sql.open_connection(db_path, row_factory=True) as cur:
if not args:
cur.execute("SELECT DISTINCT project FROM help")
return {"Available Projects": sorted([row["project"] for row in cur.fetchall()])}
rows = []
# Case 1: help("web.site.view_help")
if len(norm_args) == 1 and "." in norm_args[0]:
parts = norm_args[0].split(".")
if len(parts) >= 2:
project = ".".join(parts[:-1])
function = parts[-1]
cur.execute("SELECT * FROM help WHERE project = ? AND function = ?", (project, function))
rows = cur.fetchall()
if not rows:
try:
cur.execute("SELECT * FROM help WHERE help MATCH ?", (f'"{norm_args[0]}"',))
rows = cur.fetchall()
except sqlite3.OperationalError as e:
gw.warning(f"FTS query failed for {norm_args[0]}: {e}")
else:
return {"error": f"Could not parse dotted input: {norm_args[0]}"}
# Case 2: help("web", "view_help") or help("builtin", "hello_world")
elif len(norm_args) >= 2:
*proj_parts, maybe_func = norm_args
project = ".".join(proj_parts)
function = maybe_func
cur.execute("SELECT * FROM help WHERE project = ? AND function = ?", (project, function))
rows = cur.fetchall()
if not rows:
fuzzy_query = ".".join(norm_args)
try:
cur.execute("SELECT * FROM help WHERE help MATCH ?", (f'"{fuzzy_query}"',))
rows = cur.fetchall()
except sqlite3.OperationalError as e:
gw.warning(f"FTS fallback failed for {fuzzy_query}: {e}")
# Final fallback: maybe it's a builtin like help("hello_world")
if not rows and len(norm_args) == 1:
name = norm_args[0]
cur.execute("SELECT * FROM help WHERE project = ? AND function = ?", ("builtin", name))
rows = cur.fetchall()
if not rows:
fuzzy_query = ".".join(norm_args)
try:
cur.execute("SELECT * FROM help WHERE help MATCH ?", (f'"{fuzzy_query}"',))
rows = cur.fetchall()
except sqlite3.OperationalError as e:
gw.warning(f"FTS final fallback failed for {fuzzy_query}: {e}")
return {"error": f"No help found and fallback failed for: {joined_args}"}
results = []
for row in rows:
project = row["project"]
function = row["function"]
prefix = f"gway {project} {function.replace('_', '-')}" if project != "builtin" else f"gway {function.replace('_', '-')}"
entry = {
"Project": project,
"Function": function,
"Sample CLI": prefix,
"References": sorted(extract_gw_refs(row["source"])),
}
if full:
entry["Full Code"] = row["source"]
else:
entry["Signature"] = textwrap.fill(row["signature"], 100).strip()
entry["Docstring"] = row["docstring"].strip() if row["docstring"] else None
entry["TODOs"] = row["todos"].strip() if row["todos"] else None
results.append({k: v for k, v in entry.items() if v})
return results[0] if len(results) == 1 else {"Matches": results}