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)
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
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
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)
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
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)
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)
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]
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()
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)
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("")
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)
def setup(self): self.log_args = (PhotonsAppError("stuff happens", one=1),) self.log_kwargs = { "pkt": DeviceMessages.SetPower(level=65535), "other": [1, 2], "more": True, }
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
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)
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)
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)
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)
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)
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)
async def execute_task(self, notify, output, **kwargs): try: notify() raise PhotonsAppError("WAT") finally: with open(output, "w") as fle: fle.write("FINALLY")
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)
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()
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)
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))
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)
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)
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)
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
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}")