class Wat(dictobj.Spec): one = dictobj.Field(format_into=sb.string_spec) two = dictobj.Field(format_into=sb.string_spec) @property def thing(self): return "{0}.{1}".format(self.one, self.two)
class Result(dictobj.Spec): specs = dictobj.Field(sb.dictof(spec_key_spec(), sb.has("normalise"))) extra = dictobj.Field( no_such_key_spec("Use extras instead (notice the s!)")) extras = dictobj.Field( sb.listof(sb.tuple_spec(sb.string_spec(), sb.tupleof(sb.string_spec()))))
class TilePacmanOptions(AnimationOptions): user_coords = dictobj.Field(sb.boolean, default=False) num_iterations = dictobj.Field(sb.integer_spec, default=-1) def final_iteration(self, iteration): if self.num_iterations == -1: return False return self.num_iterations <= iteration
class Addon(dictobj.Spec): name = dictobj.Field(sb.string_spec) extras = dictobj.Field( sb.listof(sb.tuple_spec(sb.string_spec(), sb.string_spec()))) resolver = dictobj.Field(sb.any_spec) namespace = dictobj.Field(sb.string_spec) class BadHook(DelfickError): desc = "Bad Hook" @property def resolved(self): errors = [] if getattr(self, "_resolved", None) is None: try: self._resolved = list(self.resolver()) except Exception as error: errors.append( self.BadHook("Failed to resolve a hook", name=self.name, namespace=self.namespace, error=str(error))) if errors: raise self.BadHook(_errors=errors) return self._resolved def process(self, collector): for result in self.resolved: if collector is not None: collector.register_converters(result.get("specs", {}), Meta, collector.configuration, sb.NotSpecified) def post_register(self, **kwargs): list(self.resolver(post_register=True, **kwargs)) def unresolved_dependencies(self): for namespace, name in self.extras: yield (namespace, name) def resolved_dependencies(self): for result in self.resolved: for namespace, names in result.get("extras", []): if not isinstance(names, (tuple, list)): names = (names, ) for name in names: yield (namespace, name) def dependencies(self, all_deps): for dep in self.unresolved_dependencies(): yield dep if hasattr(self, "_resolved"): for dep in self.resolved_dependencies(): yield dep
class C(dictobj.Spec): hue = dictobj.Field(sb.float_spec, default=h) saturation = dictobj.Field(sb.float_spec, default=s) brightness = dictobj.Field(sb.float_spec, default=b) kelvin = dictobj.Field(sb.integer_spec, default=k) @property def color(self): return Color(self.hue, self.saturation, self.brightness, self.kelvin)
class TileTwinklesOptions(AnimationOptions): num_iterations = dictobj.Field(sb.integer_spec, default=-1) palette = dictobj.Field(choose_palette()) num_twinkles = dictobj.Field(sb.integer_spec, default=20) fade_in_speed = dictobj.Field(sb.float_spec, default=0.125) fade_out_speed = dictobj.Field(sb.float_spec, default=0.078) def final_iteration(self, iteration): if self.num_iterations == -1: return False return self.num_iterations <= iteration
class BackgroundOption(dictobj.Spec): type = dictobj.Field(sb.string_choice_spec(["specified", "current"]), default="specified") hue = dictobj.Field(sb.float_spec, default=0) saturation = dictobj.Field(sb.float_spec, default=0) brightness = dictobj.Field(sb.float_spec, default=0) kelvin = dictobj.Field(sb.float_spec, default=3500) @property def default_color(self): return Color(self.hue, self.saturation, self.brightness, self.kelvin)
class TileFallingOptions(AnimationOptions): num_iterations = dictobj.Field(sb.integer_spec, default=-1) random_orientations = dictobj.Field(sb.boolean, default=False) hue_ranges = dictobj.NullableField(split_by_comma(hue_range_spec()), default=[]) line_tip_hue = dictobj.NullableField(hue_range_spec(), default=HueRange(60, 60)) blinking_pixels = dictobj.Field(sb.boolean, default=True) def final_iteration(self, iteration): if self.num_iterations == -1: return False return self.num_iterations <= iteration
class Capability(dictobj.Spec): """Represents the capability for a device""" name = dictobj.Field(sb.string_spec, wrapper=sb.required) company = dictobj.Field(sb.string_spec, wrapper=sb.required) identifier = dictobj.Field(sb.string_spec, wrapper=sb.required) has_color = dictobj.Field(sb.boolean, default=True) has_ir = dictobj.Field(sb.boolean, default=False) has_multizone = dictobj.Field(sb.boolean, default=False) has_variable_color_temp = dictobj.Field(sb.boolean, default=True) has_chain = dictobj.Field(sb.boolean, default=False) min_kelvin = dictobj.Field(sb.integer_spec, default=2500) max_kelvin = dictobj.Field(sb.integer_spec, default=9000)
class TileMarqueeOptions(AnimationOptions): text_color = dictobj.Field(ColorOption(200, 0.24, 0.5, 3500)) text = dictobj.Field(sb.string_spec, default="LIFX is awesome!") user_coords = dictobj.Field(sb.boolean, default=False) num_iterations = dictobj.Field(sb.integer_spec, default=-1) direction = dictobj.Field(enum_spec(None, MarqueeDirection, unpacking=True), default=MarqueeDirection.LEFT) @property def text_width(self): return len(self.text) * 8 def final_iteration(self, iteration): if self.num_iterations == -1: return False return self.num_iterations <= iteration
class TileGameOfLifeOptions(AnimationOptions): user_coords = dictobj.Field(sb.boolean, default=True) num_iterations = dictobj.Field(sb.integer_spec, default=-1) new_color_style = dictobj.Field(sb.string_choice_spec(["random", "average"]), default="average") iteration_delay = dictobj.Field(sb.float_spec, default=0.1) def final_iteration(self, iteration): if self.num_iterations == -1: return False return self.num_iterations <= iteration def make_new_color(self, surrounding): if self.new_color_style == "random": return Color(random.randrange(0, 360), 1, 1, 3500) else: return Color.average(surrounding)
class Options(dictobj.Spec): colors = dictobj.Field(sb.listof(Color.FieldSpec()), wrapper=sb.required) theme = dictobj.Field(sb.string_choice_spec(appliers.keys()), default="SPLOTCH") duration = dictobj.Field(sb.float_spec(), default=1) hue = dictobj.NullableField(sb.float_spec()) saturation = dictobj.NullableField(sb.float_spec()) brightness = dictobj.NullableField(sb.float_spec()) kelvin = dictobj.NullableField(sb.float_spec()) @property def overrides(self): o = {} for key in ("duration", "hue", "saturation", "brightness", "kelvin"): if self[key] is not None: o[key] = self[key] return o
class SocketTarget(TransportTarget): """ A subclass of ``photons_protocol.target.traget.TransportTarget`` using our ``SocketItem`` and ``SocketBridge`` """ item_kls = lambda s: SocketItem bridge_kls = lambda s: SocketBridge description = dictobj.Field( sb.string_spec, default="Understands how to talk to a device over a TCP socket")
class Collection(dictobj.Spec): """ Represents either a group or a location. It understands the relationship between label and updated_at such that the collections' name corresponds to the name with the newest updated_at property """ typ = dictobj.Field(sb.string_spec, wrapper=sb.required) uuid = dictobj.Field(sb.string_spec, wrapper=sb.required) name = dictobj.Field(sb.string_spec, default="") def setup(self, *args, **kwargs): super(Collection, self).setup(*args, **kwargs) self.newest_timestamp = None def add_name(self, timestamp, name): if self.newest_timestamp is None or self.newest_timestamp < timestamp: self.name = name self.newest_timestamp = timestamp def __eq__(self, other): return isinstance( other, Collection) and self.typ == other.typ and self.uuid == other.uuid
class TileNyanOptions(AnimationOptions): user_coords = dictobj.Field(sb.boolean, default=False) num_iterations = dictobj.Field(sb.integer_spec, default=-1) random_orientations = dictobj.Field(sb.boolean, default=False) @property def direction(self): return MarqueeDirection.RIGHT @property def text_width(self): return 11 @property def text_color(self): class Color: color = None return Color def final_iteration(self, iteration): if self.num_iterations == -1: return False return self.num_iterations <= iteration
class DocOptions(dictobj.Spec): out = dictobj.Field(wrapper=sb.required, format_into=sb.string_spec) src = dictobj.Field(wrapper=sb.required, format_into=sb.directory_spec)
class TileDiceRollOptions(AnimationOptions): num_iterations = dictobj.Field(sb.integer_spec, default=1) roll_time = dictobj.Field(sb.float_spec, default=2) dice_color = dictobj.Field(ColorOption(200, 1, 1, 3500))
class Filter(dictobj.Spec): """ The options for a filter. Usage looks like: .. code-block:: python filtr = Filter.FieldSpec().empty_normalise(force_refresh=True, firmware_version=1.22) # or filtr = Filter.from_json_str('{"force_refresh": true, "firmware_version": 1.22}') # or filtr = Filter.from_options({"force_refresh": True, "firmware_version": 1.22}) # or filtr = Filter.from_kwargs(force_refresh=True, firmware_version=1.22) # or filtr = Filter.from_key_value_str("force_refresh=true firmware_version=1.22") # or filtr = Filter.from_url_str("force_refresh=true&firmware_version=1.22") .. automethod:: photons_device_finder.Filter.from_options .. automethod:: photons_device_finder.Filter.from_kwargs .. automethod:: photons_device_finder.Filter.empty .. automethod:: photons_device_finder.Filter.from_json_str .. automethod:: photons_device_finder.Filter.from_key_value_str .. automethod:: photons_device_finder.Filter.from_url_str .. autoattribute:: photons_device_finder.Filter.matches_all .. automethod:: photons_device_finder.Filter.matches .. automethod:: photons_device_finder.Filter.has Finally, we have ``has`` which takes in a ``field_name`` and says whether """ force_refresh = dictobj.Field(boolean, default=False) serial = dictobj.Field(sb.listof(sb.string_spec()), wrapper=sb.optional_spec) label = dictobj.Field(sb.listof(sb.string_spec()), wrapper=sb.optional_spec) power = dictobj.Field(sb.listof(sb.string_spec()), wrapper=sb.optional_spec) group_id = dictobj.Field(sb.listof(sb.string_spec()), wrapper=sb.optional_spec) group_name = dictobj.Field(sb.listof(sb.string_spec()), wrapper=sb.optional_spec) location_id = dictobj.Field(sb.listof(sb.string_spec()), wrapper=sb.optional_spec) location_name = dictobj.Field(sb.listof(sb.string_spec()), wrapper=sb.optional_spec) hue = dictobj.Field(str_ranges, wrapper=sb.optional_spec) saturation = dictobj.Field(str_ranges, wrapper=sb.optional_spec) brightness = dictobj.Field(str_ranges, wrapper=sb.optional_spec) kelvin = dictobj.Field(str_ranges, wrapper=sb.optional_spec) firmware_version = dictobj.Field(sb.listof(sb.string_spec()), wrapper=sb.optional_spec) product_id = dictobj.Field(sb.listof(sb.integer_spec()), wrapper=sb.optional_spec) product_identifier = dictobj.Field(sb.listof(sb.string_spec()), wrapper=sb.optional_spec) cap = dictobj.Field(sb.listof(sb.string_spec()), wrapper=sb.optional_spec) @classmethod def from_json_str(kls, s): """ Interpret s as a json string and use it to create a Filter using from_options """ try: options = json.loads(s) except (TypeError, ValueError) as error: raise InvalidJson(error=error) else: if type(options) is not dict: raise InvalidJson("Expected a dictionary", got=type(options)) return kls.from_options(options) @classmethod def from_key_value_str(kls, s): """ Create a Filter based on the ``key=value key2=value2`` string provided. Each key=value pair is separated by a space and arrays are formed by separating values by a comma. Note that values may not have spaces in them because of how we split the key=value pairs. If you need values to have spaces use from_json_str or from_options. """ options = {} for part in s.split(" "): m = regexes["key_value"].match(part) if m: groups = m.groupdict() if groups["key"] not in ("hue", "saturation", "brightness", "kelvin", "force_refresh"): options[groups["key"]] = groups["value"].split(',') else: options[groups["key"]] = groups["value"] return kls.from_options(options) @classmethod def from_url_str(kls, s): """ Create a Filter based on ``key=value&otherkey=value2`` string provided Where the string is url encoded. """ return kls.from_options(parse_qs(s)) @classmethod def from_kwargs(kls, **kwargs): """Create a Filter based on the provided kwarg arguments""" return kls.from_options(kwargs) @classmethod def empty(kls, force_refresh=False): """Create an empty filter""" return kls.from_options({"force_refresh": force_refresh}) @classmethod def from_options(kls, options): """Create a Filter based on the provided dictionary""" if isinstance(options, dict): for option in options: if option not in kls.fields: log.warning( hp.lc("Unknown option provided for filter", wanted=option)) return kls.FieldSpec().normalise(Meta.empty(), options) def has(self, field): """Say whether the filter has an opinion on this field""" return field in self.fields and self[field] != sb.NotSpecified def matches(self, field_name, val): """ Says whether this filter matches against provided filed_name/val pair * Always say False for ``force_refresh`` * Say False if the value on the filter for field_name is NotSpecified * Say True if a hsbk value and we are within the range specified in val * Say True if value on the filter is a list, and val exists in that list * Say True if value on the filter is not a list and matches val """ if field_name == "force_refresh": return False if field_name in self.fields: f = self[field_name] if f is not sb.NotSpecified: if field_name in ("hue", "saturation", "brightness", "kelvin"): return any(val >= pair[0] and val <= pair[1] for pair in f) if field_name in self.label_fields and type(val) is str: if type(f) is list: return any(fnmatch.fnmatch(val, pat) for pat in f) else: return fnmatch.fnmatch(val, f) if type(f) is list: if type(val) is list: return any(v in val for v in f) else: return val in f else: return val == f return False @property def matches_all(self): """True if this Filter matches against any device""" for field in self.fields: if field != "force_refresh": if self[field] != sb.NotSpecified: return False return True @property def points(self, for_info=False): """Provide InfoPoints enums that match the keys on this filter with values""" for e in InfoPoints: for key in e.value.keys: if self[key] != sb.NotSpecified: yield e @property def label_fields(self): return ("product_identifier", "label", "location_name", "group_name")
class Device(dictobj.Spec): """ An object representing a single device. Users shouldn't have to interact with these directly """ serial = dictobj.Field(sb.string_spec, wrapper=sb.required) label = dictobj.Field(sb.string_spec, wrapper=sb.optional_spec) power = dictobj.Field(sb.string_spec, wrapper=sb.optional_spec) group = dictobj.Field(sb.any_spec, wrapper=sb.optional_spec) location = dictobj.Field(sb.any_spec, wrapper=sb.optional_spec) hue = dictobj.Field(sb.integer_spec, wrapper=sb.optional_spec) saturation = dictobj.Field(sb.float_spec, wrapper=sb.optional_spec) brightness = dictobj.Field(sb.float_spec, wrapper=sb.optional_spec) kelvin = dictobj.Field(sb.integer_spec, wrapper=sb.optional_spec) firmware_version = dictobj.Field(sb.string_spec, wrapper=sb.optional_spec) product_id = dictobj.Field(sb.integer_spec, wrapper=sb.optional_spec) product_identifier = dictobj.Field(sb.string_spec, wrapper=sb.optional_spec) cap = dictobj.Field(sb.listof(sb.string_spec()), wrapper=sb.optional_spec) @property def property_fields(self): return ["group_id", "group_name", "location_name", "location_id"] @property def group_id(self): if self.group is sb.NotSpecified: return sb.NotSpecified return self.group.uuid @property def group_name(self): if self.group is sb.NotSpecified: return sb.NotSpecified return self.group.name @property def location_name(self): if self.location is sb.NotSpecified: return sb.NotSpecified return self.location.name @property def location_id(self): if self.location is sb.NotSpecified: return sb.NotSpecified return self.location.uuid def as_dict(self): actual = super(Device, self).as_dict() del actual["group"] del actual["location"] for key in self.property_fields: actual[key] = self[key] return actual def matches(self, filtr): """ Say whether we match against the provided filter """ if filtr.matches_all: return True fields = list(self.fields) + self.property_fields has_atleast_one_field = False for field in fields: val = self[field] if val is not sb.NotSpecified: has_field = filtr.has(field) if has_field: has_atleast_one_field = True if has_field and not filtr.matches(field, val): return False return has_atleast_one_field def set_from_pkt(self, pkt, collections): """ Set information from the provided pkt. collections is used for determining the group/location based on the pkt. We return a InfoPoints enum representing what type of information was set. """ if pkt | LightMessages.LightState: self.label = pkt.label self.power = "off" if pkt.power is 0 else "on" self.hue = pkt.hue self.saturation = pkt.saturation self.brightness = pkt.brightness self.kelvin = pkt.kelvin return InfoPoints.LIGHT_STATE elif pkt | DeviceMessages.StateGroup: uuid = binascii.hexlify(pkt.group).decode() self.group = collections.add_group(uuid, pkt.updated_at, pkt.label) return InfoPoints.GROUP elif pkt | DeviceMessages.StateLocation: uuid = binascii.hexlify(pkt.location).decode() self.location = collections.add_location(uuid, pkt.updated_at, pkt.label) return InfoPoints.LOCATION elif pkt | DeviceMessages.StateHostFirmware: self.firmware_version = pkt.version return InfoPoints.FIRMWARE elif pkt | DeviceMessages.StateVersion: self.product_id = pkt.product capability = capability_for_ids(pkt.product, pkt.vendor) self.product_identifier = capability.identifier cap = [] for prop in ("has_color", "has_ir", "has_multizone", "has_chain", "has_variable_color_temp"): if getattr(capability, prop): cap.append(prop[4:]) else: cap.append("not_{}".format(prop[4:])) self.cap = sorted(cap) return InfoPoints.VERSION
class PhotonsApp(dictobj.Spec): """ The main photons_app object. .. dictobj_params:: """ config = dictobj.Field(sb.file_spec, wrapper=sb.optional_spec, help="The root configuration file") extra = dictobj.Field( sb.string_spec, default="", help="The arguments after the ``--`` in the commandline") debug = dictobj.Field(sb.boolean, default=False, help="Whether we are in debug mode or not") target = dictobj.Field(wrapper=sb.optional_spec, format_into=sb.string_spec, help="The target to use when executing the task") artifact = dictobj.Field(default="", format_into=sb.string_spec, help="The artifact string from the commandline") reference = dictobj.Field(default="", format_into=sb.string_spec, help="The device(s) to send commands to") extra_files = dictobj.Field(sb.string_spec, wrapper=sb.listof, help="Extra files to load") chosen_task = dictobj.Field(default="list_tasks", format_into=sb.string_spec, help="The task that is being executed") cleaners = dictobj.Field( lambda: sb.overridden([]), help= "A list of functions to call when cleaning up at the end of the program" ) final_future = dictobj.Field( sb.overridden("{final_future}"), formatted=True, help="A future representing the end of the program") default_activate_all_modules = dictobj.Field( sb.boolean, default=False, help= "The collector looks at this to determine if we should default to activating all photons modules" ) @memoized_property def loop(self): loop = asyncio.get_event_loop() if self.debug: loop.set_debug(True) return loop @memoized_property def extra_as_json(self): options = "{}" if self.extra in (None, "", sb.NotSpecified) else self.extra try: return json.loads(options) except (TypeError, ValueError) as error: raise BadOption("The options after -- wasn't valid json", error=error) async def cleanup(self, targets): for cleaner in self.cleaners: try: await cleaner() except asyncio.CancelledError: break except KeyboardInterrupt: break except (RuntimeWarning, Exception): exc_info = sys.exc_info() log.error(exc_info[1], exc_info=exc_info) for target in targets: try: if hasattr(target, "finish"): await target.finish() except asyncio.CancelledError: break except KeyboardInterrupt: break except (RuntimeWarning, Exception): exc_info = sys.exc_info() log.error(exc_info[1], exc_info=exc_info)
class Original(dictobj.Spec): one = dictobj.Field(sb.string_spec) two = dictobj.Field(sb.integer_spec, default=3) three = dictobj.Field(sb.string_spec, help="three") four = dictobj.Field(sb.any_spec)
class Original(dictobj.Spec): one = dictobj.Field(sb.string_spec) two = dictobj.Field(sb.integer_spec) three = dictobj.Field(sb.string_spec) four = dictobj.Field(sb.any_spec)
class TransportTarget(dictobj.Spec): """ This is responsible for bringing together the TransportBridge and the TransportItems It implements the ability to create and destroy args_for_run (the bridge), as well as creating a `script` that may be run with `script.run_with`. We also have higher order functions for finding and forgetting devices. When creating your own target do something like: .. code-block:: python class SocketTarget(TransportTarget): item_kls = lambda s: SocketItem bridge_kls = lambda s: SocketBridge description = dictobj.Field(sb.string_spec, default="Understands how to talk to a device over a TCP socket") ``protocol_register`` and ``final_future`` are retrieved automatically from ``Meta`` if we create the transport by doing ``TransportTarget.normalise(meta, **kwargs)`` Note that the path on the meta cannot be root. So make you meta like: .. code-block:: python from input_algorithms.meta import Meta from option_merge import MergedOptions configuration = MergedOptions.using({"protocol_register": ..., "final_future": asyncio.Future()}) # By saying `at("options")` on the meta we are putting it not at root # So when we resolve final_future we don't get recursive option errors meta = Meta(configuration, []).at("options") Generally you'll be passed in a transport via the ``tasks`` mechanism and you won't have to instantiate it yourself. """ protocol_register = dictobj.Field(sb.overridden("{protocol_register}"), formatted=True) final_future = dictobj.Field(sb.overridden("{final_future}"), formatted=True) default_broadcast = dictobj.Field(sb.defaulted(sb.string_spec(), "255.255.255.255")) item_kls = lambda s: TransportItem bridge_kls = lambda s: TransportBridge description = dictobj.Field(sb.string_spec, default="Base transport functionality") @classmethod def create(kls, configuration, options=None): options = options if options is not None else configuration meta = Meta(configuration, []).at("options") return kls.FieldSpec(formatter=MergedOptionStringFormatter).normalise(meta, options) def script(self, raw): """Return us a ScriptRunnerIterator for the given `raw` against this `target`""" items = list(self.simplify(raw)) if len(items) > 1: items = Pipeline(*items) else: items = items[0] return ScriptRunnerIterator(items, target=self) def session(self): info = {} class Session: async def __aenter__(s): afr = info["afr"] = await self.args_for_run() return afr async def __aexit__(s, exc_type, exc, tb): if "afr" in info: await self.close_args_for_run(info["afr"]) return Session() async def args_for_run(self): """Create an instance of args_for_run. This is designed to be shared amongst many `script`""" afr = self.bridge_kls()(self.final_future, self , protocol_register=self.protocol_register , default_broadcast=self.default_broadcast ) await afr.start() return afr async def close_args_for_run(self, args_for_run): """Close an args_for_run""" args_for_run.finish() async def get_list(self, args_for_run, broadcast=sb.NotSpecified, **kwargs): """Return us the targets that we can find from this bridge""" addr = broadcast if broadcast is not sb.NotSpecified else self.default_broadcast found = await args_for_run.find_devices(addr, **kwargs) return sorted([binascii.hexlify(target[:6]).decode() for target in found]) def device_forgetter(self, args_for_run): """Return a function that may be used to forget a device on this args_for_run""" return args_for_run.forget def find(self, args_for_run): """Return a function that may be used to find a device on this args_for_run""" return args_for_run.find def simplify(self, script_part, chain=None): """ Used by ``self.script`` to convert ``raw`` into TransportItems For each leaf child that is found, we gather messages into groups of messages with a ``pack`` method and yield ``self.item_kls()(group)`` with messages that don't have a ``pack`` method yield as is. For example, let's say we have ``[p1, p2, m1, p3]`` where ``m1`` does not have a ``pack`` method and the others do, we'll yield: * ``self.item_kls()([p1, p2])`` * ``m1`` * ``self.item_kls()([p3])`` """ chain = [] if chain is None else chain if type(script_part) is not list: script_part = [script_part] final = [] errors = [] for p in script_part: if getattr(p, "has_children", False): final.append(p.simplified(self.simplify, chain + [p.name])) continue else: if not hasattr(p, "pack"): errors.append(p) else: final.append(p) if errors: raise InvalidScript("Script part has no pack method!", parts=errors, chain=chain) buf = [] for p in final: if hasattr(p, "pack"): buf.append(p) else: if buf: yield self.item_kls()(buf) buf = [] yield p if buf: yield self.item_kls()(buf)
class TileTimeOptions(AnimationOptions): hour24 = dictobj.Field(sb.boolean, default=True) number_color = dictobj.Field(ColorOption(200, 0.24, 0.5, 3500)) progress_bar_color = dictobj.Field(ColorOption(0, 1, 0.4, 3500)) full_height_progress = dictobj.Field(sb.boolean, default=False)
class T(TransportTarget): one = dictobj.Field(sb.integer_spec)
class AnimationOptions(dictobj.Spec): background = dictobj.Field(BackgroundOption.FieldSpec()) combine_tiles = dictobj.Field(sb.boolean, default=False)
class Color(dictobj.Spec): hue = dictobj.Field(sb.integer_spec(), wrapper=sb.required) saturation = dictobj.Field(sb.float_spec(), wrapper=sb.required) brightness = dictobj.Field(sb.float_spec(), wrapper=sb.required) kelvin = dictobj.Field(sb.integer_spec(), wrapper=sb.required)