def update(self): """ Get data from GPIO through nanpy""" if self.node.connected: self.check_connection() try: data = self.node.api.analogRead( self.pin) if self.analog else self.node.api.digitalRead( self.pin) if self.type != 'potentiometer': self.previous_state = self._state self._state = data else: if (data < self.previous_state - self.buffer) or ( data > self.previous_state + self.buffer): self.previous_state = self._state self._state = data self.handle_state() except (SerialManagerError, SocketManagerError, BrokenPipeError, ConnectionResetError, OSError, socket.timeout) as e: if self.node.connected: Logger.log_formatted( LOG_LEVEL["warning"], f'{self.node.key} -> Broken Connection', 'Timeout', 'notice') self.node.reset_connection() self._pin_setup = False else: self._pin_setup = False return None
def next_step(self, event_data=None): """ Advance to the next sequnce step Makes sure any delays and durations are done """ # Step must be flagged complete to advance if self._step_complete: if self.active: # If skipping steps trigger unperformed actions if not self._step_triggered: if self.evaluate_thresholds(): self.trigger() # Sequence is already active, advance to next step if self._current_step < self.total_steps - 1: self.reset_step() self._current_step += 1 self.fire({"event": "SequenceStepStarted"}) Logger.log_formatted( LOG_LEVEL["info"], f'Sequence: {FONT_CYAN}{self.name}{FONT_RESET}', f'Step {self._current_step+1}/{self.total_steps}' ) else: # Last step of sequence completed self.active = False self.fire({"event": "SequenceEnded"}) Logger.log_formatted( LOG_LEVEL["info"], f'Sequence {FONT_CYAN}{self.name}{FONT_RESET}', 'Completed', 'success' ) self.reset_duration()
def update(self): """ Get data from DHT through nanpy""" if self.node.connected: try: self.check_connection() if self._dht: _temp_format = self.mudpi.unit_system == IMPERIAL_SYSTEM temperature = self._dht.readTemperature(_temp_format) humidity = self._dht.readHumidity() data = { 'temperature': round(temperature, 2), 'humidity': round(humidity, 2) } self._state = data except (SerialManagerError, SocketManagerError, BrokenPipeError, ConnectionResetError, OSError, socket.timeout) as e: if self.node.connected: Logger.log_formatted( LOG_LEVEL["warning"], f'{self.node.key} -> Broken Connection', 'Timeout', 'notice') self.node.reset_connection() self._dht = None else: self._dht = False return None
def get_extension_importer(mudpi, extension, install_requirements=False): """ Find or create an extension importer, Loads it if not loaded, Checks cache first. Set install_requirements to True to also have all requirements checked through pip. """ # First we check if the namespace is disabled. Could be due to errors or configs disabled_cache = mudpi.cache.setdefault("disabled_namespaces", {}) if extension in disabled_cache: raise MudPiError(f"Extension is {extension} is disabled.") if install_requirements: extension_importer = _extension_with_requirements_installed( mudpi, extension) if extension_importer is not None: return extension_importer importer_cache = mudpi.cache.setdefault("extension_importers", {}) try: extension_importer = importer_cache.get(extension) if extension_importer is not None: return extension_importer except Exception as error: extension_importer = None if extension_importer is None: extension_importer = _get_custom_extensions(mudpi).get(extension) if extension_importer is not None: Logger.log( LOG_LEVEL["warning"], f'{FONT_YELLOW}You are using {extension} which is not provided by MudPi.{FONT_RESET}\nIf you experience errors, remove it.' ) return extension_importer # Component not found look in internal extensions from mudpi import extensions extension_importer = ExtensionImporter.create(mudpi, extension, extensions) if extension_importer is not None: importer_cache[extension] = extension_importer Logger.log_formatted( LOG_LEVEL["info"], f'{extension_importer.namespace.title()} Ready for Import', 'Ready', 'success') else: Logger.log_formatted(LOG_LEVEL["debug"], f'Import Preperations for {extension.title()}', 'error', 'error') Logger.log( LOG_LEVEL["debug"], f'{FONT_YELLOW}`{extension.title()}` was not found.{FONT_RESET}') disabled_cache[extension] = 'Not Found' raise ExtensionNotFound(extension) return extension_importer
def update(self): """ Main run loop for sequence to check time past and if it should fire actions """ if self.mudpi.is_prepared: try: if self.active: if not self._step_complete: if not self._delay_complete: if self.step_delay is not None: if self.duration > self.step_delay: self._delay_complete = True self._delay_actual = self.duration self.reset_duration() else: # Waiting break early return else: self._delay_complete = True self.reset_duration() if self._delay_complete: if not self._step_triggered: if self.evaluate_thresholds(): self.trigger() else: if self.current_step.get('thresholds') is not None: # Thresholds failed skip step without trigger self._step_triggered = True self._step_complete = True if self.step_duration is not None and not self._step_complete: if self.duration > self.step_duration: self._step_complete = True self._duration_actual = self.duration self.reset_duration() else: # Waiting break early return else: # No duration set meaning step only advances # manualy by calling actions and events. RTM pass if self._step_complete: self.fire({"event": "SequenceStepEnded"}) # Logger.log( # LOG_LEVEL["debug"], # f'Sequence {FONT_CYAN}{self.name}{FONT_RESET} Step {self._current_step+1} Debug\n' \ # f'Delay: {self.step_delay} Actual: {self._delay_actual} Duration: {self.step_duration} Actual: {self._duration_actual}' # ) return self.next_step() else: # Sequence is not active. self.reset_duration() except Exception as e: Logger.log_formatted(LOG_LEVEL["error"], f'Sequence {self.id}', 'Unexpected Error', 'error') Logger.log(LOG_LEVEL["critical"], e)
def run(self, func=None): """ Create a thread and return it """ if not self._thread: self._thread = threading.Thread(target=self.work, args=(func, )) Logger.log_formatted(LOG_LEVEL["debug"], f"Worker {self.key} ", "Starting", "notice") self._thread.start() Logger.log_formatted(LOG_LEVEL["info"], f"Worker {self.key} ", "Started", "success") return self._thread
def reset(self, event_data=None): """ Reset the entire sequence """ self._current_step = 0 self.reset_step() self.fire({ "event": "SequenceReset" }) Logger.log_formatted( LOG_LEVEL["info"], f'Sequence {FONT_CYAN}{self.name}{FONT_RESET}', 'Reset', 'warning' )
def initialize_logging(self, config=None): """ Enable logging module and attach to MudPi """ config = config or self.mudpi.config.to_dict() Logger.logger = self.mudpi.logger = Logger(config) time.sleep(0.05) Logger.log_formatted( LOG_LEVEL["info"], "Initializing Logger ", "Complete", 'success' ) Logger.log_to_file(LOG_LEVEL["debug"], "Dumping the config file: ") for index, config_item in config.items(): Logger.log_to_file(LOG_LEVEL["debug"], f'{index}: {config_item}') Logger.log_to_file(LOG_LEVEL["debug"], "End of config file dump!\n")
def connect(self): """ Setup connections for all adaptors """ connection_data = {} for key, adaptor in self.adaptors.items(): Logger.log_formatted(LOG_LEVEL["debug"], f"Preparing Event System for {key} ", 'Pending', 'notice') connection_data[key] = adaptor.connect() Logger.log_formatted(LOG_LEVEL["info"], f"Event System Ready on {key} ", 'Connected', 'success') return connection_data
def _install_extension_requirements(mudpi, extension): """ Installs all the extension requirements """ cache = mudpi.cache.setdefault('extensions_requirements_installed', {}) if cache.get(extension.namespace) is not None: # Already processed and installed return cache[extension.namespace] # Handle all the dependencies requirements if extension.has_dependencies: if extension.import_dependencies(): for dependency in extension.loaded_dependencies: try: dependency_extension = get_extension_importer( mudpi, dependency) except Exception as error: Logger.log( LOG_LEVEL["error"], f'Error getting extension <{extension}> dependency: {FONT_YELLOW}{dependency}{FONT_RESET}' ) if not dependency_extension.install_requirements(): Logger.log( LOG_LEVEL["error"], f'Error with extension <{extension}> dependency: {FONT_YELLOW}{dependency}{FONT_RESET} requirements.' ) if not extension.has_requirements: cache[extension.namespace] = extension return cache[extension.namespace] requirement_cache = mudpi.cache.setdefault('requirement_installed', {}) for requirement in extension.requirements: if requirement not in requirement_cache: if not utils.is_package_installed(requirement): Logger.log_formatted( LOG_LEVEL["info"], f'{FONT_YELLOW}{extension.namespace.title()}{FONT_RESET} requirements', 'Installing', 'notice') Logger.log( LOG_LEVEL["debug"], f'Installing package {FONT_YELLOW}{requirement}{FONT_RESET}', ) if not utils.install_package(requirement): Logger.log( LOG_LEVEL["error"], f'Error installing <{extension.title()}> requirement: {FONT_YELLOW}{requirement}{FONT_RESET}' ) return False requirement_cache[requirement] = True # extension.requirements_installed = True cache[extension.namespace] = extension return cache[extension.namespace]
def stop(self, event_data=None): """ Stop the sequence """ if self.active: self._current_step = 0 self.active = False self.reset_step() self.fire({ "event": "SequenceStopped" }) Logger.log_formatted( LOG_LEVEL["info"], f'Sequence {FONT_CYAN}{self.name}{FONT_RESET}', 'Stopped', 'error' )
def start(self, event_data=None): """ Start the sequence """ if not self.active: self._current_step = 0 self.active = True self.reset_step() self.fire({ "event": "SequenceStarted" }) Logger.log_formatted( LOG_LEVEL["info"], f'Sequence {FONT_CYAN}{self.name}{FONT_RESET}', 'Started', 'success' )
def update(self): """ Control LCD display nanpy""" if self.node.connected: try: self.check_connection() super().update() except (SerialManagerError, SocketManagerError, BrokenPipeError, ConnectionResetError, OSError, socket.timeout) as e: if self.node.connected: Logger.log_formatted( LOG_LEVEL["warning"], f'{self.node.key} -> Broken Connection', 'Timeout', 'notice') self.node.reset_connection() self._lcd = None return None
def get_or_load_interface(self, interface_name): """ Get a interface from the extension, import it if not in cache""" cache = self.mudpi.cache.setdefault("interface_modules", {}) component_fullname = f'{self.namespace}.{interface_name}' if component_fullname not in cache: Logger.log_formatted( LOG_LEVEL["debug"], f'Importing {self.namespace}:{interface_name}', 'Pending', 'notice') cache[component_fullname] = \ importlib.import_module(f'{self.extension_path}.{interface_name}') Logger.log_formatted( LOG_LEVEL["info"], f'Imported {self.namespace}:{interface_name}', 'Success', 'success') return cache[component_fullname]
def work(self, func=None): while self.mudpi.is_prepared: if self.mudpi.is_running: if not self._server_running: self._server = threading.Thread(target = self.server, args = ()) self._server.start() self._server_running = True time.sleep(0.1) self._server_ready.clear() # Connect a client to prevent hanging on accept() with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: s.connect((self.host, self.port)) self._server.join() if len(self.client_threads) > 0: for client in self.client_threads: client.join() Logger.log_formatted(LOG_LEVEL['info'], f'Socket Server {self.key}', 'Offline', 'error')
def server(self): """ Socket server main loop """ self.sock.listen(0) # number of clients to listen for. Logger.log_formatted(LOG_LEVEL['info'], 'MudPi Server', 'Online', 'success') while self._server_ready.is_set(): try: client, address = self.sock.accept() client.settimeout(600) ip, port = client.getpeername() Logger.log(LOG_LEVEL['info'], f'Socket Client {port} from {ip} Connected') t = threading.Thread(target = self.listenToClient, args = (client, address, ip)) self.client_threads.append(t) t.start() except Exception as e: Logger.log(LOG_LEVEL['error'], e) time.sleep(1) self.sock.close()
def __init__(self, mudpi, redis_conf=None): self.mudpi = mudpi self.states = {} self._lock = threading.RLock() host = '127.0.0.1' port = 6379 try: if redis_conf: host = redis_conf.get('host', '127.0.0.1') port = redis_conf.get('port', 6379) self.redis = redis.Redis(host=host, port=port) except Exception as error: Logger.log(LOG_LEVEL["error"], f"State Manager Error Connecting to Redis") self.restore_states() Logger.log_formatted(LOG_LEVEL["info"], f"Preparing State Manager ", "Complete", "success")
def update(self): """ Wrap the failsafe detection in connection handler for node """ if self.node.connected: self.check_connection() try: # Pass to the Base Toggle for failsafe handling super().update() except (SerialManagerError, SocketManagerError, BrokenPipeError, ConnectionResetError, OSError, socket.timeout) as e: if self.node.connected: Logger.log_formatted( LOG_LEVEL["warning"], f'{self.node.key} -> Broken Connection', 'Timeout', 'notice') self.node.reset_connection() self._pin_setup = False else: self._pin_setup = False return None
def work(self, func=None): """ Perform work each cycle like checking devices, polling sensors, or listening to events. Worker should sleep based on `update_interval` """ while self.mudpi.is_prepared: if self.mudpi.is_running: if callable(func): func() for key, component in self.components.items(): if component.should_update: component.update() component.store_state() self.reset_duration() self._wait(self.update_interval) # # MudPi Shutting Down, Perform Cleanup Below Logger.log_formatted(LOG_LEVEL["debug"], f"Worker {self.key} ", "Stopping", "notice") for key, component in self.components.items(): component.unload() Logger.log_formatted(LOG_LEVEL["info"], f"Worker {self.key} ", "Offline", "error")
def shutdown(self, data=None): """ Signal MudPi to Stop and Unload """ self.stop() self.events.publish('core', {'event': 'ShuttingDown'}) self.unload_extensions() self.thread_events['mudpi_running'].clear() self.state = CoreState.not_running _closed_threads = [] # First pass of threads to find out which ones are slower for thread_name, thread in self.threads.items(): thread.join(2) if not thread.is_alive(): _closed_threads.append(thread_name) else: Logger.log_formatted( LOG_LEVEL["warning"], f"Worker {thread_name} is Still Stopping ", "Delayed", "notice") for _thread in _closed_threads: del self.threads[_thread] # Now attempt to clean close slow threads _closed_threads = [] for thread_name, thread in self.threads.items(): thread.join(3) if not thread.is_alive(): _closed_threads.append(thread_name) else: Logger.log_formatted( LOG_LEVEL["error"], f"Worker {thread_name} Failed to Shutdown ", "Not Responding", "error") self.events.publish('core', {'event': 'Shutdown'}) self.events.disconnect() return True
def listenToClient(self, client, address, ip): size = 1024 while self.mudpi.is_prepared: try: data = client.recv(size) if data: data = decode_event_data(data) if data.get("topic", None) is not None: self.mudpi.events.publish(data["topic"], data) Logger.log(LOG_LEVEL['debug'], f"Socket Event {data['event']} from {data['source']} Dispatched") # response = { "status": "OK", "code": 200 } # client.send(json.dumps(response).encode('utf-8')) else: Logger.log(LOG_LEVEL['error'], f"Socket Data Recieved. {FONT_RED}Dispatch Failed:{FONT_RESET} Missing Data 'Topic'") Logger.log(LOG_LEVEL['debug'], data) else: pass # raise error('Client Disconnected') except Exception as e: Logger.log(LOG_LEVEL['info'], f"Socket Client {ip} Disconnected") client.close() return False Logger.log_formatted(LOG_LEVEL['info'], 'Closing Client Connection ', 'Complete', 'success')
def get_or_load_extension(self): """ Get the extension base module, import if not """ module_cache = self.mudpi.cache.setdefault('extension_modules', {}) if self.namespace not in module_cache: Logger.log_formatted(LOG_LEVEL["debug"], f'Importing {self.namespace.title()}', 'Pending', 'notice') module_cache[self.namespace] = importlib.import_module( self.extension_path) Logger.log_formatted(LOG_LEVEL["info"], f'Imported {self.namespace.title()}', 'Success', 'success') self.module = module_cache[self.namespace] extension_cache = self.mudpi.cache.setdefault("extensions", {}) if self.namespace not in extension_cache: if not hasattr(self.module, 'Extension'): raise ExtensionNotFound(self.namespace) extension = extension_cache[ self.namespace] = self.module.Extension(self.mudpi) return extension_cache[self.namespace]
def update(self): """ Get data from GPIO through nanpy""" if self.node.connected: self.check_connection() try: data = None if self.analog: data = self.node.api.analogRead(self.pin) else: data = self.node.api.digitalRead(self.pin) self._state = data except (SerialManagerError, SocketManagerError, BrokenPipeError, ConnectionResetError, OSError, socket.timeout) as e: if self.node.connected: Logger.log_formatted( LOG_LEVEL["warning"], f'{self.node.key} -> Broken Connection', 'Timeout', 'notice') self.node.reset_connection() self._pin_setup = True else: self._pin_setup = False return None
def work(self, func=None): """ Main NFC read cycle """ while self.mudpi.is_prepared: if self.mudpi.is_running: try: with nfc.ContactlessFrontend(self.address) as clf: while self.mudpi.is_running: try: tag = clf.connect(rdwr={ 'on-connect': self.handle_read, 'beep-on-connect': self.beep_enabled, 'on-release': self.handle_release }, terminate=self.terminate) time.sleep(self.read_delay) except Exception as error: Logger.log( LOG_LEVEL["error"], f"Worker {self.key} NFC Scan Error: {error}" ) time.sleep(2) except Exception as error: Logger.log_formatted( LOG_LEVEL["error"], f"Worker {self.key} NFC Reader Error: {error}") time.sleep(2) # MudPi Shutting Down, Perform Cleanup Below Logger.log_formatted(LOG_LEVEL["debug"], f"Worker {self.key} ", "Stopping", "notice") Logger.log_formatted(LOG_LEVEL["info"], f"Worker {self.key} ", "Offline", "error")
def main(args=None): """ The main run entry. """ if args is None: args = sys.argv[1:] arguments = get_arguments() ################################### ### Make a default config file ### if arguments.make_config: config_location = os.path.join(os.getcwd(), arguments.config) print(f"Generating a default config file at {config_location}") if os.path.exists(config_location) and not arguments.overwrite: print(f'{FONT_RED}{"File already exists and `--overwrite` was not set."}{FONT_RESET}') return Config().save_to_file(config_location) return ###################################### ### Convert v0.9 configs to v0.10+ ### if arguments.migrate_config: config_location = os.path.join(os.getcwd(), arguments.config) print(f"Converting config file at {config_location}") config = Config(config_location) config.load_from_file(config_location) new_config_path = os.path.join(os.getcwd(), arguments.migrate_config) if os.path.exists(new_config_path) and not arguments.overwrite: print(f"{FONT_RED}File already exists at {new_config_path}.") print(f"Use `--overwrite` to save over existing file.{FONT_RESET}") return # convert the old configs convert_old_config(config) config.save_to_file(new_config_path) print(f"{FONT_GREEN}Successfully converted config to: {new_config_path}{FONT_RESET}") return ####################### ### Configurations ### config_path = os.path.abspath(os.path.join(os.getcwd(), arguments.config)) print('Loading MudPi Configs \r', end="", flush=True) print(chr(27) + "[2J") display_greeting() manager = CoreManager() manager.load_mudpi_from_config(config_path) print(f'{"Loading MudPi Configs ":.<{FONT_PADDING+1}} {FONT_GREEN}Complete{FONT_RESET}') ##################### ### Bootstrapping ### if arguments.debug: print(f'{YELLOW_BACK}DEBUG MODE ENABLED{FONT_RESET}') manager.mudpi.config.config["mudpi"]["debug"] = True # print(arguments) #DEBUG print(f'Config path: {config_path}') #DEBUG print(f'Current Directory: {os.getcwd()}') # print(f'Config keys {mudpi.config.keys()}') time.sleep(1) # Logging Module try: print('Initializing Logger \r', end='', flush=True) manager.initialize_logging() except Exception as e: print(f'{"Initializing Logger ":.<{FONT_PADDING}} {FONT_RED}Disabled{FONT_RESET}') # MudPi Core Systems manager.load_mudpi_core() Logger.log_formatted(LOG_LEVEL["warning"], "Initializing Core ", "Complete", 'success') Logger.log(LOG_LEVEL["debug"], f'{" Detecting Configurations ":_^{FONT_PADDING+8}}') # Load the Extension System loaded_extensions = manager.load_all_extensions() Logger.log_formatted( LOG_LEVEL["warning"], f"Loaded {len(loaded_extensions)} Extensions ", "Complete", 'success' ) Logger.log_formatted(LOG_LEVEL["warning"], "MudPi Fully Loaded", 'Complete', 'success') # Restore Previous States Logger.log_formatted(LOG_LEVEL["info"], "Checking for Previous Component States", 'Pending', 'warning') manager.mudpi.restore_states() Logger.log_formatted(LOG_LEVEL["warning"], "Restored Previous Component States", 'Complete', 'success') ######################### ### Start All Systems ### Logger.log(LOG_LEVEL["debug"], f'{" Start Systems ":_^{FONT_PADDING+8}}') Logger.log_formatted(LOG_LEVEL["debug"], "Starting All Workers ", 'Pending', 'notice') manager.mudpi.start_workers() Logger.log_formatted(LOG_LEVEL["info"], "Started All Workers ", 'Complete', 'success') # Everything should be loaded and running Logger.log_formatted(LOG_LEVEL["info"], "MudPi Systems ", 'Online', 'success') print(f'{"":_<{FONT_PADDING+8}}\n') """ Debug Mode Dump After System Online """ if arguments.debug and arguments.dump: manager.debug_dump(cache_dump=arguments.cache_dump) time.sleep(1) ############################### """ MAIN PROGRAM HEARTBEAT """ manager.mudpi.start() PROGRAM_RUNNING = True while PROGRAM_RUNNING: try: # Keep messages being processed manager.mudpi.events.get_message() current_clock = datetime.datetime.now().replace(microsecond=0) manager.mudpi.events.publish('clock', {"clock":current_clock.strftime("%m-%d-%Y %H-%M-%S"), "date":str(current_clock.date()), "time": str(current_clock.time())}) for i in range(10): time.sleep(0.1) manager.mudpi.events.get_message() except KeyboardInterrupt as error: PROGRAM_RUNNING = False except Exception as error: Logger.log( LOG_LEVEL["error"], f"Runtime Error: {error}" ) PROGRAM_RUNNING = False """ PROGRAM SHUTDOWN """ print(f'{"":_<{FONT_PADDING+8}}') Logger.log_formatted( LOG_LEVEL["info"], "Stopping All Workers for Shutdown ", 'Pending', 'notice' ) manager.shutdown() Logger.log_formatted( LOG_LEVEL["info"], "All MudPi Systems ", 'Offline', 'error' )
def validate_config(self, config): """ Validate configs for an extension Returns False or the validated configs """ if not self.namespace: Logger.log( LOG_LEVEL["error"], f'{FONT_YELLOW}{self.namespace.title()}{FONT_RESET} is missing a namespace in `extension.json`' ) return False namespace = self.namespace if self.extension is None: Logger.log( LOG_LEVEL["error"], f'{FONT_YELLOW}{namespace.title()}{FONT_RESET} is not imported. Call `import_extension()` first!' ) return False if config is None: Logger.log( LOG_LEVEL["error"], f'{FONT_YELLOW}{namespace.title()}{FONT_RESET} config is None and unable to validate.' ) return False # Check for overrided validate() function to determine if custom validation is set validated_config = config if self.module.Extension.validate != self.module.Extension.__bases__[ 0].validate: # Logger.log_formatted( # LOG_LEVEL["debug"], # f'Checking Extension {namespace} Configuration', # "Validating", 'notice' # ) try: validated_config = self.extension.validate(config) Logger.log_formatted(LOG_LEVEL["debug"], f'{namespace.title()} Configuration', "Validated", 'success') except (ConfigError, MudPiError) as error: Logger.log_formatted( LOG_LEVEL["error"], f'{namespace.title()} Configuration Validation', "Failed", 'error') Logger.log(LOG_LEVEL["error"], f'{namespace.title()} validation error: {error}') return False except Exception as error: Logger.log( LOG_LEVEL["error"], f'{namespace.title()} Validator encountered unknown error. \n{error}' ) return False return validated_config # Custom validator not set proceed with default validation # Todo: Change this to a regex match to prevent false matches # (i.e. action matches action_other) conf_keys = [key for key in config.keys() if namespace in key] # Logger.log_formatted( # LOG_LEVEL["debug"], # f'Checking Extension {namespace} Configuration', # "Validating", 'notice' # ) interfaces = [] disabled_cache = self.mudpi.cache.setdefault("disabled_namespaces", {}) for conf_key in conf_keys: for interface_config in config[conf_key]: # Empty configs found if not interface_config: continue # List wrapper if not isinstance(interface_config, list): interface_config = [interface_config] for entry in interface_config: try: # Search for interface property interface_name = entry.get("interface") except AttributeError as error: interface_name = None # No interface to load which is ok. Not all extensions # support interfaces. i.e. actions if interface_name is None: interfaces.append(interface_config) continue if interface_name in disabled_cache: # raise MudPiError(f"Interface is in disabled namespace: {interface}.") Logger.log_formatted( LOG_LEVEL["warning"], f'Interface {interface_name} is in disabled namespace', "Disabled", 'error') continue # Interface found, attempt to load in order to validate config try: interface_importer = get_extension_importer( self.mudpi, interface_name, install_requirements=True) except Exception as error: continue interface = None try: interface_module = interface_importer.get_or_load_interface( namespace) if not hasattr(interface_module, 'Interface'): raise MudPiError( f'No `Interface()` class to load for {namespace}:{interface_name}.' ) if utils.is_interface(interface_module.Interface): interface = self.interfaces[ interface_name] = interface_module.Interface( self.mudpi, namespace, interface_name) else: raise MudPiError( f'Interface {namespace}:{interface} does not extend BaseInterface.' ) except MudPiError as error: Logger.log(LOG_LEVEL["error"], error) continue except Exception as error: Logger.log_formatted( LOG_LEVEL["error"], f'Interface {interface_name}:{namespace} error during validation ', "Errors", 'error') Logger.log(LOG_LEVEL["debug"], error) disabled_cache[interface_name] = error continue validated_interface_config = interface_config if interface: try: validated_interface_config = interface.validate( interface_config) Logger.log_formatted( LOG_LEVEL["debug"], f'Configuration for {interface_importer.namespace}:{namespace}', "Validated", 'success') except (ConfigError, MudPiError) as error: Logger.log_formatted( LOG_LEVEL["error"], f'{interface_importer.namespace}:{namespace} Configuration Validation', "Failed", 'error') Logger.log( LOG_LEVEL["error"], f'{namespace.title()} validation error: {error}') continue except Exception as error: Logger.log( LOG_LEVEL["error"], f'{interface_importer.namespace.title()} `validate()` encountered unknown error. \n{error}' ) continue interfaces.append(validated_interface_config) Logger.log_formatted(LOG_LEVEL["debug"], f'{namespace.title()} Configuration', "Validated", 'success') # Copy old config and replace old data with validated validated_config = validated_config.copy() for key in conf_keys: del validated_config[key] validated_config[namespace] = interfaces config = validated_config return validated_config
def import_extension(self, config): """ Prepare and import the actual extension module """ disabled_cache = self.mudpi.cache.setdefault("disabled_namespaces", {}) if self.mudpi.extensions.exists(self.namespace): # Extension already loaded self.extension = self.mudpi.extensions.get(self.namespace) return self.extension # Load all Dependencies and install requirements if not self.prepare_for_import(): # The requirements were not able to install return False # Import the actual package now that requirements and dependencies are done try: self.extension = self.get_or_load_extension() # Call extension post import hook self.extension.extension_imported(importer=self) except ImportError as error: Logger.log( LOG_LEVEL["error"], f'Error during import of extension: {FONT_YELLOW}{error}{FONT_RESET}' ) return False except Exception as error: Logger.log( LOG_LEVEL["error"], f'Extension {self.namespace} error during import: {FONT_YELLOW}{error}{FONT_RESET}' ) return False ### Config ### # Now we can deal with the config validated_config = self.validate_config(config) if not validated_config: Logger.log( LOG_LEVEL["error"], f'Extension {FONT_YELLOW}{self.namespace}{FONT_RESET} has invalid or empty configs.' ) return False ### Init ### # Call the extension init with the validated configs if self.extension.__class__.init == self.extension.__class__.__bases__[ 0].init: Logger.log( LOG_LEVEL["debug"], f"Notice: Extension {self.namespace} did not define an `init()` method." ) Logger.log_formatted(LOG_LEVEL["debug"], f"Initializing {self.namespace.title()}", "Pending", 'notice') init_result = self.extension.init(validated_config) if init_result: Logger.log_formatted(LOG_LEVEL["warning"], f"Initialized {self.namespace.title()}", "Success", 'success') self.extension.setup_complete = True # Call extension post init hook self.extension.extension_initialized( importer=self, validated_config=validated_config) else: Logger.log_formatted( LOG_LEVEL["error"], f"{self.namespace.title()} `init()` failed to return True", 'Failed', 'error') return False self.mudpi.extensions.register(self.namespace, self.extension) self.mudpi.events.publish('core', { 'event': 'ExtensionRegistered', 'namespace': self.namespace }) # Call extension post registry hook self.extension.extension_registered(importer=self, validated_config=validated_config) return self.extension
def load_all_extensions(self, config=None): """ Import extensions for MudPi based on loaded Config """ config = config or self.mudpi.config.config if not self.import_config_dir(): raise ConfigError("Could not import the config_path and load extensions") Logger.log_formatted( LOG_LEVEL["warning"], "Detecting Configurations", "Pending", 'notice' ) core_configs = ['mudpi', 'logging', 'debug'] # Get all the non-core extensions to load extensions_to_load = [ key for key in config.keys() if key not in core_configs ] Logger.log_formatted( LOG_LEVEL["warning"], f"Detected {len(extensions_to_load)} Non-Core Configurations", "Complete", 'success' ) Logger.log_formatted( LOG_LEVEL["warning"], f"Preparing {len(extensions_to_load)} Configurations to be Loaded ", "Pending", 'notice' ) extension_count = len(extensions_to_load) extension_error_count = 0 extensions_with_errors = [] _extensions_needing_load = extensions_to_load _importer_cache = {} disabled_cache = self.mudpi.cache.setdefault("disabled_namespaces", {}) # A loop to fetch all extensions and their dependencies while _extensions_needing_load: _extension_load_list = _extensions_needing_load.copy() _extensions_needing_load = [] extension_importers = [] # Load extension importers for detected configs for key in _extension_load_list: try: extension_importer = importer.get_extension_importer(self.mudpi, key) except ExtensionNotFound as error: extension_importer = None extensions_to_load.remove(key) extension_error_count += 1 extensions_with_errors.append(key) disabled_cache[key] = 'Not Found' if isinstance(extension_importer, importer.ExtensionImporter): extension_importers.append(extension_importer) # Loop through importers and prepare extensions for setup for extension_importer in extension_importers: # Load dependenies if there are any if not extension_importer.dependencies_ready: extension_importer.import_dependencies() # Check if all the component dependencies imported if extension_importer.dependencies_ready: _importer_cache[extension_importer.namespace] = extension_importer for dependency in extension_importer.loaded_dependencies: if dependency in extensions_to_load: continue extensions_to_load.append(dependency) _extensions_needing_load.append(dependency) if extension_error_count > 0: Logger.log_formatted( LOG_LEVEL["warning"], f"Errors Preparing {extension_error_count} Configurations ", "Errors", 'error' ) Logger.log( "debug", f'Failed to prepare: {FONT_RED}{", ".join(extensions_with_errors)}{FONT_RESET}' ) if len(extensions_to_load): Logger.log_formatted( LOG_LEVEL["warning"], f"{len(extensions_to_load)} Configurations Ready to Load ", "Complete", 'success' ) Logger.log(LOG_LEVEL["debug"], f'{" Load Extensions ":_^{FONT_PADDING+8}}') Logger.log_formatted( LOG_LEVEL["warning"], f"Loading {len(extensions_to_load)} Configurations into Extensions ", "Pending", 'notice' ) Logger.log( LOG_LEVEL["debug"], f'Loading: {FONT_YELLOW}{", ".join(extensions_to_load)}{FONT_RESET}' ) # Import and setup the extensions self.load_extensions(extensions_to_load, config) # Cache important data like requirements installed self.mudpi.states.cache() return self.mudpi.extensions.all()