"""herethere.there.commands.core"""
import asyncio
from dataclasses import dataclass
from functools import wraps
import time
from typing import Callable, TextIO
import click
from herethere.there.client import Client
class EmptyCode(Exception):
    """Command was started without code."""
class NeedDisplay(Exception):
    """Background command was started without display."""
    def __init__(self, maxlen: int):
        self.maxlen = maxlen
        super().__init__("Display required.")
@dataclass
class ContextObject:
    """Context to pass to `there` group commands."""
    client: Client
    code: str
    stdout: TextIO = None
    stderr: TextIO = None
    background: bool = False
    def runcode(self):
        """Execute python code on the remote side."""
        if not self.code:
            raise EmptyCode("Code to execute is not specified.")
        # prepend with "\n" so error message line matches cell line number
        code = "# %%there ... \n" + self.code
        if self.background:
            self._run_background_command("runcode_background", code)
        else:
            self._run_command("runcode", code)
    def shell(self):
        """Execute shell command on the remote side."""
        if not self.code:
            raise EmptyCode("Code to execute is not specified.")
        if self.background:
            self._run_background_command("shell", self.code)
        else:
            self._run_command("shell", self.code)
    def _run_command(self, command: str, code: str):
        """Execute SSH command with a code."""
        handler = getattr(self.client, command)
        asyncio.run(handler(code, stdout=self.stdout, stderr=self.stderr))
    def _run_background_command(self, command: str, code: str):
        """Execute SSH command with a code, in background."""
        async def run():
            client = await self.client.copy()
            handler = getattr(client, command)
            await handler(code, stdout=self.stdout, stderr=self.stderr)
            await client.disconnect()
        asyncio.create_task(run())
@click.group(invoke_without_command=True)
@click.option(
    "-b", "--background", is_flag=True, default=False, help="Run in background"
)
@click.option(
    "-l",
    "--limit",
    default=24,
    type=click.IntRange(1, 1000),
    help="Number of lines to show when in background mode",
)
@click.option(
    "-d",
    "--delay",
    type=float,
    default=0,
    help="The time to wait in seconds before executing a command",
)
@click.pass_context
def there_group(ctx, background, limit, delay):
    """Group of commands to run on remote side."""
    if background:
        if not all((ctx.obj.stdout, ctx.obj.stderr)):
            raise NeedDisplay(limit)
        ctx.obj.background = True
    if delay:
        time.sleep(delay)
    if ctx.invoked_subcommand is None:
        # Execute python code if no command specified
        ctx.obj.runcode()
@there_group.command()
@click.pass_context
def shell(ctx):
    """Execute shell command on remote side."""
    ctx.obj.shell()
@there_group.command()
@click.pass_context
@click.argument("localpaths", type=click.Path(exists=True), nargs=-1, required=True)
@click.argument("remotepath", nargs=1)
def upload(ctx, localpaths, remotepath):
    """Upload files and directories to `remotepath`."""
    if len(localpaths) == 1:
        localpaths = localpaths[0]
    asyncio.run(ctx.obj.client.upload(localpaths, remotepath))
[docs]def there_code_shortcut(
    handler: Callable[[str], str]
) -> Callable[[click.Context], None]:
    """Decorator to register %there subcommand to execute Python code.
    :param handler:
        a function, that receives text from the Jupyter cell,
        and returns Python code to execute on the remote side
    """
    @there_group.command(handler.__name__)
    @click.pass_context
    @wraps(handler)
    def _wrapper(ctx, *args, **kwargs):
        ctx.obj.code = handler(ctx.obj.code, *args, **kwargs)
        ctx.obj.runcode()
    _wrapper.__click_params__ = getattr(handler, "__click_params__", [])
    return _wrapper