def parse_target(self, meta, target_options, target_name, tokens): tokens = [ (tokenize.NAME, "dict"), *[(toknum, tokval) for toknum, tokval, *_ in tokens], ] overrides = {} untokenized = tokenize.untokenize(tokens) try: val = ast.parse(untokenized) except SyntaxError as e: raise BadSpecValue( "Target options must be valid dictionary syntax", error=e, got=untokenized) else: for kw in val.body[0].value.keywords: try: overrides[kw.arg] = ast.literal_eval(kw.value) except ValueError: bad = self.determine_bad(kw.value, untokenized) raise BadSpecValue( f"target options can only be python literals: not ({bad})" ) if "options" not in target_options: target_options["options"] = {} target_options["options"].update(overrides) return self.target_spec.normalise(meta, target_options)
def make_command(self, meta, val, existing): v = sb.set_options(path=sb.required(sb.string_spec()), allow_ws_only=sb.defaulted(sb.boolean(), False)).normalise( meta, val) path = v["path"] allow_ws_only = v["allow_ws_only"] if path not in self.paths: raise NoSuchPath(path, sorted(self.paths)) val = sb.set_options(body=sb.required( sb.set_options(args=sb.dictionary_spec(), command=sb.required(sb.string_spec())))).normalise( meta, val) args = val["body"]["args"] name = val["body"]["command"] if existing: name = val["body"]["command"] = f"{existing['path']}:{name}" extra_context = {} if existing: extra_context["_parent_command"] = existing["command"] everything = meta.everything if isinstance(meta.everything, MergedOptions): everything = meta.everything.wrapped() everything.update(extra_context) meta = Meta(everything, []).at("body") available_commands = self.paths[path] if name not in available_commands: raise BadSpecValue( "Unknown command", wanted=name, available=self.available(available_commands, allow_ws_only=allow_ws_only), meta=meta.at("command"), ) command = available_commands[name]["spec"].normalise( meta.at("args"), args) if not allow_ws_only and command.__whirlwind_ws_only__: raise BadSpecValue( "Command is for websockets only", wanted=name, available=self.available(available_commands, allow_ws_only=allow_ws_only), meta=meta.at("command"), ) return command, name
def normalise_filled(self, meta, val): val = sb.string_spec().normalise(meta, val) if len(val) != 12: raise BadSpecValue("serial must be 12 characters long", got=len(val)) try: binascii.unhexlify(val) except binascii.Error: raise BadSpecValue("serial must be a hex value") return val
def normalise_filled(self, meta, val): if not val: raise BadSpecValue("Animation option must be non empty", meta=meta) if isinstance(val, str): val = [val] if isinstance(val, (list, tuple)) and hasattr(val[0], "resolve"): val = val[0] if hasattr(val, "resolve"): return val if isinstance(val, (list, tuple)): if len(val) == 1: val = [val[0], sb.NotSpecified, sb.NotSpecified] if len(val) == 2: val = [val[0], sb.NotSpecified, val[1]] val = {"name": val[0], "background": val[1], "options": val[2]} if not hasattr(val["name"], "resolver"): val["name"] = resolve(val["name"]) return val["name"].resolver(val["options"], val["background"])
def normalise_filled(self, meta, val): val = sb.listof(self.spec).normalise(meta, val) if len(val) != self.length: raise BadSpecValue( "Expected certain number of parts", want=self.length, got=len(val), meta=meta ) return val
def normalise(self, meta, val): if type(val) not in (tuple, list): raise BadSpecValue("Expected a list", got=type(val), meta=meta) if len(val) != len(self.specs): raise BadSpecValue( "Expected a value with certain number of items", wanted=len(self.specs), got=len(val), meta=meta, ) res = [] for i, v in enumerate(val): res.append(self.specs[i].normalise(meta.indexed_at(i), v)) return res
def normalise_filled(self, meta, val): if val in ("on", "ON", True, 1): return "on" elif val in ("off", "OFF", False, 0): return "off" raise BadSpecValue("Power must be on/off, True/False or 0/1", wanted=val, meta=meta)
def normalise_filled(self, meta, val): if val == "rainbow": return HueRange(0, 360) was_list = False if type(val) is list: was_list = True if len(val) not in (1, 2): raise BadSpecValue("A hue range must be 2 or 1 items", got=val, meta=meta) if len(val) == 1: val = [val[0], val[0]] try: val = int(val) if val < 0 or val > 360: raise BadSpecValue("A hue number must be between 0 and 360", got=val, meta=meta) val = [val, val] except (ValueError, TypeError): pass if type(val) is str: val = val.split("-", 1) for part in val: if type(part) is str and (not part or not part.isdigit()): msg = "Hue range must be the string 'rainbow' or a string of '<min>-<max>'" if was_list: msg = f"{msg} or a list of [<min>, <max>]" raise BadSpecValue(msg, got=val, meta=meta) rnge = [int(p) for p in val] for i, number in enumerate(rnge): if number < 0 or number > 360: raise BadSpecValue("A hue number must be between 0 and 360", got=number, meta=meta.indexed_at(i)) return HueRange(*rnge)
def normalise_filled(self, meta, val): val = self.spec.normalise(meta, val) if val < self.minimum or val > self.maximum: raise BadSpecValue( "Value must be between min and max values", wanted=val, minimum=self.minimum, maximum=self.maximum, meta=meta, ) return val
def normalise_filled(self, meta, val): val = self.spec.normalise(meta, val) if val < self.minimum or val > self.maximum: raise BadSpecValue( "Number must be between min and max", minimum=self.minimum, maximum=self.maximum, got=val, meta=meta, ) return val
def normalise(self, meta, val): if val is sb.NotSpecified: return 0 if isinstance(val, bool): return 0 if val is False else 0xFFFF elif isinstance(val, int): return 0 if val == 0 else 0xFFFF elif val in ("on", "off"): return 0 if val == "off" else 0xFFFF else: raise BadSpecValue("Unknown value for power", got=val, meta=meta)
def normalise_filled(self, meta, val): val = sb.string_spec().normalise(meta, val) if not val.startswith("d073d5"): raise BadSpecValue("serials must start with d073d5", got=val, meta=meta) if len(val) != 12: raise BadSpecValue( "serials must be 12 characters long, like d073d5001337", got=val, meta=meta) try: binascii.unhexlify(val) except binascii.Error as error: raise BadSpecValue("serials must be valid hex", error=error, got=val, meta=meta) return val
def normalise_filled(self, meta, val): if isinstance(val, int): return False if val == 0 else True elif isinstance(val, str): return False if val.lower() in ("no", "false") else True elif isinstance(val, list): if len(val) != 1: raise BadSpecValue( "Lists can only be turned into a boolean if they have only one item", got=len(val), meta=meta, ) return boolean().normalise(meta.indexed_at(0), val[0]) return sb.boolean().normalise(meta, val)
def normalise_filled(self, meta, value): if isinstance(value, str): value = value.split("-") if len(value) == 1: value *= 2 elif isinstance(value, (int, float)): value = (value, value) if not isinstance(value, (list, tuple)): raise BadSpecValue("Speed option must be 'min-max' or [min, max]", got=value, meta=meta) kls = Rate if self.rate else Range return kls(value[0], value[1], **self.kwargs)
def normalise(self, meta, val): if "HARDCODED_DISCOVERY" in os.environ and self.see_env: meta = Meta(meta.everything, []).at("${HARDCODED_DISCOVERY}") try: val = json.loads(os.environ["HARDCODED_DISCOVERY"]) except (TypeError, ValueError) as error: raise BadSpecValue( "Found HARDCODED_DISCOVERY in environment, but it was not valid json", reason=error, meta=meta, ) if val in (sb.NotSpecified, None): return val return self.spec.normalise(meta, val)
def normalise(self, meta, val): if isinstance(val, Services): return val val = sb.string_spec().normalise(meta, val) available = [] for name, member in Services.__members__.items(): if not name.startswith("RESERVED"): available.append(name) if name == val: return member raise BadSpecValue("Unknown service type", want=val, available=sorted(available), meta=meta)
def normalise_filled(self, meta, val): if type(val) is str: try: v = json.loads(val) except (TypeError, ValueError) as error: raise BadSpecValue("Value was not valid json", error=error, meta=meta) else: normalised = self.spec.normalise(meta, v) if not self.storing: return normalised return json.dumps(normalised, default=lambda o: repr(o)) else: v = self.spec.normalise(meta, val) if not self.storing: return v return json.dumps(v)
def initial_parse(self, val): lines = [val.encode(), b""] readline = lambda size=-1: lines.pop(0) try: tokens = list(tokenize.tokenize(readline)) except tokenize.TokenError as e: raise BadSpecValue("Failed to parse specifier", error=e) if tokens[0].type is tokenize.ENCODING: tokens.pop(0) if tokens[-1].type is tokenize.ENDMARKER: tokens.pop(-1) if tokens[-1].type is tokenize.NEWLINE: tokens.pop(-1) return tokens
def normalise(self, meta, val): if val is sb.NotSpecified: return hp.Color(0, 0, 1, 3500) keys = ("hue", "saturation", "brightness", "kelvin") if isinstance(val, (list, tuple)): while len(val) < 4: val = (*val, 0) elif isinstance(val, dict): for k in keys: if k not in val: val = {**val, k: 0} val = tuple(val[k] for k in keys) elif any(hasattr(val, k) for k in keys): val = tuple(getattr(val, k, 0) for k in keys) else: raise BadSpecValue("Unknown value for color", got=val, meta=meta) return hp.Color(*val[:4])
def parse_overridden_target(self, meta, val, collector, target_register, target_name, tokens): if target_name not in target_register.targets: raise TargetNotFound(name=target_name, available=list( target_register.targets.keys())) if tokens[0].string != "(" or tokens[-1].string != ")": raise BadSpecValue( "target with options should have options in parenthesis", got=val) parent_target_options = collector.configuration.get( ["targets", target_name], ignore_converters=True).as_dict() target = self.parse_target(meta, parent_target_options, target_name, tokens) name = f"{target_name}_{uuid.uuid4().hex}" target_register.add_target(name, target) return name
def expand(self, meta, val): if isinstance(val, str): return {"UDP": {"host": val, "port": 56700}} if isinstance(val, list): val = {"UDP": val} v = {} for service, options in val.items(): if isinstance(options, str): options = {"host": options, "port": 56700} elif isinstance(options, list): if len(options) not in (1, 2): raise BadSpecValue("A list must be [host] or [host, port]", got=options, meta=meta.at(service)) if len(options) == 1: options = {"host": options[0], "port": 56700} else: options = {"host": options[0], "port": options[1]} v[service] = options return v
def normalise_empty(self, meta): raise BadSpecValue("Preset must have a non empty list of animations", meta=meta)
"d073d500133": "192.168.0.14", "e073d5001338": "192.168.0.15", "d073d5zz1339": "192.168.0.16", True: "192.168.0.17", } class S: def __init__(s, expected): s.expected = expected def __eq__(s, other): return s.expected == str(other) e1 = BadSpecValue( "serials must be 12 characters long, like d073d5001337", got="d073d500133", meta=meta.at("d073d500133"), ) e2 = BadSpecValue( "serials must start with d073d5", got="e073d5001338", meta=meta.at("e073d5001338") ) e3 = BadSpecValue( "serials must be valid hex", error=S("Non-hexadecimal digit found"), got="d073d5zz1339", meta=meta.at("d073d5zz1339"), ) e4 = BadSpecValue("Expected a string", got=bool, meta=meta.at(True)) errors = [e1, e2, e3, e4] with assertRaises(BadSpecValue, _errors=errors):
def interpret(self, meta, val): if not isinstance(val, (tuple, list, str)): raise BadSpecValue("Each color specifier must be a list or string", got=val, meta=meta) if isinstance(val, str): val = val.split(",") if len(val) == 0: return elif len(val) == 1: val = (*val, (1, 1), (1, 1), (3500, 3500)) elif len(val) == 2: val = (*val, (1, 1), (3500, 3500)) if val[0] == "rainbow": val[2] = (1, 1) elif len(val) == 3: val = (*val, (3500, 3500)) elif len(val) > 4: raise BadSpecValue("Each color must be 4 or less specifiers", got=val, meta=meta) result = [] for i, v in enumerate(val): m = meta.indexed_at(i) if not isinstance(v, (tuple, list, str)): raise BadSpecValue( "Each color specifier must be a list or string", got=val, meta=m) if i != 0 and v == "rainbow": raise BadSpecValue("Only hue may be given as 'rainbow'", meta=m) if v == "rainbow": result.append((0, 360)) continue if isinstance(v, str): v = v.split("-") if isinstance(v, (int, float)): v = [v] if len(v) > 2: raise BadSpecValue("A specifier must be two values", got=v, meta=m) if len(v) == 0: continue if len(v) == 1: v = v * 2 if i in (1, 2): result.append((float(v[0]) * 1000, float(v[1]) * 1000)) else: result.append((float(v[0]), float(v[1]))) return OneColorRange(*result)
def normalise_filled(self, meta, val): val = sb.listof(self.spec).normalise(meta, val) if not val: raise BadSpecValue( "Preset must have a non empty list of animations", meta=meta) return val