diff --git a/pyros.py b/pyros.py index bc9eb55..6388ad6 100755 --- a/pyros.py +++ b/pyros.py @@ -626,8 +626,7 @@ def install_or_update(UPDATE: bool = False, with_packages: bool = True, with_dat # self.addExecuted(self.current_command, command) from git import Repo else: - log.error( - "GitPython package (required for obsconfig class) installation failed") + log.error("GitPython package (required for obsconfig class) installation failed") ''' print("Guitastro : Generating (updating) API documentation (using Sphinx)") # Make html doc from RST diff --git a/src/core/pyros_django/agent/Agent.py b/src/core/pyros_django/agent/Agent.py index 5fa42e5..289aef4 100755 --- a/src/core/pyros_django/agent/Agent.py +++ b/src/core/pyros_django/agent/Agent.py @@ -40,8 +40,10 @@ and execute them on reception at each iteration : # For cmd parsing -from typing import List, Tuple +from array import array +from typing import List, Tuple, Union, Any import ast +from inspect import signature import os from pathlib import Path @@ -256,8 +258,11 @@ class StoppableThreadEvenWhenSleeping(threading.Thread): class AgentCmdException(Exception): ''' Base class for all Agent command exceptions ''' # pass - def __init__(self, cmd:AgentCmd): + def __init__( self, cmd: Union[AgentCmd,str] ): self.cmd = cmd + print(type(cmd)) + exit + self.cmd_name = cmd if isinstance(cmd,str) else cmd.name ''' def __str__(self): return f"The Agent command '{self.cmd.name}' is unknown to the agent" @@ -267,19 +272,19 @@ class AgentCmdException(Exception): class UnknownCmdException(AgentCmdException): ''' Raised when an Agent (specific) cmd is NOT known by the agent ''' def __str__(self): - return f"The Agent command '{self.cmd.name}' is unknown to the agent" + return f"The Agent command '{self.cmd_name}' is unknown to the agent" #return f"({type(self).__name__}): Device Generic command has no implementation in the controller" class AgentCmdUnimplementedException(AgentCmdException): ''' Raised when an Agent Specific cmd is known by the agent but not implemented ''' def __str__(self): - return f"The Agent command '{self.cmd.name}' is known by the agent but not implemented" + return f"The Agent command '{self.cmd_name}' is known by the agent but not implemented" #return f"({type(self).__name__}): Device Generic command has no implementation in the controller" class AgentCmdBadArgsException(AgentCmdException): ''' Raised when an Agent cmd has bad, missing, or too many argument(s) ''' def __str__(self): - return f"The Agent command '{self.cmd.name}' has bad, missing, or too many argument(s)" + return f"The Agent command '{self.cmd_name}' has bad, missing, or too many argument(s)" #return f"({type(self).__name__}): Device Generic command has no implementation in the controller" @@ -327,18 +332,19 @@ class Agent: # Default LOG level is INFO #PYROS_DEFAULT_GLOBAL_LOG_LEVEL = LogPyros.LOG_LEVEL_INFO # INFO - # This Agent Specific commands list, that he is able to execute - # To be overriden by subclasses (here are just a few examples of commands) - # Format : ("cmd",timeout) with : - # - cmd : the command name - # - timeout : the command timeout (in sec) - AGENT_SPECIFIC_COMMANDS = [ - #"do_specific1", - #"set_specific2", - #"do_specific3", - ("do_specific1", 10), - ("set_specific2", 5), - ("do_specific3", 3), + ''' + This Agent Specific commands list, that he is able to execute + To be overriden by subclasses (here are just a few examples of commands) + Format : ("cmd", args, timeout) with : + - cmd (str) : the command name + - timeout (int) : the command timeout (in sec) + - exec_mode (int) : 0 (sequential), 1 (thread), or 2 (process) + ''' + AGENT_SPECIFIC_COMMANDS: List[ Tuple[str, int, int] ] = [ + # Format : (“cmd_name”, timeout, exec_mode) + ("do_specific1", 10, 0), + #("set_specific2", 5, 0), + ("do_specific3", 3, 0), ] # @@ -384,7 +390,11 @@ class Agent: # - "self set_state ATTENTIVE" => means to send the command "set_state ATTENTIVE" to MYSELF # - "self do_restart_loop" => means to send the command "do_restart_loop" to MYSELF (no args) # - TEST_COMMANDS_LIST = [ + TEST_COMMANDS_LIST: List[ Tuple[ str, int, str, Union[int,None] ] ] = [ + + # Format : ("self cmd_name cmd_args", timeout, "expected_result", expected_status), + ("self do_specific1 1 2 3.5 titi (3,'titi',5) [1,3,5,7,9]", 200, '15.5', None), + # 1) First, 3 EXCEPTION CASES (uncomment to activate exception) # Each of these lines will stop execution with an exception @@ -416,15 +426,16 @@ class Agent: ("self get_mode", 100, "MODE = ATTENTIVE", None), # => should get "7" - ("self do_eval 3+5-1", 200, 7, None), + ("self do_eval 3+5-1", 200, '7', None), # END, will not go further #("self do_exit", 2, "STOPPING"), # Agent specific commands => should be executed - ("self do_specific3", 200, None, None), + ("self do_specific3", 200, '', None), ("self do_exit", 500, "STOPPING", None), - ("self do_specific1 1 2 3.5 titi (3,'titi',5) [1,3,5,7,9]", 200, 7, None), + ("self do_specific1 1 2 3.5 titi (3,'titi',5) [1,3,5,7,9]", 200, '15.5', None), + ("self set_mode ROUTINE", 200, "MODE = ROUTINE", None), # => should get "ROUTINE" @@ -1042,15 +1053,15 @@ class Agent: log.info("*"*10 + " NEXT COMMAND PROCESSING (START) " + "*"*10 + '\n') try : cmd = self._process_next_command_if_exists() - print("after get") print(cmd) except (AgentCmdUnimplementedException, AgentCmdBadArgsException, UnknownCmdException) as e : print(e) - log.error(f"EXCEPTION on Agent command '{e.cmd.name}'") - if type(e) is UnknownCmdException: - e.cmd.set_as_skipped("EXCEPTION: unknown command") - else : - e.cmd.set_as_processed("EXCEPTION: command known but unimplemented or bad args") + log.error(f"EXCEPTION on Agent command '{e.cmd_name}'") + if isinstance(e.cmd, AgentCmd) : + if type(e) is AgentCmdUnimplementedException: + e.cmd.set_as_unimplemented("EXCEPTION: command known but unimplemented") + else: + e.cmd.set_as_invalid("EXCEPTION: command unknown or bad args") self._cleanup_before_exit() raise log.info("*"*10 + " NEXT COMMAND PROCESSING (END) " + "*"*10 + "\n") @@ -1421,6 +1432,22 @@ class Agent: def show_mode_and_status(self): log.info(f"CURRENT MODE is {self.mode} (status is {self.status})") + def get_specific_cmds(self)->str: + specific_commands = "" + for command_tuple in self.AGENT_SPECIFIC_COMMANDS: + cmd_name = command_tuple[0] + specific_commands += cmd_name + # Exception if exists an unimplemented command + try: + f = getattr(self, cmd_name) + except AttributeError: + raise AgentCmdUnimplementedException(cmd_name) from None + args = signature(f) + specific_commands += str(args) + specific_commands += ";" + if specific_commands: specific_commands = specific_commands[0:-1] + return specific_commands + ''' def get_specifics_cmd(self): specific_commands = "" for index, command_tuple in enumerate(self.AGENT_SPECIFIC_COMMANDS): @@ -1428,6 +1455,7 @@ class Agent: if index != len(self.AGENT_SPECIFIC_COMMANDS)-1: specific_commands += ";" return specific_commands + ''' def get_mode(self): return self.mode @@ -1982,7 +2010,7 @@ class Agent: # Execute method self."cmd.name"() # This can raise an exception (caught by this method caller) try: - res = self.exec_cmd_from_its_name(cmd) + res = self._exec_cmd_from_its_name(cmd) except (AgentCmdUnimplementedException, AgentCmdBadArgsException) as e: # These exceptions are managed at higher level : raise @@ -2013,11 +2041,25 @@ class Agent: self.printd("Logging data...") ''' - def exec_cmd_from_its_name(self, cmd:AgentCmd): + def _exec_cmd_from_its_name(self, cmd:AgentCmd)->Any: + #print(dir(self)) + ''' + for method in dir(self): + if callable(getattr(self, method)): + m = getattr(self, method) + print(method, ' => ', signature(m)) + ''' + #print("***************") + #print(self.get_specific_cmds()) + #print("***************") + methods_list = [method for method in dir(self) if callable(getattr(self, method))] #print(methodsList) func = cmd.name if func not in methods_list: raise AgentCmdUnimplementedException(cmd) + f = getattr(self, func) + ###print(func, ' => ', signature(f)) + #for arg in cmd.args: print(arg) #print(cmd.args) #print(*cmd.args) @@ -2046,9 +2088,17 @@ class Agent: for arg in cmd.args: #print(arg) #try: - # Evaluate arg only if it is not a word (letters) + # Evaluate arg only if it is not a word (letters) : + # - a word like "toto" is not evaluated => stays as is (=str) + # Are evaluated : + # - an int or float + # - a (tuple) + # - a [list] + # - ... + #print('********', arg, " :") if not arg[0].isalpha(): arg = ast.literal_eval(arg) + #print("evaluated to", type(arg), arg) #except ValueError as e: newarg = arg args.append(arg) try: @@ -2063,6 +2113,7 @@ class Agent: ##raise AgentCmdUnimplementedException(cmd).with_traceback(tb) ##raise AgentCmdUnimplementedException(cmd).with_traceback(None) + def _is_agent_level_cmd(self, cmd:AgentCmd): return self._is_agent_general_cmd(cmd) or self._is_agent_specific_cmd(cmd) @@ -2092,7 +2143,7 @@ class Agent: def _is_agent_specific_cmd(self, cmd:AgentCmd): #return cmd.name in self.AGENT_SPECIFIC_COMMANDS #return (cmd.name,) in self.AGENT_SPECIFIC_COMMANDS - for (cmd_name,timeout) in self.AGENT_SPECIFIC_COMMANDS: + for (cmd_name,_,_) in self.AGENT_SPECIFIC_COMMANDS: if cmd.name == cmd_name : return True ''' @@ -2118,7 +2169,7 @@ class Agent: def do_flush_commands(self): AgentCmd.delete_pending_commands_for_agent(self.name) - def do_exec_commands(self, what:str): + def do_exec_command(self, what:str): # - Temporaly stop execution of new commands (let them accumulate) if what == "stop": pass # - Resume execution of commands (accumulated since "do_stop_exec_cmd") @@ -2183,6 +2234,7 @@ class Agent: #def do_specific1(self, arg1:int, arg2:int=2, arg3:int=3) -> int: #def do_specific1(self, arg1:int, arg2:int=2, arg3:float=3.1, arg4:list=[1,2,3]) -> float: + def do_specific1(self, arg1:int, arg2:int, @@ -2200,11 +2252,12 @@ class Agent: print(arg5[1]) arg6 = ast.literal_eval(arg6) ''' - print(arg4) + #print(arg4) res = arg1 + arg2 + arg3 + arg5[0] + arg5[2] if arg6: res += arg6[0] return res + # Undefined specific cmd #def set_specific2(self, arg1:str, arg2:int): pass def do_specific3(self): @@ -2303,7 +2356,7 @@ class Agent: log.debug(cmd.result) log.debug(self._cmdts.expected_res) if cmd.is_executed() and self._cmdts.expected_res: - assert(cmd.result == self._cmdts.expected_res) + assert(str(cmd.result) == self._cmdts.expected_res) log.debug(cmd.state) log.debug(self._cmdts.expected_status) @@ -2374,7 +2427,6 @@ class Agent: # Execution was completeted => get result elif self._cmdts.is_executed(): cmdts_res = self._cmdts.get_result() - print("toto") log.info(f"Cmd executed. Result is '{cmdts_res}'") #cmdts_is_processed = True ''' Optimisation possible pour gagner une iteration: diff --git a/src/core/pyros_django/common/models.py b/src/core/pyros_django/common/models.py index 4409731..f944830 100644 --- a/src/core/pyros_django/common/models.py +++ b/src/core/pyros_django/common/models.py @@ -1,5 +1,8 @@ ##from __future__ import unicode_literals +# (EP 21/9/22) To allow autoreferencing (ex: AgentCmd.create() returns a AgentCmd) +from __future__ import annotations + # Stdlib imports from numpy import False_ from src.device_controller.abstract_component.device_controller import DeviceCmd @@ -626,10 +629,9 @@ class AgentCmd(models.Model): # -------------- Command CLASS (static) METHODS -------------- # Avoid to override the Model __init__ method + # See https://docs.djangoproject.com/en/stable/ref/models/instances/#creating-objects @classmethod - # TODO: return type AgentCmd - #def create(cls, agent_from:str, agent_to:str, cmd_name:str, cmd_args:str=None, cmd_validity:int=None) -> AgentCmd : - def create(cls, agent_from:str, agent_to:str, cmd_name:str, cmd_args:str=None, cmd_validity:int=None) : + def create(cls, agent_from:str, agent_to:str, cmd_name:str, cmd_args:str=None, cmd_validity:int=None) -> AgentCmd : #print(agent_to) if '.' in agent_to: agent_to, component_name = agent_to.split('.') @@ -654,9 +656,8 @@ class AgentCmd(models.Model): """ @classmethod - # TODO: return type AgentCmd - #def send_cmd_from_to(cls, agent_from, agent_to, cmd_name, cmd_args=None, cmd_validity:int=None)->AgentCmd: - def send_cmd_from_to(cls, agent_from, agent_to, cmd_name, cmd_args=None, cmd_validity:int=None): + def send_cmd_from_to(cls, agent_from, agent_to, cmd_name, cmd_args=None, cmd_validity:int=None)->AgentCmd: + """ Create and send a command, then return it """ # def send(cls, from_agent, to_agent, cmd_name, cmd_args=None): """ ex: send("AgentA",“AgentB”,"EVAL”,“3+4”) @@ -968,7 +969,47 @@ class AgentCmd(models.Model): # Optimization: update only 1 field if do_save: self.save(update_fields=["r_read_time"]) + # + # Set cmd status + # + + def set_as_pending(self): + self.set_state_to(self.CMD_STATUS_CODES.CMD_PENDING) + + # from PENDING + def set_as_unimplemented(self, result:str=None): + assert self.is_pending() + self.set_state_to(self.CMD_STATUS_CODES.CMD_UNIMPLEMENTED) + + # from PENDING + def set_as_invalid(self, result:str=None): + assert self.is_pending() + self.set_state_to(self.CMD_STATUS_CODES.CMD_INVALID) + + # from PENDING + def set_as_skipped(self, result:str=None): + assert self.is_pending() + self.set_state_to(self.CMD_STATUS_CODES.CMD_SKIPPED, result=result) + + # from PENDING + def set_as_expired(self): + assert self.is_pending() + print(f"- Set this command as expired (older than its validity duration of {self.validity_duration}s): {self}") + self.set_state_to(self.CMD_STATUS_CODES.CMD_EXPIRED) + + # from PENDING + def set_as_running(self): + assert self.is_pending() + printd(f"- Set command {self.name} as running") + self.set_state_to(self.CMD_STATUS_CODES.CMD_RUNNING) + + ''' + def set_as_executed(self): + self.set_state_to(self.CMD_STATUS_CODES.CMD_EXECUTED) + ''' + # from RUNNING def set_as_processed(self, result:str=None): + assert self.is_running() printd(f"- Set command {self.name} as processed") self.set_state_to(self.CMD_STATUS_CODES.CMD_EXECUTED, result=result) # print(self) @@ -979,32 +1020,30 @@ class AgentCmd(models.Model): """ # Optimization: update the related fields, but does not work, why ? ##self.save(update_fields=["state", "r_processed_time"]) - #def set_as_outofdate(self): - def set_as_expired(self): - print( - f"- Set this command as expired (older than its validity duration of {self.validity_duration}s): {self}") - self.set_state_to(self.CMD_STATUS_CODES.CMD_EXPIRED) - def set_as_pending(self): - self.set_state_to(self.CMD_STATUS_CODES.CMD_PENDING) + # from RUNNING + def set_as_exec_error(self, result:str=None): + assert self.is_running() + self.set_state_to(self.CMD_STATUS_CODES.CMD_EXEC_ERROR, result=result) - def set_as_skipped(self, result:str=None): - self.set_state_to(self.CMD_STATUS_CODES.CMD_SKIPPED, result=result) + # from RUNNING + def set_as_exec_timeout(self, result:str=None): + assert self.is_running() + self.set_state_to(self.CMD_STATUS_CODES.CMD_EXEC_TIMEOUT, result=result) + # from RUNNING + def set_as_exec_timeout(self, result:str=None): + assert self.is_running() + self.set_state_to(self.CMD_STATUS_CODES.CMD_EXEC_TIMEOUT, result=result) + + # from RUNNING def set_as_killed_by(self, author_agent: str): + assert self.is_running() printd(f"- Set command {self.name} as killed") #print(f"- Set this command as killed: {self}") self.set_state_to(self.CMD_STATUS_CODES.CMD_EXEC_KILLED, author_agent) - def set_as_running(self): - printd(f"- Set command {self.name} as running") - self.set_state_to(self.CMD_STATUS_CODES.CMD_RUNNING) - ''' - def set_as_executed(self): - self.set_state_to(self.CMD_STATUS_CODES.CMD_EXECUTED) - ''' - def set_state_to(self, status: str, author_agent_name: str = None, result:str=None): ''' If comment is present it will be set in the "result" field -- libgit2 0.21.2