Coverage for melissa/server/main.py: 54%
57 statements
« prev ^ index » next coverage.py v7.10.1, created at 2025-11-19 09:33 +0100
« prev ^ index » next coverage.py v7.10.1, created at 2025-11-19 09:33 +0100
1"""This script is the main Melissa server entrypoint."""
3import argparse
4import importlib.util
5import sys
6import logging
7import os
8from pathlib import Path
9from typing import Any, Dict
11import rapidjson
13from melissa.launcher.__main__ import validate_config, CONFIG_PARSE_MODE
14from melissa.server.base_server import BaseServer
15from melissa.server.exceptions import MelissaError
17logger = logging.getLogger(__name__)
20def main() -> None:
21 """
22 Initializes and starts the Melissa server.
24 ### Overview
25 This function is the entry point for the Melissa server. It is **not intended to be called
26 directly by users** but is invoked by the `melissa-launcher`.
28 ### Steps Performed
29 1. Parses command-line arguments, including flags like `--config_name` to
30 locate the project directory.
31 2. Resolves the appropriate custom server class, if specified in the configuration.
32 3. Instantiates the server using the user-provided configuration.
33 4. Starts the server by calling `server.start()`.
35 ### Usage
36 This function is typically invoked through the server shell script `server.sh`:
38 ```
39 exec python -m melissa --config_name /path/to/config
40 ```"""
42 parser = argparse.ArgumentParser(
43 prog="melissa", description="Melissa"
44 )
46 # CLI flags
47 parser.add_argument(
48 "--project_dir",
49 help="Directory to all necessary files:\n"
50 " client.sh\n"
51 " server.sh\n"
52 " config.py\n"
53 " data_generator\n"
54 " CustomServer.py"
55 )
57 parser.add_argument(
58 "--config_name",
59 help="Defaults to `config` but user can change"
60 "the search name by indicating it with this flag",
61 default=None
62 )
64 args = parser.parse_args()
65 if not args.config_name:
66 conf_name = 'config'
67 else:
68 conf_name = args.config_name
70 # load the config into a python dictionary
71 with open(Path(args.project_dir) / f"{conf_name}.json") as json_file:
72 config_dict = rapidjson.load(json_file, parse_mode=CONFIG_PARSE_MODE)
74 config_dict['user_data_dir'] = Path('user_data')
76 # ensure user passed the necessary information for software configuration
77 args, config_dict = validate_config(args, config_dict)
79 # Resolve the server
80 myserver = get_resolved_server(args, config_dict)
82 try:
83 myserver.configure_logger()
84 myserver.initialize_connections()
85 myserver.start()
86 except MelissaError as e:
87 logger.exception(e)
88 if myserver.no_fault_tolerance:
89 myserver.close_connection(1)
90 myserver.mpi_abort(1)
93def safe_join(base_path: str, new_path: str) -> Path:
94 """Safely joins a base path and a new path, ensuring the result is an expanded absolute path.
96 ### Parameters
97 - **base_path (str)**: The base directory path.
98 - **new_path (str)**: The path to join with the base path.
100 ### Returns
101 - **Path**: An expanded and absolute path combining `base_path` and `new_path`."""
103 new_path = os.path.expandvars(new_path)
104 new_path_ = Path(new_path)
105 if new_path_.is_absolute():
106 return new_path_
107 return Path(base_path) / new_path_
110def get_resolved_server(args: argparse.Namespace,
111 config_dict: Dict[str, Any]) -> BaseServer:
112 """Resolves and returns a server object based on the provided configurations.
114 ### Parameters
115 - **args (argparse.Namespace)**: Parsed command-line arguments containing
116 server-related options.
117 - **config_dict (Dict[str, Any])**: Configuration dictionary with server settings.
119 ### Returns
120 - `BaseServer`: An instance of the resolved server class, initialized with the
121 provided configurations.
123 ### Raises
124 - `ImportError`: If the server file or class cannot be imported.
125 - `AttributeError`: If the specified server class is not found in the module.
126 - `Exception`: If there is an issue with initializing the server class."""
128 # Resolve the server file name and path
129 server_file_name = config_dict.get("server_filename", "server.py")
130 server_path = safe_join(args.project_dir, server_file_name)
131 server_module_name = Path(server_path).stem
132 # Import the server module dynamically
133 spec_server = importlib.util.spec_from_file_location(server_module_name, server_path)
134 if not (spec_server and spec_server.loader):
135 logger.warning("Unable to import server module from path.")
136 raise ImportError(f"Failed to import server module from {server_path}.")
138 sys.path.append(str(Path(server_path).parent))
139 server = importlib.util.module_from_spec(spec_server)
140 spec_server.loader.exec_module(server)
142 # Resolve the server class name
143 server_class_name = config_dict.get("server_class", "MyServer")
144 try:
145 MyServerClass = getattr(server, server_class_name)
146 except AttributeError:
147 raise AttributeError(
148 f"Server class '{server_class_name}' not found in '{server_file_name}'."
149 )
151 return MyServerClass(config_dict)