Interactive Commands

When you use the commander to create commands, you can make a command that can take in multiple requests to the same command. For example:

@store.command("interactive")
class Interactive(store.Command):
    progress_cb = store.injected("progress_cb")

    max_commands = dictobj.Field(sb.integer_spec, default=10)

    async def execute(self, messages):
        count = 0
        interactives = []

        # Good idea to give a progress cb saying we have started
        # So that clients know they can add more commands
        self.progres_cb("started")

        async for message in messages:
            count += 1
            t = message.process()

            if t.interactive:
                interactives.append(t)
            else:
                res = await message.process()
                self.progress_cb({
                    "processed": message.command.__name__,
                    "result": res
                })

            if count >= self.max_commands:
                break

        if interactives:
            for i in interactives:
                i.cancel()
            await asyncio.wait([interactives])

@store.command("command1", parent=Interactive)
class Command1(store.Command):
    option1 = dictobj.Field(sb.string_spec, wrapper=sb.required)

    async def execute(self):
        return self.option1

@store.command("command2", parent=Interactive)
class Command2(store.Command):
    flag = dictobj.Field(sb.boolean, default=False)

    async def execute(self):
        return self.flag

@store.command("command3", parent=Interactive)
class Command3(store.Command):
    flag = dictobj.Field(sb.boolean, default=False)

    async def execute(self, messages):
        self.progress_cb("started")

        async for message in messages:
            # Important to mark the message as received
            message.no_process()

            self.progress_cb(f"got {message.command.__name__}")

@store.command("command4", parent=Command3)
class Command4(store.Command):
    async def execute(self):
        pass

Interactive commands may themselves have interactive commands as children and when you call message.process() you will get a task back that represents the completion of that command.

You can determine if the message is also interactive by accessing the interactive property on the message. And you can access the command itself by looking at message.command. If you don’t want to run the execute method on the command, then run message.no_process() so that finishing the parent command doesn’t hang waiting for the message to be resolved.

Note

interactive commands and their children are not exposed to the http endpoints that can be made from the commander.

To use the children commands you supply message_id as a tuple of all the message_id values that led to that command.

So say we started the Interactive command above with:

{
  "path": "/v1",
  "body": {
    "command": "interactive",
    "message_id": "MSG1",
    "args": {"max_commands": 5}
  }
}

We can then add a command by saying:

{
  "path": "/v1",
  "body": {
    "command": "command1",
    "message_id": ["MSG1", "MSG2"]
    "args": {"option1": 5}
  }
}

And to go deeper we could do:

{
  "path": "/v1",
  "body": {
    "command": "command3",
    "message_id": ["MSG1", "MSG3"]
    "args": {"option1": 5}
  }
}

{
  "path": "/v1",
  "body": {
    "command": "command4",
    "message_id": ["MSG1", "MSG3", "MSG4"]
  }
}