Source code for ferrosoft.cli.server

#!/usr/bin/env python3
"""``platform-server`` — ASGI server launcher for the Ferrosoft platform.

Builds an argument list for either **granian** (default) or **uvicorn** and
replaces the current process via :func:`os.execvp`, so the chosen server
becomes PID 1-compatible in container deployments.

Network modes
-------------
unix (default)
    Listen on a Unix-domain socket (``--socket``, default
    ``/run/platform.sock``).  Suitable for reverse-proxy setups (nginx,
    Caddy) where the proxy and app share a filesystem namespace.
tcp
    Listen on a TCP address/port (``--address`` / ``--port``).  Suitable for
    direct exposure or Docker-based deployments where socket sharing is
    impractical.

CLI synopsis::

    platform-server [--impl {granian,uvicorn}]
                    [-n {unix,tcp}]
                    [-S SOCKET] [-a ADDRESS] [-p PORT]
                    [-w WORKERS] [-R]
"""

import argparse
import os
import sys


ASGI_APP = "ferrosoft.asgi:application"


def _granian_args(args) -> (str, list[str]):
    """Build the granian argv list from parsed CLI arguments.

    Args:
        args: Parsed :class:`argparse.Namespace` from :func:`main`.

    Returns:
        A tuple of ``(executable_name, argv)`` suitable for :func:`os.execvp`.
    """
    server_args = ["granian", "--interface", "asgi"]

    if args.network == "unix":
        server_args += ["--uds", args.socket]
    elif args.network == "tcp":
        server_args += ["--host", args.address, "--port", args.port]

    if args.reload:
        server_args += ["--reload"]

    server_args += [ASGI_APP]

    return "granian", server_args


def _uvicorn_args(args) -> (str, list[str]):
    """Build the uvicorn argv list from parsed CLI arguments.

    Args:
        args: Parsed :class:`argparse.Namespace` from :func:`main`.

    Returns:
        A tuple of ``(executable_name, argv)`` suitable for :func:`os.execvp`.
    """
    server_args = ["uvicorn"]

    if args.network == "unix":
        server_args += ["--uds", args.socket]
    elif args.network == "tcp":
        server_args += ["--host", args.address, "--port", args.port]

    if args.reload:
        server_args += ["--reload"]

    server_args += [ASGI_APP]

    return "uvicorn", server_args


[docs] def main(): """Parse CLI arguments and exec the chosen ASGI server process. This function never returns on success — it calls :func:`os.execvp` to replace the current process with the server binary. On argument error or unsupported ``--impl`` value it exits with a non-zero status code. """ parser = argparse.ArgumentParser( prog="platform-server", description="Run Ferrosoft platform application server", ) parser.add_argument( "--impl", choices=["granian", "uvicorn"], default="granian", ) parser.add_argument( "-n", "--network", choices=["unix", "tcp"], default="unix", ) parser.add_argument( "-S", "--socket", help="Socket file name (only for Unix network)", default="/run/platform.sock", ) parser.add_argument( "-p", "--port", help="Port number (only for TCP)", default="8000", ) parser.add_argument( "-a", "--address", help="IP address (only for TCP)", default="0.0.0.0", ) parser.add_argument( "-w", "--workers", help="Number of workers", type=int, default=4, ) parser.add_argument( "-R", "--reload", action="store_true", help="Enable auto-reload", ) args = parser.parse_args() server_name = "platform-server" server_args = ["--help"] match args.impl: case "granian": server_name, server_args = _granian_args(args) case "uvicorn": server_name, server_args = _uvicorn_args(args) case _: print( "Server implementation not supported: %s" % args.impl, file=sys.stderr, ) sys.exit(1) print( """ ░█▀▀░█▀▀░█▀▄░█▀▄░█▀█░█▀▀░█▀█░█▀▀░▀█▀ ░█▀▀░█▀▀░█▀▄░█▀▄░█░█░▀▀█░█░█░█▀▀░░█░ ░▀░░░▀▀▀░▀░▀░▀░▀░▀▀▀░▀▀▀░▀▀▀░▀░░░░▀░ """, file=sys.stderr, ) print("Running %s" % " ".join(server_args), file=sys.stderr) os.execvp(server_name, server_args)
if __name__ == "__main__": main()