Exemple #1
0
    async def execute_task(self, **kwargs):
        extra = self.photons_app.extra
        assets = self.collector.configuration["arranger"].assets
        available = ["run", "install", "static", "watch"]

        if self.reference is sb.NotSpecified:
            raise PhotonsAppError("Please specify what command to run",
                                  available=available)

        assets.ensure_npm()

        try:
            if self.reference == "install":
                assets.run("install", *shlex.split(extra))
                return

            if self.reference == "run":
                assets.run(*shlex.split(extra))
                return

            if assets.needs_install:
                assets.run("ci", no_node_env=True)

            if self.reference == "static":
                assets.run("run", "build")

            elif self.reference == "watch":
                assets.run("run", "generate")

            else:
                raise PhotonsAppError("Didn't get a recognised command",
                                      want=self.reference,
                                      available=available)
        except subprocess.CalledProcessError as error:
            raise PhotonsAppError("Failed to run command", error=error)
Exemple #2
0
def make_plans(*by_key, **plans):
    """
    Given by_key which is a list of strings and plans which is a dictionary of key to
    plan, return a dictionary of key to plans.

    We will complain if:

    * A key in by_key is not a registered plan
    * A key in by_key is also in plans
    """
    if not by_key and not plans:
        return {}

    count = defaultdict(int)
    for key in by_key:
        count[key] += 1
        if key in plans:
            raise PhotonsAppError(
                "Cannot specify plan by label and by Plan class", specified_twice=key
            )
        if count[key] > 1:
            raise PhotonsAppError(
                "Cannot specify plan by label more than once", specified_multiple_times=key
            )

    for key in by_key:
        if key not in plan_by_key:
            raise PhotonsAppError(
                "No default plan for key", wanted=key, available=list(plan_by_key)
            )
        plans[key] = plan_by_key[key]()

    return plans
Exemple #3
0
    async def cypress(typ):
        try:
            import asynctest  # noqa
        except ImportError:
            raise PhotonsAppError(
                'You must `pip install -e ".[tests]"` before you can run integration tests'
            )

        from whirlwind.test_helpers import free_port, port_connected

        port = free_port()
        env = {"CYPRESS_BASE_URL": f"http://127.0.0.1:{port}"}

        final_future = collector.configuration["photons_app"].final_future

        t = None
        collector.configuration["interactor"].host = "127.0.0.1"
        collector.configuration["interactor"].port = port
        collector.configuration["interactor"].fake_devices = True
        collector.configuration[
            "interactor"].database.uri = "sqlite:///:memory:"

        t = hp.async_as_background(serve(collector))

        start = time.time()
        while time.time() - start < 5:
            if port_connected(port):
                break
            await asyncio.sleep(0.01)

        if not port_connected(port):
            raise PhotonsAppError("Failed to start server for tests")

        loop = asyncio.get_event_loop()

        def doit():
            if os.environ.get("NO_BUILD_ASSETS") != "1":
                log.info("Building assets")
                assets.run("run-script", "build")

            log.info("Running cypress")
            assets.run("run-script", f"cypress:{typ}", extra_env=env)

        try:
            await loop.run_in_executor(None, doit)
        finally:
            exc_info = sys.exc_info()
            photons_app = collector.configuration["photons_app"]

            if exc_info[1]:
                photons_app.graceful_final_future.set_exception(exc_info[1])
            else:
                photons_app.graceful_final_future.set_result(None)

            await t
Exemple #4
0
async def set_chain_state(collector, target, reference, artifact, **kwargs):
    """
    Set the state of colors on your tile

    ``lan:set_chain_state d073d5f09124 -- '{"colors": [[[0, 0, 0, 3500], [0, 0, 0, 3500], ...], [[0, 0, 1, 3500], ...], ...], "tile_index": 1, "length": 1, "x": 0, "y": 0, "width": 8}'``

    Where the colors is a grid of 8 rows of 8 ``[h, s, b, k]`` values.
    """  # noqa
    options = collector.photons_app.extra_as_json

    if "colors" in options:
        spec = sb.listof(
            sb.listof(
                list_spec(sb.integer_spec(), sb.float_spec(), sb.float_spec(),
                          sb.integer_spec())))
        colors = spec.normalise(Meta.empty().at("colors"), options["colors"])

        row_lengths = [len(row) for row in colors]
        if len(set(row_lengths)) != 1:
            raise PhotonsAppError(
                "Please specify colors as a grid with the same length rows",
                got=row_lengths)

        num_cells = sum(len(row) for row in colors)
        if num_cells != 64:
            raise PhotonsAppError("Please specify 64 colors", got=num_cells)

        cells = []
        for row in colors:
            for col in row:
                cells.append({
                    "hue": col[0],
                    "saturation": col[1],
                    "brightness": col[2],
                    "kelvin": col[3]
                })

        options["colors"] = cells
    else:
        raise PhotonsAppError(
            "Please specify colors in options after -- as a grid of [h, s, b, k]"
        )

    missing = []
    for field in TileMessages.Set64.Payload.Meta.all_names:
        if field not in options and field not in ("duration", "reserved6"):
            missing.append(field)

    if missing:
        raise PhotonsAppError("Missing options for the SetTileState message",
                              missing=missing)

    options["res_required"] = False
    msg = TileMessages.Set64.empty_normalise(**options)
    await target.send(msg, reference)
Exemple #5
0
    def make_hsbks(self, colors, overrides):
        results = list(make_hsbks(colors, overrides))

        if len(results) > 82:
            raise PhotonsAppError("colors can only go up to 82 colors",
                                  got=len(results))

        if not results:
            raise PhotonsAppError("No colors were specified")

        return results
Exemple #6
0
async def help(collector, tasks, reference, target, **kwargs):
    """
    Display more help information for specified target:task

    This task takes an extra argument that can be:

    <target>
        A specific target, will show associated tasks for that target

    <target type>
        Will show what targets are available for this type and their
        associated tasks

    <task>
        Will show expanded help information for this task

    You can also be tricky and do something like ``<target>:help`` instead
    of ``help <target>``
    """
    task_name = sb.NotSpecified
    target_name = sb.NotSpecified
    if target is not sb.NotSpecified:
        target_name = collector.configuration["photons_app"].target

    if reference not in (None, "", sb.NotSpecified):
        if ":" in reference:
            target_name, task_name = reference.split(":", 1)
        else:
            task_name = reference

    target_register = collector.configuration["target_register"]
    if task_name in target_register.targets or task_name in target_register.types:
        target_name = task_name
        task_name = sb.NotSpecified

    if target_name is not sb.NotSpecified:
        if target_name in target_register.targets or target_name in target_register.types:
            kwargs["specific_target"] = target_name

        if target_name not in target_register.targets and target_name not in target_register.types:
            raise PhotonsAppError(
                "Sorry, cannot find help for non existing target",
                wanted=target_name)

    if task_name is not sb.NotSpecified:
        kwargs["specific_task"] = task_name
        if task_name not in tasks:
            raise PhotonsAppError(
                "Sorry, cannot find help for non existing task",
                wanted=task_name)

    await list_tasks(collector, tasks, **kwargs)
Exemple #7
0
    def __init__(self, filename):
        self.filename = filename
        try:
            with open(self.filename) as fle:
                serials = [s.strip() for s in fle.readlines() if s.strip()]
        except OSError as error:
            raise PhotonsAppError("Failed to read serials from a file",
                                  filename=self.filename,
                                  error=error)

        if not serials:
            raise PhotonsAppError("Found no serials in file",
                                  filename=self.filename)

        self.reference = HardCodedSerials(serials)
Exemple #8
0
    def spawn(self, address, backoff=0.05, timeout=10):
        """Spawn a socket for this address"""

        if self.stop_fut.cancelled():
            raise PhotonsAppError("The target has been cancelled")

        # Don't care about port, only care about host
        if type(address) is tuple:
            address = address[0]

        if address in self.sockets:
            fut = self.sockets[address]
            if not fut.done():
                return fut

            if not fut.cancelled() and not fut.exception():
                transport = fut.result()
                if self.is_transport_active(transport):
                    return fut

            del self.sockets[address]

        # Initialize a spot for this address
        if address not in self.sockets:
            self.sockets[address] = hp.ChildOfFuture(self.stop_fut)

        # Ok, let's do this!
        self._spawn(address, self.sockets[address], backoff, timeout)

        # And return our future
        return self.sockets[address]
Exemple #9
0
    async def execute_task(self, graceful_final_future, **kwargs):
        directory = os.path.join(self.collector.configuration["documentation"].out, "result")
        http_server = HTTPServer(
            Application(
                [
                    (
                        r"/(.*)",
                        StaticFileHandler,
                        {"path": directory, "default_filename": "index.html"},
                    )
                ]
            )
        )

        port = int(os.environ.get("PHOTONS_DOCS_PORT", 0))

        with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
            s.bind(("0.0.0.0", port))
            port = s.getsockname()[1]

        http_server.listen(port, "127.0.0.1")

        start = time.time()
        while not port_connected(port) and time.time() - start < 3:
            await asyncio.sleep(0.1)

        if not port_connected(port):
            raise PhotonsAppError("Failed to start the server")

        try:
            log.info(f"Running server on http://127.0.0.1:{port}")
            webbrowser.open(f"http://127.0.0.1:{port}")
            await graceful_final_future
        finally:
            http_server.stop()
Exemple #10
0
async def transform(collector, target, reference, **kwargs):
    """
    Do a http-api like transformation over whatever target you specify

    ``target:transform d073d5000000 -- '{"color": "red", "effect": "pulse"}'``

    It takes in ``color``, ``effect``, ``power`` and valid options for a
    ``SetWaveformOptional``.

    You may also specify ``transform_options`` that change how the transform works.

    keep_brightness
        Ignore brightness options in the request

    transition_color
        If the light is off and we power on, setting this to True will mean the
        color of the light is not set to the new color before we make it appear
        to be on. This defaults to False, which means it will appear to turn on
        with the new color
    """
    extra = collector.photons_app.extra_as_json
    extra = sb.dictionary_spec().normalise(Meta.empty(), extra)

    transform_options = sb.set_options(
        transform_options=sb.dictionary_spec()).normalise(
            Meta.empty(), extra)["transform_options"]

    msg = Transformer.using(extra, **transform_options)

    if not msg:
        raise PhotonsAppError(
            'Please specify valid options after --. For example ``transform -- \'{"power": "on", "color": "red"}\'``'
        )

    await target.send(msg, reference)
Exemple #11
0
async def get_chain_state(collector, target, reference, **kwargs):
    """
    Get the colors of the tiles in your chain
    """
    options = collector.configuration['photons_app'].extra_as_json

    missing = []
    for field in TileMessages.GetState64.Payload.Meta.all_names:
        if field not in options and field not in ("reserved6", ):
            missing.append(field)

    if missing:
        raise PhotonsAppError("Missing options for the GetTileState message",
                              missing=missing)

    response_kls = TileMessages.State64

    got = defaultdict(list)

    msg = TileMessages.GetState64.empty_normalise(**options)

    async for pkt, _, _ in target.script(msg).run_with(reference):
        if pkt | response_kls:
            got[pkt.serial].append((pkt.tile_index, pkt))

    for serial, states in got.items():
        print(serial)
        for i, state in sorted(states):
            print("    Tile {0}".format(i))
            for index, color in enumerate(pkt.colors):
                print("        color {0}".format(index), repr(color))
            print("")
Exemple #12
0
async def tile_effect(collector, target, reference, artifact, **kwargs):
    """
    Set an animation on your tile!

    ``lan:tile_effect d073d5000001 <type> -- '{<options>}'``

    Where type is one of the available effect types:

    OFF
        Turn of the animation off

    MORPH
        Move through a perlin noise map, assigning pixel values from a
        16-color palette

    FLAME
        A flame effect

    For effects that take in a palette option, you may specify palette as
    ``[{"hue": 0, "saturation": 1, "brightness": 1, "kelvin": 2500}, ...]``

    or as ``[[0, 1, 1, 2500], ...]`` or as ``[[0, 1, 1], ...]``

    or as ``["red", "hue:100 saturation:1", "blue"]``
    """
    options = collector.photons_app.extra_as_json

    if artifact in ("", None, sb.NotSpecified):
        raise PhotonsAppError("Please specify type of effect with --artifact")

    await target.send(SetTileEffect(artifact, **options), reference)
Exemple #13
0
 def setup(self):
     self.log_args = (PhotonsAppError("stuff happens", one=1),)
     self.log_kwargs = {
         "pkt": DeviceMessages.SetPower(level=65535),
         "other": [1, 2],
         "more": True,
     }
Exemple #14
0
 async def execute_task(self, collector, notify, output,
                        graceful_final_future, **kwargs):
     self._final_future = collector.photons_app.final_future
     self._graceful = graceful_final_future
     self._error = PhotonsAppError("HI")
     notify()
     raise self._error
Exemple #15
0
def noncancelled_results_from_futs(futs):
    """
    Get back (exception, results) from a list of futures

    exception
        A single exception if all the errors are the same type
        or if there is only one exception

        otherwise it is None

    results
        A list of the results that exist
    """
    errors = set()
    results = []
    for f in futs:
        if f.done() and not f.cancelled():
            exc = f.exception()
            if exc:
                errors.add(exc)
            else:
                results.append(f.result())

    if errors:
        errors = list(errors)
        if len(errors) is 1:
            errors = errors[0]
        else:
            errors = PhotonsAppError(_errors=errors)
    else:
        errors = None

    return (errors, results)
Exemple #16
0
            async def execute_task(self, collector, **kwargs):
                called.append(1)

                async def cleanup1():
                    called.append("c1a")
                    fut = hp.create_future()
                    fut.set_result(True)
                    await fut
                    called.append("c1b")

                async def cleanup2():
                    called.append("c2a")
                    fut = hp.create_future()
                    fut.set_result(True)
                    await fut
                    called.append("c2b")

                collector.photons_app.cleaners.extend([cleanup1, cleanup2])
                called.append(2)
                try:
                    raise PhotonsAppError("YO")
                except:
                    fut = hp.create_future()
                    fut.set_result(True)
                    await fut
                    called.append(3)
                    raise
                finally:
                    called.append(4)
Exemple #17
0
    async def execute_task(self, **kwargs):
        extra = self.photons_app.extra_as_json
        positions = sb.listof(sb.listof(sb.float_spec())).normalise(
            Meta.empty(), extra)

        if any(len(position) != 2 for position in positions):
            raise PhotonsAppError(
                "Please enter positions as a list of two item lists of user_x, user_y"
            )

        async def gen(reference, sender, **kwargs):
            ps = sender.make_plans("capability")
            async for serial, _, info in sender.gatherer.gather(
                    ps, reference, **kwargs):
                if info["cap"].has_matrix:
                    for i, (user_x, user_y) in enumerate(positions):
                        yield TileMessages.SetUserPosition(
                            tile_index=i,
                            user_x=user_x,
                            user_y=user_y,
                            res_required=False,
                            target=serial,
                        )

        await self.target.send(FromGenerator(gen), self.reference)
Exemple #18
0
async def set_tile_positions(collector, target, reference, **kwargs):
    """
    Set the positions of the tiles in your chain.

    ``lan:set_tile_positions d073d5f09124 -- '[[0, 0], [-1, 0], [-1, 1]]'``
    """
    extra = collector.photons_app.extra_as_json
    positions = sb.listof(sb.listof(sb.float_spec())).normalise(
        Meta.empty(), extra)
    if any(len(position) != 2 for position in positions):
        raise PhotonsAppError(
            "Please enter positions as a list of two item lists of user_x, user_y"
        )

    async def gen(reference, sender, **kwargs):
        ps = sender.make_plans("capability")
        async for serial, _, info in sender.gatherer.gather(
                ps, reference, **kwargs):
            if info["cap"].has_matrix:
                for i, (user_x, user_y) in enumerate(positions):
                    yield TileMessages.SetUserPosition(
                        tile_index=i,
                        user_x=user_x,
                        user_y=user_y,
                        res_required=False,
                        target=serial,
                    )

    await target.send(FromGenerator(gen), reference)
Exemple #19
0
    async def run_with(self, references, args_for_run, **kwargs):
        error_catcher = kwargs.get("error_catcher")
        if not callable(error_catcher):
            raise PhotonsAppError(
                "error_catcher must be specified as a callable when Repeater is used"
            )

        while True:
            start = time.time()

            for m in self.msg:
                async for info in m.run_with(references, args_for_run,
                                             **kwargs):
                    yield info

            if isinstance(references, SpecialReference):
                references.reset()

            if callable(self.on_done_loop):
                try:
                    await self.on_done_loop()
                except Repeater.Stop:
                    break

            took = time.time() - start
            diff = self.min_loop_time - took
            if diff > 0:
                await asyncio.sleep(diff)
Exemple #20
0
async def multizone_effect(collector, target, reference, artifact, **kwargs):
    """
    Set an animation on your multizone device

    ``lan:multizone_effect d073d5000001 <type> -- '{<options>}'``

    Where type is one of the available effect types:

    OFF
        Turn the animation off

    MOVE
        A moving animation

    Options include:
    - offset
    - speed
    - duration
    """
    options = collector.photons_app.extra_as_json

    if artifact in ("", None, sb.NotSpecified):
        raise PhotonsAppError("Please specify type of effect with --artifact")

    await target.send(SetZonesEffect(artifact, **options), reference)
Exemple #21
0
 async def execute_task(self, notify, output, **kwargs):
     try:
         notify()
         raise PhotonsAppError("WAT")
     finally:
         with open(output, "w") as fle:
             fle.write("FINALLY")
Exemple #22
0
    async def set_futures(self):
        """Get results from the result_queue and set that result on the appropriate future"""
        while True:
            res = await self.result_queue.get()
            if self.stop_fut.finished():
                break

            if not res:
                continue

            if type(res) is tuple and len(res) is 3:
                key, result, exception = res
            else:
                error = PhotonsAppError("Unknown item on queue", got=res)
                self.onerror(error)
                continue

            try:
                self.find_and_set_future(key, result, exception)
            except asyncio.CancelledError:
                break
            except KeyboardInterrupt:
                raise
            except:
                exc_info = sys.exc_info()
                self.onerror(exc_info)
                log.error(exc_info[1], exc_info=exc_info)
Exemple #23
0
    async def execute_task(self, **kwargs):
        task_name = sb.NotSpecified
        target_name = self.target

        if self.reference is not sb.NotSpecified:
            if ":" in self.reference:
                target_name, task_name = task_specifier_spec().normalise(
                    Meta.empty(), self.reference)
            else:
                task_name = self.reference

        target_register = self.collector.configuration["target_register"]

        if task_name in target_register.registered or task_name in target_register.types:
            target_name = task_name
            task_name = sb.NotSpecified

        for name, target in target_register.created.items():
            if target is target_name:
                target_name = name
                break

        if target_name is not sb.NotSpecified:
            if target_name in target_register.registered or target_name in target_register.types:
                kwargs["specific_target"] = target_name

            if (target_name not in target_register.registered
                    and target_name not in target_register.types):
                raise PhotonsAppError(
                    "Sorry, cannot find help for non existing target",
                    wanted=target_name)

        if task_name is not sb.NotSpecified:
            kwargs["specific_task"] = task_name
            if task_name not in task_register:
                raise PhotonsAppError(
                    "Sorry, cannot find help for non existing task",
                    wanted=task_name,
                    available=task_register.names,
                )

        await task_register.fill_task(
            self.collector,
            list_tasks,
            specific_task_groups=self.specific_task_groups,
            **kwargs).run()
Exemple #24
0
    async def execute_task(self, **kwargs):
        overrides = self.photons_app.extra_as_json

        if self.artifact is sb.NotSpecified:
            raise PhotonsAppError("Please specify a color as artifact")

        msg = ColourParser.msg(self.artifact, overrides)
        await self.target.send(msg, self.reference)
Exemple #25
0
 async def restart_session(self):
     if self.last_final_future is None or self.last_final_future.done():
         raise PhotonsAppError(
             "The IO does not have a valid final future to restart the session from"
         )
     await self.shutting_down(Events.SHUTTING_DOWN(self.device))
     await self.start_session(self.last_final_future, self.parent_ts)
     await self.power_on(Events.POWER_ON(self.device))
Exemple #26
0
    async def execute_task(self, **kwargs):
        options = self.photons_app.extra_as_json

        if self.artifact is sb.NotSpecified:
            raise PhotonsAppError(
                "Please specify type of effect with --artifact")

        await self.target.send(SetZonesEffect(self.artifact, **options),
                               self.reference)
Exemple #27
0
    async def execute_task(self, **kwargs):
        options = self.photons_app.extra_as_json

        if "colors" not in options:
            raise PhotonsAppError(
                """Say something like ` -- '{"colors": [["red", 10], ["blue", 3]]}'`"""
            )

        await self.target.send(SetZones(**options), self.reference)
Exemple #28
0
 def listener_impl(self, nxt, *args):
     """Just call out to process"""
     if type(nxt) is tuple and len(nxt) is 2:
         key, proc = nxt
         self.process(key, wraps(proc)(self.wrap_request(proc, args)))
     else:
         error = PhotonsAppError("Unknown item in the queue", got=nxt)
         self.onerror(error)
         log.error(error)
Exemple #29
0
 async def __anext__(self):
     self.index += 1
     if self.index == 0:
         return (
             DeviceMessages.StatePower(level=0, target="d073d5000001"),
             ("192.168.0.1", 56700),
             "192.168.0.1",
         )
     elif self.index == 1:
         self.kwargs["error_catcher"](PhotonsAppError("failure", serial="d073d5000002"))
         raise StopAsyncIteration
Exemple #30
0
    async def open_browser(self):
        async with hp.tick(0.1, max_time=3) as ticker:
            async for _ in ticker:
                if port_connected(self.options.port):
                    break

        if not port_connected(self.options.port):
            self.photons_app.final_future.set_exception(
                PhotonsAppError("Failed to start the server"))
            return

        if "NO_WEB_OPEN" not in os.environ:
            webbrowser.open(f"http://{self.options.host}:{self.options.port}")