	\usepackage[public]{eso-pdm}
        \pdmProgram{ELT}
        \pdmProject{Instrumentation Framework}
        \pdmTitle{ELT ICS Framework - Sequencer - User Manual}
        \pdmDocId{ESO-363358}
        \pdmDocVersion{1}
        \pdmDocType{Manual (MAN)}
        \pdmDocDate{2021-04-24}
        \pdmSignatureList{
            \pdmSignature{Owner}{Muñoz, Iván}
            \pdmSignature{Validated by PM}{Kornweibel, Nick}
            \pdmSignature{Validated by SE}{González Herrera, Juan Carlos}
            \pdmSignature{Validated by PE}{Biancat Marchet, Fabio}
            \pdmSignature{Appoved by PGM}{Tamai, Roberto}
        }
        % Disable pagestyle changes as it breaks ESO PDM style
        \renewcommand{\pagestyle}[1]{}
        
hmargin={0.7in,0.7in}, vmargin={1in,1in},         verbatimwithframe=true,         OuterLinkColor={rgb}{0,0,0.6},         InnerLinkColor={rgb}{0,0,0},         warningBorderColor={rgb}{0.8,0,0},         cautionBorderColor={rgb}{1,0.8,0},         TitleColor={rgb}{0,0,0}

    \pdmmaketitle

    % Scope redefinition of clearpage to relax to avoid new pages for each
    \begingroup
    \let\clearpage\relax

    \section*{Release}
    This document corresponds to
    \sphinxhref{https://gitlab.eso.org/ifw/seq}{\texttt{seq}\footnote{\sphinxnolinkurl{https://gitlab.eso.org/ifw/seq}}} v4.0.0-pre3.

    \section*{Authors}
    \begin{tabularx}{\linewidth}{|p{0.25\linewidth}|X|}
      \hline
      \multicolumn{1}{|l|}{\textbf{Name}}\tbspa &
      \multicolumn{1}{l|}{\textbf{Affiliation}} \tbspb\\
      \hline
      \tbspa Muñoz, Iván & ESO/DOE/CSE\tbspb\\ \hline
    \end{tabularx}

    \section*{Change Record from previous Version}
      \hline
      \multicolumn{1}{|l|}{\textbf{Affected Section(s)}}\tbspa &
      \multicolumn{1}{l|}{\textbf{Changes / Reason / Remarks}} \tbspb\\
      \hline
      \tbspa & See CRE ET-1084 \tbspb\\ \hline
      \tbspa All & All sections updated, new GUI section \tbspb\\ \hline

    % Restore \clearpage
    \endgroup
    
f-strings support = for self-documenting expressions and debugging

ZERO-CLAUSE BSD LICENSE FOR CODE IN THE PYTHON 3.11.3 DOCUMENTATION

email.message.Message: Representing an email message using the compat32 API

What is the most efficient way to concatenate many strings together?

Why does a_tuple[i] += [‘item’] raise an exception when the addition works?

What does the slash(/) in the parameter list of a function mean?

Why am I getting an UnboundLocalError when the variable has a value?

PEP 3118: New memoryview implementation and buffer protocol documentation

PEP 476: Enabling certificate verification by default for stdlib http clients

PEP 471 - os.scandir() function – a better and faster directory iterator

PEP 655: Marking individual TypedDict items as required or not-required

Why must ‘self’ be used explicitly in method definitions and calls?

argparse — Parser for command-line options, arguments and sub-commands

functools — Higher-order functions and operations on callable objects

multiprocessing.shared_memory — Shared memory for direct access across processes

sysconfig — Provide access to Python’s configuration information

"""
Logging config
"""
# pylint: disable= invalid-name

import logging
import logging.config
import socket
import threading
import datetime
import time
import json

from pathlib import Path
from seq.lib import keywords as KW

from ModSeqif import Seqif
import elt.pymal as mal

logger = logging.getLogger(__name__)
_old_log_factory = logging.getLogRecordFactory()  # Old factory

def _log_record_factory(*args, **kwargs):
    from seq.lib.nodes import (Action, Sequence)

    record = _old_log_factory(*args, **kwargs)
    current_node = Action.current_node.get()
    current_seq = Sequence.current_seq.get()
    sn = -1                      # safe default
    
    if current_node:
       sn = current_node.serial_number
    elif current_seq:
        sn = current_seq.serial_number
    record._seq_node_sn = sn
    return record

def _merge_log_config(d1, d2):
    def keyval(k, a, b):
        # print("KVAL", k, a.get(k,None) or b.get(k,None))
        return b.get(k, None) or a.get(k, None)

    mykeys = {*d1, *d2}
    return {
        k: _merge_log_config(d1.get(k, {}), d2.get(k, {}))
        if isinstance(d1.get(k, None), dict)
        else keyval(k, d1, d2)
        for k in mykeys
    }

def logConfig(level=logging.INFO, filename=None, remote=False, pub_id=None):
    """
    Configs logging for seq module

    Args:
        level(int): logging level.
        filename(str): logging file name

    Returns:
        None
    """

    d = LOGGING
    if remote:
        d = _merge_log_config(LOGGING, _REMOTE_CFG)
        if pub_id:
            d["handlers"]["publisher"]["pub_id"] = pub_id

    d["loggers"]["seq"]["level"] = level
    if filename is None:
        filename = "seq.log"

    p = Path(filename)
    p = p.with_name("seq_user.log")

    if "filename" not in d["handlers"]["logfile"]:
        d["handlers"]["logfile"]["filename"] = filename

    if "filename" not in d["handlers"]["logfileUser"]:
        d["handlers"]["logfileUser"]["filename"] = p.as_posix()

    logging.config.dictConfig(d)
    logging.setLogRecordFactory(_log_record_factory)

    seq_logger = logging.getLogger("seq")
    seq_logger.info("seq logger initialized")

    seq_lib_logger = logging.getLogger("seq.lib")
    seq_lib_logger.info("seq.lib logger initialized")

    user_logger = logging.getLogger("seq.user")
    logger.info("log filename: %s", d["handlers"]["logfile"]["filename"])
    user_logger.info("User's Logging initialized")

    # if remote:
    #     seq_logger.info("REMOTE LOG CONFIG: %s", json.dumps(d, indent=4))

def getUserLogger():
    """Gets  the user's Logger."""
    return logging.getLogger("seq.user")

LOGGING = {
    "version": 1,
    "disable_existing_loggers": True,
    "formatters": {
        "simple": {
            "format": "%(levelname)s:(%(module)s.%(funcName)s): %(message)s"
        },
        "verbose": {
            "format": "%(asctime)s [%(_seq_node_sn)s] <%(name)s> %(levelname)s:(%(module)s.%(funcName)s):%(lineno)-d: %(message)s"
        },
        "timed": {
            "format": "%(asctime)s %(levelname)s: %(message)s",
        },
    },
    "handlers": {
        "console": {
            "class": "logging.StreamHandler",
            "formatter": "simple",
        },
        "logfile": {
            "class": "logging.FileHandler",
            "formatter": "verbose",
            # "filename": "dummy.log",
            # "filename": "%(filename)s",
            "mode": "w",
        },
        "logfileUser": {
        },
    },
    "loggers": {
        "seq": {
            "level": logging.INFO,
            "handlers": ["console", "logfile"],
            "propagate": False,
        },
        "seq.lib": {
            "handlers": ["logfile"],
        },
        "seq.user": {
            "handlers": ["logfileUser","console"],
        },
    },
}

_REMOTE_CFG = {
        "publisher": {
            "class": "seq.lib.log.FastDDSPublisherHandler",
            "pub_id": "UNDEFINED",
        },
    },
        # "": {  # Allows to use any logger name in sequencer scripts
        #     "level": logging.INFO,
        #     "handlers": ["console", "publisher"],
        #     "propagate": False,
        #     "formatter": "verbose",
        # },
        "seq": {
            "handlers": ["console", "logfile", "publisher"],
        },
            "handlers": ["logfile", "publisher"],
        },
            "handlers": ["logfileUser", "publisher"],
        },
    },
}

class FastDDSPublisherHandler(logging.Handler):
    """
    FastDDSPublisherHandler
    ======================
    A python logging handler.
    It relies on fastdds publisher that will publish every new record to subscribers.

    """

    def __init__(self, pub_id=""):
        super(FastDDSPublisherHandler, self).__init__()
        uri = "dds.ps:///{}{}".format(KW.SEQ_LOG_TOPIC, pub_id)
        self._ddsMal = mal.loadMal("dds", {})
        factory = mal.CiiFactory.getInstance()
        factory.registerMal("dds", self._ddsMal)
        self._publisher = factory.getPublisher(
            uri, Seqif.LogRecord, qos=mal.ps.qos.DEFAULT
        )

    def emit(self, record: logging.LogRecord):
        try:
            timestamp = "%s.%03d" % (
                record.__getattribute__("asctime"),
                record.__getattribute__("msecs"),
            )
        except AttributeError:
            timestamp = ""

        pub_log_record = self._publisher.createDataEntity()
        pub_log_record.setSwmodule(record.module)
        pub_log_record.setFuncname(record.funcName)
        pub_log_record.setLineno(record.lineno)
        pub_log_record.setTimestamp(timestamp)
        pub_log_record.setLevelname(record.levelname)
        pub_log_record.setName(record.name)
        pub_log_record.setSerialNumber(record._seq_node_sn)

        # we store the log message in a blob to overcome length limit
        binary_log = bytearray()
        binary_log.extend(map(ord, record.getMessage()))
        vec_log = mal.SharedVectorUint8(binary_log)
        vec_log = vec_log.freeze()
        # to set the blob message, we have to read the blob message of the published
        # log record and to assign the binary data to it
        blob_log = pub_log_record.getMessage()
        blob_log.setBlob(vec_log)
        timestamp = time.time()

        self._publisher.publishWithTimestamp(
            timestamp, pub_log_record, datetime.timedelta(seconds=1)
        )

""" Implements :class:`Action` and :class:`ActionInThread` nodes.

    Actions are the leaf nodes in any Sequencer script. They execute python methods or functions.

    Coroutines are associated to :class:`Action` nodes while normal callable objects must be
    associated to :class:`ActionInThread` nodes.

"""
import inspect
import functools
import asyncio
from datetime import datetime
import attr

import contextvars as cv
from zope.interface import implementer
from seq.lib.contextvars_executor import ContextVarExecutor
from .state import T_STATE
from .rtflags import RTFLAG
from .interface import INode, _BaseNode
from .. import ob
from ..counter import Counter
from .utils import uniqueId, set_runtime_flag, iscoroutine_or_partial, set_runtime_flag_from_dict

user_logger = logging.getLogger("seq.user")

@implementer(INode)
@attr.s
class Action(_BaseNode):
    """Action ctor.

    Action nodes executes `coroutines` in a Sequencer script.

    Args:
        f(coroutine): The coroutine the node will execute

    Keyword Args:
        id (str): Unique id. If not provided an unique identifier is assigned.
        name (str): Node name. If not provided a name is assigned.

    Raises:
        TypeError: is `f` is not a coroutine.
    """

    f = attr.ib(default=None, repr=False)
    parent_tpl = attr.ib(default=None, repr=False)
    current_node = cv.ContextVar("current_node", default=None)

    def __attrs_post_init__(self):
        """
        setup object.
        Sets name, id and allows node to run.
        """
        self._check_function_type()
        if self.name is None:
            try:
                self.name = self.f.__qualname__
            except AttributeError:
                self.name = id(self.f)
        if self.id is None:
            self.id = "%s_%s" % (self.name, uniqueId())

        self.running_checkpoint.set()  # running is allowed
        desc = self.description or self.f.__doc__ or "No Description"
        self.description = desc.strip().split("\n")[0]

    def _check_function_type(self):
        if iscoroutine_or_partial(self.f) is False:
            raise TypeError("target function shall be coroutine")

    @property
    def context(self):
        """Get context from the running Sequence"""
        from .sequence import Sequence

        return Sequence.get_context()

    def _publish_state(self):
        """Requests controller object to publish node's state."""
        ctrl = ob.OB.controller.get()
        # assert(ctrl)
        ctrl and ctrl.notify_state_change(self)

    @property
    def state(self):
        return super().state

    @property
    def full_state(self):
        return super().full_state

    @state.setter
    def state(self, value):
        """Sets the node  state"""
        super(Action, self.__class__).state.fset(self, value)
        self._publish_state()

    def make_sequence(self, parent_tpl=None):
        """
        Not much to do here.
        Link back to Template object (if any)
        Get runtime flags from controller object.
        """
        from .template import Template
        
        logger.debug("ACTION: %s, (Tpl: %s)", self.name, parent_tpl)
        self.parent_tpl = parent_tpl
        if ctrl:
            self.runtime_flags = ctrl.runtime_flags.get(self.serial_number, 0)

    async def _execute_preamble(self):
        Action.current_node.set(self)
        self.in_error=False
        if self.runtime_flags & RTFLAG.PAUSE:
            self.state = T_STATE.PAUSED
            self.pause()
        # check permission to execute
        await self.running_checkpoint.wait()
        # now we are running ...
        if self.running_checkpoint.is_set():
            self.state = T_STATE.RUNNING
            self.t_start = datetime.now()

        # Skip this node?
        if self.runtime_flags & RTFLAG.SKIP:
            self.state = T_STATE.FINISHED
            self.skip = True
            self.t_end = datetime.now()
            self.result = None

    async def _execute_node_task(self, task):
        user_logger.info("Starting Task: %s", self.name)
        try:
            r = await task
            self.result = task.result()
        except asyncio.CancelledError:
            raise
        except Exception:
            user_logger.info("node in error")
            self.in_error = True
            self.exception = task.exception()
            raise  # bubble up the exception
        finally:
        user_logger.info("End Task: %s", self.name)
        return r

    async def __call__(self, resume=False):
        """
        Executes node action.

        If the action is a coroutine a task is created and passed to
        the asyncio loop for execution.
        """
        
        await self._execute_preamble()
        if self.state is T_STATE.FINISHED:  # node skipped in preamble
            return None

        if iscoroutine_or_partial(self.f):
            task = asyncio.create_task(self.f())
        else:
            # this should never happen
            raise TypeError("This don't belong here", self.f)
        r = await self._execute_node_task(task)
        return r

@attr.s
class ActionInThread(Action):
    """ActionInThread ctor.

    ActionInThread nodes executes python `callables` (functions or methods) in a Sequencer script.

    Args:
        f(callable): The `callable` the node will execute

    Raises:
        TypeError: is `f` is not a python method or function.
    """

    _executor = ContextVarExecutor()

        assert callable(self.f)
        if iscoroutine_or_partial(self.f) is True:
            raise TypeError("target function shall be a normal callable")

        """

        The action is a normal function, a special task is created
        in order to execute the action in a different thread.

        In any case the context parameter is passed to the action and
        its result saved.
        """

            # This should never happen
            raise TypeError("coroutines don't belong here")

        loop = asyncio.get_running_loop()
        task = loop.run_in_executor(ActionInThread._executor, self.f)
        return r

# @attr.s
class StartNode(Action):
    """This is Starting Node.

    marks node as unskipabble
    """

    def __init__(self, *args, **kw):
        super().__init__(*args, **kw)
        self.can_skip = False
        self.hide = True

class EndNode(Action):
    """This is the finish Node.

    marks node as unskippable
    """

def make_node(s, **kw):
    """Makes an :class:`Action` nodes out of a coroutine.

    Args:
        s(coroutine): target coroutine to be passed to :class:`Action` ctor.

    Keyword Arguments:
        **kw: Passed straight to :class:`Action` ctor.

    Returns:
        :class:`Action` node associated to `s` coroutine.

    Raises:
        TypeError: if 's' is not a coroutine.
    """
    node = None
    if isinstance(s, _BaseNode):
        return s
    elif iscoroutine_or_partial(s):
        node = Action(s, **kw)
        return node

    raise TypeError("Argument %s must be a node or coroutine type" % (s))

""" Implements a Checkbox node.

   Checkbox pauses execution of a Sequencer script until the 'check' command is given to it.
   That is to say, until the user confirms some condition and allows the script to continue.

.. warning:: This node is intended to use on interactive Sequencer scripts only. Do not
    **seq run** a script that contains it. It will hang forever.

"""

from .action import Action

@attr.s
class Checkbox(Action):
    """Checkbox ctor.

    Checkbox node waits for user to confirm some condition.

    Args:
        msg(str): Checkbox message

        id (str): Unique id. If  not provided an unique identifier is assigned.

    """

    # redefines the f attribute (from Action) to a NOP.
    f = attr.ib(default=None, init=False)
    checkmark = attr.ib(
        init=False, repr=False, default=attr.Factory(asyncio.Event)
    )

        """
        """
            self.name = "Checkbox"
        super().__attrs_post_init__()

        """Converted to NOP"""
        return

    async def _f(self):
        """Waits for checkmark event"""
        self.checkmark.clear()
        await self.checkmark.wait()
        return True

        """

        """

        task = asyncio.create_task(self._f())
        return r

    def checked(self):
        """Release the checkbox allowing the Sequencer script to continue its execution"""

        if self.state == T_STATE.PAUSED:
            raise RuntimeError("Cannot check a PAUSED node")
        self.checkmark.set()

    @staticmethod
    def create(msg, **kw):
        """
        Checkbox node constructor.

        Args:
            msg(str): Message to display along with the checkbox.
            *kw: standard sequencer node keywords.

        Keyword Args:
            id: Node id
            name: node name
        """
        return Checkbox(description=msg, **kw)

"""Implements the Sequence node for the sequencer"""
# pylint: disable= invalid-name, too-many-instance-attributes, too-many-public-methods

import networkx as nx
from .interface import (
    INode,
    _BaseNode,
)

from .action import (
    StartNode,
    EndNode,
    make_node,
)

from .utils import uniqueId

@attr.s
class Sequence(_BaseNode):
    """Basic sequencer node.

    Use :meth:`create` to  properly build :class:`Sequence` objects.

    Attributes:
        state: node state

    ============  ===============================
    Context Variables
    ---------------------------------------------
    Name           Desc
    current_seq   The parent of the current node
    root          Top level DAG's root

    Examples:

        >>> s = Sequence.create(a,b, name="my sequence")
        # execute Sequence
        >>> await s.start()
        # Get running Sequence from Inside function a():
        >>> def a():
        ...     current_seq = Sequence.current_seq
        ...     # now current_seq is the node s
        ...     assert current_seq == s"""

    _seq = attr.ib(init=False, default=attr.Factory(list), repr=False)
    graph = attr.ib(init=False, default=attr.Factory(nx.DiGraph), repr=False)
    _start_node = attr.ib(init=False, default=None, repr=False)
    _end_node = attr.ib(init=False, default=None, repr=False)
    _context = attr.ib(init=False, default=attr.Factory(dict), repr=False)
    debug = attr.ib(init=False, default=False, repr=False)
    _parameters = attr.ib(init=False, repr=False, default=attr.Factory(dict))

    # contextvar
    current_seq = cv.ContextVar("current_seq", default=None)
    root = cv.ContextVar("root", default=None)  # set by OB runner

        """
        Assigns node's name and id.
        """
        # if self.serial_number is None:
        #     self.serial_number = Counter.new_value()
            self.name = "Sequence"
            self.id = "%s_%s" % (self.name, uniqueId(id(self)))
        self._result = {}

    # make it iterable
    def __iter__(self):
        return self.graph.__iter__()

    @property
        """Get context dictionary, preferably from root node."""
        root = Sequence.root.get()
        if root is not None:
            self._context = root._context
        else:
            Sequence.root.set(self)
            # self._context = {}
        return self._context

    @context.setter
    def context(self, ctx):
        self._context = ctx

    @property
    def seq(self):
        """Retrieves the sequence list.

        :meta private:
        """
        return self._seq

    @seq.setter
    def seq(self, s):
        """
        List used to create the Sequence's graph.

        Parameters
        -----------

        s (Iterable): Sequence list.

        Members of 's' represents sequence nodes. If they are coroutines
        they are automagically converted to :class:'Action' nodes.
        """
        from collections.abc import Iterable

        assert isinstance(s, Iterable)
        self._seq = list(map(make_node, s))
        # print("SEQ", self._seq)

    @property
    def G(self):
        """returns the graph object"""
        return self.graph

    async def start_step(self):
        """Standard sequence's start step.

        Sets the sequence's state to RUNNING
        """
        # logger.info(
        # "This is the <start> of a sequence: %s", Sequence.current_seq.get()
        # )
        user_logger.info("Sequence %s starting", self.name)
        self.state = T_STATE.RUNNING
        if self.debug:
            breakpoint()

    async def end_step(self):
        """Standard sequence's end step.

        Evaluates the sequence's final state.
        Collects node's result and put them in the sequence's result attribute.
        """
        user_logger.info("Sequence %s finish", self.name)
        G = self.graph
        states = [
            G.nodes[key]["node"].in_error for key in nx.topological_sort(G)
        ]
        # grab node's state and check for ERROR
        self.in_error = True if any(states) else False
        self.state = T_STATE.FINISHED # This publishes node's state.
        # collects node results
        # self.result = {
        #     key: G.nodes[key]["node"].result for key in nx.topological_sort(G)
        # }
        # #logger.debug("collected results (%s): %s", self.id, self.result)

    @property
    def start_node(self):
        """Returns the start node.

        If it does not exist, it creates it."""
        if self._start_node is None:
            self._start_node = StartNode(
                self.start_step, name="begin", id="begin_" + self.id
            )
        return self._start_node

    @property
    def end_node(self):
        """Returns the end node.

        if self._end_node is None:
            self._end_node = EndNode(
                self.end_step, name="end", id="end_" + self.id
            )
        return self._end_node

    def append(self, s):
        """Appends a node to the Sequence"""
        self.seq.append(s)

        """Builds this sequence execution graph.
        Joins the Sequence's nodes together.
        """
        if ctrl:

        self.graph = G = nx.DiGraph()
        G.add_node(self.start_node.id, node=self.start_node)
        parent = self.start_node
        for el in self.seq:
            el.make_sequence(parent_tpl) # pass down link to Tpl if any
            el.state = T_STATE.NOT_STARTED
            G.add_node(el.id, node=el)
            G.add_edge(parent.id, el.id)
            parent = el

        leaf_nodes = [
            node
            for node in G.nodes()
            if G.in_degree(node) != 0 and G.out_degree(node) == 0
        ]
        G.add_node(self.end_node.id, node=self.end_node)
        self.start_node.state = self.end_node.state = T_STATE.NOT_STARTED

        for l in leaf_nodes:
            G.add_edge(l, self.end_node.id)

    def create_node_tasks(self, resume=False):
        """Creates Task object associated to this node"""
        g_list = list(nx.topological_sort(G))
        g_nodes = [G.nodes[key] for key in g_list]
        if resume:
            g_nodes = [
                node
                for node in g_nodes
                if node["node"].state != T_STATE.FINISHED
            ]
        for node in g_nodes:
            key = node["node"].id
            in_edges = [u for u, v in G.in_edges(key)]
            # logger.debug("%s -> IN edges: %s", self._name(node), in_edges)
            input_list = [G.nodes[key] for key in in_edges]
            new_task = node["node"].make_task(node, input_list, resume)
            node["task"] = new_task
            node["node"].state = T_STATE.SCHEDULED

    def reschedule_node(self, node_id):
        """Reschedule a node for execution"""
        # id = node.id
        g_node = G.nodes[node_id]  # get Graph's node dict
        node = g_node["node"]
        task = g_node["task"]
        # reset state
        node.state = T_STATE.SCHEDULED
        task.reset(resume=True)

    def start(self, make_sequence=True, resume=False):
        """This is the entry point for Sequence execution.

        Returns:
            Returns the :class:`SeqTask` object that executes the sequence

        Raises:
            Exception: Any exception received is re-raised and the sequence is aborted.

        """
        if resume:
            make_sequence = False

        context = self.context
        context["started"] = datetime.now()
        context["name"] = self.name
        if make_sequence:
            self.make_sequence()
        task = asyncio.create_task(self.execute(resume))
        # logger.debug("TASK ....: %s", task)
        return task

    async def run(self):
        """Runs the node -- This executes the task"""
        from ..ob import RTFLAG

        result = None
        # logger.debug("check pause flag")

        # logger.debug("check skip flag")
            user_logger.info("Sequence %s skipped", self.name)

        else:
            try:
                task = self.main_task()
                result = await task.run()
            except asyncio.CancelledError:
                self.state = T_STATE.ABORTED
                raise
            except Exception as e:
                self.in_error = True
                self.state = T_STATE.CANCELLED
                self.exception = e
                raise

        return result

    async def execute(self, resume=False, propagate=False):
        """Executes node -- this just creates the asyncio task"""

        # if propagate:
        #     for el in self.seq:
        #         el._parameters = self.parameters
        # logger.info("execute with _params (prop=%s): %s| %s", propagate, self.name, self.parameters)
        Sequence.current_seq.set(self)
        self.create_node_tasks(resume=resume)
        try:
            _result = await self.run()
        except:
            raise
        finally:
        self.context["finished"] = self.t_end
        return _result

    async def resume(self):
        """Resume node execution"""
        super().resume()
        if self.state != T_STATE.PAUSED:
            await self.start(resume=True)

    def main_task(self):
        """Returns the objective node of the sequence -- the end node"""
        node = self.end_node
        task = G.nodes[node.id]["task"]

    def _name(self, node):
        return node["node"].name

        result = await self.execute(resume)

    def abort(self):
        """
        Aborts the sequence.

        Goes trough the full graph and aborts the tasks associated to nodes (if any).
        Do not allow nodes to run by taking away its running_checkpoint attribute.
        """
        from .utils import Visitor

        for s, node_id in Visitor(self):
            #node = s.get_node(node_id)
            # node.running_checkpoint.clear()
            try:
                task = s.get_task(node_id)
                _ = task and task.abort()
            except KeyError:
                pass

    def nodes(self):
        """Return nodes from Graph"""
        return list(nx.topological_sort(self.G))
        # return self.G.nodes

    def get_node(self, node_id):
        """Get node by id"""
        return self.G.nodes[node_id]["node"]

    def get_task(self, node_id):
        """Get task by node_id"""
        return self.G.nodes[node_id]["task"]

    @property
    def parameters(self):
        """Return parameters"""
        return self._parameters

    def par(self, k):
        """Get a parameter value"""
        value = self._parameters[k]["value"]
        #user_logger.info("Get par: %s -> %s", k , value)
        return value

    def set(self, p):
        """Sets the value of a parameter"""
        if not p.name in self._parameters:
            raise KeyError(p.name)
        logger.debug("Set var: %s -> %s", p.name, p.value)
        self._parameters[p.name]["value"] = p.value

    # def set_params(self, params):
    #     """ "Update parameters from list"""
    #     for k, v in params.items():
    #         self._parameters[k] = v

    @property

        c = ob.OB.controller.get()
        if c:
            c.notify_state_change(self, datetime.now().isoformat())

        super(Sequence, self.__class__).state.fset(self, value)

    @property
    
    def get_context():
        assert Sequence.root
        assert Sequence.current_seq

        return Sequence.current_seq.get().context

    def create(*args, **kw):
        """
        Sequence node constructor.

        Args:
            *args: Variable length list of nodes or coroutines that compose the
                   sequence.

            id: Node id
        """
        tpl = Sequence(**kw)
        tpl.seq = list(args)
        return tpl

"""Implements Parallel node type for the sequenceer.
"""
from .sequence import Sequence

@attr.s
class Parallel(Sequence):
    """
    Parallel node definition.

    Use the :meth:`create` method to build properly :class:`Parallel` nodes.
    Since it inherits from :class:`Sequence` it has access to the same context variables.

    Example:
        One can access the running Sequence using Sequence.current_seq context variable.

        >>> s = Parallel.create(a,b, name="my sequence")
        ...     assert current_seq == s

    """

    seq_args = attr.ib(kw_only=True, default=attr.Factory(list), repr=False)

            self.name = "Par"
        self.seq = self.seq_args

    def __repr__(self):
        return "<Parallel (%s)...>" % (self.id)

        st = self.start_node
        end = self.end_node

        G.add_node(st.id, node=st)
        G.add_node(end.id, node=end)
        st.state = end.state = T_STATE.NOT_STARTED
        if ctrl:
            G.add_edge(st.id, el.id)
            G.add_edge(el.id, end.id)
            el.make_sequence(parent_tpl)

        """Creates a :class:`Parallel` node

       Args:
            *args: Variable length list of nodes or coroutines that compose the sequence.

            id: Node id
        """
        a = Parallel(seq_args=args, **kw)
        return a

# loop.py
"""Implements Loops for the sequencer.

A Loop node is composed of three parts.

    * *Initialization* (``optional``) initiates variables to be used
       in the loop's activity and its exit-condition; it is executed
       once on entry to the loop

    * *condition* (``callable``) Check the loop's condition.
        It shall return *False* to interrupt the Loop. Otherwise it
        shall *return* **True**.

    * *block* (``Sequence``) The loop's body. It is the sequence of
       nodes to be executed repeatedly until the Loop's condition
       produces a False value.

"""
from .action import make_node

@attr.s
class Loop(Sequence):
    """Loop node definition.

    Use the :meth:`create` method to build properly :class:`Loop` node.

    ============  =======================================================
    ---------------------------------------------------------------------
    Name           Desc
    current_seq   The parent of the current node (from :class:`Sequence`)
    root          Top level DAG's root (from :class:`Sequence`)
    index         The Loop's current index (starts at 0)

        id (Optional str): Node id.
        name (Optional str): Node name.
        init (Optional): Initialization node.
        condition (callale):  A Python method that returns a boolean value.
        block (list): The loop's body.
    """

    # my attributes
    block_args = attr.ib(kw_only=True, default=attr.Factory(list), repr=False)
    condition = attr.ib(kw_only=True, default=None, repr=False)
    init = attr.ib(kw_only=True, default=None, repr=False)
    block = attr.ib(init=False, default=None, repr=False)
    node_value = attr.ib(init=False, default=0, repr=False)
        
    # Loop's contextvar
    index = cv.ContextVar("index", default=0)

            self.name = "Loop"
        self.set_block(*self.block_args)

        if self.condition is None:
            self.condition = self._condition
        self.condition = make_node(self.condition)
        self.condition.can_skip = False

        if self.init is None:
            self.init = self._init_loop
        self.init = make_node(self.init)
        self.init.can_skip = False
    
        """Standard Loop's end step.
        Evaluates the Loop's final state.
        """
        self.state = T_STATE.FINISHED
        user_logger.info("Loop %s finished", self.name)
        states = [
        ]
        if any(states):

        """Creates the Loop execution graph"""

        G.add_node(self.condition.id, node=self.condition, label="Cond")
        if self.block:
            G.add_node(self.block.id, node=self.block, label="Loop body")
        # Adds runtime flags
        if ctrl:

    def set_block(self, *args):
        """Assigns the sequence to the loop's block"""
        if args:
            self.block = Sequence.create(
                *args, name="block", id="block_" + self.id
            )
            self.block.make_sequence(self.parent_tpl)

    def make_task(self, node, input_list, resume):
        from ..seqtask import LoopTask

        return LoopTask(self.id, node, input_list, resume=resume)

    async def _condition(self):
        return False

    async def _init_loop(self):
        pass

        return [
            self.start_node.id,
            self.condition.id,
            self.block.id,
            self.end_node.id,
        ]

        # return self.block.get_node(node_id)
        if node_id == self.condition.id:
            return self.condition
        elif node_id == self.block.id:
            return self.block
        elif node_id == self.init.id:
            return self.init
        elif node_id == self.start_node.id:
            return self.start_node
        elif node_id == self.end_node.id:
            return self.end_node

        else:
            return self.block.get_node(node_id)

        """Creates a :class:`Loop` node

        Args:
             *args: Variable length list of nodes or coroutines that comprises the Loop`s body.

         Keyword Args:
             id: Node id
             name: node name
             init (node) : initialization node :class:`Action` or :class:`ActionInThread`.
             condition(node): condition node :class:`Action` or :class:`ActionInThread`.

         Returns:
             A new :class:`Loop` object

         Example:

             Creating a loop.

         .. code-block:: python

             def eval_condition():
                 return False

             class Tpl:
                 def initialize(self, context):
                     # performs some initialization
                     pass
                 async def a():
                     pass
                 async def b():
                     pass

                 @staticmethod
                 def create()
                     t = MyClass()
                     l = Loop.create(t.a, t.b, condition=eval_condition, init=t.initialize)
        """
        a = Loop(block_args=args, **kw)
        return a

"""The Template node implementation"""
# pylint: disable= invalid-name, too-many-instance-attributes

from dataclasses import dataclass

from .utils import seq_factory

@dataclass
class TemplateParam:
    """Holds the name and value of some Template Parameter"""
    name: str
    value: None

@attr.s
class Template(Sequence):
    """The Template node

        A :class:`Template` is just a :class:`Sequence` node with access to a set
        of variables as defined in an :class:`ObservingBlock`.

        The :class:`ObservingBlock` takes care of instantiating the :class:`Template`
        objects as needed by the Observation Block.

        ============  =======================================
        Context Variables
        -----------------------------------------------------
        Name           Desc
        current_tpl   The current Template being executed

    .. warning:: Objects of type :class:`Template` should not be directly constructed.
        Leave that for the :class:`ObservingBlock`.

    """

    tpl_name = attr.ib(default=None)
    # To get default param values from file.
    params = attr.ib(default=attr.Factory(list), repr=False)
    content = attr.ib(default=attr.Factory(dict), repr=False)
    mod = attr.ib(default=None, repr=False, init=False)
    _validator = attr.ib(default=attr.Factory(dict), repr=False, init=False)

    # context variables
    P = cv.ContextVar("P", default={})
    current_tpl = cv.ContextVar("current_tpl", default=None)

        """
        """
        for el in self.params:
            self._parameters[el["name"]] = el

        Template.P.set(self._parameters)
        logger.info("execute templates: %s", self.parameters)
        return await super().execute(resume, propagate=True)

        """
        super().make_sequence(parent_tpl=self)

    def from_dict(d):
        """
        Creates a Template from keywords in dictionary.
        """
        t = Template(
            tpl_name=d["templateName"],
            params=d.get("parameters", {}),
            content=d,
        )
        user_logger.info("Loading Template module: %s", t.tpl_name)
        t.mod = ob.ModuleLoader.from_mod(t.tpl_name)
        return t

    # pylint: disable=arguments-differ
    def create(d, *args, **kw):
        """Creates a :class:`Template` node

        Args:
             *args: Variable length list of nodes or coroutines that composes the sequence.

            id: Node id.
            name: node name.
        """
        a = Template.from_dict(d)
        # get module's and ctor documentation
        ctor_docs = getattr(a.mod.ctor, "__doc__", None) or ""
        mod_docs = getattr(a.mod.module, "__doc__", None) or ""
        a.description = ctor_docs.strip().split("\n")[0] or "No description"
        a.name = mod_docs.strip().split("\n")[0] or "Template"
        # creates the actual sequence
        a.seq = [seq_factory(a.mod, *args, **kw)]
        return a

"""Observing Block class.
Represents a Sequence from an OB file (json).
"""
# pylint: disable=invalid-name, too-many-instance-attributes, logging-format-interpolation

from .template import Template
from ..exc import SeqException

value_converter = {"string": str, "number": float, "integer": int}

@attr.s
class ObservingBlock(Sequence):
    """
    Represents a Sequence from an OB file (json).
    The JSON file contains a lot of data, however the Sequencer only cares
    about the **templates** section::

        "templates": [
                {
                    "templateName": "seq.samples.a",
                    "type": "string"
                },
                {
                    "templateName": "seq.samples.tpa",
                    "type": "string",
                    "parameters": [
                        {
                            "name": "par_b",
                            "type": "integer",
                            "value": 0
                        },
                        {
                            "name": "par_c",
                            "type": "number",
                            "value": 77
                        }
                    ]
                }
            ],

    Inside the **templates** section there is  list of *templateName* objects.
    The *templateName* defines a Python module that can be directly
    imported (it is reachable from PYTHONPATH).

    Each template might contain a list of *parameters* that can be accessed
    from corresponding python code that implements the template.

    """

    fname = attr.ib(default=None)
    content = attr.ib(default=attr.Factory(dict), repr=False, init=False)
    template_parameters = {}
    current_tpl_params = attr.ib(default=attr.Factory(dict), init=False)
    ctor = attr.ib(default=None, init=False)
    descr = attr.ib(default=attr.Factory(dict), init=False)
    from_otto = attr.ib(default=False, kw_only=True)

        """
        """
            self.name = "OB"
        self.load()

    @property
        """Returns a dictionary with template's parameters

        The returned dictionary enumerates the OB's templates as keys and the values are
        Template's parameters
        """
        return {k: t.parameters for k, t in enumerate(self.seq)}

    def load(self):
        """
        Loads json file
        """
        user_logger.info("Loading OB: %s", self.fname)
        assert self.fname
        # check file exists and size > 0
        p = Path(self.fname)
        if not p.exists():
            raise SeqException(
                None,
                None,
                "File does not exists, can't load: %s" % self.fname,
            ) from None
        if p.stat().st_size == 0:
                None,
                None,
                "Invalid file content, can't load: %s" % self.fname,
            ) from None
        # load and p
        with open(self.fname) as fd:
            self.content = json.load(fd)
            self.descr = self.content.get("obsDescription", {})
            name = self.descr.get("name", "OB name")
            self.name = "{}({})".format(name, self.content.get("obId", "eng"))

    def _make_sequence(self, *args, parent=None, **kw):
        self.in_error = False
        self.state = T_STATE.NOT_STARTED

        self.seq = [
            Template.create(d, parent, *args, **kw)
            for d in self.content["templates"]
        ]
        return self

        if c:
            c.notify_state_change(
                self,
                datetime.now().isoformat(),
                obId=self.content.get("obId", ""),
                otto_report=self.from_otto,
            )

    def create(f, *args, **kw):
        """Creates a :class:`ObservingBlock` node

        Args:
            f: JSON file with OB definition

             id: Node id
        """
        a = ObservingBlock(f, **kw)
        logger.debug("Create ObservingBlock -- sn: %d", a.serial_number)
        kw.pop("from_otto", False)
        a._make_sequence(*args, **kw)  # pylint: disable=protected-access
        return a

    @property
    def module(self):
        """Returns OB filename"""
        return "OB_{}".format(self.fname)

    @property
    def doc(self):
        """Returns OB name from the corresponding kw"""
        return "OB:{} ({})".format(
            self.descr.get("name", "<name>"), self.descr.get("obId", "ID")
        )

    def clone(self, *args, **kw):
        """Fake ctor for OB nodes"""
        self.serial_number = Counter.new_value()
        return self._make_sequence(*args, **kw)