Skip to content

Commit

Permalink
Add Services functionality
Browse files Browse the repository at this point in the history
    - Add new initialized object in ExecutionEngine, the ServiceManager, which behaves as a singleton
        - When specified in the config (example & tutorial to come in a later commit), a Service is made an attribute of the ServiceManager
        - The ServiceManager can be imported in any plugin. As a singleton, when called to initialize, the original ServiceManager instance (along with instantiated Services) will be returned

    - Services are defined in the config and imported automatically when ServiceManager is first generated
  • Loading branch information
cfirth-nasa committed Nov 1, 2024
1 parent 686df36 commit 4eb5153
Show file tree
Hide file tree
Showing 5 changed files with 127 additions and 0 deletions.
Empty file added onair/services/__init__.py
Empty file.
32 changes: 32 additions & 0 deletions onair/services/service_manager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
from onair.src.util.singleton import Singleton
from onair.src.util.service_import import import_services


class ServiceManager(Singleton):
def __init__(self, service_dict=None):
# Only initialize if not already initialized
if not hasattr(self, "_initialized"):
# Ensure service info is provided on the first instantiation
if service_dict is None:
raise ValueError("'service_dict' parameter required on first instantiation")
services = import_services(service_dict)
for service_name, service in services.items():
# Set attribute name = service name
setattr(self, service_name, service)
self._initialized = True # mark as initialized to avoid re-initializing

def get_services(self):
# Return list of services and their functions
services = {}
for service_name, service in vars(self).items():
service_funcs = set()
if service_name.startswith('_'):
# avoid "private" attributes, e.g. _initialized
continue
for f in dir(service):
# only add the f if it's a function and not "private"
if callable(getattr(service, f)) and not f.startswith('_'):
service_funcs.add(f)
services[service_name] = service_funcs

return services
30 changes: 30 additions & 0 deletions onair/src/run_scripts/execution_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from time import gmtime, strftime

from ..run_scripts.sim import Simulator
from onair.services.service_manager import ServiceManager


class ExecutionEngine:
Expand Down Expand Up @@ -59,8 +60,12 @@ def __init__(self, config_file="", run_name="", save_flag=False):
self.parse_data(
self.data_source_file, self.fullTelemetryFile, self.fullMetaFile
)
self.setup_services()
self.setup_sim()

def setup_services(self):
self.service_manager = ServiceManager(self.services_dict)

def parse_configs(self, config_filepath):
config = configparser.ConfigParser()

Expand Down Expand Up @@ -97,6 +102,10 @@ def parse_configs(self, config_filepath):
config["PLUGINS"]["ComplexPluginDict"]
)

# Parse Service information
if config.has_section("SERVICES"):
self.services_dict = self.parse_services_dict(config["SERVICES"])

# Parse Optional Data: OPTIONS
# 'OPTIONS' must exist, but individual options return False if missing
if config.has_section("OPTIONS"):
Expand Down Expand Up @@ -124,6 +133,27 @@ def parse_plugins_dict(self, config_plugin_dict):
f"In config file '{self.config_filepath}' Plugin path '{plugin_file}' does not exist."
)
return temp_plugin_dict

def parse_services_dict(self, config_service_dict):
services_dict = {}
# Parse Required Data: Plugin name to path dict
for service, args in config_service_dict.items():
ast_service_dict = self.ast_parse_eval(args)
if isinstance(ast_service_dict.body, ast.Dict):
temp_service_dict = ast.literal_eval(ast_service_dict)
else:
raise ValueError(
f"Service dict {config_service_dict} from {self.config_filepath} is invalid. It must be a dict."
)

service_path = temp_service_dict['path']
if not (os.path.exists(service_path)):
raise FileNotFoundError(
f"In config file {self.config_filepath} Service path '{service_path}' does not exist."
)

services_dict.update({service: temp_service_dict})
return services_dict

def parse_data(
self,
Expand Down
51 changes: 51 additions & 0 deletions onair/src/util/service_import.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# GSC-19165-1, "The On-Board Artificial Intelligence Research (OnAIR) Platform"
#
# Copyright © 2023 United States Government as represented by the Administrator of
# the National Aeronautics and Space Administration. No copyright is claimed in the
# United States under Title 17, U.S. Code. All Other Rights Reserved.
#
# Licensed under the NASA Open Source Agreement version 1.3
# See "NOSA GSC-19165-1 OnAIR.pdf"

"""
plugin_import.py
Function to import user-specified plugins (from config files) into interfaces
"""

import importlib.util
import sys
import os


def import_services(service_dict):
services_dict = {}
init_filename = "__init__.py"
"""
services_dict format = {service1: service1_args, service2: service2_args}
e.g.
fleetinterface: {'path': 'service/redis_fleet_interface',
'agent_callsign': 'agent1',
'connection_timeout': '3'}
"""
for service, kwargs in service_dict.items():
true_path = kwargs.pop('path') # path no longer needed afterwards
# Last directory name is the module name
mod_name = os.path.basename(true_path) # redis_fleet_interface
# import module if not already available
if mod_name not in sys.modules:
# add init file to get proper path for spec
full_path = os.path.join(true_path, init_filename)
# define spec for module loading
spec = importlib.util.spec_from_file_location(mod_name, full_path)
# create uninitialize module from spec
module = importlib.util.module_from_spec(spec)
# initialize the created module
spec.loader.exec_module(module)
# add plugin module to system for importation
sys.modules[mod_name] = module
# import the created module's plugin file for use
service_name = f"{mod_name}_service"
service = __import__(f"{mod_name}.{service_name}", fromlist=[service_name])
# add an instance of the module's was an OnAIR plugin
services_dict[mod_name] = service.Service(**kwargs)
return services_dict
14 changes: 14 additions & 0 deletions onair/src/util/singleton.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# class Singleton(object):
# _instance = None
# def __new__(class_, *args, **kwargs):
# if not isinstance(class_._instance, class_):
# class_._instance = object.__new__(class_, *args, **kwargs)
# return class_._instance


class Singleton(object):
def __new__(cls, *args, **kwargs):
if not hasattr(cls, 'instance'):
# Create the one and only instance of this class
cls.instance = super(Singleton, cls).__new__(cls)
return cls.instance

0 comments on commit 4eb5153

Please sign in to comment.