Help for web.error

Sample CLI

gway web.app setup

References

Full Code

def setup(*,
    app=None,
    project="web.site",
    path=None,
    home: str = None,
    views: str = "view", 
    apis: str = "api",
    static="static",
    shared="shared",
    css="global",           # Default CSS (without .css extension)
    js="global",            # Default JS  (without .js extension)
    auth_required=False,    # Default: Don't enforce --optional security
    engine="bottle",
):
    """
    Setup Bottle web application with symmetrical static/shared public folders.
    Only one project per app. CSS/JS params are used as the only static includes.
    """
    global _ver, _homes, _enabled

    if engine != "bottle":
        raise NotImplementedError("Only Bottle is supported at the moment.")

    _ver = _ver or gw.version()
    bottle.BaseRequest.MEMFILE_MAX = UPLOAD_MB * 1024 * 1024

    if not isinstance(project, str) or not project:
        gw.abort("Project must be a non-empty string.")

    # Track project for later global static collection
    _enabled.add(project)

    # Always use the given project, never a list
    try:
        source = gw[project]
    except Exception:
        gw.abort(f"Project {project} not found in Gateway during app setup.")

    # Default path is the dotted project name, minus any leading web/
    if path is None:
        path = project.replace('.', '/')
        if path.startswith('web/'):
            path = path.removeprefix('web/')
            
    is_new_app = not (app := gw.unwrap_one(app, Bottle) if (oapp := app) else None)
    if is_new_app:
        gw.info("No Bottle app found; creating a new Bottle app.")
        app = Bottle()
        _homes.clear()
        add_home(home, path)

        @app.route("/", method=["GET", "POST"])
        def index():
            response.status = 302
            response.set_header("Location", default_home())
            return ""

        @app.error(404)
        def handle_404(error):
            return gw.web.error.redirect(f"404 Not Found: {request.url}", err=error)
    
    elif home:
        add_home(home, path)

    # Serve shared files (flat mount)
    if shared:
        @app.route(f"/{path}/{shared}/<filepath:path>")
        @app.route(f"/{shared}/<filepath:path>")
        def send_shared(filepath):
            file_path = gw.resource("work", "shared", filepath)
            if os.path.isfile(file_path):
                return static_file(os.path.basename(file_path), root=os.path.dirname(file_path))
            return HTTPResponse(status=404, body="shared file not found")

    # Serve static files (flat mount)
    if static:
        @app.route(f"/{path}/{static}/<filepath:path>")
        @app.route(f"/{static}/<filepath:path>")
        def send_static(filepath):
            file_path = gw.resource("data", "static", filepath)
            if os.path.isfile(file_path):
                return static_file(os.path.basename(file_path), root=os.path.dirname(file_path))
            return HTTPResponse(status=404, body="static file not found")
        
    # Main view dispatcher (only if views is not None)
    if views:
        @app.route(f"/{path}/<view:path>", method=["GET", "POST"])
        def view_dispatch(view):
            nonlocal home, views
            # --- AUTH CHECK ---
            if is_enabled('web.auth') and not gw.web.auth.is_authorized(strict=auth_required):
                return gw.web.error.unauthorized("Unauthorized: You are not permitted to view this page.")
            # Set current endpoint in GWAY context (for helpers/build_url etc)
            gw.context['current_endpoint'] = path
            segments = [s for s in view.strip("/").split("/") if s]
            view_name = segments[0].replace("-", "_") if segments else home
            args = segments[1:] if segments else []
            kwargs = dict(request.query)
            if request.method == "POST":
                try:
                    kwargs.update(request.json or dict(request.forms))
                except Exception as e:
                    return gw.web.error.redirect("Error loading JSON payload", error=e)

            target_func_name = f"{views}_{view_name}" if views else view_name

            view_func = getattr(source, target_func_name, None)
            if not callable(view_func):
                return gw.web.error.redirect(f"View not found: {target_func_name} in {project}")

            try:
                content = view_func(*args, **kwargs)
                if isinstance(content, HTTPResponse):
                    return content
                elif isinstance(content, bytes):
                    response.content_type = "application/octet-stream"
                    response.body = content
                    return response
                elif content is None:
                    return ""
                elif not isinstance(content, str):
                    content = gw.to_html(content)
            except HTTPResponse as res:
                return res
            except Exception as e:
                return gw.web.error.redirect("Broken view", err=e)

            media_origin = "/shared" if shared else ("static" if static else "")
            return render_template(
                title="GWAY - " + view_func.__name__.replace("_", " ").title(),
                content=content,
                css_files=(f"{media_origin}/{css}.css",),
                js_files=(f"{media_origin}/{js}.js",),
            )

    # API dispatcher (only if apis is not None)
    if apis:
        @app.route(f"/api/{path}/<view:path>", method=["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"])
        def api_dispatch(view):
            nonlocal home, apis
            # --- AUTH CHECK ---
            if is_enabled('web.auth') and not gw.web.auth.is_authorized(strict=auth_required):
                return gw.web.error.unauthorized("Unauthorized: API access denied.")
            # Set current endpoint in GWAY context (for helpers/build_url etc)
            gw.context['current_endpoint'] = path
            segments = [s for s in view.strip("/").split("/") if s]
            view_name = segments[0].replace("-", "_") if segments else home
            args = segments[1:] if segments else []
            kwargs = dict(request.query)
            if request.method == "POST":
                try:
                    kwargs.update(request.json or dict(request.forms))
                except Exception as e:
                    return gw.web.error.redirect("Error loading JSON payload", err=e)

            method = request.method.lower()
            specific_af = f"{apis}_{method}_{view_name}"
            generic_af = f"{apis}_{view_name}"

            api_func = getattr(source, specific_af, None)
            if not callable(api_func):
                api_func = getattr(source, generic_af, None)
            if not callable(api_func):
                return gw.web.error.redirect(f"API not found: {specific_af} or {generic_af} in {project}")

            try:
                result = api_func(*args, **kwargs)
                if isinstance(result, HTTPResponse):
                    return result
                response.content_type = "application/json"
                return gw.to_json(result)
            except HTTPResponse as res:
                return res
            except Exception as e:
                return gw.web.error.redirect("Broken API", err=e)

    @app.route("/favicon.ico")
    def favicon():
        proj_parts = project.split('.')
        candidate = gw.resource("data", "static", *proj_parts, "favicon.ico")
        if os.path.isfile(candidate):
            return static_file("favicon.ico", root=os.path.dirname(candidate))
        global_favicon = gw.resource("data", "static", "favicon.ico")
        if os.path.isfile(global_favicon):
            return static_file("favicon.ico", root=os.path.dirname(global_favicon))
        return HTTPResponse(status=404, body="favicon.ico not found")

    if gw.verbose:
        gw.debug(f"Registered homes: {_homes}")
        debug_routes(app)

    return oapp if oapp else app

Sample CLI

gway web.error redirect

References

Full Code

def redirect(message="", *, err=None, default=None, view_name=None):
    """
    GWAY error/redirect handler.
    Deprecated: 'view_name'. Now uses gw.web.app.current_endpoint.
    """
    from bottle import request, response

    debug_enabled = bool(getattr(gw, "debug", False))
    visited = gw.web.cookies.get("visited", "")
    visited_items = visited.split("|") if visited else []

    # --- DEPRECATED: view_name, use gw.web.app.current_endpoint instead ---
    if view_name is not None:
        import warnings
        warnings.warn(
            "redirect(): 'view_name' is deprecated. Use gw.web.app.current_endpoint instead.",
            DeprecationWarning
        )
    curr_view = getattr(gw.web.app, "current_endpoint", None)
    view_key = curr_view() if callable(curr_view) else curr_view
    if not view_key and view_name:
        view_key = view_name

    pruned = False
    if view_key and gw.web.cookies.check_consent():
        norm_broken = (view_key or "").replace("-", " ").replace("_", " ").title().lower()
        new_items = []
        for v in visited_items:
            title = v.split("=", 1)[0].strip().lower()
            if title == norm_broken:
                pruned = True
                continue
            new_items.append(v)
        if pruned:
            gw.web.cookies.set("visited", "|".join(new_items))
            visited_items = new_items

    if debug_enabled:
        return view_debug_error(
            title="GWAY Debug Error",
            message=message,
            err=err,
            status=500,
            default=default
        )

    response.status = 302
    response.set_header("Location", default or gw.web.app.default_home())
    return ""

Sample CLI

gway web.error unauthorized

Full Code

def unauthorized(message="Unauthorized: You do not have access to this resource.", *, err=None, default=None):
    """
    If in debug mode: show detailed error.
    If not in debug: return a 401 Unauthorized and a WWW-Authenticate header to trigger the browser auth dialog.
    """
    from bottle import response

    debug_enabled = bool(getattr(gw, "debug", False))
    if debug_enabled:
        return view_debug_error(
            title="401 Unauthorized",
            message=message,
            err=err,
            status=401,
            default=default
        )

    # 401 with auth header = browser will prompt for password
    response.status = 401
    response.headers['WWW-Authenticate'] = 'Basic realm="GWAY"'
    response.content_type = "text/plain"
    return message

Sample CLI

gway web.error view-debug-error

References

Full Code

def view_debug_error(
    *,
    title="GWAY Debug Error",
    message="An error occurred.",
    err=None,
    status=500,
    default=None
):
    """
    Render a debug error view with detailed traceback and request info.
    """
    from bottle import request, response
    import traceback
    import html

    tb_str = ""
    if err:
        tb_str = "".join(traceback.format_exception(type(err), err, getattr(err, "__traceback__", None)))

    debug_content = f"""
    <html>
    <head>
        <title>{html.escape(title)}</title>
        <style>
            body {{ font-family: monospace, sans-serif; background: #23272e; color: #e6e6e6; }}
            .traceback {{ background: #16181c; color: #ff8888; padding: 1em; border-radius: 5px; margin: 1em 0; white-space: pre; }}
            .kv {{ color: #6ee7b7; }}
            .section {{ margin-bottom: 2em; }}
            h1 {{ color: #ffa14a; }}
            a {{ color: #69f; }}
            .copy-btn {{ margin: 1em 0; background:#333;color:#fff;padding:0.4em 0.8em;border-radius:4px;cursor:pointer;border:1px solid #aaa; }}
        </style>
    </head>
    <body>
        <h1>{html.escape(title)}</h1>
        <div id="debug-content">
            <div class="section"><b>Message:</b> {html.escape(str(message) or "")}</div>
            <div class="section"><b>Error:</b> {html.escape(str(err) or "")}</div>
            <div class="section"><b>Path:</b> {html.escape(request.path or "")}<br>
                                 <b>Method:</b> {html.escape(request.method or "")}<br>
                                 <b>Full URL:</b> {html.escape(request.url or "")}</div>
            <div class="section"><b>Query:</b> {html.escape(str(dict(request.query)) or "")}</div>
            <div class="section"><b>Form:</b> {html.escape(str(getattr(request, "forms", "")) or "")}</div>
            <div class="section"><b>Headers:</b> {html.escape(str(dict(request.headers)) or "")}</div>
            <div class="section"><b>Cookies:</b> {html.escape(str(dict(request.cookies)) or "")}</div>
            <div class="section"><b>Traceback:</b>
                <div class="traceback">{html.escape(tb_str or '(no traceback)')}</div>
            </div>
        </div>
        <div><a href="{html.escape(default or gw.web.app.default_home())}">&#8592; Back to home</a></div>
    </body>
    </html>
    """
    response.status = status
    response.content_type = "text/html"
    return debug_content