Files
EOS/src/akkudoktoreos/server/eosdash.py
T

398 lines
12 KiB
Python
Raw Normal View History

2025-01-21 19:20:50 +01:00
import argparse
2025-01-24 00:09:28 +01:00
import os
2025-01-18 14:26:34 +01:00
import sys
2025-01-22 23:47:28 +01:00
import traceback
from pathlib import Path
2025-01-21 19:20:50 +01:00
2025-02-12 21:35:51 +01:00
import psutil
import uvicorn
2025-01-22 23:47:28 +01:00
from fasthtml.common import FileResponse, JSONResponse
from loguru import logger
2025-01-22 23:47:28 +01:00
from monsterui.core import FastHTML, Theme
from akkudoktoreos.config.config import get_config
2025-10-28 02:50:31 +01:00
from akkudoktoreos.core.logabc import LOGGING_LEVELS
from akkudoktoreos.core.logging import track_logging_config
from akkudoktoreos.core.version import __version__
from akkudoktoreos.server.dash.about import About
2025-01-22 23:47:28 +01:00
# Pages
from akkudoktoreos.server.dash.admin import Admin
from akkudoktoreos.server.dash.bokeh import BokehJS
from akkudoktoreos.server.dash.components import Page
from akkudoktoreos.server.dash.configuration import ConfigKeyUpdate, Configuration
2025-01-22 23:47:28 +01:00
from akkudoktoreos.server.dash.footer import Footer
2025-10-28 02:50:31 +01:00
from akkudoktoreos.server.dash.plan import Plan
from akkudoktoreos.server.dash.prediction import Prediction
2025-01-22 23:47:28 +01:00
from akkudoktoreos.server.server import get_default_host, wait_for_port_free
2025-10-28 02:50:31 +01:00
from akkudoktoreos.utils.stringutil import str2bool
2025-01-22 23:47:28 +01:00
config_eos = get_config()
2025-10-28 02:50:31 +01:00
# ------------------------------------
# Logging configuration at import time
# ------------------------------------
logger.remove()
track_logging_config(config_eos, "logging", None, None)
config_eos.track_nested_value("/logging", track_logging_config)
# ----------------------------
# Safe argparse at import time
# ----------------------------
parser = argparse.ArgumentParser(description="Start EOSdash server.")
parser.add_argument(
"--host",
type=str,
help="Host for the EOSdash server (default: value from config)",
)
parser.add_argument(
"--port",
type=int,
help="Port for the EOSdash server (default: value from config)",
)
parser.add_argument(
"--eos-host",
type=str,
help="Host of the EOS server (default: value from config)",
)
parser.add_argument(
"--eos-port",
type=int,
help="Port of the EOS server (default: value from config)",
)
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=str2bool,
default=False,
help="Enable or disable access logging. Options: True or False (default: False)",
)
parser.add_argument(
"--reload",
type=str2bool,
default=False,
help="Enable or disable auto-reload. Useful for development. Options: True or False (default: False)",
)
# Command line arguments
args: argparse.Namespace
args_unknown: list[str]
args, args_unknown = parser.parse_known_args()
# -----------------------------
# Prepare config at import time
# -----------------------------
# Set EOS config to actual environment variable & config file content
config_eos.reset_settings()
# Setup parameters from args, config_eos and default
# Remember parameters in config
config_eosdash = {}
# Setup EOS logging level - first to have the other logging messages logged
# - log level
if args and args.log_level is not None:
config_eosdash["log_level"] = args.log_level.upper()
else:
config_eosdash["log_level"] = "info"
# Ensure log_level from command line is in config settings
if config_eosdash["log_level"] in LOGGING_LEVELS:
# Setup console logging level using nested value
# - triggers logging configuration by track_logging_config
config_eos.set_nested_value("logging/console_level", config_eosdash["log_level"])
logger.debug(
f"logging/console_level configuration set by argument to {config_eosdash['log_level']}"
)
# Setup EOS server host
if args and args.eos_host:
config_eosdash["eos_host"] = args.eos_host
elif config_eos.server.host:
config_eosdash["eos_host"] = str(config_eos.server.host)
else:
config_eosdash["eos_host"] = get_default_host()
# Setup EOS server port
if args and args.eos_port:
config_eosdash["eos_port"] = args.eos_port
elif config_eos.server.port:
config_eosdash["eos_port"] = config_eos.server.port
else:
config_eosdash["eos_port"] = 8503
# - EOSdash host
if args and args.host:
config_eosdash["eosdash_host"] = args.host
elif config_eos.server.eosdash_host:
config_eosdash["eosdash_host"] = str(config_eos.server.eosdash_host)
else:
config_eosdash["eosdash_host"] = get_default_host()
# - EOS port
if args and args.port:
config_eosdash["eosdash_port"] = args.port
elif config_eos.server.eosdash_port:
config_eosdash["eosdash_port"] = config_eos.server.eosdash_port
else:
config_eosdash["eosdash_port"] = 8504
# - access log
if args and args.access_log:
config_eosdash["access_log"] = args.access_log
else:
config_eosdash["access_log"] = False
# - reload
if args is None and args.reload is None:
config_eosdash["reload"] = False
else:
config_eosdash["reload"] = args.reload
# ---------------------
# Prepare FastHTML app
# ---------------------
2025-01-22 23:47:28 +01:00
# The favicon for EOSdash
favicon_filepath = Path(__file__).parent.joinpath("dash/assets/favicon/favicon.ico")
if not favicon_filepath.exists():
raise ValueError(f"Does not exist {favicon_filepath}")
# Add Bokeh headers
2025-10-28 02:50:31 +01:00
# Get frankenui and tailwind headers via CDN using Theme.green.headers()
2025-01-22 23:47:28 +01:00
hdrs = (
2025-10-28 02:50:31 +01:00
*BokehJS,
2025-01-22 23:47:28 +01:00
Theme.green.headers(highlightjs=True),
)
2025-01-18 14:07:08 +01:00
2025-01-22 23:47:28 +01:00
# The EOSdash application
app: FastHTML = FastHTML(
title="EOSdash",
hdrs=hdrs,
2025-01-19 13:00:53 +01:00
secret_key=os.getenv("EOS_SERVER__EOSDASH_SESSKEY"),
)
2025-01-22 23:47:28 +01:00
def eos_server() -> tuple[str, int]:
"""Retrieves the EOS server host and port configuration.
2025-10-28 02:50:31 +01:00
Takes values from `config_eos.server` or default.
2025-01-22 23:47:28 +01:00
Returns:
tuple[str, int]: A tuple containing:
- `eos_host` (str): The EOS server hostname or IP.
- `eos_port` (int): The EOS server port.
"""
2025-10-28 02:50:31 +01:00
return config_eosdash["eos_host"], config_eosdash["eos_port"]
2025-01-22 23:47:28 +01:00
@app.get("/favicon.ico")
def get_eosdash_favicon(): # type: ignore
"""Get favicon."""
return FileResponse(path=favicon_filepath)
@app.get("/")
def get_eosdash(): # type: ignore
"""Serves the main EOSdash page.
Returns:
Page: The main dashboard page with navigation links and footer.
"""
return Page(
None,
{
2025-10-28 02:50:31 +01:00
"Plan": "/eosdash/plan",
"Prediction": "/eosdash/prediction",
2025-01-22 23:47:28 +01:00
"Config": "/eosdash/configuration",
"Admin": "/eosdash/admin",
2025-10-28 02:50:31 +01:00
"About": "/eosdash/about",
2025-01-22 23:47:28 +01:00
},
2025-10-28 02:50:31 +01:00
About(),
2025-01-22 23:47:28 +01:00
Footer(*eos_server()),
"/eosdash/footer",
)
@app.get("/eosdash/footer")
def get_eosdash_footer(): # type: ignore
"""Serves the EOSdash Foooter information.
Returns:
Footer: The Footer component.
"""
return Footer(*eos_server())
2025-10-28 02:50:31 +01:00
@app.get("/eosdash/about")
def get_eosdash_about(): # type: ignore
"""Serves the EOSdash About page.
2025-01-22 23:47:28 +01:00
Returns:
2025-10-28 02:50:31 +01:00
About: The About page component.
2025-01-22 23:47:28 +01:00
"""
2025-10-28 02:50:31 +01:00
return About()
2025-01-22 23:47:28 +01:00
@app.get("/eosdash/admin")
def get_eosdash_admin(): # type: ignore
"""Serves the EOSdash Admin page.
Returns:
Admin: The Admin page component.
"""
return Admin(*eos_server())
@app.post("/eosdash/admin")
def post_eosdash_admin(data: dict): # type: ignore
2025-10-28 02:50:31 +01:00
"""Provide control data to the Admin page.
This endpoint is called from within the Admin page on user actions.
Returns:
Admin: The Admin page component.
"""
return Admin(*eos_server(), data)
2025-01-22 23:47:28 +01:00
@app.get("/eosdash/configuration")
def get_eosdash_configuration(): # type: ignore
"""Serves the EOSdash Configuration page.
Returns:
Configuration: The Configuration page component.
"""
return Configuration(*eos_server())
@app.put("/eosdash/configuration")
def put_eosdash_configuration(data: dict): # type: ignore
return ConfigKeyUpdate(*eos_server(), data["key"], data["value"])
2025-10-28 02:50:31 +01:00
@app.get("/eosdash/plan")
def get_eosdash_plan(data: dict): # type: ignore
"""Serves the EOSdash Plan page.
2025-01-22 23:47:28 +01:00
Returns:
2025-10-28 02:50:31 +01:00
Plan: The Plan page component.
2025-01-22 23:47:28 +01:00
"""
2025-10-28 02:50:31 +01:00
return Plan(*eos_server(), data)
@app.post("/eosdash/plan")
def post_eosdash_plan(data: dict): # type: ignore
"""Provide control data to the Plan page.
This endpoint is called from within the Plan page on user actions.
Returns:
Plan: The Plan page component.
"""
return Plan(*eos_server(), data)
@app.get("/eosdash/prediction")
def get_eosdash_prediction(data: dict): # type: ignore
"""Serves the EOSdash Prediction page.
Returns:
Prediction: The Prediction page component.
"""
return Prediction(*eos_server(), data)
2025-02-12 21:35:51 +01:00
@app.get("/eosdash/health")
def get_eosdash_health(): # type: ignore
"""Health check endpoint to verify that the EOSdash server is alive."""
return JSONResponse(
{
"status": "alive",
"pid": psutil.Process().pid,
2025-10-28 02:50:31 +01:00
"version": __version__,
2025-02-12 21:35:51 +01:00
}
)
2025-01-22 23:47:28 +01:00
@app.get("/eosdash/assets/{fname:path}.{ext:static}")
def get_eosdash_assets(fname: str, ext: str): # type: ignore
"""Get assets."""
asset_filepath = Path(__file__).parent.joinpath(f"dash/assets/{fname}.{ext}")
return FileResponse(path=asset_filepath)
def run_eosdash() -> None:
2025-01-21 19:20:50 +01:00
"""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.
Returns:
2025-02-12 21:35:51 +01:00
None
2025-01-21 19:20:50 +01:00
"""
2025-02-12 21:35:51 +01:00
# Wait for EOSdash port to be free - e.g. in case of restart
2025-10-28 02:50:31 +01:00
wait_for_port_free(config_eosdash["eosdash_port"], timeout=120, waiting_app_name="EOSdash")
2025-02-12 21:35:51 +01:00
try:
uvicorn.run(
2025-01-21 19:20:50 +01:00
"akkudoktoreos.server.eosdash:app",
2025-10-28 02:50:31 +01:00
host=config_eosdash["eosdash_host"],
port=config_eosdash["eosdash_port"],
log_level=config_eosdash["log_level"].lower(),
access_log=config_eosdash["access_log"],
reload=config_eosdash["reload"],
)
except Exception as e:
2025-10-28 02:50:31 +01:00
logger.error(
f"Could not bind to host {config_eosdash['eosdash_host']}:{config_eosdash['eosdash_port']}. Error: {e}"
)
2025-01-21 19:20:50 +01:00
raise 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
2025-01-22 23:47:28 +01:00
config module if arguments are not provided. After parsing the arguments,
2025-01-21 19:20:50 +01:00
it starts the EOSdash server with the specified configurations.
Command-line Arguments:
2025-01-18 14:26:34 +01:00
--host (str): Host for the EOSdash server (default: value from config).
--port (int): Port for the EOSdash server (default: value from config).
--eos-host (str): Host for the EOS server (default: value from config).
--eos-port (int): Port for the EOS server (default: value from config).
2025-01-21 19:20:50 +01:00
--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).
"""
try:
2025-01-22 23:47:28 +01:00
run_eosdash()
2025-02-12 21:35:51 +01:00
except Exception as ex:
error_msg = f"Failed to run EOSdash: {ex}"
logger.error(error_msg)
2025-01-22 23:47:28 +01:00
traceback.print_exc()
2025-01-18 14:26:34 +01:00
sys.exit(1)
2025-01-21 19:20:50 +01:00
if __name__ == "__main__":
main()