Commit 84131ac0974c507b5bba46dfa0891e1a2722a249

Authored by Alexis Koralewski
2 parents c9a2c2f9 fbfe7abb
Exists in dev

Merge branch 'dev' of https://gitlab.irap.omp.eu/epallier/pyros into dev

.gitignore
... ... @@ -41,6 +41,7 @@ old/
41 41  
42 42 out.*
43 43  
  44 +OLD/*
44 45 *_OLD*
45 46 *_ORIG*
46 47  
... ...
CHANGELOG 0 → 100644
... ... @@ -0,0 +1,4 @@
  1 +
  2 +8-12-2021 :
  3 + - new PYROS_.py script, wrapper on pyros.py
  4 + - added this CHANGELOG file
... ...
PYROS deleted
... ... @@ -1,39 +0,0 @@
1   -#!/usr/bin/env bash
2   -
3   -# test if user passed a command as parameter
4   -if test $# -lt 1
5   -then
6   - echo "Missing command, use one of the commands below"
7   - python3 pyros.py --help
8   - exit 1
9   -fi
10   -
11   -# test if docker is installed
12   -if [ -x "$(command -v docker)" ];
13   -then
14   - docker=true
15   -else
16   - docker=false
17   -fi
18   -
19   -if $docker
20   -then
21   - # test if container is running
22   - if ! [ $(docker ps | grep 'pyros' | wc -l) -eq 2 ];
23   - then
24   - container=true;
25   - else
26   - container=false;
27   - fi
28   - if $container;
29   - then
30   - docker exec -it pyros python3 pyros.py $1
31   - else
32   - # starting container first and then launch pyros.py
33   - cd docker; docker-compose up -d
34   - docker exec -it pyros python3 pyros.py $1
35   - fi
36   -else
37   - python3 pyros.py $1
38   -fi
39   -
PYROSW.py 0 → 100755
... ... @@ -0,0 +1,198 @@
  1 +#!/usr/bin/env python3
  2 +
  3 +'''
  4 +Shell scripting with python :
  5 +- https://www.freecodecamp.org/news/python-for-system-administration-tutorial
  6 +- https://github.com/ninjaaron/replacing-bash-scripting-with-python#if-the-shell-is-so-great-what-s-the-problem
  7 +- https://linuxize.com/post/python-get-change-current-working-directory
  8 +'''
  9 +
  10 +DEBUG = False
  11 +#DEBUG = True
  12 +#print(DEBUG)
  13 +
  14 +
  15 +import sys
  16 +import os
  17 +from shutil import which
  18 +import subprocess
  19 +
  20 +
  21 +
  22 +def run(command: str) :
  23 + #print(command.split(' '))
  24 + return subprocess.run(command.split(' '))
  25 + #result = subprocess.run(command.split(' '), stdout=subprocess.PIPE, stderr=subprocess.PIPE)
  26 +
  27 +
  28 +# Guess Context (3 possibilities) :
  29 +# - NO DOCKER,
  30 +# or
  31 +# - DOCKER OUT of container,
  32 +# or
  33 +# - or DOCKER IN container
  34 +
  35 +WITH_DOCKER_IS_SET = False if os.getenv('WITH_DOCKER') is None else True
  36 +#print(WITH_DOCKER_IS_SET)
  37 +
  38 +
  39 +#APP_FOLDER=False
  40 +#[ -d ../app/ ] && APP_FOLDER=true
  41 +APP_FOLDER = os.path.exists('../app/')
  42 +
  43 +VENV = os.path.exists('./venv/')
  44 +
  45 +# test if docker is installed
  46 +#[ -x "$(command -v docker)" ] && DOCKER_CMD=true
  47 +DOCKER_CMD = which('docker') is not None
  48 +
  49 +
  50 +# Pas utile vu qu'on va redémarrer systématiquement le container
  51 +# test if container is running
  52 +##DOCKER_CONTAINER_STARTED=false
  53 +##$DOCKER_CMD && [ $(docker ps | grep 'pyros' | wc -l) -eq 2 ] && DOCKER_CONTAINER_STARTED=true
  54 +
  55 +#
  56 +# SYNTHESIS
  57 +#
  58 +
  59 +#DOCKER=false
  60 +#[[ $VENV == false && $DOCKER_CMD == true ]] && DOCKER=true
  61 +DOCKER = DOCKER_CMD and not VENV
  62 +
  63 +DOCKER_OUT_CONTAINER = DOCKER and not WITH_DOCKER_IS_SET
  64 +#[[ $DOCKER == false && $WITH_DOCKER_IS_SET == true ]] && DOCKER_IN_CONTAINER=true
  65 +
  66 +if DEBUG :
  67 + print(APP_FOLDER)
  68 + print(VENV)
  69 + print(DOCKER_CMD)
  70 + #print(container)
  71 + # Synthesis
  72 + print(DOCKER)
  73 + print(DOCKER_OUT_CONTAINER)
  74 +
  75 + print(sys.argv)
  76 + exit(0)
  77 +
  78 +# no container ? => start container first
  79 +#[ $container == false ] && cd docker/ && docker-compose up -d
  80 +
  81 +args = sys.argv[1:]
  82 +args = ' '.join(args)
  83 +#print(args)
  84 +
  85 +#PYROS_CMD = "python3 pyros.py --help"
  86 +#PYROS_CMD = "python3 pyros.py $*"
  87 +PYROS_CMD = "python3 pyros.py " + args
  88 +PYROS_CMD = PYROS_CMD.rstrip()
  89 +
  90 +# DOCKER_OUT_CONTAINER true ? => docker exec
  91 +#docker exec -it pyros python3 pyros.py $*
  92 +#[ $DOCKER_OUT_CONTAINER == true ] && cd docker/ && docker-compose up -d && PREFIX='docker exec -it pyros'
  93 +if DOCKER_OUT_CONTAINER :
  94 + #cd docker/
  95 + os.chdir('docker')
  96 + #docker-compose up -d
  97 + run('docker-compose up -d')
  98 + PYROS_CMD = 'docker exec -it pyros ' + PYROS_CMD
  99 +
  100 +#PYROS_CMD
  101 +print("\n Executing command :", PYROS_CMD, "\n")
  102 +res = run(PYROS_CMD)
  103 +
  104 +'''
  105 +#Print the stdout and stderr
  106 +print()
  107 +print(result.args)
  108 +print()
  109 +print(result.stdout)
  110 +print()
  111 +print(result.stderr)
  112 +'''
  113 +
  114 +
  115 +
  116 +
  117 +
  118 +
  119 +
  120 +'''
  121 +##########################
  122 +# Script BASH equivalent #
  123 +##########################
  124 +
  125 +#!/usr/bin/env bash
  126 +
  127 +DEBUG=true
  128 +DEBUG=false
  129 +
  130 +# test if user passed a command as parameter
  131 +#if test $# -lt 1 ; then
  132 + #echo "Missing command, use one of the commands below"
  133 + #python3 pyros.py --help
  134 + #exit
  135 +#fi
  136 +
  137 +# Guess Context :
  138 +# - NO DOCKER,
  139 +# or
  140 +# - DOCKER OUT of container,
  141 +# or
  142 +# - or DOCKER IN container
  143 +
  144 +WITH_DOCKER_IS_SET=true
  145 +[ -z $WITH_DOCKER ] && WITH_DOCKER_IS_SET=false
  146 +
  147 +APP_FOLDER=false
  148 +[ -d ../app/ ] && APP_FOLDER=true
  149 +
  150 +VENV=false
  151 +[ -d ./venv/ ] && VENV=true
  152 +
  153 +# test if docker is installed
  154 +DOCKER_CMD=false
  155 +[ -x "$(command -v docker)" ] && DOCKER_CMD=true
  156 +
  157 +
  158 +# test if container is running
  159 +DOCKER_CONTAINER_STARTED=false
  160 +$DOCKER_CMD && [ $(docker ps | grep 'pyros' | wc -l) -eq 2 ] && DOCKER_CONTAINER_STARTED=true
  161 +
  162 +#
  163 +# SYNTHESIS
  164 +#
  165 +
  166 +DOCKER=false
  167 +[[ $VENV == false && $DOCKER_CMD == true ]] && DOCKER=true
  168 +
  169 +DOCKER_OUT_CONTAINER=false
  170 +[[ $DOCKER == true && $WITH_DOCKER_IS_SET == false ]] && DOCKER_OUT_CONTAINER=true
  171 +#[[ $DOCKER == false && $WITH_DOCKER_IS_SET == true ]] && DOCKER_IN_CONTAINER=true
  172 +
  173 +if $DEBUG ; then
  174 + echo $APP_FOLDER
  175 + echo $VENV
  176 + echo $DOCKER_CMD
  177 + echo $container
  178 +
  179 + # Synthesis
  180 + echo $DOCKER
  181 + echo $DOCKER_OUT_CONTAINER
  182 +
  183 + exit
  184 +fi
  185 +
  186 +
  187 +# no container ? => start container first
  188 +#[ $container == false ] && cd docker/ && docker-compose up -d
  189 +
  190 +# DOCKER_OUT_CONTAINER true ? docker exec
  191 +PREFIX=''
  192 +#docker exec -it pyros python3 pyros.py $*
  193 +[ $DOCKER_OUT_CONTAINER == true ] && cd docker/ && docker-compose up -d && PREFIX='docker exec -it pyros'
  194 +
  195 +echo $PREFIX
  196 +$PREFIX python3 pyros.py $*
  197 +
  198 +'''
... ...
pyros.py
... ... @@ -34,9 +34,11 @@ _previous_dir = None
34 34 AGENTS = {
35 35 #"agentX" : "agent",
36 36 "agent" : "Agent",
  37 + "agent2" : "Agent",
37 38 "agentX" : "AgentX",
38 39 "agentA" : "AgentA",
39 40 "agentB" : "AgentB",
  41 + "agentC" : "AgentC",
40 42 "agentM" : "AgentM",
41 43 "agentSP" : "AgentSP",
42 44 #"agentDevice" : "AgentDevice",
... ... @@ -289,7 +291,7 @@ def printFullTerm(color: Colors, string: str):
289 291 printColor(color, "-" * (columns - value))
290 292 return 0
291 293  
292   -def set_environment_variables_if_not_configured(env_path: str,env_sample_path: str)->None:
  294 +def set_environment_variables_if_not_configured(env_path:str, env_sample_path:str)->None:
293 295 """
294 296 Set environment variables if they aren't defined in the current environment.
295 297 Get the variables from .env file if it exists or create this file using a copy of the env_sample
... ... @@ -633,7 +635,7 @@ def initdb():
633 635 #@click.option('--format', '-f', type=click.Choice(['html', 'xml', 'text']), default='html', show_default=True)
634 636 #@click.option('--port', default=8000)
635 637 #def start(agent:str, configfile:str, test, verbosity):
636   -def start(agent:str, configfile:str,observatory:str,unit:str):
  638 +def start(agent:str, configfile:str, observatory:str, unit:str):
637 639 printd("Running start command")
638 640 if configfile:
639 641 printd("With config file", configfile)
... ... @@ -660,10 +662,14 @@ def start(agent:str, configfile:str,observatory:str,unit:str):
660 662 obs_config_file_path = os.path.join(path_to_obs_config_folder,obs_config_file_name)
661 663 os.environ["PATH_TO_OBSCONF_FILE"] = obs_config_file_path
662 664 os.environ["PATH_TO_OBSCONF_FOLDER"] = path_to_obs_config_folder
  665 + os.environ["unit_name"] = unit if unit else ''
  666 + '''
663 667 if unit:
664 668 os.environ["unit_name"] = unit
665 669 else:
666 670 os.environ["unit_name"] = ""
  671 + '''
  672 +
667 673 # add path to pyros_django folder as the config class is supposed to work within this folder
668 674 #cmd_test_obs_config = f"-c \"from src.core.pyros_django.obsconfig.configpyros import ConfigPyros\nConfigPyros('{os.path.join(PYROS_DJANGO_BASE_DIR,os.environ.get('PATH_TO_OBSCONF_FILE'))}')\""
669 675 cmd_test_obs_config = f"-c \"from src.core.pyros_django.obsconfig.configpyros import ConfigPyros\nConfigPyros('{obs_config_file_path}')\""
... ... @@ -703,7 +709,6 @@ def start(agent:str, configfile:str,observatory:str,unit:str):
703 709 printd("Launching agent", agent_name, "...")
704 710 #if not test_mode(): execProcess(VENV_PYTHON + " manage.py runserver")
705 711 #if not test_mode(): execProcessFromVenv("start_agent_" + agent_name + ".py " + configfile)
706   -
707 712 current_dir = os.getcwd()
708 713  
709 714 # OLD format agents: majordome, monitoring, alert...
... ...
src/core/pyros_django/agent/Agent2.py 0 → 100755
... ... @@ -0,0 +1,1717 @@
  1 +#!/usr/bin/env python3
  2 +
  3 +VERSION = "0.5"
  4 +
  5 +#DEBUG=True
  6 +#DEBUG=False
  7 +
  8 +"""TODO:
  9 +
  10 +- 1 log par agent + lastlog (30 dernières lignes)
  11 +- table agents_log avec log minimum pour affichage dans dashboard, et ordre chrono intéressant pour suivi activité : nom agent, timestamp, message
  12 +- 1 table par agent: agent_<agent-name>_vars : nom variable, value, desc
  13 +
  14 +"""
  15 +
  16 +
  17 +"""
  18 +=================================================================
  19 + SETUP FOR DJANGO
  20 +
  21 + (see https://docs.djangoproject.com/en/dev/topics/settings)
  22 + (see also https://docs.djangoproject.com/en/dev/ref/settings)
  23 +=================================================================
  24 +"""
  25 +
  26 +import os
  27 +from pathlib import Path
  28 +import sys
  29 +
  30 +from django.conf import settings as djangosettings
  31 +
  32 +# Conseil sur le net:
  33 +#https://stackoverflow.com/questions/16853649/how-to-execute-a-python-script-from-the-django-shell
  34 +#""
  35 +#import sys, os
  36 +#sys.path.append('/path/to/your/django/app')
  37 +#os.environ['DJANGO_SETTINGS_MODULE'] = 'settings'
  38 +#from django.conf import settings
  39 +#""
  40 +
  41 +# To avoid a "ModuleNotFoundError: No module named 'dashboard'"... (not even 1 app found) :
  42 +##sys.path.insert(0, os.path.abspath(".."))
  43 +##sys.path.insert(0, os.path.abspath("src"))
  44 +##sys.path.insert(0, "../src")
  45 +##sys.path.insert(0, "src")
  46 +# To avoid a "ModuleNotFoundError: No module named 'dashboard'"
  47 +## sys.path.append("..")
  48 +py_pwd = os.path.normpath(os.getcwd() + "/..")
  49 +if (py_pwd not in os.sys.path):
  50 + (os.sys.path).append(py_pwd)
  51 +# To avoid a "ModuleNotFoundError: No module named 'src'"
  52 +## sys.path.append("../../../..")
  53 +py_pwd = os.path.normpath(os.getcwd() + "/../../../..")
  54 +if (py_pwd not in os.sys.path):
  55 + (os.sys.path).append(py_pwd)
  56 +##sys.path.append("src")
  57 +
  58 +
  59 +def printd(*args, **kwargs):
  60 + if os.environ.get('PYROS_DEBUG', '0')=='1': print(*args, **kwargs)
  61 +
  62 +
  63 +printd("Starting with this sys.path", sys.path)
  64 +
  65 +# DJANGO setup
  66 +# self.printd("file is", __file__)
  67 +# mypath = os.getcwd()
  68 +# Go into src/
  69 +##os.chdir("..")
  70 +##os.chdir("src")
  71 +printd("Current directory : " + str(os.getcwd()))
  72 +
  73 +#os.environ.setdefault("DJANGO_SETTINGS_MODULE", "src.core.pyros_django.pyros.settings")
  74 +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "src.core.pyros_django.pyros.settings")
  75 +# os.environ['SECRET_KEY'] = 'abc'
  76 +# os.environ['ENVIRONMENT'] = 'production'
  77 +import django
  78 +
  79 +django.setup()
  80 +
  81 +printd("DB2 used is:", djangosettings.DATABASES["default"]["NAME"])
  82 +printd()
  83 +
  84 +
  85 +"""
  86 +=================================================================
  87 + IMPORT PYTHON PACKAGES
  88 +=================================================================
  89 +"""
  90 +
  91 +# --- GENERAL PURPOSE IMPORT ---
  92 +#from __future__ import absolute_import
  93 +##import utils.Logger as L
  94 +import platform
  95 +import random
  96 +import threading
  97 +#import multiprocessing
  98 +from threading import Thread
  99 +import time
  100 +import socket
  101 +#import ctypes
  102 +#import copy
  103 +
  104 +# --- DJANGO IMPORT ---
  105 +from django.db import transaction
  106 +from django import db
  107 +# from django.core.exceptions import ObjectDoesNotExist
  108 +# from django.db.models import Q
  109 +#from django.shortcuts import get_object_or_404
  110 +#from django.conf import settings as djangosettings
  111 +
  112 +# --- SPECIFIC IMPORT ---
  113 +# Many ways to import configuration settings:
  114 +##import config
  115 +import config.old_config as config_old
  116 +#from config import PYROS_ENV, ROOT_DIR, DOC_DIR
  117 +#from config import *
  118 +
  119 +from common.models import AgentSurvey, AgentCmd, AgentLogs
  120 +#from config.configpyros import ConfigPyros
  121 +from config.old_config.configpyros import ConfigPyros
  122 +#from dashboard.views import get_sunelev
  123 +#from devices.TelescopeRemoteControlDefault import TelescopeRemoteControlDefault
  124 +#from utils.JDManipulator import *
  125 +
  126 +##from agent.logpyros import LogPyros
  127 +from src.logpyros import LogPyros
  128 +
  129 +from device_controller.abstract_component.device_controller import (
  130 + DCCNotFoundException, UnknownGenericCmdException, UnimplementedGenericCmdException, UnknownNativeCmdException
  131 +)
  132 +
  133 +
  134 +"""
  135 +=================================================================
  136 + GENERAL MODULE CONSTANT & FUNCTIONS DEFINITIONS
  137 +=================================================================
  138 +"""
  139 +
  140 +#DEBUG_FILE = False
  141 +
  142 +##log = L.setupLogger("AgentLogger", "Agent")
  143 +
  144 +IS_WINDOWS = platform.system() == "Windows"
  145 +
  146 +
  147 +class Colors:
  148 + HEADER = "\033[95m"
  149 + BLUE = "\033[94m"
  150 + GREEN = "\033[92m"
  151 + WARNING = "\033[93m"
  152 + FAIL = "\033[91m"
  153 + ENDC = "\033[0m"
  154 + BOLD = "\033[1m"
  155 + UNDERLINE = "\033[4m"
  156 +
  157 +def printColor(color: Colors, message, file=sys.stdout, eol=os.linesep, forced=False):
  158 + #system = platform.system()
  159 + """
  160 + if (self.disp == False and forced == False):
  161 + return 0
  162 + """
  163 + #if system == "Windows":
  164 + if IS_WINDOWS:
  165 + print(message, file=file, end=eol)
  166 + else:
  167 + print(color + message + Colors.ENDC, file=file, end=eol)
  168 + return 0
  169 +
  170 +def printFullTerm(color: Colors, string: str):
  171 + #system = platform.system()
  172 + columns = 100
  173 + row = 1000
  174 + disp = True
  175 + value = int(columns / 2 - len(string) / 2)
  176 + printColor(color, "-" * value, eol="")
  177 + printColor(color, string, eol="")
  178 + value += len(string)
  179 + printColor(color, "-" * (columns - value))
  180 + return 0
  181 +
  182 +
  183 +"""
  184 +=================================================================
  185 + class StoppableThread
  186 +=================================================================
  187 +"""
  188 +
  189 +class StoppableThreadEvenWhenSleeping(threading.Thread):
  190 + # Thread class with a stop() method. The thread itself has to check
  191 + # regularly for the stopped() condition.
  192 + # It stops even if sleeping
  193 + # See https://python.developpez.com/faq/?page=Thread#ThreadKill
  194 + # See also https://www.oreilly.com/library/view/python-cookbook/0596001673/ch06s03.html
  195 +
  196 + def __init__(self, *args, **kwargs):
  197 + #super(StoppableThreadSimple, self).__init__(*args, **kwargs)
  198 + super().__init__(*args, **kwargs)
  199 + self._stop_event = threading.Event()
  200 +
  201 + #def stop(self):
  202 + def terminate(self):
  203 + self._stop_event.set()
  204 +
  205 + def stopped(self):
  206 + return self._stop_event.is_set()
  207 +
  208 + def wait(self, nbsec:float=2.0):
  209 + self._stop_event.wait(nbsec)
  210 +
  211 +
  212 +
  213 +"""
  214 +=================================================================
  215 + class Agent
  216 +=================================================================
  217 +"""
  218 +
  219 +class Agent2:
  220 + """
  221 + See Agent_activity_diag.pu for PlantUML activity diagram
  222 +
  223 + Behavior of an agent:
  224 + - If idle :
  225 + - still does routine_process() and general_process()
  226 + - does not do specific_process()
  227 + - Once a command has been sent to another agent :
  228 + - It waits (non blocking) for the end of execution of the command and get its result
  229 + - If command is timed out or has been skipped or killed, then it is NOT re-executed at next iteration (except if needed explicitely)
  230 + """
  231 +
  232 + # ---
  233 + # --- CLASS (STATIC) attributes (CONSTANTS)
  234 + # --- If agent is instance of Agent:
  235 + # --- - CLASS attributes are accessible via agent.__class__.__dict__
  236 + # --- - INSTANCE attributes are accessible via agent.__dict__
  237 + # ---
  238 +
  239 + # Default modes
  240 + DEBUG_MODE = False
  241 + WITH_SIMULATOR = False
  242 + TEST_MODE = False
  243 +
  244 + # Default LOG level is INFO
  245 + #PYROS_DEFAULT_GLOBAL_LOG_LEVEL = LogPyros.LOG_LEVEL_INFO # INFO
  246 +
  247 + # To be overriden by subclasses (empty by default, no agent specific command)
  248 + AGENT_SPECIFIC_COMMANDS = [
  249 + #"do_eval",
  250 + #"set_state",
  251 + ]
  252 +
  253 + # Maximum duration of this agent (only for SIMULATION mode)
  254 + # If set to None, it will never exit except if asked (or CTRL-C)
  255 + # If set to 20, it will exit after 20s
  256 + TEST_MAX_DURATION_SEC = None
  257 + #TEST_MAX_DURATION_SEC = 30
  258 +
  259 + # FOR TEST ONLY
  260 + # Run this agent in simulator mode
  261 + TEST_MODE = True
  262 + WITH_SIMULATOR = False
  263 + # Run the assertion tests at the end
  264 + TEST_WITH_FINAL_TEST = False
  265 + # Who should I send commands to ?
  266 + TEST_COMMANDS_DEST = "myself"
  267 + # Default scenario to be executed
  268 + #TEST_COMMANDS = iter([
  269 + TEST_COMMANDS_LIST = [
  270 + "set_state:active",
  271 + "set_state:idle",
  272 +
  273 + # specific0 not_executed_because_idle
  274 + "specific0",
  275 +
  276 + "set_state:active",
  277 +
  278 + # specific1 executed_because_not_idle, should complete ok
  279 + "specific1",
  280 +
  281 + # specific2 will be executed only when specific1 is finished,
  282 + # and should be aborted before end of execution,
  283 + # because of the 1st coming "do_abort" command below
  284 + "specific2",
  285 +
  286 + # specific3 should be executed only when specific2 is finished (in fact, aborted),
  287 + # and should be aborted before end of execution,
  288 + # because of the 2nd coming "do_abort" command below
  289 + "specific3",
  290 +
  291 + # These commands should not have the time to be processed
  292 + # because the "do_exit" command below should be executed before
  293 + "specific4",
  294 + "specific5",
  295 + "specific6",
  296 + "specific7",
  297 + "specific8",
  298 +
  299 + # Should abort the current running command (which should normally be specific2)
  300 + # even if commands above (specific3, ..., specific8) are already pending
  301 + "do_abort",
  302 +
  303 + # These commands (except abort) won't be executed
  304 + # because too many commands are already pending (above)
  305 + "specific9",
  306 + "do_abort",
  307 + "set_state:active",
  308 + "set_state:idle",
  309 +
  310 + # Should stop the agent even before the previous pending commands are executed
  311 + "do_exit",
  312 +
  313 + # Because of the previous "do_exit" command,
  314 + # these following commands should not be executed,
  315 + # and not even be added to the database command table
  316 + "set_state:active",
  317 + "specific10"
  318 + ]
  319 + #TEST_COMMANDS = iter(TEST_COMMANDS_LIST)
  320 +
  321 +
  322 +
  323 + """
  324 + How to run this agent exec_specific_cmd() method ?
  325 + - True = inside a Thread (cannot be killed, must be asked to stop, and inadequate for computation)
  326 + - False = inside a Process
  327 + If thread, displays :
  328 + >>>>> Thread: starting execution of command specific1
  329 + >>>>> Thread: PID: 2695, Process Name: MainProcess, Thread Name: Thread-1
  330 + ...
  331 + >>>>> Thread: starting execution of command specific2
  332 + >>>>> Thread: PID: 2695, Process Name: MainProcess, Thread Name: Thread-2
  333 + ...
  334 + >>>>> Thread: starting execution of command specific3
  335 + >>>>> Thread: PID: 2695, Process Name: MainProcess, Thread Name: Thread-3
  336 + If process, displays :
  337 + >>>>> Thread: starting execution of command specific1
  338 + >>>>> Thread: PID: 2687, Process Name: Process-1, Thread Name: MainThread
  339 + ...
  340 + >>>>> Thread: starting execution of command specific2
  341 + >>>>> Thread: PID: 2689, Process Name: Process-2, Thread Name: MainThread
  342 + ...
  343 + >>>>> Thread: starting execution of command specific3
  344 + >>>>> Thread: PID: 2690, Process Name: Process-3, Thread Name: MainThread
  345 + """
  346 + # with thread
  347 + RUN_IN_THREAD = True
  348 + # with process
  349 + #RUN_IN_THREAD = False
  350 +
  351 + _thread_total_steps_number = 1
  352 +
  353 + #COMMANDS_PEREMPTION_HOURS = 48
  354 + #COMMANDS_PEREMPTION_HOURS = 60/60
  355 +
  356 + name = "Generic Agent"
  357 + mainloop_waittime = 3
  358 + subloop_waittime = 2
  359 + status = None
  360 + mode = None
  361 + config = None
  362 +
  363 + # Statuses
  364 + STATUS_LAUNCH = "LAUNCHED"
  365 + STATUS_INIT = "INITIALIZING"
  366 + STATUS_MAIN_LOOP = "IN_MAIN_LOOP"
  367 + STATUS_GET_NEXT_COMMAND = "IN_GET_NEXT_COMMAND"
  368 + STATUS_GENERAL_PROCESS = "IN_GENERAL_PROCESS"
  369 + STATUS_ROUTINE_PROCESS = "IN_ROUTINE_PROCESS"
  370 + STATUS_SPECIFIC_PROCESS = "IN_SPECIFIC_PROCESS"
  371 + STATUS_EXIT = "EXITING"
  372 +
  373 + # Modes
  374 + MODE_ACTIVE = "ACTIVE"
  375 + MODE_IDLE = "IDLE"
  376 +
  377 + ''' Moved to more central file : config.config_base
  378 + PYROS_DJANGO_BASE_DIR = Path("src/core/pyros_django") # pathlib
  379 + DEFAULT_CONFIG_FILE_NAME = "config_unit_simulunit1.xml"
  380 + CONFIG_DIR_NAME = "config"
  381 + '''
  382 +
  383 + # Parameters from config file
  384 + # for /src/
  385 + #_path_data = '../../config'
  386 + # for /src/core/pyros_django/
  387 + #_path_data = '../../../../config'
  388 + # Path to config
  389 + _path_data = ''
  390 + _computer_alias = ''
  391 + _computer_description = ''
  392 +
  393 + # Current and next command to send
  394 + _cmdts:AgentCmd = None
  395 + _next_cmdts = None
  396 +
  397 + _agent_survey = None
  398 + _pending_commands = []
  399 +
  400 + '''
  401 + _current_device_cmd = None
  402 + _current_device_cmd_thread = None
  403 + '''
  404 +
  405 + # List of agents I will send commands to
  406 + _my_client_agents_aliases = []
  407 + _my_client_agents = {}
  408 +
  409 + _iter_num = None
  410 +
  411 + # Log object
  412 + _log = None
  413 +
  414 + #def __init__(self, name:str="Agent", config_filename:str=None, RUN_IN_THREAD=True):
  415 + #def __init__(self, config_filename:str=None, RUN_IN_THREAD=True, DEBUG_MODE=False):
  416 + def __init__(self, config_filename:str=None, RUN_IN_THREAD=True):
  417 +
  418 + '''
  419 + print('PYROS_ENV', PYROS_ENV)
  420 + print('ROOT_DIR', ROOT_DIR)
  421 + print('DOC_DIR', DOC_DIR)
  422 + '''
  423 + print('config file is', config_filename)
  424 + ##print('PYROS_ENV', config.PYROS_ENV)
  425 + print('PYROS_ENV', config_old.PYROS_ENV)
  426 + print('ROOT_DIR', config_old.ROOT_DIR)
  427 + print('DOC_DIR', config_old.DOC_DIR)
  428 + ##if config.is_dev_env(): print("DEV ENV")
  429 + if config_old.is_dev_env(): print("DEV ENV")
  430 + if config_old.is_prod_env(): print("PROD ENV")
  431 + if config_old.is_debug(): print("IN DEBUG MODE")
  432 +
  433 + #self.name = name
  434 + self.name = self.__class__.__name__
  435 + '''
  436 + printd("*** ENVIRONMENT VARIABLE PYROS_DEBUG is:", os.environ.get('PYROS_DEBUG'), '***')
  437 + ##self.DEBUG_MODE = DEBUG_MODE
  438 + self.DEBUG_MODE = os.environ.get('PYROS_DEBUG', '0')=='1'
  439 + '''
  440 + #self.DEBUG_MODE = config.PYROS_ENV
  441 + self.log = LogPyros(self.name, AgentLogs)
  442 + ##self.DEBUG_MODE = config.is_debug()
  443 + self.DEBUG_MODE = config_old.is_debug()
  444 + ##self.log.debug_level = DEBUG_MODE
  445 + '''
  446 + # Default LOG level is INFO
  447 + log_level = LogPyros.LOG_LEVEL_INFO # INFO
  448 + self.log.set_global_log_level(LogPyros.LOG_LEVEL_DEBUG) if self.DEBUG_MODE else self.log.set_global_log_level(log_level)
  449 + '''
  450 + #global_log_level = LogPyros.LOG_LEVEL_DEBUG if self.DEBUG_MODE else self.PYROS_DEFAULT_GLOBAL_LOG_LEVEL
  451 + ##global_log_level = LogPyros.LOG_LEVEL_DEBUG if self.DEBUG_MODE else config.PYROS_DEFAULT_GLOBAL_LOG_LEVEL
  452 + global_log_level = LogPyros.LOG_LEVEL_DEBUG if self.DEBUG_MODE else config_old.PYROS_DEFAULT_GLOBAL_LOG_LEVEL
  453 + self.log.set_global_log_level(global_log_level)
  454 + ##self.printd("LOG LEVEL IS:", self.log.debug_level)
  455 + self.print("LOG LEVEL IS:", self.log.get_global_log_level())
  456 +
  457 + # Est-ce bien utile ???
  458 + # New way with PathLib
  459 + my_parent_abs_dir = Path(__file__).resolve().parent
  460 + #TODO: on doit pouvoir faire mieux avec pathlib (sans utiliser str())
  461 + ##self._path_data = str( Path( str(my_parent_abs_dir).split(str(self.PYROS_DJANGO_BASE_DIR))[0] ) / self.CONFIG_DIR_NAME )
  462 + ##self._path_data = config.CONFIG_DIR
  463 + self._path_data = config_old.CONFIG_DIR
  464 +
  465 + #self._set_mode(self.MODE_IDLE)
  466 + config_filename = self.get_config_filename(config_filename)
  467 + self.printd(f"*** Config file used is={config_filename}")
  468 + self.config = ConfigPyros(config_filename)
  469 + if self.config.get_last_errno() != self.config.NO_ERROR:
  470 + raise Exception(f"Bad config file name '{config_filename}', error {str(self.config.get_last_errno())}: {str(self.config.get_last_errmsg())}")
  471 +
  472 + #TODO: : à mettre dans la config
  473 + '''
  474 + 'AgentDeviceTelescope1': 'AgentDeviceTelescopeGemini',
  475 + 'AgentDeviceFilterSelector1': 'AgentDeviceSBIG',
  476 + 'AgentDeviceShutter1': 'AgentDeviceSBIG',
  477 + 'AgentDeviceSensor1': 'AgentDeviceSBIG',
  478 + '''
  479 + #self._my_client_agents = {}
  480 +
  481 + ### self._agent_logs = AgentLogs.objects.create(name=self.name, message="Step __init__")
  482 + self.printdb("Step __init__")
  483 +
  484 + self.TEST_COMMANDS = iter(self.TEST_COMMANDS_LIST)
  485 + self.RUN_IN_THREAD = RUN_IN_THREAD
  486 + self._set_status(self.STATUS_LAUNCH)
  487 + self._set_idle()
  488 +
  489 + self._set_agent_device_aliases_from_config(self.name)
  490 + self._set_mode_from_config(self.name)
  491 + # TODO: remove
  492 + #self._set_idle()
  493 + self._set_active()
  494 +
  495 + # Create 1st survey if none
  496 + #tmp = AgentSurvey.objects.filter(name=self.name)
  497 + #if len(tmp) == 0:
  498 + #nb_agents = AgentSurvey.objects.filter(name=self.name).count()
  499 + #if nb_agents == 0:
  500 + if AgentSurvey.objects.filter(name=self.name).exists():
  501 + self._agent_survey = AgentSurvey.objects.get(name=self.name)
  502 + else:
  503 + self._agent_survey = AgentSurvey.objects.create(name=self.name, validity_duration=60, mode=self.mode, status=self.status, iteration=-1)
  504 + self.printd("Agent survey is", self._agent_survey)
  505 +
  506 +
  507 + def get_config_filename(self, config_filename: str):
  508 + if not config_filename:
  509 + #config_filename = self.DEFAULT_CONFIG_FILE_NAME
  510 + ##config_filename = config.DEFAULT_CONFIG_FILE_NAME
  511 + config_filename = config_old.DEFAULT_CONFIG_FILE_NAME
  512 + # If config file name is RELATIVE (i.e. without path, just the file name)
  513 + # => give it an absolute path (and remove "src/agent/" from it)
  514 + if config_filename == os.path.basename(config_filename):
  515 + ##config_filename = os.path.join(config.CONFIG_DIR, config_filename)
  516 + config_filename = os.path.join(config_old.CONFIG_DIR, config_filename)
  517 + '''
  518 + # Build abs path including current agent dir
  519 + config_filename = os.path.abspath(self.CONFIG_DIR_NAME + os.sep + config_filename)
  520 + # Remove "src/agent_name/" from abs dir :
  521 + # (1) Remove "src/core/pyros_django/"
  522 + config_filename = config_filename.replace(str(self.PYROS_DJANGO_BASE_DIR)+os.sep, os.sep)
  523 + # (2) Remove "agent_name/"
  524 + #TODO: bidouille, faire plus propre
  525 + config_filename = config_filename.replace(os.sep+"agent"+os.sep, os.sep)
  526 + config_filename = config_filename.replace(os.sep+"monitoring"+os.sep, os.sep)
  527 + '''
  528 + return os.path.normpath(config_filename)
  529 +
  530 + def __repr__(self):
  531 + return "I am agent " + self.name
  532 +
  533 + def __str__(self):
  534 + return self.__repr__()
  535 + #return "I am agent " + self.name
  536 +
  537 + # Normal print
  538 + def print(self, *args, **kwargs): self.log.print(*args, **kwargs)
  539 + """
  540 + if args:
  541 + self.printd(f"({self.name}): ", *args, **kwargs)
  542 + else:
  543 + self.printd()
  544 + """
  545 + # DEBUG print shortcut
  546 + def printd(self, *args, **kwargs): self.log.printd(*args, **kwargs)
  547 + """
  548 + if DEBUG: self.printd(d(*args, **kwargs)
  549 + """
  550 + def log_d(self, *args, **kwargs): self.log.log_d(*args, **kwargs)
  551 + def log_i(self, *args, **kwargs): self.log.log_i(*args, **kwargs)
  552 + def log_w(self, *args, **kwargs): self.log.log_w(*args, **kwargs)
  553 + def log_e(self, *args, **kwargs): self.log.log_e(*args, **kwargs)
  554 + def log_c(self, *args, **kwargs): self.log.log_c(*args, **kwargs)
  555 + def printdb(self, *args, **kwargs): self.log.db( *args, **kwargs)
  556 +
  557 + def sleep(self, nbsec:float=2.0):
  558 + '''
  559 + # thread
  560 + if self._current_device_cmd_thread and self.RUN_IN_THREAD:
  561 + self._current_device_cmd_thread.wait(nbsec)
  562 + # process (or main thread)
  563 + else:
  564 + time.sleep(nbsec)
  565 + '''
  566 + time.sleep(nbsec)
  567 +
  568 + def _get_real_agent_name(self, agent_alias_name:str)->str:
  569 + #self.printd("key is", agent_alias_name)
  570 + '''
  571 + if not self._my_client_agents: return agent_alias_name
  572 + return self._my_client_agents[agent_alias_name]
  573 + '''
  574 + return self._my_client_agents.get(agent_alias_name)
  575 +
  576 +
  577 +
  578 + def run(self, nb_iter:int=None, FOR_REAL:bool=True):
  579 + """
  580 + FOR_REAL: set to False if you don't want Agent to send commands to devices but just print messages without really doing anything
  581 + """
  582 +
  583 + # TEST MODE ONLY
  584 + # IF in test mode but with REAL devices (no SIMULATOR), delete all dangerous commands from the test commands list scenario:
  585 + if self.TEST_MODE:
  586 + self.printd("\n!!! In TEST mode !!! => preparing to run a scenario of test commands")
  587 + self.printd("- Current test commands list scenario is:\n", self.TEST_COMMANDS_LIST)
  588 + if not self.WITH_SIMULATOR:
  589 + self.printd("\n!!! In TEST but no SIMULATOR mode (using REAL device) !!! => removing dangerous commands for real devices... :")
  590 + TEST_COMMANDS_LIST_copy = self.TEST_COMMANDS_LIST.copy()
  591 + for cmd in TEST_COMMANDS_LIST_copy:
  592 + cmd_key = cmd.split()[1]
  593 + if ("set_" in cmd_key) or ("do_start" in cmd_key) or cmd_key in ["do_init", "do_goto", "do_open", "do_close"]:
  594 + self.TEST_COMMANDS_LIST.remove(cmd)
  595 + self.printd("- NEW test commands list scenario is:\n", self.TEST_COMMANDS_LIST, '\n')
  596 +
  597 + self._DO_EXIT = False
  598 + self._DO_RESTART = True
  599 +
  600 + # Will loop again only if _DO_RESTART is True
  601 + while self._DO_RESTART:
  602 + self._DO_RESTART = False
  603 + self.start_time = time.time()
  604 + self.printd("on est ici: ", os.getcwd())
  605 +
  606 + self._load_config()
  607 +
  608 + self.print_TEST_MODE()
  609 +
  610 + self.init()
  611 + ''' testing log:
  612 + self.log_e("ERROR")
  613 + self.log_c("FATAL critical ERROR")
  614 + '''
  615 + self.log_w("WARNING", "watch your step !")
  616 +
  617 + # Avoid blocking on false "running" commands
  618 + # (old commands that stayed with "running" status when agent was killed)
  619 + AgentCmd.delete_commands_with_running_status_for_agent(self.name)
  620 +
  621 + self._iter_num = 1
  622 + self._DO_MAIN_LOOP = True
  623 + # Main loop
  624 + while self._DO_MAIN_LOOP:
  625 + try:
  626 + self._main_loop(nb_iter,FOR_REAL)
  627 + if not self._DO_MAIN_LOOP: break
  628 + except KeyboardInterrupt: # CTRL-C
  629 + # In case of CTRL-C, kill the current thread (process) before dying (in error)
  630 + self.print("CTRL-C Interrupted, I kill the current thread (process) before exiting (if exists)")
  631 + self._kill_running_device_cmd_if_exists("USER_CTRLC")
  632 + exit(1)
  633 +
  634 + if self.TEST_MODE and self.TEST_WITH_FINAL_TEST: self._TEST_test_results()
  635 + #if self._DO_EXIT: exit(0)
  636 +
  637 +
  638 +
  639 + # DEVICE level
  640 + def _kill_running_device_cmd_if_exists(self, abort_cmd_sender):
  641 + pass
  642 +
  643 +
  644 + def _main_loop(self, nb_iter:int=None, FOR_REAL:bool=True):
  645 +
  646 + self._main_loop_start(nb_iter)
  647 + if not self._DO_MAIN_LOOP: return
  648 +
  649 + self._load_config() # only if changed
  650 +
  651 + # Log this agent status (update my current mode and status in DB)
  652 + self._log_agent_status()
  653 +
  654 + #self.printd("====== START COMMMANDS PROCESSING ======")
  655 +
  656 + # ROUTINE process
  657 + # To be overriden to be specified by subclass
  658 + self._routine_process()
  659 + #self.printd("I am IDLE, so I bypass the routine_process (do not send any new command)")
  660 +
  661 + # Processing the next pending command if exists
  662 + self._command_process_if_exists()
  663 +
  664 + # if restart, exit this loop to restart from beginning
  665 + if self._DO_RESTART or self._DO_EXIT:
  666 + self._DO_MAIN_LOOP = False
  667 + return
  668 +
  669 + #self.printd("====== END COMMMANDS PROCESSING ======")
  670 +
  671 + #self.waitfor(self.mainloop_waittime)
  672 +
  673 + self.print("*"*20, "MAIN LOOP ITERATION (END)", "*"*20)
  674 + #self.do_log(LOG_DEBUG, "Ending main loop iteration")
  675 +
  676 + self._iter_num += 1
  677 +
  678 +
  679 + def _main_loop_start(self, nb_iter:int):
  680 +
  681 + for i in range(3): self.print()
  682 + #self.printd("-"*80)
  683 + self.print("*"*73)
  684 + self.print("*"*20, f"MAIN LOOP ITERATION {self._iter_num} (START)", "*"*20)
  685 + self.print("*"*73, '\n')
  686 + #self.print(f"Iteration {self._iter_num}")
  687 +
  688 + # EXIT because of nb of iterations ?
  689 + if nb_iter is not None:
  690 + # Bad number of iterations or nb iterations reached => exit
  691 + if nb_iter <= 0 or nb_iter < self._iter_num:
  692 + self.print(f"Exit because number of iterations asked ({nb_iter}) has been reached")
  693 + self._DO_MAIN_LOOP = False
  694 + return
  695 +
  696 + # Wait a random number of sec before starting iteration
  697 + # (to let another agent having the chance to send a command before me)
  698 + random_waiting_sec = random.randint(0,5)
  699 + self.print(f"Waiting {random_waiting_sec} sec (random) before starting new iteration...")
  700 + time.sleep(random_waiting_sec)
  701 +
  702 + # (Test only)
  703 + # EXIT because max duration reached ?
  704 + if self.TEST_MAX_DURATION_SEC and (time.time()-self.start_time > self.TEST_MAX_DURATION_SEC):
  705 + self.print("Exit because of max duration set to ", self.TEST_MAX_DURATION_SEC, "s")
  706 + self._kill_running_device_cmd_if_exists(self.name)
  707 + #if self.TEST_MODE and self.TEST_WITH_FINAL_TEST: self._TEST_test_results()
  708 + self._DO_MAIN_LOOP = False
  709 + return
  710 +
  711 + self._set_status(self.STATUS_MAIN_LOOP)
  712 + self.show_mode_and_status()
  713 +
  714 +
  715 +
  716 + def _command_process_if_exists(self):
  717 + ''' Processing the next pending command if exists '''
  718 +
  719 + self.print()
  720 + self.print()
  721 + self.print("*"*10, "NEXT COMMAND PROCESSING (START)", "*"*10, '\n')
  722 +
  723 +
  724 + # Purge commands (every N iterations, delete old commands)
  725 + N=5
  726 + if ((self._iter_num-1) % N) == 0:
  727 + self.print("Purging old commands if exists")
  728 + #AgentCmd.purge_old_commands_for_agent(self.name)
  729 + self._purge_old_commands_sent_to_me()
  730 +
  731 + # Get next command and process it (if exists)
  732 + cmd = self._get_next_valid_and_not_running_command()
  733 + #self._set_status(self.STATUS_GENERAL_PROCESS)
  734 + #if cmd: self.command_process(cmd)
  735 + if cmd:
  736 + self.print('-'*6)
  737 + self.print('-'*6, "RECEIVED NEW COMMAND TO PROCESS:")
  738 + self.print('-'*6, cmd)
  739 + self.print('-'*6)
  740 +
  741 + # CASE 1 - AGENT LEVEL command
  742 + # => I process it directly without asking my DC
  743 + # => Simple action, short execution time, so I execute it directly (in current main thread, not in parallel)
  744 + if self.is_agent_level_cmd(cmd):
  745 + self.print("(AGENT LEVEL CMD)")
  746 + try:
  747 + self._exec_agent_cmd(cmd)
  748 + except AttributeError as e:
  749 + self.log_e(f"EXCEPTION: Agent level specific command '{cmd.name}' unknown (not implemented as a function) :", e)
  750 + self.log_e("Thus => I ignore this command...")
  751 + cmd.set_result("ERROR: INVALID AGENT LEVEL SPECIFIC COMMAND")
  752 + cmd.set_as_pending()
  753 + cmd.set_as_skipped()
  754 +
  755 + # CASE 2 - DEVICE LEVEL command
  756 + # => I delegate it to my DC
  757 + # => Execute it only if I am active and no currently running another device level cmd
  758 + # => Long execution time, so I will execute it in parallel (in a new thread or process)
  759 + elif self.is_device_level_cmd(cmd):
  760 + self.print("(DEVICE LEVEL CMD)")
  761 + try:
  762 + self.exec_device_cmd_if_possible(cmd)
  763 + except (UnimplementedGenericCmdException) as e:
  764 + #except (UnknownGenericCmdException, UnimplementedGenericCmdException, UnknownNativeCmdException) as e:
  765 + self.log_e(f"EXCEPTION caught by {type(self).__name__} (from Agent mainloop) for command '{cmd.name}'", e)
  766 + self.log_e("Thus ==> ignore this command")
  767 + cmd.set_result(e)
  768 + #cmd.set_as_killed_by(type(self).__name__)
  769 + cmd.set_as_skipped()
  770 + #raise
  771 +
  772 + # CASE 3 - INVALID COMMAND
  773 + else:
  774 + #raise Exception("INVALID COMMAND: " + cmd.name)
  775 + self.print("******************************************************")
  776 + self.print("*************** ERROR: INVALID COMMAND ***************")
  777 + self.print("******************************************************")
  778 + self.print("Thus => I ignore this command...")
  779 + cmd.set_result("ERROR: INVALID COMMAND")
  780 + cmd.set_as_skipped()
  781 +
  782 + self.print()
  783 + self.print("*"*10, "NEXT COMMAND PROCESSING (END)", "*"*10, "\n")
  784 +
  785 +
  786 +
  787 + def print_TEST_MODE(self):
  788 + if self.TEST_MODE:
  789 + self.printd("[IN TEST MODE]")
  790 + self.print("Flush previous commands to be sure to start in clean state")
  791 + AgentCmd.delete_pending_commands_for_agent(self.name)
  792 + else:
  793 + self.printd("[IN NORMAL MODE]")
  794 + self.TEST_MAX_DURATION_SEC=None
  795 +
  796 +
  797 + def _purge_old_commands_sent_to_me(self):
  798 + AgentCmd.purge_old_commands_sent_to_agent(self.name)
  799 +
  800 +
  801 + def _routine_process(self):
  802 + """
  803 + Routine processing.
  804 + This is a command or set of commands that this agent sends regularly,
  805 + (or just a regular processing)
  806 + at each iteration
  807 + """
  808 + self.print()
  809 + self.print()
  810 + self.print("*"*10, "ROUTINE PROCESSING (START)", "*"*10, '\n')
  811 +
  812 + self._set_status(self.STATUS_ROUTINE_PROCESS)
  813 + self.routine_process_body()
  814 + self.print()
  815 + self.print("*"*10, "ROUTINE PROCESSING (END)", "*"*10)
  816 +
  817 + # To be overridden by subclasses
  818 + def routine_process_body(self):
  819 + if self.TEST_MODE: self._test_routine_process()
  820 +
  821 +
  822 + """
  823 + def purge_commands(self):
  824 + ###
  825 + Delete commands (which I am recipient of) older than COMMANDS_PEREMPTION_HOURS (like 48h)
  826 + ATTENTION !!! EXCEPT the RUNNING command !!!
  827 +
  828 + NB: datetime.utcnow() is equivalent to datetime.now(timezone.utc)
  829 + ###
  830 +
  831 + self.printd("Looking for old commands to purge...")
  832 + ###
  833 + COMMAND_PEREMPTION_DATE_FROM_NOW = datetime.utcnow() - timedelta(hours = self.COMMANDS_PEREMPTION_HOURS)
  834 + #self.printd("peremption date", COMMAND_PEREMPTION_DATE_FROM_NOW)
  835 + old_commands = AgentCmd.objects.filter(
  836 + # only commands for me
  837 + recipient = self.name,
  838 + # only pending commands
  839 + sender_deposit_time__lt = COMMAND_PEREMPTION_DATE_FROM_NOW,
  840 + )
  841 + ###
  842 + old_commands = AgentCmd.get_old_commands_for_agent(self.name)
  843 + if old_commands.exists():
  844 + self.printd("Found old commands to delete:")
  845 + for cmd in old_commands: self.printd(cmd)
  846 + old_commands.delete()
  847 + """
  848 +
  849 + def waitfor(self, nbsec):
  850 + self.print(f"Now, waiting for {nbsec} seconds...")
  851 + time.sleep(nbsec)
  852 +
  853 + def _set_status(self, status:str):
  854 + #self.printd(f"[{status}] (switching from status {self.status})")
  855 + self.printd(f"[{status}]")
  856 + self.status = status
  857 + return False
  858 +
  859 + def _set_mode(self, mode:str):
  860 + #self.printd(f"Switching from mode {self.mode} to mode {mode}")
  861 + self.print(f"[NEW MODE {mode}]")
  862 + self.mode = mode
  863 +
  864 + def _is_active(self):
  865 + return self.mode == self.MODE_ACTIVE
  866 + def _is_idle(self):
  867 + return not self._is_active()
  868 +
  869 + def _set_active(self):
  870 + self._set_mode(self.MODE_ACTIVE)
  871 +
  872 + def _set_idle(self):
  873 + self._set_mode(self.MODE_IDLE)
  874 +
  875 + def show_mode_and_status(self):
  876 + self.print(f"CURRENT MODE is {self.mode} (status is {self.status})")
  877 +
  878 + def die(self):
  879 + self._set_status(self.STATUS_EXIT)
  880 +
  881 + """
  882 + suspend/resume
  883 + """
  884 + def suspend(self):
  885 + """
  886 + TODO:
  887 + Mode IDLE (doit rester à l'écoute d'un resume,
  888 + et doit continuer à alimenter les tables pour informer de son état via tables agents_logs,
  889 + et lire table agents_command pour reprendre via resume,
  890 + et update la table agents_survey pour donner son status "idle"
  891 + """
  892 + self._set_idle()
  893 + return True
  894 +
  895 + def resume(self):
  896 + """
  897 + Quit suspend() mode
  898 + """
  899 + self._set_active()
  900 + return True
  901 +
  902 +
  903 + #TODO:
  904 + def _set_agent_device_aliases_from_config(self, agent_alias):
  905 + for a in self._my_client_agents_aliases:
  906 + # TODO: activer
  907 + ##self._my_client_agents[a] = self.config.get_paramvalue(a,'general','real_agent_device_name')
  908 + pass
  909 +
  910 + def _set_mode_from_config(self, agent_name):
  911 + # --- Get the startmode of the AgentX
  912 + modestr = self.config.get_paramvalue(agent_name,'general','startmode')
  913 + if self.config.get_last_errno() != self.config.NO_ERROR:
  914 + raise Exception(f"error {str(self.config.get_last_errno())}: {str(self.config.get_last_errmsg())}")
  915 + if (modestr == None):
  916 + return True
  917 + # --- Set the mode according the startmode value
  918 + mode = self.MODE_IDLE
  919 + if modestr.upper() == 'RUN':
  920 + mode = self.MODE_ACTIVE
  921 + self._set_mode(mode)
  922 + return True
  923 +
  924 +
  925 + """
  926 + =================================================================
  927 + Generic methods that may be specialized (overriden) by subclasses
  928 + =================================================================
  929 + """
  930 +
  931 + def init(self):
  932 + self.printd("Initializing...")
  933 + self._set_status(self.STATUS_INIT)
  934 +
  935 + def _load_config(self):
  936 + """
  937 + TODO:
  938 + only si date fichier xml changée => en RAM, un objet Config avec méthodes d'accès, appelle le parser de AK (classe Config.py indépendante)
  939 + """
  940 + '''
  941 + # SETUP
  942 + try:
  943 + self.config = get_object_or_404(Config, id=1)
  944 + # By default, set mode to SCHEDULER (False = REMOTE, which should never be the default)
  945 + self.config.global_mode = True
  946 + self.config.save()
  947 + # self.config = Config.objects.get(pk=1)
  948 + # self.config = Config.objects.get()[0]
  949 + except Exception as e:
  950 + # except Config.ObjectDoesNotExist:
  951 + self.printd("Config read (or write) exception", str(e))
  952 + return -1
  953 + '''
  954 + self.printd("Loading the config file...")
  955 + self.config.load()
  956 + if self.config.get_last_errno() != self.config.NO_ERROR:
  957 + raise Exception(f"error {str(self.config.get_last_errno())}: {str(self.config.get_last_errmsg())}")
  958 + if not self.config.is_config_contents_changed():
  959 + return
  960 +
  961 + # === display informations
  962 + # --- Get the assembly aliases of the unit
  963 + assembled_mount_aliases = []
  964 + assembled_channel_aliases = []
  965 + assembled_aliases = []
  966 + unit_alias = self.config.get_aliases('unit')[0]
  967 + params = self.config.get_params(unit_alias)
  968 + for param in params:
  969 + if param['section']=="assembly" and param['key']=="alias":
  970 + assembled_aliases.append(param['value'])
  971 + #self.printd(f"Unit {unit_alias} is the assembly of {assembled_aliases}")
  972 +
  973 + self.printd("--------- Components of the unit -----------")
  974 + self.printd("Configuration file is {}".format(self.config.get_configfile()))
  975 + alias = self.config.get_aliases('unit')[0]
  976 + namevalue = self.config.get_paramvalue(alias,'unit','description')
  977 + self.printd(f"Unit alias is {alias}. Description is {namevalue}:")
  978 + unit_subtags = self.config.get_unit_subtags()
  979 + for unit_subtag in unit_subtags:
  980 + aliases = self.config.get_aliases(unit_subtag)
  981 + for alias in aliases:
  982 + namevalue = self.config.get_paramvalue(alias,unit_subtag,'description')
  983 + self.printd(f"- {unit_subtag} alias is {alias}. Description is {namevalue}")
  984 + # --- fill the list of mount and channel assembled
  985 + if alias in assembled_aliases:
  986 + if unit_subtag=="mount":
  987 + assembled_mount_aliases.append(alias)
  988 + elif unit_subtag=="channel":
  989 + assembled_channel_aliases.append(alias)
  990 +
  991 + self.printd("--------- Assembly of the unit -----------")
  992 + self.printd(f"Assembled mount aliases: {assembled_mount_aliases}")
  993 + self.printd(f"Assembled channel aliases: {assembled_channel_aliases}")
  994 +
  995 + # --- Get the home of the mount[0]
  996 + mount_alias = assembled_mount_aliases[0]
  997 + home = self.config.get_paramvalue(mount_alias,'MountPointing','home')
  998 +
  999 + self.printd("------------------------------------------")
  1000 + hostname = socket.gethostname()
  1001 + self._computer_alias = ''
  1002 + unit_subtag = 'computer'
  1003 + aliases = self.config.get_aliases(unit_subtag)
  1004 + for alias in aliases:
  1005 + self.printd("alias", alias)
  1006 + value = self.config.get_paramvalue(alias,'local','hostname')
  1007 + self.printd("value", value)
  1008 + if value == hostname:
  1009 + self.printd("value", value)
  1010 + self._computer_alias = alias
  1011 + value = self.config.get_paramvalue(alias,unit_subtag,'description')
  1012 + self._computer_description = value
  1013 + value = self.config.get_paramvalue(alias,'path','data')
  1014 + # Overrides default value
  1015 + self._path_data = value
  1016 + break
  1017 + self.printd(f"hostname = {hostname}")
  1018 + self.printd(f"path_data = {self._path_data}")
  1019 + self.printd(f"home = {home}")
  1020 + self.printd("------------------------------------------")
  1021 +
  1022 + # --- update the log parameters
  1023 + ##self.log.path_data = self._path_data
  1024 + ##print("new self.log.path_data is", self.log.path_data)
  1025 + self.log.set_global_path_data(self._path_data)
  1026 + self.printd("new self.log.global_path_data is", self.log.get_global_path_data())
  1027 + self.log.home = home
  1028 +
  1029 +
  1030 + #def update_survey(self):
  1031 + def _log_agent_status(self):
  1032 + """
  1033 + Save (update) this agent current mode and status in DB
  1034 + """
  1035 + self.printd("Updating the agent survey database table...")
  1036 + #self.printd("- fetching table line for agent", self.name)
  1037 + # only necessary when using process (not necessary with threads)
  1038 + #with transaction.atomic():
  1039 + #self._agent_survey = AgentSurvey.objects.get(name=self.name)
  1040 + self._agent_survey.mode = self.mode
  1041 + self._agent_survey.status = self.status
  1042 + self._agent_survey.iteration = self._iter_num
  1043 + self._agent_survey.save()
  1044 + #self._agent_survey.save(update_fields=["mode", "status"])
  1045 +
  1046 +
  1047 + """
  1048 + def send_command(self, cmd_name):
  1049 + recipient_agent = self.name if self.TEST_COMMANDS_DEST=="myself" else self.TEST_COMMANDS_DEST
  1050 + AgentCmd.objects.create(sender=self.name, recipient=recipient_agent, name=cmd_name)
  1051 + """
  1052 + #def send_command(self, to_agent, cmd_type, cmd_name, cmd_args=None):
  1053 + def send_cmd_to(self, to_agent, cmd_name, cmd_args=None):
  1054 + """
  1055 + #ex: send_command(“AgentX”,”GENERIC”,”EVAL”,“3+4”)
  1056 + ex: send_command(“AgentX”,"EVAL”,“3+4”)
  1057 + """
  1058 + #return AgentCmd.send_cmd(self.name, self._get_real_agent_name_for_alias(to_agent), cmd_name, cmd_args)
  1059 + cmd = self.create_cmd_for(to_agent, cmd_name, cmd_args)
  1060 + cmd.send()
  1061 + return cmd
  1062 +
  1063 + def create_cmd_for(self, to_agent, cmd_name, cmd_args=None)->AgentCmd:
  1064 + '''
  1065 + real_agent_name = self._get_real_agent_name(to_agent)
  1066 + real_cmd_name = cmd_name
  1067 + if '.' in real_agent_name:
  1068 + real_agent_name, component_name = real_agent_name.split('.')
  1069 + real_cmd_name = component_name+'.'+cmd_name
  1070 + return AgentCmd.create(self.name, real_agent_name, real_cmd_name, cmd_args)
  1071 + try:
  1072 + real_agent_name = self._get_real_agent_name(to_agent)
  1073 + except KeyError as e:
  1074 + '''
  1075 + real_agent_name = self._get_real_agent_name(to_agent)
  1076 + if not real_agent_name:
  1077 + self.log_e("UNKNOWN AgentDevice ALIAS", to_agent)
  1078 + #self.log_e("Exception raised", e)
  1079 + self.log_e(f"=> Thus, I do not send this command '{cmd_name}'")
  1080 + return None
  1081 + return AgentCmd.create(self.name, real_agent_name, cmd_name, cmd_args)
  1082 + '''
  1083 + return AgentCmd(
  1084 + sender=self.name,
  1085 + recipient=self._get_real_agent_name_for_alias(recipient_agent_alias_name),
  1086 + name=cmd_name
  1087 + )
  1088 + '''
  1089 +
  1090 + def _get_next_valid_and_not_running_command(self)->AgentCmd:
  1091 + """
  1092 + Return next VALID (not expired) command (read from the DB command table)
  1093 + which is relevant to this agent.
  1094 + Commands are read in chronological order
  1095 + """
  1096 + self._set_status(self.STATUS_GET_NEXT_COMMAND)
  1097 + self.print("Looking for a new command to process (sent by another agent):")
  1098 +
  1099 + # 1) Get all pending commands for me (return if None)
  1100 + # Not sure this is necessary to do it in a transaction,
  1101 + # but there might be a risk
  1102 + # that a command status is modified while we are reading...
  1103 + with transaction.atomic():
  1104 + self._pending_commands = AgentCmd.get_pending_and_running_commands_for_agent(self.name)
  1105 + commands = self._pending_commands
  1106 + if not commands.exists():
  1107 + self.print("<None>")
  1108 + return None
  1109 + self.printd("Current pending (or running) commands are (time ordered):")
  1110 + AgentCmd.show_commands(commands)
  1111 +
  1112 + # 2) If there is a "do_exit" or "do_abort" command pending (even at the end of the list),
  1113 + # which is VALID (not expired),
  1114 + # then pass it straight away to general_process() for execution
  1115 + for cmd in commands:
  1116 + #if cmd.name in ("do_exit", "do_abort", "do_flush_commands"): break
  1117 + if cmd.name in ("do_exit", "do_abort"): break
  1118 + #if cmd.name in ("do_exit", "do_abort", "do_flush_commands"):
  1119 + if cmd.name in ("do_exit", "do_abort"):
  1120 + if cmd.is_running():
  1121 + return None
  1122 + if cmd.is_expired():
  1123 + cmd.set_as_outofdate()
  1124 + return None
  1125 + return cmd
  1126 +
  1127 + # 3) If first (oldest) command is currently running
  1128 + # (status CMD_RUNNING), then do nothing and return
  1129 + """
  1130 + cmd_executing = Command.objects.filter(
  1131 + # only commands for me
  1132 + recipient = self.name,
  1133 + # only pending commands
  1134 + recipient_status_code = Command.CMD_STATUS_CODES.CMD_RUNNING,
  1135 + ).first()
  1136 + #if cmd_executing.exists():
  1137 + if cmd_executing:
  1138 + """
  1139 + #cmd = commands[0]
  1140 + cmd = commands.first()
  1141 + if cmd.is_expired():
  1142 + cmd.set_as_outofdate()
  1143 + return None
  1144 + if cmd.is_running():
  1145 + #self.printd(f"There is currently a running command ({cmd_executing.first().name}), so I do nothing (wait for end of execution)")
  1146 + self.print(f"There is currently a running command ({cmd.name})")
  1147 + """
  1148 + # Check that this command is not expired
  1149 + if cmd.is_expired():
  1150 + self.printd("But this command is expired, so set its status to OUTOFDATE, and go on")
  1151 + cmd_executing.set_as_outofdate()
  1152 + else:
  1153 + """
  1154 + self.print(f"Thus, I won't execute any new command until this command execution is finished")
  1155 + # TODO: kill si superieur a MAX_EXEC_TIME
  1156 + return None
  1157 +
  1158 + '''
  1159 + # 4) Tag all expired commands
  1160 + for cmd in commands:
  1161 + if cmd.is_expired(): cmd.set_as_outofdate()
  1162 + # break at 1st "valid" command (not expired)
  1163 + else: break
  1164 +
  1165 + # 5) If no more commands to process, return None
  1166 + if cmd.is_expired(): return None
  1167 + '''
  1168 +
  1169 + # 6) Current cmd must now be a valid (not expired) and PENDING one,
  1170 + # so return it for execution
  1171 + #self.printd(f"Got command {cmd.name} sent by agent {cmd.sender} at {cmd.sender_deposit_time}")
  1172 + #self.printd(f"Starting processing of this command")
  1173 + return cmd
  1174 +
  1175 +
  1176 +
  1177 + #def _exec_agent_general_cmd(self, cmd:Command):
  1178 + def _exec_agent_cmd(self, cmd:AgentCmd):
  1179 +
  1180 + #self.print(f"Starting execution of an AGENT LEVEL cmd {cmd}...")
  1181 + self.print(f"Starting execution of an AGENT LEVEL cmd...")
  1182 +
  1183 + # Update read time to say that the command has been READ
  1184 + cmd.set_read_time()
  1185 + cmd.set_as_running()
  1186 +
  1187 + # SPECIFIC command (only related to me, not to any agent)
  1188 + if self._is_agent_specific_cmd(cmd):
  1189 + self.print("(Agent level SPECIFIC cmd)")
  1190 + # Execute method self."cmd.name"()
  1191 + # This can raise an exception (caught by this method caller)
  1192 + self.exec_cmd_from_its_name(cmd)
  1193 + '''
  1194 + try:
  1195 + except AttributeError as e:
  1196 + self.printd(f"EXCEPTION: Agent level specific command '{cmd.name}' unknown (not implemented as a function) :", e)
  1197 + self.printd("Thus => I ignore this command...")
  1198 + cmd.set_result("ERROR: INVALID AGENT LEVEL SPECIFIC COMMAND")
  1199 + cmd.set_as_pending()
  1200 + cmd.set_as_skipped()
  1201 + return
  1202 + '''
  1203 + cmd.set_result("Agent level SPECIFIC cmd done")
  1204 + cmd.set_as_processed()
  1205 + self.print("...Agent level SPECIFIC cmd has been executed")
  1206 + return
  1207 +
  1208 + # GENERAL command (related to any agent)
  1209 + self.print("(Agent level GENERAL CMD)")
  1210 + _,cmd_name,cmd_args = cmd.get_full_name_parts()
  1211 + #cmd_name, cmd_args = cmd.tokenize()
  1212 + #if cmd.name == "set_state:active":
  1213 + #elif cmd.name == "set_state:idle":
  1214 + if cmd_name == "set_state":
  1215 + if not cmd_args: raise ValueError()
  1216 + state = cmd_args[0]
  1217 + if state == "active": self._set_active()
  1218 + if state == "idle": self._set_idle()
  1219 + cmd.set_result("I am now " + state)
  1220 + time.sleep(1)
  1221 + elif cmd_name in ("do_flush_commands"):
  1222 + self.printd("flush_commands received: Delete all pending commands")
  1223 + AgentCmd.delete_pending_commands_for_agent(self.name)
  1224 + cmd.set_result('DONE')
  1225 + elif cmd_name in ("do_abort", "do_exit", "do_restart_init"):
  1226 + #self.printd("Current pending commands are:")
  1227 + #Command.show_commands(self._pending_commands)
  1228 + self.print("Aborting current executing command if exists:")
  1229 + self._kill_running_device_cmd_if_exists(cmd.sender)
  1230 + if cmd_name == "do_restart_init":
  1231 + self.print("restart_init received: Restarting from init()")
  1232 + self._DO_RESTART=True
  1233 + elif cmd.name == "do_exit":
  1234 + self._DO_EXIT=True
  1235 + cmd.set_result('SHOULD BE DONE NOW')
  1236 + else:
  1237 + '''
  1238 + name = cmd.name
  1239 + args = None
  1240 + if " " in name: name,args = name.split()
  1241 + if name == "do_eval":
  1242 + if args==None: raise(ValueError)
  1243 + cmd.set_result(eval(args))
  1244 + '''
  1245 + if cmd_name == "do_eval":
  1246 + if not cmd_args: raise ValueError()
  1247 + cmd.set_result(eval(cmd_args))
  1248 +
  1249 + cmd.set_as_processed()
  1250 + self.print("...Agent level GENERAL cmd has been executed")
  1251 +
  1252 + # If cmd is "do_exit", kill myself (without any question, this is an order soldier !)
  1253 + # This "do_exit" should normally kill any current thread (to be checked...)
  1254 + if cmd.name == "do_exit":
  1255 + self.print("Before exiting, Here are (if exists) the current (still) pending commands (time ordered) :")
  1256 + commands = AgentCmd.get_pending_and_running_commands_for_agent(self.name)
  1257 + AgentCmd.show_commands(commands, True)
  1258 + #if self.TEST_MODE and self.TEST_WITH_FINAL_TEST and self.TEST_COMMANDS_DEST == "myself": self.simulator_test_results()
  1259 + if self.TEST_MODE and self.TEST_WITH_FINAL_TEST:
  1260 + self._TEST_test_results()
  1261 + #self._DO_EXIT=True
  1262 + #exit(0)
  1263 +
  1264 +
  1265 +
  1266 +
  1267 + '''
  1268 + def do_log(self):
  1269 + #""
  1270 + log à 2 endroits ou 1 seul
  1271 + - in file
  1272 + - in db
  1273 + #""
  1274 + self.printd("Logging data...")
  1275 + '''
  1276 +
  1277 + def exec_cmd_from_its_name(self, cmd:AgentCmd):
  1278 + func = cmd.name
  1279 + if cmd.args:
  1280 + return getattr(self, func)(*cmd.args)
  1281 + else:
  1282 + return getattr(self, func)()
  1283 +
  1284 + def is_agent_level_cmd(self, cmd:AgentCmd):
  1285 + return cmd.is_agent_general_cmd() or self._is_agent_specific_cmd(cmd)
  1286 +
  1287 + '''
  1288 + def _exec_agent_cmd(self, cmd:Command):
  1289 + # AGENT "GENERAL LEVEL" command
  1290 + # => I process it directly without asking my DC
  1291 + # => Simple action, short execution time, so I execute it directly (in current main thread, not in parallel)
  1292 + if cmd.is_agent_level_general_cmd():
  1293 + self.printd("********** -- AGENT LEVEL GENERAL CMD *********")
  1294 + self._exec_agent_general_cmd(cmd)
  1295 +
  1296 + # AGENT "SPECIFIC LEVEL" command
  1297 + # => I process it directly without asking my DC
  1298 + # => Simple action, short execution time, so I execute it directly (in current main thread, not in parallel)
  1299 + #elif self._is_agent_level_specific_cmd(cmd):
  1300 + else:
  1301 + self.printd("********** -- AGENT LEVEL SPECIFIC CMD *********")
  1302 + self._exec_agent_specific_cmd(cmd)
  1303 + '''
  1304 +
  1305 + def _is_agent_specific_cmd(self, cmd:AgentCmd):
  1306 + return cmd.name in self.AGENT_SPECIFIC_COMMANDS
  1307 +
  1308 + '''
  1309 + def _exec_agent_specific_cmd(self, cmd:Command):
  1310 + # Execute method self."cmd.name"()
  1311 + self.exec_cmd_from_its_name(cmd)
  1312 + '''
  1313 +
  1314 + def is_in_test_mode(self):
  1315 + return self.TEST_MODE
  1316 +
  1317 +
  1318 + """
  1319 + ================================================================================================
  1320 + DEVICE SPECIFIC FUNCTIONS (abstract for Agent, overriden and implemented by AgentDevice)
  1321 + ================================================================================================
  1322 + """
  1323 +
  1324 + # To be overriden by subclass (AgentDevice)
  1325 + # @abstract
  1326 + def is_device_level_cmd(self, cmd):
  1327 + return False
  1328 +
  1329 + # to be overriden by subclass (AgentDevice)
  1330 + # @abstract
  1331 + def exec_device_cmd_if_possible(self, cmd:AgentCmd):
  1332 + pass
  1333 +
  1334 + # TO BE OVERRIDEN by subclass (AgentDevice)
  1335 + # @abstract
  1336 + def exec_device_cmd(self, cmd:AgentCmd):
  1337 + #self.exec_cmd_from_its_name(cmd)
  1338 + pass
  1339 +
  1340 +
  1341 +
  1342 +
  1343 +
  1344 + """
  1345 + =================================================================
  1346 + TEST DEDICATED FUNCTIONS
  1347 + =================================================================
  1348 + """
  1349 +
  1350 + def _set_debug_mode(self, mode:bool):
  1351 + self.DEBUG_MODE=mode
  1352 + def _set_with_simulator(self, mode:bool):
  1353 + self.WITH_SIMULATOR=mode
  1354 + def _set_test_mode(self, mode:bool):
  1355 + self.TEST_MODE=mode
  1356 +
  1357 + def _TEST_get_next_command_to_send(self)->AgentCmd:
  1358 + cmd_full_name = next(self.TEST_COMMANDS, None)
  1359 + #return cmd_name
  1360 + if cmd_full_name is None: return None
  1361 + if ' ' not in cmd_full_name: raise Exception('Command is malformed:', cmd_full_name)
  1362 + agent_recipient,cmd_name = cmd_full_name.split(' ', 1)
  1363 + ##recipient_agent = self.name if self.TEST_COMMANDS_DEST=="myself" else self.TEST_COMMANDS_DEST
  1364 + #return Command(sender=self.name, recipient=recipient_agent, name=cmd_name)
  1365 + cmd = self.create_cmd_for(agent_recipient, cmd_name)
  1366 + # If no cmd created (because of error, bad AgentDevice name), call again this method for next cmd
  1367 + if cmd is None: return self._TEST_get_next_command_to_send()
  1368 + return cmd
  1369 +
  1370 + """
  1371 + def simulator_send_next_command(self):
  1372 + #self._current_test_cmd = "set_state:idle" if self._current_test_cmd=="set_state:active" else "set_state:active"
  1373 + #if self._nb_test_cmds == 4: self._current_test_cmd = "do_exit"
  1374 + cmd_name = next(self.TEST_COMMANDS, None)
  1375 + #self.printd("next cmd is ", cmd_name)
  1376 + if cmd_name is None: return
  1377 + #Command.objects.create(sender=self.name, recipient=self.name, name=cmd_name)
  1378 + recipient_agent = self.name if self.TEST_COMMANDS_DEST=="myself" else self.TEST_COMMANDS_DEST
  1379 + Command.objects.create(sender=self.name, recipient=recipient_agent, name=cmd_name)
  1380 + #time.sleep(1)
  1381 + #self._TEST_current_cmd_idx += 1
  1382 + #self._nb_test_cmds += 1
  1383 + """
  1384 +
  1385 + def _test_routine_process(self):
  1386 + """
  1387 + TEST MODE ONLY
  1388 + """
  1389 +
  1390 + self.print("(TEST mode) Trying to send a new command if possible...")
  1391 + # There is a current command being processed
  1392 + # => check if next command is "do_abort"
  1393 + # => if so, instantly send a "do_abort" to abort previous command
  1394 + if self._cmdts is not None:
  1395 + self.print(f"Waiting for end execution of cmd '{self._cmdts.name}' (sent to {self._cmdts.recipient}) ...")
  1396 + # Update cmdts fields from DB
  1397 + self._cmdts.refresh_from_db()
  1398 + if self._cmdts.is_pending() or self._cmdts.is_running():
  1399 + if self._next_cmdts is None:
  1400 + # If next command is "do_abort" then abort becomes the new current command (to be sent)
  1401 + self._next_cmdts = self._TEST_get_next_command_to_send()
  1402 + if self._next_cmdts and self._next_cmdts.name == "do_abort":
  1403 + # Wait a little to give a chance to agentB to start execution of current command,
  1404 + # so that we can abort it then (otherwise it won't be aborted!!)
  1405 + time.sleep(4)
  1406 + self._cmdts = self._next_cmdts
  1407 + self._next_cmdts = None
  1408 + self.print("***")
  1409 + #self.print(f"*** SEND ", self._cmdts)
  1410 + self.print(f"***", self._cmdts)
  1411 + self.print("***")
  1412 + self._cmdts.send()
  1413 +
  1414 + # Current cmd is no more running
  1415 + else:
  1416 + # Execution was not completed
  1417 + #if self._cmdts.is_expired() or self._cmdts.is_skipped() or self._cmdts.is_killed():
  1418 + if self._cmdts.is_skipped() or self._cmdts.is_killed():
  1419 + self.print("Command was not completed")
  1420 + # 2 possible scenarios:
  1421 + # - (1) Send the SAME command again
  1422 + '''
  1423 + self.printd("Command was not completed, so I send it again")
  1424 + # The command was not completed, so, make a copy of it and send it again
  1425 + # For this, it is enough to set primary key to None,
  1426 + # then the send() command below will save a NEW command
  1427 + #self._cmdts = copy.copy(self._cmdts)
  1428 + self._cmdts.id = None
  1429 + SEND_A_NEW_COMMAND = True
  1430 + '''
  1431 + # - (2) Send next command
  1432 + #self._cmdts = None
  1433 + # Execution was not complete => get result
  1434 + elif self._cmdts.is_executed():
  1435 + cmdts_res = self._cmdts.get_result()
  1436 + self.print(f"Cmd executed. Result is '{cmdts_res}'")
  1437 + #cmdts_is_processed = True
  1438 + ''' Optimisation possible pour gagner une iteration:
  1439 + self._cmdts = self.simulator_get_next_command_to_send()
  1440 + # No more command to send (from simulator) => return
  1441 + if self._cmdts is None: return
  1442 + SEND_A_NEW_COMMAND = True
  1443 + '''
  1444 + # Set cmdts to None so that a new command will be sent at next iteration
  1445 + self._cmdts = None
  1446 +
  1447 + # No currently running command => get a new command and SEND it
  1448 + if self._cmdts is None:
  1449 + if self._next_cmdts is not None:
  1450 + self._cmdts = self._next_cmdts
  1451 + self._next_cmdts = None
  1452 + else:
  1453 + self._cmdts = self._TEST_get_next_command_to_send()
  1454 + # No more command to send (from simulator) => return and EXIT
  1455 + if self._cmdts is None:
  1456 + self._DO_MAIN_LOOP = False
  1457 + return
  1458 + # Send cmd (= set as pending and save)
  1459 + self.print("***")
  1460 + #self.printd(f"*** SEND ", self._cmdts)
  1461 + self.print(f"*** NEW COMMAND TO SEND is:", self._cmdts)
  1462 + self.print("***")
  1463 + #self._cmdts.set_as_pending()
  1464 + # SEND
  1465 + self._cmdts.send()
  1466 + #cmdts_is_processed = False
  1467 + #cmdts_res = None
  1468 +
  1469 + def _TEST_test_results(self):
  1470 + if self.TEST_COMMANDS_LIST == [] : return
  1471 + nb_commands_to_send = len(self.TEST_COMMANDS_LIST)
  1472 + nb_commands_sent, commands = self._TEST_test_results_start()
  1473 + #nb_commands_to_send = len(self.TEST_COMMANDS_LIST)
  1474 +
  1475 + # General (default) test
  1476 + #self.printd(commands[0].name, "compared to", self.TEST_COMMANDS_LIST[0].split()[1])
  1477 + assert commands[0].name == self.TEST_COMMANDS_LIST[0].split()[1]
  1478 + last_cmd = commands[-1]
  1479 + assert last_cmd.name == self.TEST_COMMANDS_LIST[-1].split()[1]
  1480 + assert last_cmd.name == "do_exit"
  1481 + assert last_cmd.is_executed()
  1482 + assert last_cmd.get_result() == "SHOULD BE DONE NOW"
  1483 +
  1484 + nb_asserted = 0
  1485 + nb_agent_general = 0
  1486 + nb_unknown = 0
  1487 + nb_unimplemented = 0
  1488 + for cmd in commands:
  1489 + assert cmd.is_executed() or cmd.is_killed() or cmd.is_skipped()
  1490 + nb_asserted += 1
  1491 + if cmd.is_agent_general_cmd():
  1492 + nb_agent_general += 1
  1493 + if cmd.name == "do_unknown":
  1494 + assert cmd.is_skipped()
  1495 + #assert "UnimplementedGenericCmdException" in cmd.get_result()
  1496 + assert "INVALID COMMAND" in cmd.get_result()
  1497 + nb_unknown += 1
  1498 + #if cmd.name in ["do_unimplemented", "do_unknown"]:
  1499 + if cmd.name == "do_unimplemented":
  1500 + assert cmd.is_skipped()
  1501 + assert "UnimplementedGenericCmdException" in cmd.get_result()
  1502 + nb_unimplemented += 1
  1503 + assert nb_asserted == nb_commands_sent
  1504 + self.print(nb_commands_to_send, "cmds I had to send <==>", nb_asserted, "cmds executed (or killed), ", nb_commands_to_send-nb_commands_sent, "cmd ignored")
  1505 + self.print("Among executed commands:")
  1506 + self.print(f"- {nb_agent_general} AGENT general command(s)")
  1507 + self.print("-", nb_unimplemented, "unimplemented command(s) => UnimplementedGenericCmdException raised then command was skipped")
  1508 + self.print("-", nb_unknown, "unknown command(s) => skipped")
  1509 +
  1510 + # Now test that any "AD get_xx" following a "AD set_xx value" command has result = value
  1511 + for i,cmd_set in enumerate(commands):
  1512 + if cmd_set.name.startswith('set_'):
  1513 + commands_after = commands[i+1:]
  1514 + for cmd_get in commands_after:
  1515 + if cmd_get.name.startswith('get_') and cmd_get.name[4:]==cmd_set.name[4:] and cmd_get.device_type==cmd_set.device_type:
  1516 + self.print("cmd_get.result == cmd_set.args ?", cmd_get.result, cmd_set.args)
  1517 + assert cmd_get.get_result() == ','.join(cmd_set.args), "A get_xx command did not gave the expected result as set by a previous set_xx command"
  1518 + break
  1519 +
  1520 + # Specific (detailed) test (to be overriden by subclass)
  1521 + nb_asserted2 = self.TEST_test_results_main(commands)
  1522 + self._TEST_test_results_end(nb_asserted)
  1523 +
  1524 + def _TEST_test_results_start(self):
  1525 + self.print()
  1526 + self.print("--- Testing if the commands I SENT had the awaited result")
  1527 + self.print("Here are the last commands I sent:")
  1528 + #commands = list(Command.get_last_N_commands_for_agent(self.name, 16))
  1529 + #commands = Command.get_last_N_commands_sent_to_agent(self.name, 16)
  1530 + nb_commands = len(self.TEST_COMMANDS_LIST)
  1531 + if "ad_unknown get_dec" in self.TEST_COMMANDS_LIST: nb_commands -= 1
  1532 + commands = AgentCmd.get_last_N_commands_sent_by_agent(self.name, nb_commands)
  1533 + AgentCmd.show_commands(commands)
  1534 + return nb_commands, commands
  1535 + """ OLD SCENARIO
  1536 + nb_asserted = 0
  1537 + for cmd in commands:
  1538 + if cmd.name == "specific0":
  1539 + assert cmd.is_skipped()
  1540 + nb_asserted+=1
  1541 + if cmd.name == "specific1":
  1542 + assert cmd.result == "in step #5/5"
  1543 + assert cmd.is_executed()
  1544 + nb_asserted+=1
  1545 + if cmd.name in ("specific2","specific3"):
  1546 + assert cmd.is_killed()
  1547 + nb_asserted+=1
  1548 + if cmd.name in ("specific4", "specific5", "specific6", "specific7", "specific8"):
  1549 + assert cmd.is_pending()
  1550 + nb_asserted+=1
  1551 + # 2 cmds abort
  1552 + if cmd.name in ("do_abort"):
  1553 + assert cmd.is_executed()
  1554 + nb_asserted+=1
  1555 + if cmd.name in ("do_exit"):
  1556 + assert cmd.is_executed()
  1557 + nb_asserted+=1
  1558 + assert nb_asserted == 12
  1559 + self.printd("--- Finished testing => result is ok")
  1560 + """
  1561 +
  1562 + # To be overriden by subclass
  1563 + def TEST_test_results_main(self, commands):
  1564 + return 0
  1565 + '''
  1566 + nb_asserted = 0
  1567 + self.printd("from simulator_test_results_main", commands)
  1568 + for cmd in commands:
  1569 + assert cmd.is_executed()
  1570 + nb_asserted+=1
  1571 + return nb_asserted
  1572 + '''
  1573 +
  1574 + def _TEST_test_results_end(self, nb_asserted):
  1575 + #nb_commands_to_send = len(self.TEST_COMMANDS_LIST)
  1576 + #self.printd(nb_asserted, "vs", nb_commands_to_send)
  1577 + #assert nb_asserted == nb_commands_to_send
  1578 + #self.printd(f"************** Finished testing => result is ok ({nb_asserted} assertions) **************")
  1579 + printFullTerm(Colors.GREEN, f"************** Finished testing => result is ok ({nb_asserted} assertions) **************")
  1580 +
  1581 +
  1582 +
  1583 +
  1584 +
  1585 +
  1586 +"""
  1587 +=================================================================
  1588 + MAIN
  1589 +=================================================================
  1590 +"""
  1591 +
  1592 +def extract_parameters():
  1593 + """ Usage: Agent.py [-t] [configfile] """
  1594 + # arg 1 : -t
  1595 + # arg 2 : configfile
  1596 + DEBUG_MODE = False
  1597 + TEST_MODE = False
  1598 + WITH_SIM = False
  1599 + VERBOSE_MODE = False
  1600 + configfile = None
  1601 + printd("args:", sys.argv)
  1602 + for arg in sys.argv[1:] :
  1603 + if arg == "-t": TEST_MODE = True
  1604 + elif arg == "-s": WITH_SIM = True
  1605 + elif arg == "-d": DEBUG_MODE = True
  1606 + elif arg == "-v": VERBOSE_MODE = True
  1607 + else: configfile = arg
  1608 + '''
  1609 + if len(sys.argv) > 1:
  1610 + if sys.argv[1] == "-t":
  1611 + TEST_MODE = True
  1612 + if len(sys.argv) == 3:
  1613 + configfile = sys.argv[2]
  1614 + else:
  1615 + configfile = sys.argv[1]
  1616 + '''
  1617 + return DEBUG_MODE, WITH_SIM, TEST_MODE, VERBOSE_MODE, configfile
  1618 +
  1619 +#def build_agent(Agent_type:Agent, name="GenericAgent", RUN_IN_THREAD=True):
  1620 +def build_agent(Agent_type:Agent2, RUN_IN_THREAD=True):
  1621 + DEBUG_MODE, WITH_SIM, TEST_MODE, VERBOSE_MODE, configfile = extract_parameters()
  1622 + #agent = Agent("GenericAgent", configfile, RUN_IN_THREAD=True)
  1623 + #agent = Agent_type(configfile, RUN_IN_THREAD, DEBUG_MODE=DEBUG_MODE)
  1624 + agent = Agent_type(configfile, RUN_IN_THREAD)
  1625 + #agent = Agent_type(name, configfile, RUN_IN_THREAD)
  1626 + agent._set_with_simulator(WITH_SIM)
  1627 + agent._set_test_mode(TEST_MODE)
  1628 + #agent._set_debug_mode(DEBUG_MODE)
  1629 + return agent
  1630 +
  1631 +
  1632 +if __name__ == "__main__":
  1633 +
  1634 + # with thread
  1635 + RUN_IN_THREAD=True
  1636 + # with process
  1637 + #RUN_IN_THREAD=False
  1638 +
  1639 + agent = build_agent(Agent2, RUN_IN_THREAD=RUN_IN_THREAD)
  1640 + obs_config_file_path = os.environ["PATH_TO_OBSCONF_FILE"]
  1641 + path_to_obs_config_folder = os.environ["PATH_TO_OBSCONF_FOLDER"]
  1642 + unit = os.environ["unit_name"]
  1643 + print("TOTO")
  1644 + agent.printd(obs_config_file_path)
  1645 + agent.printd(path_to_obs_config_folder)
  1646 + agent.printd(unit)
  1647 + from src.core.pyros_django.obsconfig.configpyros import ConfigPyros
  1648 + oc = ConfigPyros(obs_config_file_path)
  1649 +
  1650 + print("\n")
  1651 + print("- Observatory:", oc.get_obs_name())
  1652 +
  1653 + my_unit_name = oc.get_units_name()[0]
  1654 + my_unit = (oc.get_units()[my_unit_name])
  1655 + #print("- Unit description:", my_unit)
  1656 +
  1657 + print("\n")
  1658 + print("- Computers:", oc.get_computers())
  1659 +
  1660 + print("\n")
  1661 + print("- Active Computers:", oc.get_active_computers())
  1662 +
  1663 + print("\n")
  1664 + print("- Active Devices:", oc.get_active_devices())
  1665 +
  1666 + print("\n")
  1667 + print("- Unit:", my_unit_name)
  1668 + print(oc.get_unit_by_name(my_unit_name))
  1669 +
  1670 + print("\n")
  1671 + print("- Unit topology:", oc.get_topology(my_unit_name))
  1672 +
  1673 + print("\n")
  1674 + print("- Unit active Agents:", oc.get_active_agents(my_unit_name))
  1675 + print(oc.get_agents(my_unit_name))
  1676 +
  1677 + print("\n")
  1678 + print("- Unit Agents per computer:", oc.get_agents_per_computer(my_unit_name))
  1679 +
  1680 + print("\n")
  1681 + print("- Unit Agents per device:", oc.get_agents_per_device(my_unit_name))
  1682 +
  1683 + print("\n")
  1684 + print("- Unit Channel groups:", oc.get_channel_groups(my_unit_name))
  1685 +
  1686 + print("\n")
  1687 + print("- Unit Channels:", oc.get_channels(my_unit_name))
  1688 +
  1689 + print("\n")
  1690 + print("- Unit/Channel info:", oc.get_channel_information(my_unit_name, 'OpticalChannel_up'))
  1691 +
  1692 + print("\n")
  1693 + print("- Unit Components agents:", oc.get_components_agents(my_unit_name))
  1694 +
  1695 + print("\n")
  1696 + print("- Unit database:", oc.get_database_for_unit(my_unit_name))
  1697 +
  1698 + print("\n")
  1699 + print("- Devices names:", oc.get_devices_names())
  1700 + print("\n")
  1701 + print("- Devices names & files:", oc.get_devices_names_and_file())
  1702 + print("\n")
  1703 + print("- Devices:", oc.get_devices())
  1704 +
  1705 + print("\n")
  1706 +
  1707 + exit(0)
  1708 + #agent = build_agent(Agent, name="GenericAgent", RUN_IN_THREAD=RUN_IN_THREAD)
  1709 + '''
  1710 + TEST_MODE, WITH_SIM, configfile = extract_parameters()
  1711 + agent = Agent("GenericAgent", configfile, RUN_IN_THREAD=True)
  1712 + #agent.setSimulatorMode(TEST_MODE)
  1713 + agent.setTestMode(TEST_MODE)
  1714 + agent.setWithSimulator(WITH_SIM)
  1715 + self.printd(agent)
  1716 + '''
  1717 + agent.run()
... ...
src/core/pyros_django/obsconfig/configpyros.py
... ... @@ -280,11 +280,8 @@ class ConfigPyros:
280 280 capability["attributes"] = component_attributes
281 281 return capability
282 282  
283   -
284   -
285   - def get_devices_names_and_file(self)->dict:
  283 + def get_devices_names_and_file(self) -> dict:
286 284 """
287   -
288 285 Return a dictionary giving the device file name by the device name
289 286 Returns:
290 287 dict: key is device name, value is file name
... ... @@ -455,7 +452,7 @@ class ConfigPyros:
455 452 exit(1)
456 453 #return None
457 454  
458   - def __init__(self,observatory_config_file:str,unit_name:str="") -> None:
  455 + def __init__(self, observatory_config_file:str, unit_name:str="") -> None:
459 456 """
460 457 Initiate class with the config file
461 458 set content attribute to a dictionary containing all values from the config file
... ... @@ -470,8 +467,7 @@ class ConfigPyros:
470 467 else:
471 468 self.unit_name = unit_name
472 469  
473   -
474   - def get_obs_name(self)->str:
  470 + def get_obs_name(self) -> str:
475 471 """
476 472 Return name of the observatory
477 473  
... ... @@ -480,7 +476,7 @@ class ConfigPyros:
480 476 """
481 477 return self.obs_config["OBSERVATORY"]["name"]
482 478  
483   - def get_channels(self,unit_name:str)->dict:
  479 + def get_channels(self, unit_name: str) -> dict:
484 480  
485 481 """
486 482 return dictionary of channels
... ... @@ -498,7 +494,7 @@ class ConfigPyros:
498 494 channels[channel["name"]] = channel
499 495 return channels
500 496  
501   - def get_computers(self)->dict:
  497 + def get_computers(self) -> dict:
502 498 """
503 499 return dictionary of computers
504 500  
... ... @@ -515,11 +511,11 @@ class ConfigPyros:
515 511 computer["computer_config"]= self.read_and_check_config_file(self.CONFIG_PATH+computer["file"])["COMPUTER"]
516 512 computers[computer["name"]] = computer
517 513 return computers
518   -
519   - def get_devices(self)->dict:
  514 +
  515 + def get_devices(self) -> dict:
520 516 """
521 517 return dictionary of devices
522   -
  518 +
523 519 Returns:
524 520 dict: [description]
525 521 """
... ... @@ -570,7 +566,7 @@ class ConfigPyros:
570 566 agents[agent["name"]] = agent
571 567 return agents
572 568  
573   - def get_channel_groups(self,unit_name:str)->dict:
  569 + def get_channel_groups(self, unit_name: str) -> dict:
574 570 """
575 571 Return dictionary of channel groups, tell the logic between groups of channels and within a group of channels
576 572  
... ... @@ -588,7 +584,7 @@ class ConfigPyros:
588 584 info["groups"][group_id] = group
589 585 return info
590 586  
591   - def get_channel_information(self,unit_name:str,channel_name:str)->dict:
  587 + def get_channel_information(self, unit_name: str, channel_name: str) -> dict:
592 588 """
593 589 Return information of the given channel name of a unit
594 590  
... ... @@ -602,7 +598,7 @@ class ConfigPyros:
602 598 channels = self.get_channels(unit_name)
603 599 return channels[channel_name]
604 600  
605   - def get_topology(self,unit_name:str)->dict:
  601 + def get_topology(self, unit_name:str)->dict:
606 602 """
607 603 Return dictionary of the topology of the observatory
608 604  
... ... @@ -623,7 +619,7 @@ class ConfigPyros:
623 619 topology[key] = branch
624 620 return topology
625 621  
626   - def get_active_agents(self,unit_name:str)->list:
  622 + def get_active_agents(self, unit_name: str) -> list:
627 623 """
628 624 Return the list of active agents (i.e. agents that have an association with a device)
629 625  
... ... @@ -649,7 +645,7 @@ class ConfigPyros:
649 645 result[unit["name"]] = unit
650 646 return result
651 647  
652   - def get_components_agents(self,unit_name:str)->dict:
  648 + def get_components_agents(self, unit_name: str) -> dict:
653 649 """
654 650 Return dictionary of component_agents of the given unit
655 651  
... ... @@ -683,9 +679,9 @@ class ConfigPyros:
683 679 """
684 680 return list(self.get_units().keys())
685 681  
686   - def get_unit_by_name(self,name:str)->dict:
  682 + def get_unit_by_name(self, name: str) -> dict:
687 683 """
688   - Return dictionary containing definition of the unit that match the given name
  684 + Return dictionary containing definition of the unit that matches the given name
689 685  
690 686 Args:
691 687 name (str): name of the unit
... ... @@ -695,7 +691,7 @@ class ConfigPyros:
695 691 """
696 692 return self.get_units()[name]
697 693  
698   - def get_agents_per_computer(self,unit_name:str)->dict:
  694 + def get_agents_per_computer(self, unit_name: str) -> dict:
699 695 """
700 696 Return dictionary that give for each computer, what are the associated agents to it as a list
701 697  
... ... @@ -795,7 +791,7 @@ class ConfigPyros:
795 791 """
796 792 return self.get_devices()[device_name]
797 793  
798   - def get_database_for_unit(self,unit_name:str)->dict:
  794 + def get_database_for_unit(self, unit_name: str) -> dict:
799 795 """
800 796 Return dictionary of attributes of the database for an unit
801 797  
... ... @@ -807,7 +803,7 @@ class ConfigPyros:
807 803 """
808 804 return self.get_unit_by_name(unit_name)["DATABASE"]
809 805  
810   - def get_device_for_agent(self,unit_name:str,agent_name:str)->str:
  806 + def get_device_for_agent(self, unit_name: str, agent_name: str) -> str:
811 807 """
812 808 Return device name associated to the agent
813 809  
... ... @@ -822,7 +818,8 @@ class ConfigPyros:
822 818 for device in agents_per_device:
823 819 if agent_name in agents_per_device[device]:
824 820 return self.get_device_information(device)
825   - def get_unit_of_computer(self,computer_name:str)->str:
  821 +
  822 + def get_unit_of_computer(self, computer_name: str) -> str:
826 823 """
827 824 Return the name of the unit where the computer is used
828 825  
... ... @@ -836,7 +833,7 @@ class ConfigPyros:
836 833 if(computer_name in self.get_agents_per_computer(unit_name)):
837 834 return unit_name
838 835  
839   - def get_unit_of_device(self,device_name:str)->str:
  836 + def get_unit_of_device(self, device_name:str)->str:
840 837 """
841 838 Return the name of the unit where the device is used
842 839  
... ... @@ -864,7 +861,7 @@ class ConfigPyros:
864 861 return self.get_devices()[device_name]["device_config"].get("power")
865 862  
866 863  
867   - def get_device_capabilities(self,device_name:str)->list:
  864 + def get_device_capabilities(self, device_name:str)->list:
868 865 """
869 866 Return dictionary that contains informations about capabilities if this information is present in the device config file
870 867  
... ... @@ -892,7 +889,7 @@ class ConfigPyros:
892 889 """
893 890 return self.get_devices()[device_name]["device_config"].get("connector")
894 891  
895   - def get_computer_power(self,computer_name:str)->dict:
  892 + def get_computer_power(self, computer_name: str) -> dict:
896 893 """
897 894 Return dictionary that contains informations about power if this information is present in the device config file
898 895  
... ...
src/core/pyros_django/pyros/settings.py
... ... @@ -404,5 +404,6 @@ python_version = subprocess.run( &quot;python --version | cut -d &#39; &#39; -f 2 | cut -d &#39;.
404 404 python_version = python_version.stdout
405 405 today = "2021-11-09"
406 406 django_version_major,django_version_minor = django.VERSION[:2][0],django.VERSION[:2][1]
407   -pyros_version = "0.2.12.0"
408   -VERSION_NUMBER = f"{pyros_version}_{django_version_major}.{django_version_minor}_{python_version}_{today}"
409 407 \ No newline at end of file
  408 +pyros_version = "0.3.0.0"
  409 +#pyros_version = "0.2.12.0"
  410 +VERSION_NUMBER = f"{pyros_version}_{django_version_major}.{django_version_minor}_{python_version}_{today}"
... ...