Fix EOSdash startup. (#394)
Some checks are pending
docker-build / platform-excludes (push) Waiting to run
docker-build / build (push) Blocked by required conditions
docker-build / merge (push) Blocked by required conditions
pre-commit / pre-commit (push) Waiting to run
Run Pytest on Pull Request / test (push) Waiting to run

Revert to EOSdash startup as a sub-process.
Add arguments to control the complete startup of both servers.
Also development reload can now be given as an argument.

Signed-off-by: Bobby Noelte <b0661n0e17e@gmail.com>
This commit is contained in:
Bobby Noelte 2025-01-21 19:20:50 +01:00 committed by GitHub
parent e255e97141
commit 9c91a64c1b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 280 additions and 38 deletions

View File

@ -84,12 +84,12 @@ clean: clean-docs
@echo "Deletion complete." @echo "Deletion complete."
run: run:
@echo "Starting EOS server, please wait..." @echo "Starting EOS production server, please wait..."
.venv/bin/fastapi run --port 8503 src/akkudoktoreos/server/eos.py .venv/bin/python src/akkudoktoreos/server/eos.py
run-dev: run-dev:
@echo "Starting EOS development server, please wait..." @echo "Starting EOS development server, please wait..."
.venv/bin/fastapi dev --port 8503 src/akkudoktoreos/server/eos.py .venv/bin/python src/akkudoktoreos/server/eos.py --host localhost --port 8503 --reload true
# Target to setup tests. # Target to setup tests.
test-setup: pip-dev test-setup: pip-dev

View File

@ -27,23 +27,23 @@ python -m venv .venv
Windows: Windows:
```bash ```cmd
python -m venv .venv python -m venv .venv
.venv\Scripts\pip install -r requirements.txt .venv\Scripts\pip install -r requirements.txt
``` ```
Finally, start EOS fastapi server: Finally, start the EOS server:
Linux: Linux:
```bash ```bash
.venv/bin/fastapi run --port 8503 src/akkudoktoreos/server/eos.py .venv/bin/python src/akkudoktoreos/server/eos.py
``` ```
Windows: Windows:
``` ```cmd
.venv\Scripts\fastapi run --port 8503 src/akkudoktoreos/server/eos.py .venv\Scripts\python src/akkudoktoreos/server/eos.py
``` ```
### Docker ### Docker

View File

@ -38,13 +38,13 @@ Start the EOS fastapi server:
.. code-block:: powershell .. code-block:: powershell
.venv\Scripts\fastapi run src/akkudoktoreos/server/eos.py .venv\Scripts\python src/akkudoktoreos/server/eos.py
.. tab:: Linux .. tab:: Linux
.. code-block:: bash .. code-block:: bash
.venv/bin/fastapi run src/akkudoktoreos/server/eos.py .venv/bin/python src/akkudoktoreos/server/eos.py
``` ```

View File

@ -1,5 +1,6 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import argparse
import subprocess import subprocess
import sys import sys
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
@ -36,6 +37,9 @@ measurement_eos = get_measurement()
prediction_eos = get_prediction() prediction_eos = get_prediction()
ems_eos = get_ems() ems_eos = get_ems()
# Command line arguments
args = None
ERROR_PAGE_TEMPLATE = """ ERROR_PAGE_TEMPLATE = """
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
@ -129,27 +133,76 @@ def create_error_page(
) )
# ----------------------
# EOSdash server startup
# ----------------------
def start_eosdash() -> subprocess.Popen: def start_eosdash() -> subprocess.Popen:
"""Start the fasthtml server as a subprocess.""" """Start the EOSdash server as a subprocess.
Returns:
server_process: The process of the EOSdash server
"""
eosdash_path = Path(__file__).parent.resolve().joinpath("eosdash.py")
if args is None:
# No command line arguments
host = config_eos.server_eosdash_host
port = config_eos.server_eosdash_port
eos_host = config_eos.server_eos_host
eos_port = config_eos.server_eos_port
log_level = "info"
access_log = False
reload = False
else:
host = args.host
port = config_eos.server_eosdash_port if config_eos.server_eosdash_port else (args.port + 1)
eos_host = args.host
eos_port = args.port
log_level = args.log_level
access_log = args.access_log
reload = args.reload
cmd = [
sys.executable,
str(eosdash_path),
"--host",
str(host),
"--port",
str(port),
"--eos-host",
str(eos_host),
"--eos-port",
str(eos_port),
"--log_level",
log_level,
"--access_log",
str(access_log),
"--reload",
str(reload),
]
server_process = subprocess.Popen( server_process = subprocess.Popen(
[sys.executable, str(server_dir.joinpath("eosdash.py"))], cmd,
stdout=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=subprocess.PIPE, stderr=subprocess.PIPE,
) )
return server_process return server_process
# ----------------------
# EOS REST Server
# ----------------------
@asynccontextmanager @asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]: async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
"""Lifespan manager for the app.""" """Lifespan manager for the app."""
# On startup # On startup
if ( if config_eos.server_eos_startup_eosdash:
config_eos.server_eos_startup_eosdash
and config_eos.server_eosdash_host
and config_eos.server_eosdash_port
):
try: try:
fasthtml_process = start_eosdash() eosdash_process = start_eosdash()
except Exception as e: except Exception as e:
logger.error(f"Failed to start EOSdash server. Error: {e}") logger.error(f"Failed to start EOSdash server. Error: {e}")
sys.exit(1) sys.exit(1)
@ -169,8 +222,10 @@ app = FastAPI(
"url": "https://www.apache.org/licenses/LICENSE-2.0.html", "url": "https://www.apache.org/licenses/LICENSE-2.0.html",
}, },
lifespan=lifespan, lifespan=lifespan,
root_path=str(Path(__file__).parent),
) )
# That's the problem # That's the problem
opt_class = optimization_problem(verbose=bool(config_eos.server_eos_verbose)) opt_class = optimization_problem(verbose=bool(config_eos.server_eos_verbose))
@ -827,7 +882,7 @@ async def proxy_put(request: Request, path: str) -> Response:
async def proxy(request: Request, path: str) -> Union[Response | RedirectResponse | HTMLResponse]: async def proxy(request: Request, path: str) -> Union[Response | RedirectResponse | HTMLResponse]:
if config_eos.server_eosdash_host and config_eos.server_eosdash_port: if config_eos.server_eosdash_host and config_eos.server_eosdash_port:
# Proxy to fasthtml server # Proxy to EOSdash server
url = f"http://{config_eos.server_eosdash_host}:{config_eos.server_eosdash_port}/{path}" url = f"http://{config_eos.server_eosdash_host}:{config_eos.server_eosdash_port}/{path}"
headers = dict(request.headers) headers = dict(request.headers)
@ -869,22 +924,102 @@ set 'server_eosdash_host' or 'server_eosdash_port' to None.
return RedirectResponse(url="/docs") return RedirectResponse(url="/docs")
def start_eos() -> None: def run_eos(host: str, port: int, log_level: str, access_log: bool, reload: bool) -> None:
"""Start EOS server.""" """Run the EOS server with the specified configurations.
This function starts the EOS server using the Uvicorn ASGI server. It accepts
arguments for the host, port, log level, access log, and reload options. The
log level is converted to lowercase to ensure compatibility with Uvicorn's
expected log level format. If an error occurs while attempting to bind the
server to the specified host and port, an error message is logged and the
application exits.
Parameters:
host (str): The hostname to bind the server to.
port (int): The port number to bind the server to.
log_level (str): The log level for the server. Options include "critical", "error",
"warning", "info", "debug", and "trace".
access_log (bool): Whether to enable or disable the access log. Set to True to enable.
reload (bool): Whether to enable or disable auto-reload. Set to True for development.
Returns:
None
"""
# Make hostname human (and Windows) friendly
if host == "0.0.0.0":
host = "localhost"
try: try:
uvicorn.run( uvicorn.run(
app, "akkudoktoreos.server.eos:app",
host=str(config_eos.server_eos_host), host=host,
port=config_eos.server_eos_port, port=port,
log_level="debug", log_level=log_level.lower(), # Convert log_level to lowercase
access_log=True, access_log=access_log,
reload=reload,
) )
except Exception as e: except Exception as e:
logger.error( logger.error(f"Could not bind to host {host}:{port}. Error: {e}")
f"Could not bind to host {config_eos.server_eos_host}:{config_eos.server_eos_port}. Error: {e}" raise e
)
sys.exit(1)
def main() -> None:
"""Parse command-line arguments and start the EOS server with the specified options.
This function sets up the argument parser to accept command-line arguments for
host, port, log_level, access_log, and reload. It uses default values from the
config_eos module if arguments are not provided. After parsing the arguments,
it starts the EOS server with the specified configurations.
Command-line Arguments:
--host (str): Host for the EOS server (default: value from config_eos).
--port (int): Port for the EOS server (default: value from config_eos).
--log_level (str): Log level for the server. Options: "critical", "error", "warning", "info", "debug", "trace" (default: "info").
--access_log (bool): Enable or disable access log. Options: True or False (default: False).
--reload (bool): Enable or disable auto-reload. Useful for development. Options: True or False (default: False).
"""
parser = argparse.ArgumentParser(description="Start EOS server.")
# Host and port arguments with defaults from config_eos
parser.add_argument(
"--host",
type=str,
default=str(config_eos.server_eos_host),
help="Host for the EOS server (default: value from config_eos)",
)
parser.add_argument(
"--port",
type=int,
default=config_eos.server_eos_port,
help="Port for the EOS server (default: value from config_eos)",
)
# Optional arguments for log_level, access_log, and reload
parser.add_argument(
"--log_level",
type=str,
default="info",
help='Log level for the server. Options: "critical", "error", "warning", "info", "debug", "trace" (default: "info")',
)
parser.add_argument(
"--access_log",
type=bool,
default=False,
help="Enable or disable access log. Options: True or False (default: True)",
)
parser.add_argument(
"--reload",
type=bool,
default=False,
help="Enable or disable auto-reload. Useful for development. Options: True or False (default: False)",
)
args = parser.parse_args()
try:
run_eos(args.host, args.port, args.log_level, args.access_log, args.reload)
except:
exit(1)
if __name__ == "__main__": if __name__ == "__main__":
start_eos() main()

View File

@ -1,3 +1,5 @@
import argparse
import uvicorn import uvicorn
from fasthtml.common import H1, FastHTML, Table, Td, Th, Thead, Titled, Tr from fasthtml.common import H1, FastHTML, Table, Td, Th, Thead, Titled, Tr
@ -8,6 +10,8 @@ logger = get_logger(__name__)
config_eos = get_config() config_eos = get_config()
# Command line arguments
args = None
configs = [] configs = []
for field_name in config_eos.model_fields: for field_name in config_eos.model_fields:
@ -44,15 +48,118 @@ def get(): # type: ignore
return Titled("EOS Dashboard", H1("Configuration"), config_table()) return Titled("EOS Dashboard", H1("Configuration"), config_table())
if __name__ == "__main__": def run_eosdash(host: str, port: int, log_level: str, access_log: bool, reload: bool) -> None:
"""Run the EOSdash server with the specified configurations.
This function starts the EOSdash server using the Uvicorn ASGI server. It accepts
arguments for the host, port, log level, access log, and reload options. The
log level is converted to lowercase to ensure compatibility with Uvicorn's
expected log level format. If an error occurs while attempting to bind the
server to the specified host and port, an error message is logged and the
application exits.
Parameters:
host (str): The hostname to bind the server to.
port (int): The port number to bind the server to.
log_level (str): The log level for the server. Options include "critical", "error",
"warning", "info", "debug", and "trace".
access_log (bool): Whether to enable or disable the access log. Set to True to enable.
reload (bool): Whether to enable or disable auto-reload. Set to True for development.
Returns:
None
"""
# Make hostname human (and Windows) friendly
if host == "0.0.0.0":
host = "localhost"
try: try:
logger.info(f"Starting {config_eos.server_eosdash_host}:{config_eos.server_eosdash_port}.")
uvicorn.run( uvicorn.run(
app, host=str(config_eos.server_eosdash_host), port=config_eos.server_eosdash_port "akkudoktoreos.server.eosdash:app",
host=host,
port=port,
log_level=log_level.lower(), # Convert log_level to lowercase
access_log=access_log,
reload=reload,
) )
except Exception as e: except Exception as e:
# Error handling for binding issues logger.error(f"Could not bind to host {host}:{port}. Error: {e}")
logger.error( raise e
f"Could not bind to host {config_eos.server_eosdash_host}:{config_eos.server_eosdash_port}. Error: {e}"
)
def main() -> None:
"""Parse command-line arguments and start the EOSdash server with the specified options.
This function sets up the argument parser to accept command-line arguments for
host, port, log_level, access_log, and reload. It uses default values from the
config_eos module if arguments are not provided. After parsing the arguments,
it starts the EOSdash server with the specified configurations.
Command-line Arguments:
--host (str): Host for the EOSdash server (default: value from config_eos).
--port (int): Port for the EOSdash server (default: value from config_eos).
--eos-host (str): Host for the EOS server (default: value from config_eos).
--eos-port (int): Port for the EOS server (default: value from config_eos).
--log_level (str): Log level for the server. Options: "critical", "error", "warning", "info", "debug", "trace" (default: "info").
--access_log (bool): Enable or disable access log. Options: True or False (default: False).
--reload (bool): Enable or disable auto-reload. Useful for development. Options: True or False (default: False).
"""
parser = argparse.ArgumentParser(description="Start EOSdash server.")
# Host and port arguments with defaults from config_eos
parser.add_argument(
"--host",
type=str,
default=str(config_eos.server_eosdash_host),
help="Host for the EOSdash server (default: value from config_eos)",
)
parser.add_argument(
"--port",
type=int,
default=config_eos.server_eosdash_port,
help="Port for the EOSdash server (default: value from config_eos)",
)
# EOS Host and port arguments with defaults from config_eos
parser.add_argument(
"--eos-host",
type=str,
default=str(config_eos.server_eos_host),
help="Host for the EOS server (default: value from config_eos)",
)
parser.add_argument(
"--eos-port",
type=int,
default=config_eos.server_eos_port,
help="Port for the EOS server (default: value from config_eos)",
)
# Optional arguments for log_level, access_log, and reload
parser.add_argument(
"--log_level",
type=str,
default="info",
help='Log level for the server. Options: "critical", "error", "warning", "info", "debug", "trace" (default: "info")',
)
parser.add_argument(
"--access_log",
type=bool,
default=False,
help="Enable or disable access log. Options: True or False (default: True)",
)
parser.add_argument(
"--reload",
type=bool,
default=False,
help="Enable or disable auto-reload. Useful for development. Options: True or False (default: False)",
)
args = parser.parse_args()
try:
run_eosdash(args.host, args.port, args.log_level, args.access_log, args.reload)
except:
exit(1) exit(1)
if __name__ == "__main__":
main()