From 3926f3224f1d9146e24fd53e8b14fa5565c4b9ce Mon Sep 17 00:00:00 2001 From: Teemu R Date: Sun, 7 Nov 2021 02:41:12 +0100 Subject: [PATCH 01/17] Add module support & query their information during update cycle (#243) * Add module support & modularize existing query This creates a base to expose more features on the supported devices. At the moment, the most visible change is that each update cycle gets information from all available modules: * Basic system info * Cloud (new) * Countdown (new) * Antitheft (new) * Schedule (new) * Time (existing, implements the time/timezone handling) * Emeter (existing, partially separated from smartdevice) * Fix imports * Fix linting * Use device host instead of alias in module repr * Add property to list available modules, print them in cli state report * usage: fix the get_realtime query * separate usage from schedule to avoid multi-inheritance * Fix module querying * Add is_supported property to modules --- kasa/cli.py | 29 +++- kasa/modules/__init__.py | 10 ++ kasa/modules/antitheft.py | 9 ++ kasa/modules/cloud.py | 50 ++++++ kasa/modules/countdown.py | 6 + kasa/modules/emeter.py | 20 +++ kasa/modules/module.py | 59 +++++++ kasa/modules/rulemodule.py | 83 ++++++++++ kasa/modules/schedule.py | 6 + kasa/modules/time.py | 34 +++++ kasa/modules/usage.py | 40 +++++ kasa/smartbulb.py | 11 +- kasa/smartdevice.py | 46 +++++- kasa/smartplug.py | 6 + kasa/smartstrip.py | 7 + poetry.lock | 305 +++++++++++++++++++++---------------- pyproject.toml | 3 +- 17 files changed, 587 insertions(+), 137 deletions(-) create mode 100644 kasa/modules/__init__.py create mode 100644 kasa/modules/antitheft.py create mode 100644 kasa/modules/cloud.py create mode 100644 kasa/modules/countdown.py create mode 100644 kasa/modules/emeter.py create mode 100644 kasa/modules/module.py create mode 100644 kasa/modules/rulemodule.py create mode 100644 kasa/modules/schedule.py create mode 100644 kasa/modules/time.py create mode 100644 kasa/modules/usage.py diff --git a/kasa/cli.py b/kasa/cli.py index 696dd9aab..f2fd66d12 100755 --- a/kasa/cli.py +++ b/kasa/cli.py @@ -221,7 +221,7 @@ async def state(ctx, dev: SmartDevice): click.echo() click.echo(click.style("\t== Generic information ==", bold=True)) - click.echo(f"\tTime: {await dev.get_time()}") + click.echo(f"\tTime: {dev.time} (tz: {dev.timezone}") click.echo(f"\tHardware: {dev.hw_info['hw_ver']}") click.echo(f"\tSoftware: {dev.hw_info['sw_ver']}") click.echo(f"\tMAC (rssi): {dev.mac} ({dev.rssi})") @@ -236,6 +236,13 @@ async def state(ctx, dev: SmartDevice): emeter_status = dev.emeter_realtime click.echo(f"\t{emeter_status}") + click.echo(click.style("\n\t== Modules ==", bold=True)) + for module in dev.modules.values(): + if module.is_supported: + click.echo(click.style(f"\t+ {module}", fg="green")) + else: + click.echo(click.style(f"\t- {module}", fg="red")) + @cli.command() @pass_dev @@ -430,7 +437,7 @@ async def led(dev, state): @pass_dev async def time(dev): """Get the device time.""" - res = await dev.get_time() + res = dev.time click.echo(f"Current time: {res}") return res @@ -488,5 +495,23 @@ async def reboot(plug, delay): return await plug.reboot(delay) +@cli.group() +@pass_dev +async def schedule(dev): + """Scheduling commands.""" + + +@schedule.command(name="list") +@pass_dev +@click.argument("type", default="schedule") +def _schedule_list(dev, type): + """Return the list of schedule actions for the given type.""" + sched = dev.modules[type] + for rule in sched.rules: + print(rule) + else: + click.echo(f"No rules of type {type}") + + if __name__ == "__main__": cli() diff --git a/kasa/modules/__init__.py b/kasa/modules/__init__.py new file mode 100644 index 000000000..dd9d1072f --- /dev/null +++ b/kasa/modules/__init__.py @@ -0,0 +1,10 @@ +# flake8: noqa +from .antitheft import Antitheft +from .cloud import Cloud +from .countdown import Countdown +from .emeter import Emeter +from .module import Module +from .rulemodule import Rule, RuleModule +from .schedule import Schedule +from .time import Time +from .usage import Usage diff --git a/kasa/modules/antitheft.py b/kasa/modules/antitheft.py new file mode 100644 index 000000000..c885a70c2 --- /dev/null +++ b/kasa/modules/antitheft.py @@ -0,0 +1,9 @@ +"""Implementation of the antitheft module.""" +from .rulemodule import RuleModule + + +class Antitheft(RuleModule): + """Implementation of the antitheft module. + + This shares the functionality among other rule-based modules. + """ diff --git a/kasa/modules/cloud.py b/kasa/modules/cloud.py new file mode 100644 index 000000000..32d3a26d0 --- /dev/null +++ b/kasa/modules/cloud.py @@ -0,0 +1,50 @@ +"""Cloud module implementation.""" +from pydantic import BaseModel + +from .module import Module + + +class CloudInfo(BaseModel): + """Container for cloud settings.""" + + binded: bool + cld_connection: int + fwDlPage: str + fwNotifyType: int + illegalType: int + server: str + stopConnect: int + tcspInfo: str + tcspStatus: int + username: str + + +class Cloud(Module): + """Module implementing support for cloud services.""" + + def query(self): + """Request cloud connectivity info.""" + return self.query_for_command("get_info") + + @property + def info(self) -> CloudInfo: + """Return information about the cloud connectivity.""" + return CloudInfo.parse_obj(self.data["get_info"]) + + def get_available_firmwares(self): + """Return list of available firmwares.""" + return self.query_for_command("get_intl_fw_list") + + def set_server(self, url: str): + """Set the update server URL.""" + return self.query_for_command("set_server_url", {"server": url}) + + def connect(self, username: str, password: str): + """Login to the cloud using given information.""" + return self.query_for_command( + "bind", {"username": username, "password": password} + ) + + def disconnect(self): + """Disconnect from the cloud.""" + return self.query_for_command("unbind") diff --git a/kasa/modules/countdown.py b/kasa/modules/countdown.py new file mode 100644 index 000000000..9f3e59c16 --- /dev/null +++ b/kasa/modules/countdown.py @@ -0,0 +1,6 @@ +"""Implementation for the countdown timer.""" +from .rulemodule import RuleModule + + +class Countdown(RuleModule): + """Implementation of countdown module.""" diff --git a/kasa/modules/emeter.py b/kasa/modules/emeter.py new file mode 100644 index 000000000..f8144e39e --- /dev/null +++ b/kasa/modules/emeter.py @@ -0,0 +1,20 @@ +"""Implementation of the emeter module.""" +from ..emeterstatus import EmeterStatus +from .usage import Usage + + +class Emeter(Usage): + """Emeter module.""" + + def query(self): + """Prepare query for emeter data.""" + return self._device._create_emeter_request() + + @property # type: ignore + def realtime(self) -> EmeterStatus: + """Return current energy readings.""" + return EmeterStatus(self.data["get_realtime"]) + + async def erase_stats(self): + """Erase all stats.""" + return await self.call("erase_emeter_stat") diff --git a/kasa/modules/module.py b/kasa/modules/module.py new file mode 100644 index 000000000..1f7f3829f --- /dev/null +++ b/kasa/modules/module.py @@ -0,0 +1,59 @@ +"""Base class for all module implementations.""" +import collections +from abc import ABC, abstractmethod +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from kasa import SmartDevice + + +# TODO: This is used for query construcing +def merge(d, u): + """Update dict recursively.""" + for k, v in u.items(): + if isinstance(v, collections.abc.Mapping): + d[k] = merge(d.get(k, {}), v) + else: + d[k] = v + return d + + +class Module(ABC): + """Base class implemention for all modules. + + The base classes should implement `query` to return the query they want to be + executed during the regular update cycle. + """ + + def __init__(self, device: "SmartDevice", module: str): + self._device: "SmartDevice" = device + self._module = module + + @abstractmethod + def query(self): + """Query to execute during the update cycle. + + The inheriting modules implement this to include their wanted + queries to the query that gets executed when Device.update() gets called. + """ + + @property + def data(self): + """Return the module specific raw data from the last update.""" + return self._device._last_update[self._module] + + @property + def is_supported(self) -> bool: + """Return whether the module is supported by the device.""" + return "err_code" not in self.data + + def call(self, method, params=None): + """Call the given method with the given parameters.""" + return self._device._query_helper(self._module, method, params) + + def query_for_command(self, query, params=None): + """Create a request object for the given parameters.""" + return self._device._create_request(self._module, query, params) + + def __repr__(self) -> str: + return f"" diff --git a/kasa/modules/rulemodule.py b/kasa/modules/rulemodule.py new file mode 100644 index 000000000..e73b2d03e --- /dev/null +++ b/kasa/modules/rulemodule.py @@ -0,0 +1,83 @@ +"""Base implementation for all rule-based modules.""" +import logging +from enum import Enum +from typing import Dict, List, Optional + +from pydantic import BaseModel + +from .module import Module, merge + + +class Action(Enum): + """Action to perform.""" + + Disabled = -1 + TurnOff = 0 + TurnOn = 1 + Unknown = 2 + + +class TimeOption(Enum): + """Time when the action is executed.""" + + Disabled = -1 + Enabled = 0 + AtSunrise = 1 + AtSunset = 2 + + +class Rule(BaseModel): + """Representation of a rule.""" + + id: str + name: str + enable: bool + wday: List[int] + repeat: bool + + # start action + sact: Optional[Action] + stime_opt: TimeOption + smin: int + + eact: Optional[Action] + etime_opt: TimeOption + emin: int + + # Only on bulbs + s_light: Optional[Dict] + + +_LOGGER = logging.getLogger(__name__) + + +class RuleModule(Module): + """Base class for rule-based modules, such as countdown and antitheft.""" + + def query(self): + """Prepare the query for rules.""" + q = self.query_for_command("get_rules") + return merge(q, self.query_for_command("get_next_action")) + + @property + def rules(self) -> List[Rule]: + """Return the list of rules for the service.""" + try: + return [ + Rule.parse_obj(rule) for rule in self.data["get_rules"]["rule_list"] + ] + except Exception as ex: + _LOGGER.error("Unable to read rule list: %s (data: %s)", ex, self.data) + return [] + + async def set_enabled(self, state: bool): + """Enable or disable the service.""" + return await self.call("set_overall_enable", state) + + async def delete_rule(self, rule: Rule): + """Delete the given rule.""" + return await self.call("delete_rule", {"id": rule.id}) + + async def delete_all_rules(self): + """Delete all rules.""" + return await self.call("delete_all_rules") diff --git a/kasa/modules/schedule.py b/kasa/modules/schedule.py new file mode 100644 index 000000000..62371692b --- /dev/null +++ b/kasa/modules/schedule.py @@ -0,0 +1,6 @@ +"""Schedule module implementation.""" +from .rulemodule import RuleModule + + +class Schedule(RuleModule): + """Implements the scheduling interface.""" diff --git a/kasa/modules/time.py b/kasa/modules/time.py new file mode 100644 index 000000000..0bd3f1714 --- /dev/null +++ b/kasa/modules/time.py @@ -0,0 +1,34 @@ +"""Provides the current time and timezone information.""" +from datetime import datetime + +from .module import Module, merge + + +class Time(Module): + """Implements the timezone settings.""" + + def query(self): + """Request time and timezone.""" + q = self.query_for_command("get_time") + + merge(q, self.query_for_command("get_timezone")) + return q + + @property + def time(self) -> datetime: + """Return current device time.""" + res = self.data["get_time"] + return datetime( + res["year"], + res["month"], + res["mday"], + res["hour"], + res["min"], + res["sec"], + ) + + @property + def timezone(self): + """Return current timezone.""" + res = self.data["get_timezone"] + return res diff --git a/kasa/modules/usage.py b/kasa/modules/usage.py new file mode 100644 index 000000000..2a5b6dc0b --- /dev/null +++ b/kasa/modules/usage.py @@ -0,0 +1,40 @@ +"""Implementation of the usage interface.""" +from datetime import datetime + +from .module import Module, merge + + +class Usage(Module): + """Baseclass for emeter/usage interfaces.""" + + def query(self): + """Return the base query.""" + year = datetime.now().year + month = datetime.now().month + + req = self.query_for_command("get_realtime") + req = merge( + req, self.query_for_command("get_daystat", {"year": year, "month": month}) + ) + req = merge(req, self.query_for_command("get_monthstat", {"year": year})) + req = merge(req, self.query_for_command("get_next_action")) + + return req + + async def get_daystat(self, year, month): + """Return stats for the current day.""" + if year is None: + year = datetime.now().year + if month is None: + month = datetime.now().month + return await self.call("get_daystat", {"year": year, "month": month}) + + async def get_monthstat(self, year): + """Return stats for the current month.""" + if year is None: + year = datetime.now().year + return await self.call("get_monthstat", {"year": year}) + + async def erase_stats(self): + """Erase all stats.""" + return await self.call("erase_runtime_stat") diff --git a/kasa/smartbulb.py b/kasa/smartbulb.py index c2d095395..f941dcf1f 100644 --- a/kasa/smartbulb.py +++ b/kasa/smartbulb.py @@ -3,6 +3,7 @@ import re from typing import Any, Dict, NamedTuple, cast +from .modules import Antitheft, Cloud, Countdown, Emeter, Schedule, Time, Usage from .smartdevice import DeviceType, SmartDevice, SmartDeviceException, requires_update @@ -109,13 +110,19 @@ class SmartBulb(SmartDevice): """ LIGHT_SERVICE = "smartlife.iot.smartbulb.lightingservice" - TIME_SERVICE = "smartlife.iot.common.timesetting" SET_LIGHT_METHOD = "transition_light_state" + emeter_type = "smartlife.iot.common.emeter" def __init__(self, host: str) -> None: super().__init__(host=host) - self.emeter_type = "smartlife.iot.common.emeter" self._device_type = DeviceType.Bulb + self.add_module("schedule", Schedule(self, "smartlife.iot.common.schedule")) + self.add_module("usage", Usage(self, "smartlife.iot.common.schedule")) + self.add_module("antitheft", Antitheft(self, "smartlife.iot.common.anti_theft")) + self.add_module("time", Time(self, "smartlife.iot.common.timesetting")) + self.add_module("emeter", Emeter(self, self.emeter_type)) + self.add_module("countdown", Countdown(self, "countdown")) + self.add_module("cloud", Cloud(self, "smartlife.iot.common.cloud")) @property # type: ignore @requires_update diff --git a/kasa/smartdevice.py b/kasa/smartdevice.py index 7a66a864d..5e6877417 100755 --- a/kasa/smartdevice.py +++ b/kasa/smartdevice.py @@ -22,6 +22,7 @@ from .emeterstatus import EmeterStatus from .exceptions import SmartDeviceException +from .modules import Emeter, Module from .protocol import TPLinkSmartHomeProtocol _LOGGER = logging.getLogger(__name__) @@ -186,6 +187,7 @@ class SmartDevice: """ TIME_SERVICE = "time" + emeter_type = "emeter" def __init__(self, host: str) -> None: """Create a new SmartDevice instance. @@ -195,7 +197,6 @@ def __init__(self, host: str) -> None: self.host = host self.protocol = TPLinkSmartHomeProtocol(host) - self.emeter_type = "emeter" _LOGGER.debug("Initializing %s of type %s", self.host, type(self)) self._device_type = DeviceType.Unknown # TODO: typing Any is just as using Optional[Dict] would require separate checks in @@ -203,9 +204,21 @@ def __init__(self, host: str) -> None: # are not accessed incorrectly. self._last_update: Any = None self._sys_info: Any = None # TODO: this is here to avoid changing tests + self.modules: Dict[str, Any] = {} self.children: List["SmartDevice"] = [] + def add_module(self, name: str, module: Module): + """Register a module.""" + if name in self.modules: + _LOGGER.debug("Module %s already registered, ignoring..." % name) + return + + assert name not in self.modules + + _LOGGER.debug("Adding module %s", module) + self.modules[name] = module + def _create_request( self, target: str, cmd: str, arg: Optional[Dict] = None, child_ids=None ): @@ -268,6 +281,14 @@ def features(self) -> Set[str]: _LOGGER.debug("Device does not have feature information") return set() + @property # type: ignore + @requires_update + def supported_modules(self) -> List[str]: + """Return a set of modules supported by the device.""" + # TODO: this should rather be called `features`, but we don't want to break + # the API now. Maybe just deprecate it and point the users to use this? + return list(self.modules.keys()) + @property # type: ignore @requires_update def has_emeter(self) -> bool: @@ -303,7 +324,12 @@ async def update(self, update_children: bool = True): _LOGGER.debug( "The device has emeter, querying its information along sysinfo" ) - req.update(self._create_emeter_request()) + self.add_module("emeter", Emeter(self, self.emeter_type)) + + for module in self.modules.values(): + q = module.query() + _LOGGER.debug("Adding query for %s: %s", module, q) + req = merge(req, module.query()) self._last_update = await self.protocol.query(req) self._sys_info = self._last_update["system"]["get_sysinfo"] @@ -337,6 +363,18 @@ async def set_alias(self, alias: str) -> None: """Set the device name (alias).""" return await self._query_helper("system", "set_dev_alias", {"alias": alias}) + @property # type: ignore + @requires_update + def time(self) -> datetime: + """Return current time from the device.""" + return self.modules["time"].time + + @property # type: ignore + @requires_update + def timezone(self) -> Dict: + """Return the current timezone.""" + return self.modules["time"].timezone + async def get_time(self) -> Optional[datetime]: """Return current time from the device, if available.""" try: @@ -435,7 +473,7 @@ async def set_mac(self, mac): def emeter_realtime(self) -> EmeterStatus: """Return current energy readings.""" self._verify_emeter() - return EmeterStatus(self._last_update[self.emeter_type]["get_realtime"]) + return EmeterStatus(self.modules["emeter"].realtime) async def get_emeter_realtime(self) -> EmeterStatus: """Retrieve current energy readings.""" @@ -555,7 +593,7 @@ async def get_emeter_monthly(self, year: int = None, kwh: bool = True) -> Dict: async def erase_emeter_stats(self) -> Dict: """Erase energy meter statistics.""" self._verify_emeter() - return await self._query_helper(self.emeter_type, "erase_emeter_stat", None) + return await self.modules["emeter"].erase_stats() @requires_update async def current_consumption(self) -> float: diff --git a/kasa/smartplug.py b/kasa/smartplug.py index d23bc9396..58144b58a 100644 --- a/kasa/smartplug.py +++ b/kasa/smartplug.py @@ -2,6 +2,7 @@ import logging from typing import Any, Dict +from kasa.modules import Antitheft, Cloud, Schedule, Time, Usage from kasa.smartdevice import DeviceType, SmartDevice, requires_update _LOGGER = logging.getLogger(__name__) @@ -40,6 +41,11 @@ def __init__(self, host: str) -> None: super().__init__(host) self.emeter_type = "emeter" self._device_type = DeviceType.Plug + self.add_module("schedule", Schedule(self, "schedule")) + self.add_module("usage", Usage(self, "schedule")) + self.add_module("antitheft", Antitheft(self, "anti_theft")) + self.add_module("time", Time(self, "time")) + self.add_module("cloud", Cloud(self, "cnCloud")) @property # type: ignore @requires_update diff --git a/kasa/smartstrip.py b/kasa/smartstrip.py index 391381bcc..bbdf2a3fb 100755 --- a/kasa/smartstrip.py +++ b/kasa/smartstrip.py @@ -13,6 +13,8 @@ ) from kasa.smartplug import SmartPlug +from .modules import Antitheft, Countdown, Schedule, Time, Usage + _LOGGER = logging.getLogger(__name__) @@ -80,6 +82,11 @@ def __init__(self, host: str) -> None: super().__init__(host=host) self.emeter_type = "emeter" self._device_type = DeviceType.Strip + self.add_module("antitheft", Antitheft(self, "anti_theft")) + self.add_module("schedule", Schedule(self, "schedule")) + self.add_module("usage", Usage(self, "schedule")) + self.add_module("time", Time(self, "time")) + self.add_module("countdown", Countdown(self, "countdown")) @property # type: ignore @requires_update diff --git a/poetry.lock b/poetry.lock index d3af30c3f..3ce2e28bf 100644 --- a/poetry.lock +++ b/poetry.lock @@ -87,7 +87,7 @@ python-versions = ">=3.6.1" [[package]] name = "charset-normalizer" -version = "2.0.12" +version = "2.0.10" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." category = "main" optional = false @@ -118,7 +118,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "coverage" -version = "6.3.2" +version = "6.3" description = "Code coverage measurement for Python" category = "dev" optional = false @@ -148,7 +148,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "filelock" -version = "3.6.0" +version = "3.4.2" description = "A platform independent file lock." category = "dev" optional = false @@ -160,7 +160,7 @@ testing = ["covdefaults (>=1.2.0)", "coverage (>=4)", "pytest (>=4)", "pytest-co [[package]] name = "identify" -version = "2.4.12" +version = "2.4.6" description = "File identification library for Python" category = "dev" optional = false @@ -187,7 +187,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "importlib-metadata" -version = "4.11.3" +version = "4.10.1" description = "Read metadata from Python packages" category = "main" optional = false @@ -198,9 +198,9 @@ typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} zipp = ">=0.5" [package.extras] -docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)"] +docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] perf = ["ipython"] -testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)", "importlib-resources (>=1.3)"] +testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] [[package]] name = "iniconfig" @@ -238,11 +238,11 @@ mistune = "*" [[package]] name = "markupsafe" -version = "2.1.1" +version = "2.0.1" description = "Safely add untrusted strings to HTML/XML markup." category = "main" optional = true -python-versions = ">=3.7" +python-versions = ">=3.6" [[package]] name = "mistune" @@ -273,7 +273,7 @@ pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" [[package]] name = "platformdirs" -version = "2.5.1" +version = "2.4.1" description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." category = "dev" optional = false @@ -323,6 +323,21 @@ category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +[[package]] +name = "pydantic" +version = "1.9.0" +description = "Data validation and settings management using python 3.6 type hinting" +category = "main" +optional = false +python-versions = ">=3.6.1" + +[package.dependencies] +typing-extensions = ">=3.7.4.3" + +[package.extras] +dotenv = ["python-dotenv (>=0.10.4)"] +email = ["email-validator (>=1.0.3)"] + [[package]] name = "pygments" version = "2.11.2" @@ -344,11 +359,11 @@ diagrams = ["jinja2", "railroad-diagrams"] [[package]] name = "pytest" -version = "7.1.1" +version = "6.2.5" description = "pytest: simple powerful testing with Python" category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.6" [package.dependencies] atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} @@ -359,14 +374,14 @@ iniconfig = "*" packaging = "*" pluggy = ">=0.12,<2.0" py = ">=1.8.2" -tomli = ">=1.0.0" +toml = "*" [package.extras] -testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] +testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] [[package]] name = "pytest-asyncio" -version = "0.18.2" +version = "0.17.2" description = "Pytest support for asyncio" category = "dev" optional = false @@ -374,7 +389,7 @@ python-versions = ">=3.7" [package.dependencies] pytest = ">=6.1.0" -typing-extensions = {version = ">=3.7.2", markers = "python_version < \"3.8\""} +typing-extensions = {version = ">=4.0", markers = "python_version < \"3.8\""} [package.extras] testing = ["coverage (==6.2)", "hypothesis (>=5.7.1)", "flaky (>=3.5.0)", "mypy (==0.931)"] @@ -424,7 +439,7 @@ termcolor = ">=1.1.0" [[package]] name = "pytz" -version = "2022.1" +version = "2021.3" description = "World timezone definitions, modern and historical" category = "main" optional = true @@ -626,7 +641,7 @@ python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" [[package]] name = "tomli" -version = "2.0.1" +version = "2.0.0" description = "A lil' TOML parser" category = "dev" optional = false @@ -657,7 +672,7 @@ testing = ["flaky (>=3.4.0)", "freezegun (>=0.3.11)", "pytest (>=4.0.0)", "pytes [[package]] name = "typing-extensions" -version = "4.1.1" +version = "4.0.1" description = "Backported and Experimental Type Hints for Python 3.6+" category = "main" optional = false @@ -665,20 +680,20 @@ python-versions = ">=3.6" [[package]] name = "urllib3" -version = "1.26.9" +version = "1.26.8" description = "HTTP library with thread-safe connection pooling, file post, and more." category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" [package.extras] -brotli = ["brotlicffi (>=0.8.0)", "brotli (>=1.0.9)", "brotlipy (>=0.6.0)"] +brotli = ["brotlipy (>=0.6.0)"] secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] [[package]] name = "virtualenv" -version = "20.13.4" +version = "20.13.0" description = "Virtual Python Environment builder" category = "dev" optional = false @@ -739,7 +754,7 @@ docs = ["sphinx", "sphinx_rtd_theme", "m2r", "mistune", "sphinxcontrib-programou [metadata] lock-version = "1.1" python-versions = "^3.7" -content-hash = "9c4aaea750c8c2cb4ed6d37c53ec3884a10d698ceb77716c33b04eed12a08506" +content-hash = "eb7bb96b826fec8ad7207e838f02a68333d806f5908f3e07675cbc151a165b25" [metadata.files] alabaster = [ @@ -774,8 +789,8 @@ cfgv = [ {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"}, ] charset-normalizer = [ - {file = "charset-normalizer-2.0.12.tar.gz", hash = "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597"}, - {file = "charset_normalizer-2.0.12-py3-none-any.whl", hash = "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"}, + {file = "charset-normalizer-2.0.10.tar.gz", hash = "sha256:876d180e9d7432c5d1dfd4c5d26b72f099d503e8fcc0feb7532c9289be60fcbd"}, + {file = "charset_normalizer-2.0.10-py3-none-any.whl", hash = "sha256:cb957888737fc0bbcd78e3df769addb41fd1ff8cf950dc9e7ad7793f1bf44455"}, ] codecov = [ {file = "codecov-2.1.12-py2.py3-none-any.whl", hash = "sha256:585dc217dc3d8185198ceb402f85d5cb5dbfa0c5f350a5abcdf9e347776a5b47"}, @@ -787,47 +802,50 @@ colorama = [ {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, ] coverage = [ - {file = "coverage-6.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9b27d894748475fa858f9597c0ee1d4829f44683f3813633aaf94b19cb5453cf"}, - {file = "coverage-6.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:37d1141ad6b2466a7b53a22e08fe76994c2d35a5b6b469590424a9953155afac"}, - {file = "coverage-6.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9987b0354b06d4df0f4d3e0ec1ae76d7ce7cbca9a2f98c25041eb79eec766f1"}, - {file = "coverage-6.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:26e2deacd414fc2f97dd9f7676ee3eaecd299ca751412d89f40bc01557a6b1b4"}, - {file = "coverage-6.3.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4dd8bafa458b5c7d061540f1ee9f18025a68e2d8471b3e858a9dad47c8d41903"}, - {file = "coverage-6.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:46191097ebc381fbf89bdce207a6c107ac4ec0890d8d20f3360345ff5976155c"}, - {file = "coverage-6.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6f89d05e028d274ce4fa1a86887b071ae1755082ef94a6740238cd7a8178804f"}, - {file = "coverage-6.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:58303469e9a272b4abdb9e302a780072c0633cdcc0165db7eec0f9e32f901e05"}, - {file = "coverage-6.3.2-cp310-cp310-win32.whl", hash = "sha256:2fea046bfb455510e05be95e879f0e768d45c10c11509e20e06d8fcaa31d9e39"}, - {file = "coverage-6.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:a2a8b8bcc399edb4347a5ca8b9b87e7524c0967b335fbb08a83c8421489ddee1"}, - {file = "coverage-6.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:f1555ea6d6da108e1999b2463ea1003fe03f29213e459145e70edbaf3e004aaa"}, - {file = "coverage-6.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5f4e1edcf57ce94e5475fe09e5afa3e3145081318e5fd1a43a6b4539a97e518"}, - {file = "coverage-6.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7a15dc0a14008f1da3d1ebd44bdda3e357dbabdf5a0b5034d38fcde0b5c234b7"}, - {file = "coverage-6.3.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21b7745788866028adeb1e0eca3bf1101109e2dc58456cb49d2d9b99a8c516e6"}, - {file = "coverage-6.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:8ce257cac556cb03be4a248d92ed36904a59a4a5ff55a994e92214cde15c5bad"}, - {file = "coverage-6.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b0be84e5a6209858a1d3e8d1806c46214e867ce1b0fd32e4ea03f4bd8b2e3359"}, - {file = "coverage-6.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:acf53bc2cf7282ab9b8ba346746afe703474004d9e566ad164c91a7a59f188a4"}, - {file = "coverage-6.3.2-cp37-cp37m-win32.whl", hash = "sha256:8bdde1177f2311ee552f47ae6e5aa7750c0e3291ca6b75f71f7ffe1f1dab3dca"}, - {file = "coverage-6.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:b31651d018b23ec463e95cf10070d0b2c548aa950a03d0b559eaa11c7e5a6fa3"}, - {file = "coverage-6.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:07e6db90cd9686c767dcc593dff16c8c09f9814f5e9c51034066cad3373b914d"}, - {file = "coverage-6.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2c6dbb42f3ad25760010c45191e9757e7dce981cbfb90e42feef301d71540059"}, - {file = "coverage-6.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c76aeef1b95aff3905fb2ae2d96e319caca5b76fa41d3470b19d4e4a3a313512"}, - {file = "coverage-6.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8cf5cfcb1521dc3255d845d9dca3ff204b3229401994ef8d1984b32746bb45ca"}, - {file = "coverage-6.3.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8fbbdc8d55990eac1b0919ca69eb5a988a802b854488c34b8f37f3e2025fa90d"}, - {file = "coverage-6.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ec6bc7fe73a938933d4178c9b23c4e0568e43e220aef9472c4f6044bfc6dd0f0"}, - {file = "coverage-6.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:9baff2a45ae1f17c8078452e9e5962e518eab705e50a0aa8083733ea7d45f3a6"}, - {file = "coverage-6.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fd9e830e9d8d89b20ab1e5af09b32d33e1a08ef4c4e14411e559556fd788e6b2"}, - {file = "coverage-6.3.2-cp38-cp38-win32.whl", hash = "sha256:f7331dbf301b7289013175087636bbaf5b2405e57259dd2c42fdcc9fcc47325e"}, - {file = "coverage-6.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:68353fe7cdf91f109fc7d474461b46e7f1f14e533e911a2a2cbb8b0fc8613cf1"}, - {file = "coverage-6.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b78e5afb39941572209f71866aa0b206c12f0109835aa0d601e41552f9b3e620"}, - {file = "coverage-6.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4e21876082ed887baed0146fe222f861b5815455ada3b33b890f4105d806128d"}, - {file = "coverage-6.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34626a7eee2a3da12af0507780bb51eb52dca0e1751fd1471d0810539cefb536"}, - {file = "coverage-6.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1ebf730d2381158ecf3dfd4453fbca0613e16eaa547b4170e2450c9707665ce7"}, - {file = "coverage-6.3.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd6fe30bd519694b356cbfcaca9bd5c1737cddd20778c6a581ae20dc8c04def2"}, - {file = "coverage-6.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:96f8a1cb43ca1422f36492bebe63312d396491a9165ed3b9231e778d43a7fca4"}, - {file = "coverage-6.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:dd035edafefee4d573140a76fdc785dc38829fe5a455c4bb12bac8c20cfc3d69"}, - {file = "coverage-6.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5ca5aeb4344b30d0bec47481536b8ba1181d50dbe783b0e4ad03c95dc1296684"}, - {file = "coverage-6.3.2-cp39-cp39-win32.whl", hash = "sha256:f5fa5803f47e095d7ad8443d28b01d48c0359484fec1b9d8606d0e3282084bc4"}, - {file = "coverage-6.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:9548f10d8be799551eb3a9c74bbf2b4934ddb330e08a73320123c07f95cc2d92"}, - {file = "coverage-6.3.2-pp36.pp37.pp38-none-any.whl", hash = "sha256:18d520c6860515a771708937d2f78f63cc47ab3b80cb78e86573b0a760161faf"}, - {file = "coverage-6.3.2.tar.gz", hash = "sha256:03e2a7826086b91ef345ff18742ee9fc47a6839ccd517061ef8fa1976e652ce9"}, + {file = "coverage-6.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e8071e7d9ba9f457fc674afc3de054450be2c9b195c470147fbbc082468d8ff7"}, + {file = "coverage-6.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:86c91c511853dfda81c2cf2360502cb72783f4b7cebabef27869f00cbe1db07d"}, + {file = "coverage-6.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c4ce3b647bd1792d4394f5690d9df6dc035b00bcdbc5595099c01282a59ae01"}, + {file = "coverage-6.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2a491e159294d756e7fc8462f98175e2d2225e4dbe062cca7d3e0d5a75ba6260"}, + {file = "coverage-6.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d008e0f67ac800b0ca04d7914b8501312c8c6c00ad8c7ba17754609fae1231a"}, + {file = "coverage-6.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4578728c36de2801c1deb1c6b760d31883e62e33f33c7ba8f982e609dc95167d"}, + {file = "coverage-6.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7ee317486593193e066fc5e98ac0ce712178c21529a85c07b7cb978171f25d53"}, + {file = "coverage-6.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2bc85664b06ba42d14bb74d6ddf19d8bfc520cb660561d2d9ce5786ae72f71b5"}, + {file = "coverage-6.3-cp310-cp310-win32.whl", hash = "sha256:27a94db5dc098c25048b0aca155f5fac674f2cf1b1736c5272ba28ead2fc267e"}, + {file = "coverage-6.3-cp310-cp310-win_amd64.whl", hash = "sha256:bde4aeabc0d1b2e52c4036c54440b1ad05beeca8113f47aceb4998bb7471e2c2"}, + {file = "coverage-6.3-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:509c68c3e2015022aeda03b003dd68fa19987cdcf64e9d4edc98db41cfc45d30"}, + {file = "coverage-6.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:e4ff163602c5c77e7bb4ea81ba5d3b793b4419f8acd296aae149370902cf4e92"}, + {file = "coverage-6.3-cp311-cp311-win_amd64.whl", hash = "sha256:d1675db48490e5fa0b300f6329ecb8a9a37c29b9ab64fa9c964d34111788ca2d"}, + {file = "coverage-6.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:7eed8459a2b81848cafb3280b39d7d49950d5f98e403677941c752e7e7ee47cb"}, + {file = "coverage-6.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b4285fde5286b946835a1a53bba3ad41ef74285ba9e8013e14b5ea93deaeafc"}, + {file = "coverage-6.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a4748349734110fd32d46ff8897b561e6300d8989a494ad5a0a2e4f0ca974fc7"}, + {file = "coverage-6.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:823f9325283dc9565ba0aa2d240471a93ca8999861779b2b6c7aded45b58ee0f"}, + {file = "coverage-6.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:fff16a30fdf57b214778eff86391301c4509e327a65b877862f7c929f10a4253"}, + {file = "coverage-6.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:da1a428bdbe71f9a8c270c7baab29e9552ac9d0e0cba5e7e9a4c9ee6465d258d"}, + {file = "coverage-6.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:7d82c610a2e10372e128023c5baf9ce3d270f3029fe7274ff5bc2897c68f1318"}, + {file = "coverage-6.3-cp37-cp37m-win32.whl", hash = "sha256:11e61c5548ecf74ea1f8b059730b049871f0e32b74f88bd0d670c20c819ad749"}, + {file = "coverage-6.3-cp37-cp37m-win_amd64.whl", hash = "sha256:8e0c3525b1a182c8ffc9bca7e56b521e0c2b8b3e82f033c8e16d6d721f1b54d6"}, + {file = "coverage-6.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a189036c50dcd56100746139a459f0d27540fef95b09aba03e786540b8feaa5f"}, + {file = "coverage-6.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:32168001f33025fd756884d56d01adebb34e6c8c0b3395ca8584cdcee9c7c9d2"}, + {file = "coverage-6.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5d79c9af3f410a2b5acad91258b4ae179ee9c83897eb9de69151b179b0227f5"}, + {file = "coverage-6.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:85c5fc9029043cf8b07f73fbb0a7ab6d3b717510c3b5642b77058ea55d7cacde"}, + {file = "coverage-6.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a7596aa2f2b8fa5604129cfc9a27ad9beec0a96f18078cb424d029fdd707468d"}, + {file = "coverage-6.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ce443a3e6df90d692c38762f108fc4c88314bf477689f04de76b3f252e7a351c"}, + {file = "coverage-6.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:012157499ec4f135fc36cd2177e3d1a1840af9b236cbe80e9a5ccfc83d912a69"}, + {file = "coverage-6.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:0a34d313105cdd0d3644c56df2d743fe467270d6ab93b5d4a347eb9fec8924d6"}, + {file = "coverage-6.3-cp38-cp38-win32.whl", hash = "sha256:6e78b1e25e5c5695dea012be473e442f7094d066925604be20b30713dbd47f89"}, + {file = "coverage-6.3-cp38-cp38-win_amd64.whl", hash = "sha256:433b99f7b0613bdcdc0b00cc3d39ed6d756797e3b078d2c43f8a38288520aec6"}, + {file = "coverage-6.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9ed3244b415725f08ca3bdf02ed681089fd95e9465099a21c8e2d9c5d6ca2606"}, + {file = "coverage-6.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ab4fc4b866b279740e0d917402f0e9a08683e002f43fa408e9655818ed392196"}, + {file = "coverage-6.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c8582e9280f8d0f38114fe95a92ae8d0790b56b099d728cc4f8a2e14b1c4a18c"}, + {file = "coverage-6.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c72bb4679283c6737f452eeb9b2a0e570acaef2197ad255fb20162adc80bea76"}, + {file = "coverage-6.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca29c352389ea27a24c79acd117abdd8a865c6eb01576b6f0990cd9a4e9c9f48"}, + {file = "coverage-6.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:152cc2624381df4e4e604e21bd8e95eb8059535f7b768c1fb8b8ae0b26f47ab0"}, + {file = "coverage-6.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:51372e24b1f7143ee2df6b45cff6a721f3abe93b1e506196f3ffa4155c2497f7"}, + {file = "coverage-6.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:72d9d186508325a456475dd05b1756f9a204c7086b07fffb227ef8cee03b1dc2"}, + {file = "coverage-6.3-cp39-cp39-win32.whl", hash = "sha256:649df3641eb351cdfd0d5533c92fc9df507b6b2bf48a7ef8c71ab63cbc7b5c3c"}, + {file = "coverage-6.3-cp39-cp39-win_amd64.whl", hash = "sha256:e67ccd53da5958ea1ec833a160b96357f90859c220a00150de011b787c27b98d"}, + {file = "coverage-6.3-pp36.pp37.pp38-none-any.whl", hash = "sha256:27ac7cb84538e278e07569ceaaa6f807a029dc194b1c819a9820b9bb5dbf63ab"}, + {file = "coverage-6.3.tar.gz", hash = "sha256:987a84ff98a309994ca77ed3cc4b92424f824278e48e4bf7d1bb79a63cfe2099"}, ] distlib = [ {file = "distlib-0.3.4-py2.py3-none-any.whl", hash = "sha256:6564fe0a8f51e734df6333d08b8b94d4ea8ee6b99b5ed50613f731fd4089f34b"}, @@ -838,12 +856,12 @@ docutils = [ {file = "docutils-0.16.tar.gz", hash = "sha256:c2de3a60e9e7d07be26b7f2b00ca0309c207e06c100f9cc2a94931fc75a478fc"}, ] filelock = [ - {file = "filelock-3.6.0-py3-none-any.whl", hash = "sha256:f8314284bfffbdcfa0ff3d7992b023d4c628ced6feb957351d4c48d059f56bc0"}, - {file = "filelock-3.6.0.tar.gz", hash = "sha256:9cd540a9352e432c7246a48fe4e8712b10acb1df2ad1f30e8c070b82ae1fed85"}, + {file = "filelock-3.4.2-py3-none-any.whl", hash = "sha256:cf0fc6a2f8d26bd900f19bf33915ca70ba4dd8c56903eeb14e1e7a2fd7590146"}, + {file = "filelock-3.4.2.tar.gz", hash = "sha256:38b4f4c989f9d06d44524df1b24bd19e167d851f19b50bf3e3559952dddc5b80"}, ] identify = [ - {file = "identify-2.4.12-py2.py3-none-any.whl", hash = "sha256:5f06b14366bd1facb88b00540a1de05b69b310cbc2654db3c7e07fa3a4339323"}, - {file = "identify-2.4.12.tar.gz", hash = "sha256:3f3244a559290e7d3deb9e9adc7b33594c1bc85a9dd82e0f1be519bf12a1ec17"}, + {file = "identify-2.4.6-py2.py3-none-any.whl", hash = "sha256:cf06b1639e0dca0c184b1504d8b73448c99a68e004a80524c7923b95f7b6837c"}, + {file = "identify-2.4.6.tar.gz", hash = "sha256:233679e3f61a02015d4293dbccf16aa0e4996f868bd114688b8c124f18826706"}, ] idna = [ {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, @@ -854,8 +872,8 @@ imagesize = [ {file = "imagesize-1.3.0.tar.gz", hash = "sha256:cd1750d452385ca327479d45b64d9c7729ecf0b3969a58148298c77092261f9d"}, ] importlib-metadata = [ - {file = "importlib_metadata-4.11.3-py3-none-any.whl", hash = "sha256:1208431ca90a8cca1a6b8af391bb53c1a2db74e5d1cef6ddced95d4b2062edc6"}, - {file = "importlib_metadata-4.11.3.tar.gz", hash = "sha256:ea4c597ebf37142f827b8f39299579e31685c31d3a438b59f469406afd0f2539"}, + {file = "importlib_metadata-4.10.1-py3-none-any.whl", hash = "sha256:899e2a40a8c4a1aec681feef45733de8a6c58f3f6a0dbed2eb6574b4387a77b6"}, + {file = "importlib_metadata-4.10.1.tar.gz", hash = "sha256:951f0d8a5b7260e9db5e41d429285b5f451e928479f19d80818878527d36e95e"}, ] iniconfig = [ {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, @@ -869,46 +887,40 @@ m2r = [ {file = "m2r-0.2.1.tar.gz", hash = "sha256:bf90bad66cda1164b17e5ba4a037806d2443f2a4d5ddc9f6a5554a0322aaed99"}, ] markupsafe = [ - {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a49907dd8420c5685cfa064a1335b6754b74541bbb3706c259c02ed65b644b3e"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10c1bfff05d95783da83491be968e8fe789263689c02724e0c691933c52994f5"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b7bd98b796e2b6553da7225aeb61f447f80a1ca64f41d83612e6139ca5213aa4"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b09bf97215625a311f669476f44b8b318b075847b49316d3e28c08e41a7a573f"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:694deca8d702d5db21ec83983ce0bb4b26a578e71fbdbd4fdcd387daa90e4d5e"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:efc1913fd2ca4f334418481c7e595c00aad186563bbc1ec76067848c7ca0a933"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-win32.whl", hash = "sha256:4a33dea2b688b3190ee12bd7cfa29d39c9ed176bda40bfa11099a3ce5d3a7ac6"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:dda30ba7e87fbbb7eab1ec9f58678558fd9a6b8b853530e176eabd064da81417"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:671cd1187ed5e62818414afe79ed29da836dde67166a9fac6d435873c44fdd02"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3799351e2336dc91ea70b034983ee71cf2f9533cdff7c14c90ea126bfd95d65a"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e72591e9ecd94d7feb70c1cbd7be7b3ebea3f548870aa91e2732960fa4d57a37"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6fbf47b5d3728c6aea2abb0589b5d30459e369baa772e0f37a0320185e87c980"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d5ee4f386140395a2c818d149221149c54849dfcfcb9f1debfe07a8b8bd63f9a"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:bcb3ed405ed3222f9904899563d6fc492ff75cce56cba05e32eff40e6acbeaa3"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e1c0b87e09fa55a220f058d1d49d3fb8df88fbfab58558f1198e08c1e1de842a"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-win32.whl", hash = "sha256:8dc1c72a69aa7e082593c4a203dcf94ddb74bb5c8a731e4e1eb68d031e8498ff"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:97a68e6ada378df82bc9f16b800ab77cbf4b2fada0081794318520138c088e4a"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e8c843bbcda3a2f1e3c2ab25913c80a3c5376cd00c6e8c4a86a89a28c8dc5452"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0212a68688482dc52b2d45013df70d169f542b7394fc744c02a57374a4207003"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e576a51ad59e4bfaac456023a78f6b5e6e7651dcd383bcc3e18d06f9b55d6d1"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b9fe39a2ccc108a4accc2676e77da025ce383c108593d65cc909add5c3bd601"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96e37a3dc86e80bf81758c152fe66dbf60ed5eca3d26305edf01892257049925"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6d0072fea50feec76a4c418096652f2c3238eaa014b2f94aeb1d56a66b41403f"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:089cf3dbf0cd6c100f02945abeb18484bd1ee57a079aefd52cffd17fba910b88"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6a074d34ee7a5ce3effbc526b7083ec9731bb3cbf921bbe1d3005d4d2bdb3a63"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-win32.whl", hash = "sha256:421be9fbf0ffe9ffd7a378aafebbf6f4602d564d34be190fc19a193232fd12b1"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc7b548b17d238737688817ab67deebb30e8073c95749d55538ed473130ec0c7"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e04e26803c9c3851c931eac40c695602c6295b8d432cbe78609649ad9bd2da8a"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b87db4360013327109564f0e591bd2a3b318547bcef31b468a92ee504d07ae4f"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99a2a507ed3ac881b975a2976d59f38c19386d128e7a9a18b7df6fff1fd4c1d6"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56442863ed2b06d19c37f94d999035e15ee982988920e12a5b4ba29b62ad1f77"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ce11ee3f23f79dbd06fb3d63e2f6af7b12db1d46932fe7bd8afa259a5996603"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:33b74d289bd2f5e527beadcaa3f401e0df0a89927c1559c8566c066fa4248ab7"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:43093fb83d8343aac0b1baa75516da6092f58f41200907ef92448ecab8825135"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8e3dcf21f367459434c18e71b2a9532d96547aef8a871872a5bd69a715c15f96"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-win32.whl", hash = "sha256:d4306c36ca495956b6d568d276ac11fdd9c30a36f1b6eb928070dc5360b22e1c"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:46d00d6cfecdde84d40e572d63735ef81423ad31184100411e6e3388d405e247"}, - {file = "MarkupSafe-2.1.1.tar.gz", hash = "sha256:7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-win32.whl", hash = "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-win32.whl", hash = "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-win32.whl", hash = "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-win32.whl", hash = "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8"}, + {file = "MarkupSafe-2.0.1.tar.gz", hash = "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a"}, ] mistune = [ {file = "mistune-0.8.4-py2.py3-none-any.whl", hash = "sha256:88a1051873018da288eee8538d476dffe1262495144b33ecb586c4ab266bb8d4"}, @@ -923,8 +935,8 @@ packaging = [ {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, ] platformdirs = [ - {file = "platformdirs-2.5.1-py3-none-any.whl", hash = "sha256:bcae7cab893c2d310a711b70b24efb93334febe65f8de776ee320b517471e227"}, - {file = "platformdirs-2.5.1.tar.gz", hash = "sha256:7535e70dfa32e84d4b34996ea99c5e432fa29a708d0f4e394bbcb2a8faa4f16d"}, + {file = "platformdirs-2.4.1-py3-none-any.whl", hash = "sha256:1d7385c7db91728b83efd0ca99a5afb296cab9d0ed8313a45ed8ba17967ecfca"}, + {file = "platformdirs-2.4.1.tar.gz", hash = "sha256:440633ddfebcc36264232365d7840a970e75e1018d15b4327d11f91909045fda"}, ] pluggy = [ {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, @@ -938,6 +950,43 @@ py = [ {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, ] +pydantic = [ + {file = "pydantic-1.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cb23bcc093697cdea2708baae4f9ba0e972960a835af22560f6ae4e7e47d33f5"}, + {file = "pydantic-1.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1d5278bd9f0eee04a44c712982343103bba63507480bfd2fc2790fa70cd64cf4"}, + {file = "pydantic-1.9.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab624700dc145aa809e6f3ec93fb8e7d0f99d9023b713f6a953637429b437d37"}, + {file = "pydantic-1.9.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c8d7da6f1c1049eefb718d43d99ad73100c958a5367d30b9321b092771e96c25"}, + {file = "pydantic-1.9.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:3c3b035103bd4e2e4a28da9da7ef2fa47b00ee4a9cf4f1a735214c1bcd05e0f6"}, + {file = "pydantic-1.9.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:3011b975c973819883842c5ab925a4e4298dffccf7782c55ec3580ed17dc464c"}, + {file = "pydantic-1.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:086254884d10d3ba16da0588604ffdc5aab3f7f09557b998373e885c690dd398"}, + {file = "pydantic-1.9.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:0fe476769acaa7fcddd17cadd172b156b53546ec3614a4d880e5d29ea5fbce65"}, + {file = "pydantic-1.9.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8e9dcf1ac499679aceedac7e7ca6d8641f0193c591a2d090282aaf8e9445a46"}, + {file = "pydantic-1.9.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d1e4c28f30e767fd07f2ddc6f74f41f034d1dd6bc526cd59e63a82fe8bb9ef4c"}, + {file = "pydantic-1.9.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:c86229333cabaaa8c51cf971496f10318c4734cf7b641f08af0a6fbf17ca3054"}, + {file = "pydantic-1.9.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:c0727bda6e38144d464daec31dff936a82917f431d9c39c39c60a26567eae3ed"}, + {file = "pydantic-1.9.0-cp36-cp36m-win_amd64.whl", hash = "sha256:dee5ef83a76ac31ab0c78c10bd7d5437bfdb6358c95b91f1ba7ff7b76f9996a1"}, + {file = "pydantic-1.9.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d9c9bdb3af48e242838f9f6e6127de9be7063aad17b32215ccc36a09c5cf1070"}, + {file = "pydantic-1.9.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ee7e3209db1e468341ef41fe263eb655f67f5c5a76c924044314e139a1103a2"}, + {file = "pydantic-1.9.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0b6037175234850ffd094ca77bf60fb54b08b5b22bc85865331dd3bda7a02fa1"}, + {file = "pydantic-1.9.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b2571db88c636d862b35090ccf92bf24004393f85c8870a37f42d9f23d13e032"}, + {file = "pydantic-1.9.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8b5ac0f1c83d31b324e57a273da59197c83d1bb18171e512908fe5dc7278a1d6"}, + {file = "pydantic-1.9.0-cp37-cp37m-win_amd64.whl", hash = "sha256:bbbc94d0c94dd80b3340fc4f04fd4d701f4b038ebad72c39693c794fd3bc2d9d"}, + {file = "pydantic-1.9.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e0896200b6a40197405af18828da49f067c2fa1f821491bc8f5bde241ef3f7d7"}, + {file = "pydantic-1.9.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7bdfdadb5994b44bd5579cfa7c9b0e1b0e540c952d56f627eb227851cda9db77"}, + {file = "pydantic-1.9.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:574936363cd4b9eed8acdd6b80d0143162f2eb654d96cb3a8ee91d3e64bf4cf9"}, + {file = "pydantic-1.9.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c556695b699f648c58373b542534308922c46a1cda06ea47bc9ca45ef5b39ae6"}, + {file = "pydantic-1.9.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:f947352c3434e8b937e3aa8f96f47bdfe6d92779e44bb3f41e4c213ba6a32145"}, + {file = "pydantic-1.9.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5e48ef4a8b8c066c4a31409d91d7ca372a774d0212da2787c0d32f8045b1e034"}, + {file = "pydantic-1.9.0-cp38-cp38-win_amd64.whl", hash = "sha256:96f240bce182ca7fe045c76bcebfa0b0534a1bf402ed05914a6f1dadff91877f"}, + {file = "pydantic-1.9.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:815ddebb2792efd4bba5488bc8fde09c29e8ca3227d27cf1c6990fc830fd292b"}, + {file = "pydantic-1.9.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6c5b77947b9e85a54848343928b597b4f74fc364b70926b3c4441ff52620640c"}, + {file = "pydantic-1.9.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c68c3bc88dbda2a6805e9a142ce84782d3930f8fdd9655430d8576315ad97ce"}, + {file = "pydantic-1.9.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5a79330f8571faf71bf93667d3ee054609816f10a259a109a0738dac983b23c3"}, + {file = "pydantic-1.9.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f5a64b64ddf4c99fe201ac2724daada8595ada0d102ab96d019c1555c2d6441d"}, + {file = "pydantic-1.9.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a733965f1a2b4090a5238d40d983dcd78f3ecea221c7af1497b845a9709c1721"}, + {file = "pydantic-1.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:2cc6a4cb8a118ffec2ca5fcb47afbacb4f16d0ab8b7350ddea5e8ef7bcc53a16"}, + {file = "pydantic-1.9.0-py3-none-any.whl", hash = "sha256:085ca1de245782e9b46cefcf99deecc67d418737a1fd3f6a4f511344b613a5b3"}, + {file = "pydantic-1.9.0.tar.gz", hash = "sha256:742645059757a56ecd886faf4ed2441b9c0cd406079c2b4bee51bcc3fbcd510a"}, +] pygments = [ {file = "Pygments-2.11.2-py3-none-any.whl", hash = "sha256:44238f1b60a76d78fc8ca0528ee429702aae011c265fe6a8dd8b63049ae41c65"}, {file = "Pygments-2.11.2.tar.gz", hash = "sha256:4e426f72023d88d03b2fa258de560726ce890ff3b630f88c21cbb8b2503b8c6a"}, @@ -947,12 +996,12 @@ pyparsing = [ {file = "pyparsing-3.0.7.tar.gz", hash = "sha256:18ee9022775d270c55187733956460083db60b37d0d0fb357445f3094eed3eea"}, ] pytest = [ - {file = "pytest-7.1.1-py3-none-any.whl", hash = "sha256:92f723789a8fdd7180b6b06483874feca4c48a5c76968e03bb3e7f806a1869ea"}, - {file = "pytest-7.1.1.tar.gz", hash = "sha256:841132caef6b1ad17a9afde46dc4f6cfa59a05f9555aae5151f73bdf2820ca63"}, + {file = "pytest-6.2.5-py3-none-any.whl", hash = "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"}, + {file = "pytest-6.2.5.tar.gz", hash = "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89"}, ] pytest-asyncio = [ - {file = "pytest-asyncio-0.18.2.tar.gz", hash = "sha256:fc8e4190f33fee7797cc7f1829f46a82c213f088af5d1bb5d4e454fe87e6cdc2"}, - {file = "pytest_asyncio-0.18.2-py3-none-any.whl", hash = "sha256:20db0bdd3d7581b2e11f5858a5d9541f2db9cd8c5853786f94ad273d466c8c6d"}, + {file = "pytest-asyncio-0.17.2.tar.gz", hash = "sha256:6d895b02432c028e6957d25fc936494e78c6305736e785d9fee408b1efbc7ff4"}, + {file = "pytest_asyncio-0.17.2-py3-none-any.whl", hash = "sha256:e0fe5dbea40516b661ef1bcfe0bd9461c2847c4ef4bb40012324f2454fb7d56d"}, ] pytest-cov = [ {file = "pytest-cov-2.12.1.tar.gz", hash = "sha256:261ceeb8c227b726249b376b8526b600f38667ee314f910353fa318caa01f4d7"}, @@ -966,8 +1015,8 @@ pytest-sugar = [ {file = "pytest-sugar-0.9.4.tar.gz", hash = "sha256:b1b2186b0a72aada6859bea2a5764145e3aaa2c1cfbb23c3a19b5f7b697563d3"}, ] pytz = [ - {file = "pytz-2022.1-py2.py3-none-any.whl", hash = "sha256:e68985985296d9a66a881eb3193b0906246245294a881e7c8afe623866ac6a5c"}, - {file = "pytz-2022.1.tar.gz", hash = "sha256:1e760e2fe6a8163bc0b3d9a19c4f84342afa0a2affebfaa84b01b978a02ecaa7"}, + {file = "pytz-2021.3-py2.py3-none-any.whl", hash = "sha256:3672058bc3453457b622aab7a1c3bfd5ab0bdae451512f6cf25f64ed37f5b87c"}, + {file = "pytz-2021.3.tar.gz", hash = "sha256:acad2d8b20a1af07d4e4c9d2e9285c5ed9104354062f275f3fcd88dcef4f1326"}, ] pyyaml = [ {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, @@ -1064,24 +1113,24 @@ toml = [ {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, ] tomli = [ - {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, - {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, + {file = "tomli-2.0.0-py3-none-any.whl", hash = "sha256:b5bde28da1fed24b9bd1d4d2b8cba62300bfb4ec9a6187a957e8ddb9434c5224"}, + {file = "tomli-2.0.0.tar.gz", hash = "sha256:c292c34f58502a1eb2bbb9f5bbc9a5ebc37bee10ffb8c2d6bbdfa8eb13cc14e1"}, ] tox = [ {file = "tox-3.24.5-py2.py3-none-any.whl", hash = "sha256:be3362472a33094bce26727f5f771ca0facf6dafa217f65875314e9a6600c95c"}, {file = "tox-3.24.5.tar.gz", hash = "sha256:67e0e32c90e278251fea45b696d0fef3879089ccbe979b0c556d35d5a70e2993"}, ] typing-extensions = [ - {file = "typing_extensions-4.1.1-py3-none-any.whl", hash = "sha256:21c85e0fe4b9a155d0799430b0ad741cdce7e359660ccbd8b530613e8df88ce2"}, - {file = "typing_extensions-4.1.1.tar.gz", hash = "sha256:1a9462dcc3347a79b1f1c0271fbe79e844580bb598bafa1ed208b94da3cdcd42"}, + {file = "typing_extensions-4.0.1-py3-none-any.whl", hash = "sha256:7f001e5ac290a0c0401508864c7ec868be4e701886d5b573a9528ed3973d9d3b"}, + {file = "typing_extensions-4.0.1.tar.gz", hash = "sha256:4ca091dea149f945ec56afb48dae714f21e8692ef22a395223bcd328961b6a0e"}, ] urllib3 = [ - {file = "urllib3-1.26.9-py2.py3-none-any.whl", hash = "sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14"}, - {file = "urllib3-1.26.9.tar.gz", hash = "sha256:aabaf16477806a5e1dd19aa41f8c2b7950dd3c746362d7e3223dbe6de6ac448e"}, + {file = "urllib3-1.26.8-py2.py3-none-any.whl", hash = "sha256:000ca7f471a233c2251c6c7023ee85305721bfdf18621ebff4fd17a8653427ed"}, + {file = "urllib3-1.26.8.tar.gz", hash = "sha256:0e7c33d9a63e7ddfcb86780aac87befc2fbddf46c58dbb487e0855f7ceec283c"}, ] virtualenv = [ - {file = "virtualenv-20.13.4-py2.py3-none-any.whl", hash = "sha256:c3e01300fb8495bc00ed70741f5271fc95fed067eb7106297be73d30879af60c"}, - {file = "virtualenv-20.13.4.tar.gz", hash = "sha256:ce8901d3bbf3b90393498187f2d56797a8a452fb2d0d7efc6fd837554d6f679c"}, + {file = "virtualenv-20.13.0-py2.py3-none-any.whl", hash = "sha256:339f16c4a86b44240ba7223d0f93a7887c3ca04b5f9c8129da7958447d079b09"}, + {file = "virtualenv-20.13.0.tar.gz", hash = "sha256:d8458cf8d59d0ea495ad9b34c2599487f8a7772d796f9910858376d1600dd2dd"}, ] voluptuous = [ {file = "voluptuous-0.12.2.tar.gz", hash = "sha256:4db1ac5079db9249820d49c891cb4660a6f8cae350491210abce741fabf56513"}, diff --git a/pyproject.toml b/pyproject.toml index 9d7e084af..ac5618a4d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,7 +18,8 @@ kasa = "kasa.cli:cli" python = "^3.7" anyio = "*" # see https://github.com/python-trio/asyncclick/issues/18 importlib-metadata = "*" -asyncclick = ">=8" +asyncclick = ">=7" +pydantic = "^1" # required only for docs sphinx = { version = "^3", optional = true } From e3588047fc3edb1dd7c157280af4cbecffaa8387 Mon Sep 17 00:00:00 2001 From: Teemu R Date: Fri, 19 Nov 2021 16:41:49 +0100 Subject: [PATCH 02/17] Improve usage module, consolidate API with emeter (#249) * Consolidate API for both emeter&usage modules * Add new cli command 'usage' to query usage --- kasa/cli.py | 39 +++++++++++++++++++++++++++- kasa/modules/emeter.py | 59 ++++++++++++++++++++++++++++++++++++++---- kasa/modules/usage.py | 41 +++++++++++++++++++++++++---- kasa/smartdevice.py | 41 +++++------------------------ 4 files changed, 134 insertions(+), 46 deletions(-) diff --git a/kasa/cli.py b/kasa/cli.py index f2fd66d12..c9cab4b50 100755 --- a/kasa/cli.py +++ b/kasa/cli.py @@ -316,7 +316,6 @@ async def emeter(dev: SmartDevice, year, month, erase): usage_data = await dev.get_emeter_daily(year=month.year, month=month.month) else: # Call with no argument outputs summary data and returns - usage_data = {} emeter_status = dev.emeter_realtime click.echo("Current: %s A" % emeter_status["current"]) @@ -334,6 +333,44 @@ async def emeter(dev: SmartDevice, year, month, erase): click.echo(f"{index}, {usage}") +@cli.command() +@pass_dev +@click.option("--year", type=click.DateTime(["%Y"]), default=None, required=False) +@click.option("--month", type=click.DateTime(["%Y-%m"]), default=None, required=False) +@click.option("--erase", is_flag=True) +async def usage(dev: SmartDevice, year, month, erase): + """Query usage for historical consumption. + + Daily and monthly data provided in CSV format. + """ + click.echo(click.style("== Usage ==", bold=True)) + usage = dev.modules["usage"] + + if erase: + click.echo("Erasing usage statistics..") + click.echo(await usage.erase_stats()) + return + + if year: + click.echo(f"== For year {year.year} ==") + click.echo("Month, usage (minutes)") + usage_data = await usage.get_monthstat(year.year) + elif month: + click.echo(f"== For month {month.month} of {month.year} ==") + click.echo("Day, usage (minutes)") + usage_data = await usage.get_daystat(year=month.year, month=month.month) + else: + # Call with no argument outputs summary data and returns + click.echo("Today: %s minutes" % usage.usage_today) + click.echo("This month: %s minutes" % usage.usage_this_month) + + return + + # output any detailed usage data + for index, usage in usage_data.items(): + click.echo(f"{index}, {usage}") + + @cli.command() @click.argument("brightness", type=click.IntRange(0, 100), default=None, required=False) @click.option("--transition", type=int, required=False) diff --git a/kasa/modules/emeter.py b/kasa/modules/emeter.py index f8144e39e..bb161ce61 100644 --- a/kasa/modules/emeter.py +++ b/kasa/modules/emeter.py @@ -1,4 +1,7 @@ """Implementation of the emeter module.""" +from datetime import datetime +from typing import Dict, Optional + from ..emeterstatus import EmeterStatus from .usage import Usage @@ -6,15 +9,61 @@ class Emeter(Usage): """Emeter module.""" - def query(self): - """Prepare query for emeter data.""" - return self._device._create_emeter_request() - @property # type: ignore def realtime(self) -> EmeterStatus: """Return current energy readings.""" return EmeterStatus(self.data["get_realtime"]) + @property + def emeter_today(self) -> Optional[float]: + """Return today's energy consumption in kWh.""" + raw_data = self.daily_data + today = datetime.now().day + data = self._emeter_convert_emeter_data(raw_data) + + return data.get(today) + + @property + def emeter_this_month(self) -> Optional[float]: + """Return this month's energy consumption in kWh.""" + raw_data = self.monthly_data + current_month = datetime.now().month + data = self._emeter_convert_emeter_data(raw_data) + + return data.get(current_month) + async def erase_stats(self): - """Erase all stats.""" + """Erase all stats. + + Uses different query than usage meter. + """ return await self.call("erase_emeter_stat") + + async def get_daystat(self, *, year, month, kwh=True): + """Return daily stats for the given year & month.""" + raw_data = await super().get_daystat(year=year, month=month) + return self._emeter_convert_emeter_data(raw_data["day_list"], kwh) + + async def get_monthstat(self, *, year, kwh=True): + """Return monthly stats for the given year.""" + raw_data = await super().get_monthstat(year=year) + return self._emeter_convert_emeter_data(raw_data["month_list"], kwh) + + def _emeter_convert_emeter_data(self, data, kwh=True) -> Dict: + """Return emeter information keyed with the day/month..""" + response = [EmeterStatus(**x) for x in data] + + if not response: + return {} + + energy_key = "energy_wh" + if kwh: + energy_key = "energy" + + entry_key = "month" + if "day" in response[0]: + entry_key = "day" + + data = {entry[entry_key]: entry[energy_key] for entry in response} + + return data diff --git a/kasa/modules/usage.py b/kasa/modules/usage.py index 2a5b6dc0b..5aecb9a75 100644 --- a/kasa/modules/usage.py +++ b/kasa/modules/usage.py @@ -17,22 +17,53 @@ def query(self): req, self.query_for_command("get_daystat", {"year": year, "month": month}) ) req = merge(req, self.query_for_command("get_monthstat", {"year": year})) - req = merge(req, self.query_for_command("get_next_action")) return req - async def get_daystat(self, year, month): - """Return stats for the current day.""" + @property + def daily_data(self): + """Return statistics on daily basis.""" + return self.data["get_daystat"]["day_list"] + + @property + def monthly_data(self): + """Return statistics on monthly basis.""" + return self.data["get_monthstat"]["month_list"] + + @property + def usage_today(self): + """Return today's usage in minutes.""" + today = datetime.now().day + converted = [x["time"] for x in self.daily_data if x["day"] == today] + if not converted: + return None + + return converted.pop() + + @property + def usage_this_month(self): + """Return usage in this month in minutes.""" + this_month = datetime.now().month + converted = [x["time"] for x in self.monthly_data if x["month"] == this_month] + if not converted: + return None + + return converted.pop() + + async def get_daystat(self, *, year=None, month=None): + """Return daily stats for the given year & month.""" if year is None: year = datetime.now().year if month is None: month = datetime.now().month + return await self.call("get_daystat", {"year": year, "month": month}) - async def get_monthstat(self, year): - """Return stats for the current month.""" + async def get_monthstat(self, *, year=None): + """Return monthly stats for the given year.""" if year is None: year = datetime.now().year + return await self.call("get_monthstat", {"year": year}) async def erase_stats(self): diff --git a/kasa/smartdevice.py b/kasa/smartdevice.py index 5e6877417..d2cf80cc6 100755 --- a/kasa/smartdevice.py +++ b/kasa/smartdevice.py @@ -482,6 +482,7 @@ async def get_emeter_realtime(self) -> EmeterStatus: def _create_emeter_request(self, year: int = None, month: int = None): """Create a Internal method for building a request for all emeter statistics at once.""" + # TODO: this is currently only here for smartstrip plug support, move it there? if year is None: year = datetime.now().year if month is None: @@ -506,28 +507,14 @@ def _create_emeter_request(self, year: int = None, month: int = None): def emeter_today(self) -> Optional[float]: """Return today's energy consumption in kWh.""" self._verify_emeter() - raw_data = self._last_update[self.emeter_type]["get_daystat"]["day_list"] - data = self._emeter_convert_emeter_data(raw_data) - today = datetime.now().day - - if today in data: - return data[today] - - return None + return self.modules["emeter"].emeter_today @property # type: ignore @requires_update def emeter_this_month(self) -> Optional[float]: """Return this month's energy consumption in kWh.""" self._verify_emeter() - raw_data = self._last_update[self.emeter_type]["get_monthstat"]["month_list"] - data = self._emeter_convert_emeter_data(raw_data) - current_month = datetime.now().month - - if current_month in data: - return data[current_month] - - return None + return self.modules["emeter"].emeter_this_month def _emeter_convert_emeter_data(self, data, kwh=True) -> Dict: """Return emeter information keyed with the day/month..""" @@ -560,16 +547,7 @@ async def get_emeter_daily( :return: mapping of day of month to value """ self._verify_emeter() - if year is None: - year = datetime.now().year - if month is None: - month = datetime.now().month - - response = await self._query_helper( - self.emeter_type, "get_daystat", {"month": month, "year": year} - ) - - return self._emeter_convert_emeter_data(response["day_list"], kwh) + return await self.modules["emeter"].get_daystat(year=year, month=month, kwh=kwh) @requires_update async def get_emeter_monthly(self, year: int = None, kwh: bool = True) -> Dict: @@ -580,14 +558,7 @@ async def get_emeter_monthly(self, year: int = None, kwh: bool = True) -> Dict: :return: dict: mapping of month to value """ self._verify_emeter() - if year is None: - year = datetime.now().year - - response = await self._query_helper( - self.emeter_type, "get_monthstat", {"year": year} - ) - - return self._emeter_convert_emeter_data(response["month_list"], kwh) + return await self.modules["emeter"].get_monthstat(year=year, kwh=kwh) @requires_update async def erase_emeter_stats(self) -> Dict: @@ -599,7 +570,7 @@ async def erase_emeter_stats(self) -> Dict: async def current_consumption(self) -> float: """Get the current power consumption in Watt.""" self._verify_emeter() - response = EmeterStatus(await self.get_emeter_realtime()) + response = self.emeter_realtime return float(response["power"]) async def reboot(self, delay: int = 1) -> None: From 8c7b1b4a684c34a47e079500d30d27d8d287579a Mon Sep 17 00:00:00 2001 From: Teemu R Date: Sat, 29 Jan 2022 17:53:18 +0100 Subject: [PATCH 03/17] Implement motion & ambient light sensor modules for dimmers (#278) --- kasa/modules/__init__.py | 2 ++ kasa/modules/ambientlight.py | 47 +++++++++++++++++++++++++++ kasa/modules/motion.py | 62 ++++++++++++++++++++++++++++++++++++ kasa/smartdimmer.py | 5 +++ 4 files changed, 116 insertions(+) create mode 100644 kasa/modules/ambientlight.py create mode 100644 kasa/modules/motion.py diff --git a/kasa/modules/__init__.py b/kasa/modules/__init__.py index dd9d1072f..e5cb83d66 100644 --- a/kasa/modules/__init__.py +++ b/kasa/modules/__init__.py @@ -1,9 +1,11 @@ # flake8: noqa +from .ambientlight import AmbientLight from .antitheft import Antitheft from .cloud import Cloud from .countdown import Countdown from .emeter import Emeter from .module import Module +from .motion import Motion from .rulemodule import Rule, RuleModule from .schedule import Schedule from .time import Time diff --git a/kasa/modules/ambientlight.py b/kasa/modules/ambientlight.py new file mode 100644 index 000000000..963c73a3f --- /dev/null +++ b/kasa/modules/ambientlight.py @@ -0,0 +1,47 @@ +"""Implementation of the ambient light (LAS) module found in some dimmers.""" +from .module import Module + +# TODO create tests and use the config reply there +# [{"hw_id":0,"enable":0,"dark_index":1,"min_adc":0,"max_adc":2450, +# "level_array":[{"name":"cloudy","adc":490,"value":20}, +# {"name":"overcast","adc":294,"value":12}, +# {"name":"dawn","adc":222,"value":9}, +# {"name":"twilight","adc":222,"value":9}, +# {"name":"total darkness","adc":111,"value":4}, +# {"name":"custom","adc":2400,"value":97}]}] + + +class AmbientLight(Module): + """Implements ambient light controls for the motion sensor.""" + + def query(self): + """Request configuration.""" + return self.query_for_command("get_config") + + @property + def presets(self) -> dict: + """Return device-defined presets for brightness setting.""" + return self.data["level_array"] + + @property + def enabled(self) -> bool: + """Return True if the module is enabled.""" + return bool(self.data["enable"]) + + async def set_enabled(self, state: bool): + """Enable/disable LAS.""" + return await self.call("set_enable", {"enable": int(state)}) + + async def current_brightness(self) -> int: + """Return current brightness. + + Return value units. + """ + return await self.call("get_current_brt") + + async def set_brightness_limit(self, value: int): + """Set the limit when the motion sensor is inactive. + + See `presets` for preset values. Custom values are also likely allowed. + """ + return await self.call("set_brt_level", {"index": 0, "value": value}) diff --git a/kasa/modules/motion.py b/kasa/modules/motion.py new file mode 100644 index 000000000..d839ca98e --- /dev/null +++ b/kasa/modules/motion.py @@ -0,0 +1,62 @@ +"""Implementation of the motion detection (PIR) module found in some dimmers.""" +from enum import Enum +from typing import Optional + +from kasa.smartdevice import SmartDeviceException + +from .module import Module + + +class Range(Enum): + """Range for motion detection.""" + + Far = 0 + Mid = 1 + Near = 2 + Custom = 3 + + +# TODO: use the config reply in tests +# {"enable":0,"version":"1.0","trigger_index":2,"cold_time":60000, +# "min_adc":0,"max_adc":4095,"array":[80,50,20,0],"err_code":0}}} + + +class Motion(Module): + """Implements the motion detection (PIR) module.""" + + def query(self): + """Request PIR configuration.""" + return self.query_for_command("get_config") + + @property + def range(self) -> Range: + """Return motion detection range.""" + return Range(self.data["trigger_index"]) + + @property + def enabled(self) -> bool: + """Return True if module is enabled.""" + return bool(self.data["enable"]) + + async def set_enabled(self, state: bool): + """Enable/disable PIR.""" + return await self.call("set_enable", {"enable": int(state)}) + + async def set_range( + self, *, range: Optional[Range] = None, custom_range: Optional[int] = None + ): + """Set the range for the sensor. + + :param range: for using standard ranges + :param custom_range: range in decimeters, overrides the range parameter + """ + if custom_range is not None: + payload = {"index": Range.Custom.value, "value": custom_range} + elif range is not None: + payload = {"index": range.value} + else: + raise SmartDeviceException( + "Either range or custom_range need to be defined" + ) + + return await self.call("set_trigger_sens", payload) diff --git a/kasa/smartdimmer.py b/kasa/smartdimmer.py index 8e5cb1527..5c06b8b94 100644 --- a/kasa/smartdimmer.py +++ b/kasa/smartdimmer.py @@ -1,6 +1,7 @@ """Module for dimmers (currently only HS220).""" from typing import Any, Dict +from kasa.modules import AmbientLight, Motion from kasa.smartdevice import DeviceType, SmartDeviceException, requires_update from kasa.smartplug import SmartPlug @@ -40,6 +41,10 @@ class SmartDimmer(SmartPlug): def __init__(self, host: str) -> None: super().__init__(host) self._device_type = DeviceType.Dimmer + # TODO: need to be verified if it's okay to call these on HS220 w/o these + # TODO: need to be figured out what's the best approach to detect support for these + self.add_module("motion", Motion(self, "smartlife.iot.PIR")) + self.add_module("ambient", AmbientLight(self, "smartlife.iot.LAS")) @property # type: ignore @requires_update From 3a7836cd33d0536545436a5164dbfdfcdacc60b7 Mon Sep 17 00:00:00 2001 From: Teemu R Date: Sat, 29 Jan 2022 18:15:59 +0100 Subject: [PATCH 04/17] Do not request unsupported modules after the initial update (#298) * Do not request unsupported modules after the initial update * debugify logging --- kasa/modules/module.py | 8 ++++++++ kasa/smartdevice.py | 3 +++ 2 files changed, 11 insertions(+) diff --git a/kasa/modules/module.py b/kasa/modules/module.py index 1f7f3829f..dc4c1bfae 100644 --- a/kasa/modules/module.py +++ b/kasa/modules/module.py @@ -1,5 +1,6 @@ """Base class for all module implementations.""" import collections +import logging from abc import ABC, abstractmethod from typing import TYPE_CHECKING @@ -7,6 +8,9 @@ from kasa import SmartDevice +_LOGGER = logging.getLogger(__name__) + + # TODO: This is used for query construcing def merge(d, u): """Update dict recursively.""" @@ -45,6 +49,10 @@ def data(self): @property def is_supported(self) -> bool: """Return whether the module is supported by the device.""" + if self._module not in self._device._last_update: + _LOGGER.debug("Initial update, so consider supported: %s", self._module) + return True + return "err_code" not in self.data def call(self, method, params=None): diff --git a/kasa/smartdevice.py b/kasa/smartdevice.py index d2cf80cc6..812656c87 100755 --- a/kasa/smartdevice.py +++ b/kasa/smartdevice.py @@ -327,6 +327,9 @@ async def update(self, update_children: bool = True): self.add_module("emeter", Emeter(self, self.emeter_type)) for module in self.modules.values(): + if not module.is_supported: + _LOGGER.debug("Module %s not supported, skipping" % module) + continue q = module.query() _LOGGER.debug("Adding query for %s: %s", module, q) req = merge(req, module.query()) From bb013e75da311a3a5fdf941c3a6edce7c1ba3b67 Mon Sep 17 00:00:00 2001 From: Teemu Rytilahti Date: Sat, 29 Jan 2022 20:33:35 +0100 Subject: [PATCH 05/17] Raise an exception when trying to access data prior updating --- kasa/modules/module.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/kasa/modules/module.py b/kasa/modules/module.py index dc4c1bfae..7340d7e11 100644 --- a/kasa/modules/module.py +++ b/kasa/modules/module.py @@ -4,6 +4,8 @@ from abc import ABC, abstractmethod from typing import TYPE_CHECKING +from ..exceptions import SmartDeviceException + if TYPE_CHECKING: from kasa import SmartDevice @@ -44,6 +46,11 @@ def query(self): @property def data(self): """Return the module specific raw data from the last update.""" + if self._module not in self._device._last_update: + raise SmartDeviceException( + f"You need to call update() prior accessing module data for '{self._module}'" + ) + return self._device._last_update[self._module] @property From c8ad99abcbc6bbef8cd2360d8821e1774d26f1de Mon Sep 17 00:00:00 2001 From: Teemu Rytilahti Date: Sat, 29 Jan 2022 20:35:10 +0100 Subject: [PATCH 06/17] Use device time for on_since for smartstripplugs --- kasa/smartstrip.py | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/kasa/smartstrip.py b/kasa/smartstrip.py index bbdf2a3fb..a0502125b 100755 --- a/kasa/smartstrip.py +++ b/kasa/smartstrip.py @@ -13,7 +13,7 @@ ) from kasa.smartplug import SmartPlug -from .modules import Antitheft, Countdown, Schedule, Time, Usage +from .modules import Antitheft, Countdown, Emeter, Schedule, Time, Usage _LOGGER = logging.getLogger(__name__) @@ -87,6 +87,7 @@ def __init__(self, host: str) -> None: self.add_module("usage", Usage(self, "schedule")) self.add_module("time", Time(self, "time")) self.add_module("countdown", Countdown(self, "countdown")) + self.add_module("emeter", Emeter(self, "emeter")) @property # type: ignore @requires_update @@ -255,10 +256,32 @@ async def update(self, update_children: bool = True): Needed for properties that are decorated with `requires_update`. """ + # TODO: it needs to be checked if this still works after modularization self._last_update = await self.parent.protocol.query( self._create_emeter_request() ) + def _create_emeter_request(self, year: int = None, month: int = None): + """Create a request for requesting all emeter statistics at once.""" + if year is None: + year = datetime.now().year + if month is None: + month = datetime.now().month + + req: Dict[str, Any] = {} + from .smartdevice import merge + + merge(req, self._create_request("emeter", "get_realtime")) + merge(req, self._create_request("emeter", "get_monthstat", {"year": year})) + merge( + req, + self._create_request( + "emeter", "get_daystat", {"month": month, "year": year} + ), + ) + + return req + def _create_request( self, target: str, cmd: str, arg: Optional[Dict] = None, child_ids=None ): @@ -325,7 +348,7 @@ def on_since(self) -> Optional[datetime]: info = self._get_child_info() on_time = info["on_time"] - return datetime.now() - timedelta(seconds=on_time) + return self.time - timedelta(seconds=on_time) @property # type: ignore @requires_update From f0d66e4195079c24378c0ce36b85617188514de3 Mon Sep 17 00:00:00 2001 From: Teemu Rytilahti Date: Sat, 29 Jan 2022 20:36:08 +0100 Subject: [PATCH 07/17] move get_time{zone} out from smartdevice + some minor cleanups --- kasa/modules/emeter.py | 4 ++++ kasa/modules/motion.py | 3 +-- kasa/modules/time.py | 20 ++++++++++++++++ kasa/smartdevice.py | 53 ++++++++---------------------------------- kasa/smartplug.py | 1 - 5 files changed, 35 insertions(+), 46 deletions(-) diff --git a/kasa/modules/emeter.py b/kasa/modules/emeter.py index bb161ce61..cd92c3cce 100644 --- a/kasa/modules/emeter.py +++ b/kasa/modules/emeter.py @@ -39,6 +39,10 @@ async def erase_stats(self): """ return await self.call("erase_emeter_stat") + async def get_realtime(self): + """Return real-time statistics.""" + return await self.call("get_realtime") + async def get_daystat(self, *, year, month, kwh=True): """Return daily stats for the given year & month.""" raw_data = await super().get_daystat(year=year, month=month) diff --git a/kasa/modules/motion.py b/kasa/modules/motion.py index d839ca98e..45e272bed 100644 --- a/kasa/modules/motion.py +++ b/kasa/modules/motion.py @@ -2,8 +2,7 @@ from enum import Enum from typing import Optional -from kasa.smartdevice import SmartDeviceException - +from ..exceptions import SmartDeviceException from .module import Module diff --git a/kasa/modules/time.py b/kasa/modules/time.py index 0bd3f1714..d72e2d600 100644 --- a/kasa/modules/time.py +++ b/kasa/modules/time.py @@ -1,6 +1,7 @@ """Provides the current time and timezone information.""" from datetime import datetime +from ..exceptions import SmartDeviceException from .module import Module, merge @@ -32,3 +33,22 @@ def timezone(self): """Return current timezone.""" res = self.data["get_timezone"] return res + + async def get_time(self): + """Return current device time.""" + try: + res = await self.call("get_time") + return datetime( + res["year"], + res["month"], + res["mday"], + res["hour"], + res["min"], + res["sec"], + ) + except SmartDeviceException: + return None + + async def get_timezone(self): + """Request timezone information from the device.""" + return await self.call("get_timezone") diff --git a/kasa/smartdevice.py b/kasa/smartdevice.py index 812656c87..b589d86a9 100755 --- a/kasa/smartdevice.py +++ b/kasa/smartdevice.py @@ -186,7 +186,6 @@ class SmartDevice: """ - TIME_SERVICE = "time" emeter_type = "emeter" def __init__(self, host: str) -> None: @@ -314,11 +313,6 @@ async def update(self, update_children: bool = True): _LOGGER.debug("Performing the initial update to obtain sysinfo") self._last_update = await self.protocol.query(req) self._sys_info = self._last_update["system"]["get_sysinfo"] - # If the device has no emeter, we are done for the initial update - # Otherwise we will follow the regular code path to also query - # the emeter data also during the initial update - if not self.has_emeter: - return if self.has_emeter: _LOGGER.debug( @@ -380,22 +374,17 @@ def timezone(self) -> Dict: async def get_time(self) -> Optional[datetime]: """Return current time from the device, if available.""" - try: - res = await self._query_helper(self.TIME_SERVICE, "get_time") - return datetime( - res["year"], - res["month"], - res["mday"], - res["hour"], - res["min"], - res["sec"], - ) - except SmartDeviceException: - return None + _LOGGER.warning( + "Use `time` property instead, this call will be removed in the future." + ) + return await self.modules["time"].get_time() async def get_timezone(self) -> Dict: """Return timezone information.""" - return await self._query_helper(self.TIME_SERVICE, "get_timezone") + _LOGGER.warning( + "Use `timezone` property instead, this call will be removed in the future." + ) + return await self.modules["time"].get_timezone() @property # type: ignore @requires_update @@ -433,7 +422,7 @@ def location(self) -> Dict: loc["latitude"] = sys_info["latitude_i"] / 10000 loc["longitude"] = sys_info["longitude_i"] / 10000 else: - _LOGGER.warning("Unsupported device location.") + _LOGGER.debug("Unsupported device location.") return loc @@ -481,29 +470,7 @@ def emeter_realtime(self) -> EmeterStatus: async def get_emeter_realtime(self) -> EmeterStatus: """Retrieve current energy readings.""" self._verify_emeter() - return EmeterStatus(await self._query_helper(self.emeter_type, "get_realtime")) - - def _create_emeter_request(self, year: int = None, month: int = None): - """Create a Internal method for building a request for all emeter statistics at once.""" - # TODO: this is currently only here for smartstrip plug support, move it there? - if year is None: - year = datetime.now().year - if month is None: - month = datetime.now().month - - req: Dict[str, Any] = {} - merge(req, self._create_request(self.emeter_type, "get_realtime")) - merge( - req, self._create_request(self.emeter_type, "get_monthstat", {"year": year}) - ) - merge( - req, - self._create_request( - self.emeter_type, "get_daystat", {"month": month, "year": year} - ), - ) - - return req + return EmeterStatus(await self.modules["emeter"].get_realtime()) @property # type: ignore @requires_update diff --git a/kasa/smartplug.py b/kasa/smartplug.py index 58144b58a..b636c3e11 100644 --- a/kasa/smartplug.py +++ b/kasa/smartplug.py @@ -39,7 +39,6 @@ class SmartPlug(SmartDevice): def __init__(self, host: str) -> None: super().__init__(host) - self.emeter_type = "emeter" self._device_type = DeviceType.Plug self.add_module("schedule", Schedule(self, "schedule")) self.add_module("usage", Usage(self, "schedule")) From 1e4df7ec1bbc81f72f7ca0023266318174e3e5ce Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 5 Apr 2022 06:16:36 -1000 Subject: [PATCH 08/17] Fix modularize with strips (#326) * Fix test_deprecated_type stalling * Fix strips with modularize * Fix test_deprecated_type stalling (#325) --- kasa/protocol.py | 23 +++++++++-------------- kasa/smartdevice.py | 8 ++++++-- kasa/smartstrip.py | 11 ++++++----- kasa/tests/test_smartdevice.py | 4 +++- 4 files changed, 24 insertions(+), 22 deletions(-) diff --git a/kasa/protocol.py b/kasa/protocol.py index e2f946269..24c2cd056 100755 --- a/kasa/protocol.py +++ b/kasa/protocol.py @@ -70,20 +70,13 @@ async def query(self, request: Union[str, Dict], retry_count: int = 3) -> Dict: async with self.query_lock: return await self._query(request, retry_count, timeout) - async def _connect(self, timeout: int) -> bool: + async def _connect(self, timeout: int) -> None: """Try to connect or reconnect to the device.""" if self.writer: - return True - - with contextlib.suppress(Exception): - self.reader = self.writer = None - task = asyncio.open_connection( - self.host, TPLinkSmartHomeProtocol.DEFAULT_PORT - ) - self.reader, self.writer = await asyncio.wait_for(task, timeout=timeout) - return True - - return False + return + self.reader = self.writer = None + task = asyncio.open_connection(self.host, TPLinkSmartHomeProtocol.DEFAULT_PORT) + self.reader, self.writer = await asyncio.wait_for(task, timeout=timeout) async def _execute_query(self, request: str) -> Dict: """Execute a query on the device and wait for the response.""" @@ -123,12 +116,14 @@ def _reset(self) -> None: async def _query(self, request: str, retry_count: int, timeout: int) -> Dict: """Try to query a device.""" for retry in range(retry_count + 1): - if not await self._connect(timeout): + try: + await self._connect(timeout) + except Exception as ex: await self.close() if retry >= retry_count: _LOGGER.debug("Giving up on %s after %s retries", self.host, retry) raise SmartDeviceException( - f"Unable to connect to the device: {self.host}" + f"Unable to connect to the device: {self.host}: {ex}" ) continue diff --git a/kasa/smartdevice.py b/kasa/smartdevice.py index b589d86a9..93e01758c 100755 --- a/kasa/smartdevice.py +++ b/kasa/smartdevice.py @@ -314,6 +314,11 @@ async def update(self, update_children: bool = True): self._last_update = await self.protocol.query(req) self._sys_info = self._last_update["system"]["get_sysinfo"] + await self._modular_update(req) + self._sys_info = self._last_update["system"]["get_sysinfo"] + + async def _modular_update(self, req: dict) -> None: + """Execute an update query.""" if self.has_emeter: _LOGGER.debug( "The device has emeter, querying its information along sysinfo" @@ -326,10 +331,9 @@ async def update(self, update_children: bool = True): continue q = module.query() _LOGGER.debug("Adding query for %s: %s", module, q) - req = merge(req, module.query()) + req = merge(req, q) self._last_update = await self.protocol.query(req) - self._sys_info = self._last_update["system"]["get_sysinfo"] def update_from_discover_info(self, info): """Update state from info from the discover call.""" diff --git a/kasa/smartstrip.py b/kasa/smartstrip.py index a0502125b..353a9c44b 100755 --- a/kasa/smartstrip.py +++ b/kasa/smartstrip.py @@ -3,13 +3,14 @@ from collections import defaultdict from datetime import datetime, timedelta from typing import Any, DefaultDict, Dict, Optional - +import asyncio from kasa.smartdevice import ( DeviceType, EmeterStatus, SmartDevice, SmartDeviceException, requires_update, + merge, ) from kasa.smartplug import SmartPlug @@ -250,16 +251,16 @@ def __init__(self, host: str, parent: "SmartStrip", child_id: str) -> None: self._last_update = parent._last_update self._sys_info = parent._sys_info self._device_type = DeviceType.StripSocket + self.modules = {} + self.protocol = parent.protocol # Must use the same connection as the parent + self.add_module("time", Time(self, "time")) async def update(self, update_children: bool = True): """Query the device to update the data. Needed for properties that are decorated with `requires_update`. """ - # TODO: it needs to be checked if this still works after modularization - self._last_update = await self.parent.protocol.query( - self._create_emeter_request() - ) + await self._modular_update({}) def _create_emeter_request(self, year: int = None, month: int = None): """Create a request for requesting all emeter statistics at once.""" diff --git a/kasa/tests/test_smartdevice.py b/kasa/tests/test_smartdevice.py index d977daeb3..9138a7e5c 100644 --- a/kasa/tests/test_smartdevice.py +++ b/kasa/tests/test_smartdevice.py @@ -36,7 +36,9 @@ async def test_initial_update_no_emeter(dev, mocker): dev._last_update = None spy = mocker.spy(dev.protocol, "query") await dev.update() - assert spy.call_count == 1 + # 2 calls are necessary as some devices crash on unexpected modules + # See #105, #120, #161 + assert spy.call_count == 2 async def test_query_helper(dev): From 2a0919efd568a2c4fac848fab4a1d4b7c9abe0ff Mon Sep 17 00:00:00 2001 From: Teemu Rytilahti Date: Tue, 5 Apr 2022 18:25:41 +0200 Subject: [PATCH 09/17] Fix linting --- kasa/smartstrip.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/kasa/smartstrip.py b/kasa/smartstrip.py index 353a9c44b..ba863d059 100755 --- a/kasa/smartstrip.py +++ b/kasa/smartstrip.py @@ -3,14 +3,14 @@ from collections import defaultdict from datetime import datetime, timedelta from typing import Any, DefaultDict, Dict, Optional -import asyncio + from kasa.smartdevice import ( DeviceType, EmeterStatus, SmartDevice, SmartDeviceException, - requires_update, merge, + requires_update, ) from kasa.smartplug import SmartPlug @@ -270,7 +270,6 @@ def _create_emeter_request(self, year: int = None, month: int = None): month = datetime.now().month req: Dict[str, Any] = {} - from .smartdevice import merge merge(req, self._create_request("emeter", "get_realtime")) merge(req, self._create_request("emeter", "get_monthstat", {"year": year})) From 68038c93dfbb88fde1e2e1cc6e4dccb7e9ce0173 Mon Sep 17 00:00:00 2001 From: Teemu Rytilahti Date: Tue, 5 Apr 2022 18:36:57 +0200 Subject: [PATCH 10/17] Bump version to 0.5.0.dev0 --- poetry.lock | 287 +++++++++++++++++++++++++------------------------ pyproject.toml | 4 +- 2 files changed, 148 insertions(+), 143 deletions(-) diff --git a/poetry.lock b/poetry.lock index 3ce2e28bf..aa1231d90 100644 --- a/poetry.lock +++ b/poetry.lock @@ -87,7 +87,7 @@ python-versions = ">=3.6.1" [[package]] name = "charset-normalizer" -version = "2.0.10" +version = "2.0.12" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." category = "main" optional = false @@ -118,7 +118,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "coverage" -version = "6.3" +version = "6.3.2" description = "Code coverage measurement for Python" category = "dev" optional = false @@ -148,7 +148,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "filelock" -version = "3.4.2" +version = "3.6.0" description = "A platform independent file lock." category = "dev" optional = false @@ -160,7 +160,7 @@ testing = ["covdefaults (>=1.2.0)", "coverage (>=4)", "pytest (>=4)", "pytest-co [[package]] name = "identify" -version = "2.4.6" +version = "2.4.12" description = "File identification library for Python" category = "dev" optional = false @@ -187,7 +187,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "importlib-metadata" -version = "4.10.1" +version = "4.11.3" description = "Read metadata from Python packages" category = "main" optional = false @@ -198,9 +198,9 @@ typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} zipp = ">=0.5" [package.extras] -docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] +docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)"] perf = ["ipython"] -testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] +testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)", "importlib-resources (>=1.3)"] [[package]] name = "iniconfig" @@ -212,11 +212,11 @@ python-versions = "*" [[package]] name = "jinja2" -version = "3.0.3" +version = "3.1.1" description = "A very fast and expressive template engine." category = "main" optional = true -python-versions = ">=3.6" +python-versions = ">=3.7" [package.dependencies] MarkupSafe = ">=2.0" @@ -238,11 +238,11 @@ mistune = "*" [[package]] name = "markupsafe" -version = "2.0.1" +version = "2.1.1" description = "Safely add untrusted strings to HTML/XML markup." category = "main" optional = true -python-versions = ">=3.6" +python-versions = ">=3.7" [[package]] name = "mistune" @@ -273,7 +273,7 @@ pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" [[package]] name = "platformdirs" -version = "2.4.1" +version = "2.5.1" description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." category = "dev" optional = false @@ -300,11 +300,11 @@ testing = ["pytest", "pytest-benchmark"] [[package]] name = "pre-commit" -version = "2.17.0" +version = "2.18.1" description = "A framework for managing and maintaining multi-language pre-commit hooks." category = "dev" optional = false -python-versions = ">=3.6.1" +python-versions = ">=3.7" [package.dependencies] cfgv = ">=2.0.0" @@ -359,11 +359,11 @@ diagrams = ["jinja2", "railroad-diagrams"] [[package]] name = "pytest" -version = "6.2.5" +version = "7.1.1" description = "pytest: simple powerful testing with Python" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.dependencies] atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} @@ -374,14 +374,14 @@ iniconfig = "*" packaging = "*" pluggy = ">=0.12,<2.0" py = ">=1.8.2" -toml = "*" +tomli = ">=1.0.0" [package.extras] -testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] +testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] [[package]] name = "pytest-asyncio" -version = "0.17.2" +version = "0.18.3" description = "Pytest support for asyncio" category = "dev" optional = false @@ -389,10 +389,10 @@ python-versions = ">=3.7" [package.dependencies] pytest = ">=6.1.0" -typing-extensions = {version = ">=4.0", markers = "python_version < \"3.8\""} +typing-extensions = {version = ">=3.7.2", markers = "python_version < \"3.8\""} [package.extras] -testing = ["coverage (==6.2)", "hypothesis (>=5.7.1)", "flaky (>=3.5.0)", "mypy (==0.931)"] +testing = ["coverage (==6.2)", "hypothesis (>=5.7.1)", "flaky (>=3.5.0)", "mypy (==0.931)", "pytest-trio (>=0.7.0)"] [[package]] name = "pytest-cov" @@ -439,7 +439,7 @@ termcolor = ">=1.1.0" [[package]] name = "pytz" -version = "2021.3" +version = "2022.1" description = "World timezone definitions, modern and historical" category = "main" optional = true @@ -641,7 +641,7 @@ python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" [[package]] name = "tomli" -version = "2.0.0" +version = "2.0.1" description = "A lil' TOML parser" category = "dev" optional = false @@ -672,7 +672,7 @@ testing = ["flaky (>=3.4.0)", "freezegun (>=0.3.11)", "pytest (>=4.0.0)", "pytes [[package]] name = "typing-extensions" -version = "4.0.1" +version = "4.1.1" description = "Backported and Experimental Type Hints for Python 3.6+" category = "main" optional = false @@ -680,20 +680,20 @@ python-versions = ">=3.6" [[package]] name = "urllib3" -version = "1.26.8" +version = "1.26.9" description = "HTTP library with thread-safe connection pooling, file post, and more." category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" [package.extras] -brotli = ["brotlipy (>=0.6.0)"] +brotli = ["brotlicffi (>=0.8.0)", "brotli (>=1.0.9)", "brotlipy (>=0.6.0)"] secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] [[package]] name = "virtualenv" -version = "20.13.0" +version = "20.14.0" description = "Virtual Python Environment builder" category = "dev" optional = false @@ -712,7 +712,7 @@ testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", [[package]] name = "voluptuous" -version = "0.12.2" +version = "0.13.0" description = "" category = "dev" optional = false @@ -738,15 +738,15 @@ tests = ["codecov", "scikit-build", "cmake", "ninja", "pybind11", "pytest", "pyt [[package]] name = "zipp" -version = "3.7.0" +version = "3.8.0" description = "Backport of pathlib-compatible object wrapper for zip files" category = "main" optional = false python-versions = ">=3.7" [package.extras] -docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] -testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"] +docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)"] +testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)"] [extras] docs = ["sphinx", "sphinx_rtd_theme", "m2r", "mistune", "sphinxcontrib-programoutput"] @@ -754,7 +754,7 @@ docs = ["sphinx", "sphinx_rtd_theme", "m2r", "mistune", "sphinxcontrib-programou [metadata] lock-version = "1.1" python-versions = "^3.7" -content-hash = "eb7bb96b826fec8ad7207e838f02a68333d806f5908f3e07675cbc151a165b25" +content-hash = "cbc8eb721e3b498c25eef73c95b2aa309419fa075b878c18cac0b148113c25f9" [metadata.files] alabaster = [ @@ -789,8 +789,8 @@ cfgv = [ {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"}, ] charset-normalizer = [ - {file = "charset-normalizer-2.0.10.tar.gz", hash = "sha256:876d180e9d7432c5d1dfd4c5d26b72f099d503e8fcc0feb7532c9289be60fcbd"}, - {file = "charset_normalizer-2.0.10-py3-none-any.whl", hash = "sha256:cb957888737fc0bbcd78e3df769addb41fd1ff8cf950dc9e7ad7793f1bf44455"}, + {file = "charset-normalizer-2.0.12.tar.gz", hash = "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597"}, + {file = "charset_normalizer-2.0.12-py3-none-any.whl", hash = "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"}, ] codecov = [ {file = "codecov-2.1.12-py2.py3-none-any.whl", hash = "sha256:585dc217dc3d8185198ceb402f85d5cb5dbfa0c5f350a5abcdf9e347776a5b47"}, @@ -802,50 +802,47 @@ colorama = [ {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, ] coverage = [ - {file = "coverage-6.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e8071e7d9ba9f457fc674afc3de054450be2c9b195c470147fbbc082468d8ff7"}, - {file = "coverage-6.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:86c91c511853dfda81c2cf2360502cb72783f4b7cebabef27869f00cbe1db07d"}, - {file = "coverage-6.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c4ce3b647bd1792d4394f5690d9df6dc035b00bcdbc5595099c01282a59ae01"}, - {file = "coverage-6.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2a491e159294d756e7fc8462f98175e2d2225e4dbe062cca7d3e0d5a75ba6260"}, - {file = "coverage-6.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d008e0f67ac800b0ca04d7914b8501312c8c6c00ad8c7ba17754609fae1231a"}, - {file = "coverage-6.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4578728c36de2801c1deb1c6b760d31883e62e33f33c7ba8f982e609dc95167d"}, - {file = "coverage-6.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7ee317486593193e066fc5e98ac0ce712178c21529a85c07b7cb978171f25d53"}, - {file = "coverage-6.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2bc85664b06ba42d14bb74d6ddf19d8bfc520cb660561d2d9ce5786ae72f71b5"}, - {file = "coverage-6.3-cp310-cp310-win32.whl", hash = "sha256:27a94db5dc098c25048b0aca155f5fac674f2cf1b1736c5272ba28ead2fc267e"}, - {file = "coverage-6.3-cp310-cp310-win_amd64.whl", hash = "sha256:bde4aeabc0d1b2e52c4036c54440b1ad05beeca8113f47aceb4998bb7471e2c2"}, - {file = "coverage-6.3-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:509c68c3e2015022aeda03b003dd68fa19987cdcf64e9d4edc98db41cfc45d30"}, - {file = "coverage-6.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:e4ff163602c5c77e7bb4ea81ba5d3b793b4419f8acd296aae149370902cf4e92"}, - {file = "coverage-6.3-cp311-cp311-win_amd64.whl", hash = "sha256:d1675db48490e5fa0b300f6329ecb8a9a37c29b9ab64fa9c964d34111788ca2d"}, - {file = "coverage-6.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:7eed8459a2b81848cafb3280b39d7d49950d5f98e403677941c752e7e7ee47cb"}, - {file = "coverage-6.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b4285fde5286b946835a1a53bba3ad41ef74285ba9e8013e14b5ea93deaeafc"}, - {file = "coverage-6.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a4748349734110fd32d46ff8897b561e6300d8989a494ad5a0a2e4f0ca974fc7"}, - {file = "coverage-6.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:823f9325283dc9565ba0aa2d240471a93ca8999861779b2b6c7aded45b58ee0f"}, - {file = "coverage-6.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:fff16a30fdf57b214778eff86391301c4509e327a65b877862f7c929f10a4253"}, - {file = "coverage-6.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:da1a428bdbe71f9a8c270c7baab29e9552ac9d0e0cba5e7e9a4c9ee6465d258d"}, - {file = "coverage-6.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:7d82c610a2e10372e128023c5baf9ce3d270f3029fe7274ff5bc2897c68f1318"}, - {file = "coverage-6.3-cp37-cp37m-win32.whl", hash = "sha256:11e61c5548ecf74ea1f8b059730b049871f0e32b74f88bd0d670c20c819ad749"}, - {file = "coverage-6.3-cp37-cp37m-win_amd64.whl", hash = "sha256:8e0c3525b1a182c8ffc9bca7e56b521e0c2b8b3e82f033c8e16d6d721f1b54d6"}, - {file = "coverage-6.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a189036c50dcd56100746139a459f0d27540fef95b09aba03e786540b8feaa5f"}, - {file = "coverage-6.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:32168001f33025fd756884d56d01adebb34e6c8c0b3395ca8584cdcee9c7c9d2"}, - {file = "coverage-6.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5d79c9af3f410a2b5acad91258b4ae179ee9c83897eb9de69151b179b0227f5"}, - {file = "coverage-6.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:85c5fc9029043cf8b07f73fbb0a7ab6d3b717510c3b5642b77058ea55d7cacde"}, - {file = "coverage-6.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a7596aa2f2b8fa5604129cfc9a27ad9beec0a96f18078cb424d029fdd707468d"}, - {file = "coverage-6.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ce443a3e6df90d692c38762f108fc4c88314bf477689f04de76b3f252e7a351c"}, - {file = "coverage-6.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:012157499ec4f135fc36cd2177e3d1a1840af9b236cbe80e9a5ccfc83d912a69"}, - {file = "coverage-6.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:0a34d313105cdd0d3644c56df2d743fe467270d6ab93b5d4a347eb9fec8924d6"}, - {file = "coverage-6.3-cp38-cp38-win32.whl", hash = "sha256:6e78b1e25e5c5695dea012be473e442f7094d066925604be20b30713dbd47f89"}, - {file = "coverage-6.3-cp38-cp38-win_amd64.whl", hash = "sha256:433b99f7b0613bdcdc0b00cc3d39ed6d756797e3b078d2c43f8a38288520aec6"}, - {file = "coverage-6.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9ed3244b415725f08ca3bdf02ed681089fd95e9465099a21c8e2d9c5d6ca2606"}, - {file = "coverage-6.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ab4fc4b866b279740e0d917402f0e9a08683e002f43fa408e9655818ed392196"}, - {file = "coverage-6.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c8582e9280f8d0f38114fe95a92ae8d0790b56b099d728cc4f8a2e14b1c4a18c"}, - {file = "coverage-6.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c72bb4679283c6737f452eeb9b2a0e570acaef2197ad255fb20162adc80bea76"}, - {file = "coverage-6.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca29c352389ea27a24c79acd117abdd8a865c6eb01576b6f0990cd9a4e9c9f48"}, - {file = "coverage-6.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:152cc2624381df4e4e604e21bd8e95eb8059535f7b768c1fb8b8ae0b26f47ab0"}, - {file = "coverage-6.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:51372e24b1f7143ee2df6b45cff6a721f3abe93b1e506196f3ffa4155c2497f7"}, - {file = "coverage-6.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:72d9d186508325a456475dd05b1756f9a204c7086b07fffb227ef8cee03b1dc2"}, - {file = "coverage-6.3-cp39-cp39-win32.whl", hash = "sha256:649df3641eb351cdfd0d5533c92fc9df507b6b2bf48a7ef8c71ab63cbc7b5c3c"}, - {file = "coverage-6.3-cp39-cp39-win_amd64.whl", hash = "sha256:e67ccd53da5958ea1ec833a160b96357f90859c220a00150de011b787c27b98d"}, - {file = "coverage-6.3-pp36.pp37.pp38-none-any.whl", hash = "sha256:27ac7cb84538e278e07569ceaaa6f807a029dc194b1c819a9820b9bb5dbf63ab"}, - {file = "coverage-6.3.tar.gz", hash = "sha256:987a84ff98a309994ca77ed3cc4b92424f824278e48e4bf7d1bb79a63cfe2099"}, + {file = "coverage-6.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9b27d894748475fa858f9597c0ee1d4829f44683f3813633aaf94b19cb5453cf"}, + {file = "coverage-6.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:37d1141ad6b2466a7b53a22e08fe76994c2d35a5b6b469590424a9953155afac"}, + {file = "coverage-6.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9987b0354b06d4df0f4d3e0ec1ae76d7ce7cbca9a2f98c25041eb79eec766f1"}, + {file = "coverage-6.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:26e2deacd414fc2f97dd9f7676ee3eaecd299ca751412d89f40bc01557a6b1b4"}, + {file = "coverage-6.3.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4dd8bafa458b5c7d061540f1ee9f18025a68e2d8471b3e858a9dad47c8d41903"}, + {file = "coverage-6.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:46191097ebc381fbf89bdce207a6c107ac4ec0890d8d20f3360345ff5976155c"}, + {file = "coverage-6.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6f89d05e028d274ce4fa1a86887b071ae1755082ef94a6740238cd7a8178804f"}, + {file = "coverage-6.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:58303469e9a272b4abdb9e302a780072c0633cdcc0165db7eec0f9e32f901e05"}, + {file = "coverage-6.3.2-cp310-cp310-win32.whl", hash = "sha256:2fea046bfb455510e05be95e879f0e768d45c10c11509e20e06d8fcaa31d9e39"}, + {file = "coverage-6.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:a2a8b8bcc399edb4347a5ca8b9b87e7524c0967b335fbb08a83c8421489ddee1"}, + {file = "coverage-6.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:f1555ea6d6da108e1999b2463ea1003fe03f29213e459145e70edbaf3e004aaa"}, + {file = "coverage-6.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5f4e1edcf57ce94e5475fe09e5afa3e3145081318e5fd1a43a6b4539a97e518"}, + {file = "coverage-6.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7a15dc0a14008f1da3d1ebd44bdda3e357dbabdf5a0b5034d38fcde0b5c234b7"}, + {file = "coverage-6.3.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21b7745788866028adeb1e0eca3bf1101109e2dc58456cb49d2d9b99a8c516e6"}, + {file = "coverage-6.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:8ce257cac556cb03be4a248d92ed36904a59a4a5ff55a994e92214cde15c5bad"}, + {file = "coverage-6.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b0be84e5a6209858a1d3e8d1806c46214e867ce1b0fd32e4ea03f4bd8b2e3359"}, + {file = "coverage-6.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:acf53bc2cf7282ab9b8ba346746afe703474004d9e566ad164c91a7a59f188a4"}, + {file = "coverage-6.3.2-cp37-cp37m-win32.whl", hash = "sha256:8bdde1177f2311ee552f47ae6e5aa7750c0e3291ca6b75f71f7ffe1f1dab3dca"}, + {file = "coverage-6.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:b31651d018b23ec463e95cf10070d0b2c548aa950a03d0b559eaa11c7e5a6fa3"}, + {file = "coverage-6.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:07e6db90cd9686c767dcc593dff16c8c09f9814f5e9c51034066cad3373b914d"}, + {file = "coverage-6.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2c6dbb42f3ad25760010c45191e9757e7dce981cbfb90e42feef301d71540059"}, + {file = "coverage-6.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c76aeef1b95aff3905fb2ae2d96e319caca5b76fa41d3470b19d4e4a3a313512"}, + {file = "coverage-6.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8cf5cfcb1521dc3255d845d9dca3ff204b3229401994ef8d1984b32746bb45ca"}, + {file = "coverage-6.3.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8fbbdc8d55990eac1b0919ca69eb5a988a802b854488c34b8f37f3e2025fa90d"}, + {file = "coverage-6.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ec6bc7fe73a938933d4178c9b23c4e0568e43e220aef9472c4f6044bfc6dd0f0"}, + {file = "coverage-6.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:9baff2a45ae1f17c8078452e9e5962e518eab705e50a0aa8083733ea7d45f3a6"}, + {file = "coverage-6.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fd9e830e9d8d89b20ab1e5af09b32d33e1a08ef4c4e14411e559556fd788e6b2"}, + {file = "coverage-6.3.2-cp38-cp38-win32.whl", hash = "sha256:f7331dbf301b7289013175087636bbaf5b2405e57259dd2c42fdcc9fcc47325e"}, + {file = "coverage-6.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:68353fe7cdf91f109fc7d474461b46e7f1f14e533e911a2a2cbb8b0fc8613cf1"}, + {file = "coverage-6.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b78e5afb39941572209f71866aa0b206c12f0109835aa0d601e41552f9b3e620"}, + {file = "coverage-6.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4e21876082ed887baed0146fe222f861b5815455ada3b33b890f4105d806128d"}, + {file = "coverage-6.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34626a7eee2a3da12af0507780bb51eb52dca0e1751fd1471d0810539cefb536"}, + {file = "coverage-6.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1ebf730d2381158ecf3dfd4453fbca0613e16eaa547b4170e2450c9707665ce7"}, + {file = "coverage-6.3.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd6fe30bd519694b356cbfcaca9bd5c1737cddd20778c6a581ae20dc8c04def2"}, + {file = "coverage-6.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:96f8a1cb43ca1422f36492bebe63312d396491a9165ed3b9231e778d43a7fca4"}, + {file = "coverage-6.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:dd035edafefee4d573140a76fdc785dc38829fe5a455c4bb12bac8c20cfc3d69"}, + {file = "coverage-6.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5ca5aeb4344b30d0bec47481536b8ba1181d50dbe783b0e4ad03c95dc1296684"}, + {file = "coverage-6.3.2-cp39-cp39-win32.whl", hash = "sha256:f5fa5803f47e095d7ad8443d28b01d48c0359484fec1b9d8606d0e3282084bc4"}, + {file = "coverage-6.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:9548f10d8be799551eb3a9c74bbf2b4934ddb330e08a73320123c07f95cc2d92"}, + {file = "coverage-6.3.2-pp36.pp37.pp38-none-any.whl", hash = "sha256:18d520c6860515a771708937d2f78f63cc47ab3b80cb78e86573b0a760161faf"}, + {file = "coverage-6.3.2.tar.gz", hash = "sha256:03e2a7826086b91ef345ff18742ee9fc47a6839ccd517061ef8fa1976e652ce9"}, ] distlib = [ {file = "distlib-0.3.4-py2.py3-none-any.whl", hash = "sha256:6564fe0a8f51e734df6333d08b8b94d4ea8ee6b99b5ed50613f731fd4089f34b"}, @@ -856,12 +853,12 @@ docutils = [ {file = "docutils-0.16.tar.gz", hash = "sha256:c2de3a60e9e7d07be26b7f2b00ca0309c207e06c100f9cc2a94931fc75a478fc"}, ] filelock = [ - {file = "filelock-3.4.2-py3-none-any.whl", hash = "sha256:cf0fc6a2f8d26bd900f19bf33915ca70ba4dd8c56903eeb14e1e7a2fd7590146"}, - {file = "filelock-3.4.2.tar.gz", hash = "sha256:38b4f4c989f9d06d44524df1b24bd19e167d851f19b50bf3e3559952dddc5b80"}, + {file = "filelock-3.6.0-py3-none-any.whl", hash = "sha256:f8314284bfffbdcfa0ff3d7992b023d4c628ced6feb957351d4c48d059f56bc0"}, + {file = "filelock-3.6.0.tar.gz", hash = "sha256:9cd540a9352e432c7246a48fe4e8712b10acb1df2ad1f30e8c070b82ae1fed85"}, ] identify = [ - {file = "identify-2.4.6-py2.py3-none-any.whl", hash = "sha256:cf06b1639e0dca0c184b1504d8b73448c99a68e004a80524c7923b95f7b6837c"}, - {file = "identify-2.4.6.tar.gz", hash = "sha256:233679e3f61a02015d4293dbccf16aa0e4996f868bd114688b8c124f18826706"}, + {file = "identify-2.4.12-py2.py3-none-any.whl", hash = "sha256:5f06b14366bd1facb88b00540a1de05b69b310cbc2654db3c7e07fa3a4339323"}, + {file = "identify-2.4.12.tar.gz", hash = "sha256:3f3244a559290e7d3deb9e9adc7b33594c1bc85a9dd82e0f1be519bf12a1ec17"}, ] idna = [ {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, @@ -872,55 +869,61 @@ imagesize = [ {file = "imagesize-1.3.0.tar.gz", hash = "sha256:cd1750d452385ca327479d45b64d9c7729ecf0b3969a58148298c77092261f9d"}, ] importlib-metadata = [ - {file = "importlib_metadata-4.10.1-py3-none-any.whl", hash = "sha256:899e2a40a8c4a1aec681feef45733de8a6c58f3f6a0dbed2eb6574b4387a77b6"}, - {file = "importlib_metadata-4.10.1.tar.gz", hash = "sha256:951f0d8a5b7260e9db5e41d429285b5f451e928479f19d80818878527d36e95e"}, + {file = "importlib_metadata-4.11.3-py3-none-any.whl", hash = "sha256:1208431ca90a8cca1a6b8af391bb53c1a2db74e5d1cef6ddced95d4b2062edc6"}, + {file = "importlib_metadata-4.11.3.tar.gz", hash = "sha256:ea4c597ebf37142f827b8f39299579e31685c31d3a438b59f469406afd0f2539"}, ] iniconfig = [ {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, ] jinja2 = [ - {file = "Jinja2-3.0.3-py3-none-any.whl", hash = "sha256:077ce6014f7b40d03b47d1f1ca4b0fc8328a692bd284016f806ed0eaca390ad8"}, - {file = "Jinja2-3.0.3.tar.gz", hash = "sha256:611bb273cd68f3b993fabdc4064fc858c5b47a973cb5aa7999ec1ba405c87cd7"}, + {file = "Jinja2-3.1.1-py3-none-any.whl", hash = "sha256:539835f51a74a69f41b848a9645dbdc35b4f20a3b601e2d9a7e22947b15ff119"}, + {file = "Jinja2-3.1.1.tar.gz", hash = "sha256:640bed4bb501cbd17194b3cace1dc2126f5b619cf068a726b98192a0fde74ae9"}, ] m2r = [ {file = "m2r-0.2.1.tar.gz", hash = "sha256:bf90bad66cda1164b17e5ba4a037806d2443f2a4d5ddc9f6a5554a0322aaed99"}, ] markupsafe = [ - {file = "MarkupSafe-2.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-win32.whl", hash = "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-win32.whl", hash = "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-win32.whl", hash = "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-win32.whl", hash = "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8"}, - {file = "MarkupSafe-2.0.1.tar.gz", hash = "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a49907dd8420c5685cfa064a1335b6754b74541bbb3706c259c02ed65b644b3e"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10c1bfff05d95783da83491be968e8fe789263689c02724e0c691933c52994f5"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b7bd98b796e2b6553da7225aeb61f447f80a1ca64f41d83612e6139ca5213aa4"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b09bf97215625a311f669476f44b8b318b075847b49316d3e28c08e41a7a573f"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:694deca8d702d5db21ec83983ce0bb4b26a578e71fbdbd4fdcd387daa90e4d5e"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:efc1913fd2ca4f334418481c7e595c00aad186563bbc1ec76067848c7ca0a933"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-win32.whl", hash = "sha256:4a33dea2b688b3190ee12bd7cfa29d39c9ed176bda40bfa11099a3ce5d3a7ac6"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:dda30ba7e87fbbb7eab1ec9f58678558fd9a6b8b853530e176eabd064da81417"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:671cd1187ed5e62818414afe79ed29da836dde67166a9fac6d435873c44fdd02"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3799351e2336dc91ea70b034983ee71cf2f9533cdff7c14c90ea126bfd95d65a"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e72591e9ecd94d7feb70c1cbd7be7b3ebea3f548870aa91e2732960fa4d57a37"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6fbf47b5d3728c6aea2abb0589b5d30459e369baa772e0f37a0320185e87c980"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d5ee4f386140395a2c818d149221149c54849dfcfcb9f1debfe07a8b8bd63f9a"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:bcb3ed405ed3222f9904899563d6fc492ff75cce56cba05e32eff40e6acbeaa3"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e1c0b87e09fa55a220f058d1d49d3fb8df88fbfab58558f1198e08c1e1de842a"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-win32.whl", hash = "sha256:8dc1c72a69aa7e082593c4a203dcf94ddb74bb5c8a731e4e1eb68d031e8498ff"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:97a68e6ada378df82bc9f16b800ab77cbf4b2fada0081794318520138c088e4a"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e8c843bbcda3a2f1e3c2ab25913c80a3c5376cd00c6e8c4a86a89a28c8dc5452"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0212a68688482dc52b2d45013df70d169f542b7394fc744c02a57374a4207003"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e576a51ad59e4bfaac456023a78f6b5e6e7651dcd383bcc3e18d06f9b55d6d1"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b9fe39a2ccc108a4accc2676e77da025ce383c108593d65cc909add5c3bd601"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96e37a3dc86e80bf81758c152fe66dbf60ed5eca3d26305edf01892257049925"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6d0072fea50feec76a4c418096652f2c3238eaa014b2f94aeb1d56a66b41403f"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:089cf3dbf0cd6c100f02945abeb18484bd1ee57a079aefd52cffd17fba910b88"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6a074d34ee7a5ce3effbc526b7083ec9731bb3cbf921bbe1d3005d4d2bdb3a63"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-win32.whl", hash = "sha256:421be9fbf0ffe9ffd7a378aafebbf6f4602d564d34be190fc19a193232fd12b1"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc7b548b17d238737688817ab67deebb30e8073c95749d55538ed473130ec0c7"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e04e26803c9c3851c931eac40c695602c6295b8d432cbe78609649ad9bd2da8a"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b87db4360013327109564f0e591bd2a3b318547bcef31b468a92ee504d07ae4f"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99a2a507ed3ac881b975a2976d59f38c19386d128e7a9a18b7df6fff1fd4c1d6"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56442863ed2b06d19c37f94d999035e15ee982988920e12a5b4ba29b62ad1f77"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ce11ee3f23f79dbd06fb3d63e2f6af7b12db1d46932fe7bd8afa259a5996603"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:33b74d289bd2f5e527beadcaa3f401e0df0a89927c1559c8566c066fa4248ab7"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:43093fb83d8343aac0b1baa75516da6092f58f41200907ef92448ecab8825135"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8e3dcf21f367459434c18e71b2a9532d96547aef8a871872a5bd69a715c15f96"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-win32.whl", hash = "sha256:d4306c36ca495956b6d568d276ac11fdd9c30a36f1b6eb928070dc5360b22e1c"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:46d00d6cfecdde84d40e572d63735ef81423ad31184100411e6e3388d405e247"}, + {file = "MarkupSafe-2.1.1.tar.gz", hash = "sha256:7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b"}, ] mistune = [ {file = "mistune-0.8.4-py2.py3-none-any.whl", hash = "sha256:88a1051873018da288eee8538d476dffe1262495144b33ecb586c4ab266bb8d4"}, @@ -935,16 +938,16 @@ packaging = [ {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, ] platformdirs = [ - {file = "platformdirs-2.4.1-py3-none-any.whl", hash = "sha256:1d7385c7db91728b83efd0ca99a5afb296cab9d0ed8313a45ed8ba17967ecfca"}, - {file = "platformdirs-2.4.1.tar.gz", hash = "sha256:440633ddfebcc36264232365d7840a970e75e1018d15b4327d11f91909045fda"}, + {file = "platformdirs-2.5.1-py3-none-any.whl", hash = "sha256:bcae7cab893c2d310a711b70b24efb93334febe65f8de776ee320b517471e227"}, + {file = "platformdirs-2.5.1.tar.gz", hash = "sha256:7535e70dfa32e84d4b34996ea99c5e432fa29a708d0f4e394bbcb2a8faa4f16d"}, ] pluggy = [ {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, ] pre-commit = [ - {file = "pre_commit-2.17.0-py2.py3-none-any.whl", hash = "sha256:725fa7459782d7bec5ead072810e47351de01709be838c2ce1726b9591dad616"}, - {file = "pre_commit-2.17.0.tar.gz", hash = "sha256:c1a8040ff15ad3d648c70cc3e55b93e4d2d5b687320955505587fd79bbaed06a"}, + {file = "pre_commit-2.18.1-py2.py3-none-any.whl", hash = "sha256:02226e69564ebca1a070bd1f046af866aa1c318dbc430027c50ab832ed2b73f2"}, + {file = "pre_commit-2.18.1.tar.gz", hash = "sha256:5d445ee1fa8738d506881c5d84f83c62bb5be6b2838e32207433647e8e5ebe10"}, ] py = [ {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, @@ -996,12 +999,13 @@ pyparsing = [ {file = "pyparsing-3.0.7.tar.gz", hash = "sha256:18ee9022775d270c55187733956460083db60b37d0d0fb357445f3094eed3eea"}, ] pytest = [ - {file = "pytest-6.2.5-py3-none-any.whl", hash = "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"}, - {file = "pytest-6.2.5.tar.gz", hash = "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89"}, + {file = "pytest-7.1.1-py3-none-any.whl", hash = "sha256:92f723789a8fdd7180b6b06483874feca4c48a5c76968e03bb3e7f806a1869ea"}, + {file = "pytest-7.1.1.tar.gz", hash = "sha256:841132caef6b1ad17a9afde46dc4f6cfa59a05f9555aae5151f73bdf2820ca63"}, ] pytest-asyncio = [ - {file = "pytest-asyncio-0.17.2.tar.gz", hash = "sha256:6d895b02432c028e6957d25fc936494e78c6305736e785d9fee408b1efbc7ff4"}, - {file = "pytest_asyncio-0.17.2-py3-none-any.whl", hash = "sha256:e0fe5dbea40516b661ef1bcfe0bd9461c2847c4ef4bb40012324f2454fb7d56d"}, + {file = "pytest-asyncio-0.18.3.tar.gz", hash = "sha256:7659bdb0a9eb9c6e3ef992eef11a2b3e69697800ad02fb06374a210d85b29f91"}, + {file = "pytest_asyncio-0.18.3-1-py3-none-any.whl", hash = "sha256:16cf40bdf2b4fb7fc8e4b82bd05ce3fbcd454cbf7b92afc445fe299dabb88213"}, + {file = "pytest_asyncio-0.18.3-py3-none-any.whl", hash = "sha256:8fafa6c52161addfd41ee7ab35f11836c5a16ec208f93ee388f752bea3493a84"}, ] pytest-cov = [ {file = "pytest-cov-2.12.1.tar.gz", hash = "sha256:261ceeb8c227b726249b376b8526b600f38667ee314f910353fa318caa01f4d7"}, @@ -1015,8 +1019,8 @@ pytest-sugar = [ {file = "pytest-sugar-0.9.4.tar.gz", hash = "sha256:b1b2186b0a72aada6859bea2a5764145e3aaa2c1cfbb23c3a19b5f7b697563d3"}, ] pytz = [ - {file = "pytz-2021.3-py2.py3-none-any.whl", hash = "sha256:3672058bc3453457b622aab7a1c3bfd5ab0bdae451512f6cf25f64ed37f5b87c"}, - {file = "pytz-2021.3.tar.gz", hash = "sha256:acad2d8b20a1af07d4e4c9d2e9285c5ed9104354062f275f3fcd88dcef4f1326"}, + {file = "pytz-2022.1-py2.py3-none-any.whl", hash = "sha256:e68985985296d9a66a881eb3193b0906246245294a881e7c8afe623866ac6a5c"}, + {file = "pytz-2022.1.tar.gz", hash = "sha256:1e760e2fe6a8163bc0b3d9a19c4f84342afa0a2affebfaa84b01b978a02ecaa7"}, ] pyyaml = [ {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, @@ -1113,33 +1117,34 @@ toml = [ {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, ] tomli = [ - {file = "tomli-2.0.0-py3-none-any.whl", hash = "sha256:b5bde28da1fed24b9bd1d4d2b8cba62300bfb4ec9a6187a957e8ddb9434c5224"}, - {file = "tomli-2.0.0.tar.gz", hash = "sha256:c292c34f58502a1eb2bbb9f5bbc9a5ebc37bee10ffb8c2d6bbdfa8eb13cc14e1"}, + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] tox = [ {file = "tox-3.24.5-py2.py3-none-any.whl", hash = "sha256:be3362472a33094bce26727f5f771ca0facf6dafa217f65875314e9a6600c95c"}, {file = "tox-3.24.5.tar.gz", hash = "sha256:67e0e32c90e278251fea45b696d0fef3879089ccbe979b0c556d35d5a70e2993"}, ] typing-extensions = [ - {file = "typing_extensions-4.0.1-py3-none-any.whl", hash = "sha256:7f001e5ac290a0c0401508864c7ec868be4e701886d5b573a9528ed3973d9d3b"}, - {file = "typing_extensions-4.0.1.tar.gz", hash = "sha256:4ca091dea149f945ec56afb48dae714f21e8692ef22a395223bcd328961b6a0e"}, + {file = "typing_extensions-4.1.1-py3-none-any.whl", hash = "sha256:21c85e0fe4b9a155d0799430b0ad741cdce7e359660ccbd8b530613e8df88ce2"}, + {file = "typing_extensions-4.1.1.tar.gz", hash = "sha256:1a9462dcc3347a79b1f1c0271fbe79e844580bb598bafa1ed208b94da3cdcd42"}, ] urllib3 = [ - {file = "urllib3-1.26.8-py2.py3-none-any.whl", hash = "sha256:000ca7f471a233c2251c6c7023ee85305721bfdf18621ebff4fd17a8653427ed"}, - {file = "urllib3-1.26.8.tar.gz", hash = "sha256:0e7c33d9a63e7ddfcb86780aac87befc2fbddf46c58dbb487e0855f7ceec283c"}, + {file = "urllib3-1.26.9-py2.py3-none-any.whl", hash = "sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14"}, + {file = "urllib3-1.26.9.tar.gz", hash = "sha256:aabaf16477806a5e1dd19aa41f8c2b7950dd3c746362d7e3223dbe6de6ac448e"}, ] virtualenv = [ - {file = "virtualenv-20.13.0-py2.py3-none-any.whl", hash = "sha256:339f16c4a86b44240ba7223d0f93a7887c3ca04b5f9c8129da7958447d079b09"}, - {file = "virtualenv-20.13.0.tar.gz", hash = "sha256:d8458cf8d59d0ea495ad9b34c2599487f8a7772d796f9910858376d1600dd2dd"}, + {file = "virtualenv-20.14.0-py2.py3-none-any.whl", hash = "sha256:1e8588f35e8b42c6ec6841a13c5e88239de1e6e4e4cedfd3916b306dc826ec66"}, + {file = "virtualenv-20.14.0.tar.gz", hash = "sha256:8e5b402037287126e81ccde9432b95a8be5b19d36584f64957060a3488c11ca8"}, ] voluptuous = [ - {file = "voluptuous-0.12.2.tar.gz", hash = "sha256:4db1ac5079db9249820d49c891cb4660a6f8cae350491210abce741fabf56513"}, + {file = "voluptuous-0.13.0-py3-none-any.whl", hash = "sha256:e3b5f6cb68fcb0230701b5c756db4caa6766223fc0eaf613931fdba51025981b"}, + {file = "voluptuous-0.13.0.tar.gz", hash = "sha256:cae6a4526b434b642816b34a00e1186d5a5f5e0c948ab94d2a918e01e5874066"}, ] xdoctest = [ {file = "xdoctest-0.15.10-py3-none-any.whl", hash = "sha256:7666bd0511df59275dfe94ef94b0fde9654afd14f00bf88902fdc9bcee77d527"}, {file = "xdoctest-0.15.10.tar.gz", hash = "sha256:5f16438f2b203860e75ec594dbc38020df7524db0b41bb88467ea0a6030e6685"}, ] zipp = [ - {file = "zipp-3.7.0-py3-none-any.whl", hash = "sha256:b47250dd24f92b7dd6a0a8fc5244da14608f3ca90a5efcd37a3b1642fac9a375"}, - {file = "zipp-3.7.0.tar.gz", hash = "sha256:9f50f446828eb9d45b267433fd3e9da8d801f614129124863f9c51ebceafb87d"}, + {file = "zipp-3.8.0-py3-none-any.whl", hash = "sha256:c4f6e5bbf48e74f7a38e7cc5b0480ff42b0ae5178957d564d18932525d5cf099"}, + {file = "zipp-3.8.0.tar.gz", hash = "sha256:56bf8aadb83c24db6c4b577e13de374ccfb67da2078beba1d037c17980bf43ad"}, ] diff --git a/pyproject.toml b/pyproject.toml index ac5618a4d..43e9f6ab0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "python-kasa" -version = "0.4.3" +version = "0.5.0.dev0" description = "Python API for TP-Link Kasa Smarthome devices" license = "GPL-3.0-or-later" authors = ["Your Name "] @@ -18,7 +18,7 @@ kasa = "kasa.cli:cli" python = "^3.7" anyio = "*" # see https://github.com/python-trio/asyncclick/issues/18 importlib-metadata = "*" -asyncclick = ">=7" +asyncclick = ">=8" pydantic = "^1" # required only for docs From d8481173843a49e47bdee139e86840b7498c58d0 Mon Sep 17 00:00:00 2001 From: Teemu R Date: Wed, 6 Apr 2022 01:13:27 +0200 Subject: [PATCH 11/17] Various documentation updates (#333) * Add a note about socket sharing * Show inherited members for apidocs * Remove outdated note of emeters not being supported on smartstrips * Describe emeter and usage modules, add note about NTP for time sync * Describe lib design and modules * Bump sphinx version, ignore d001 (line-length) for doc8 * demote energy & usage to 3rd level, promote api for 2nd --- .pre-commit-config.yaml | 6 +++ docs/source/design.rst | 50 ++++++++++++++++++++++ docs/source/index.rst | 1 + docs/source/smartbulb.rst | 1 + docs/source/smartdevice.rst | 76 +++++++++++++++++++++++++++------ docs/source/smartdimmer.rst | 1 + docs/source/smartlightstrip.rst | 1 + docs/source/smartplug.rst | 1 + docs/source/smartstrip.rst | 6 +-- poetry.lock | 19 +++++---- pyproject.toml | 6 ++- 11 files changed, 140 insertions(+), 28 deletions(-) create mode 100644 docs/source/design.rst diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c99d152d9..0fe7bdc44 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -37,3 +37,9 @@ repos: hooks: - id: mypy additional_dependencies: [types-click] + +- repo: https://github.com/PyCQA/doc8 + rev: '0.11.1' + hooks: + - id: doc8 + additional_dependencies: [tomli] diff --git a/docs/source/design.rst b/docs/source/design.rst new file mode 100644 index 000000000..b3d6b3591 --- /dev/null +++ b/docs/source/design.rst @@ -0,0 +1,50 @@ +.. py:module:: kasa.modules + +.. _library_design: + +Library Design & Modules +======================== + +This page aims to provide some details on the design and internals of this library. +You might be interested in this if you want to improve this library, +or if you are just looking to access some information that is not currently exposed. + +.. _update_cycle: + +Update Cycle +************ + +When :meth:`~kasa.SmartDevice.update()` is called, +the library constructs a query to send to the device based on :ref:`supported modules `. +Internally, each module defines :meth:`~kasa.modules.Module.query()` to describe what they want query during the update. + +The returned data is cached internally to avoid I/O on property accesses. +All properties defined both in the device class and in the module classes follow this principle. + +While the properties are designed to provide a nice API to use for common use cases, +you may sometimes want to access the raw, cached data as returned by the device. +This can be done using the :attr:`~kasa.SmartDevice.internal_state` property. + +.. _modules: + +Modules +******* + +The functionality provided by all :class:`~kasa.SmartDevice` instances is (mostly) done inside separate modules. +While the individual device-type specific classes provide an easy access for the most import features, +you can also access individual modules through :attr:`kasa.SmartDevice.modules`. +You can get the list of supported modules for a given device instance using :attr:`~kasa.SmartDevice.supported_modules`. + +.. note:: + + If you only need some module-specific information, + you can call the wanted method on the module to avoid using :meth:`~kasa.SmartDevice.update`. + + +API documentation for modules +***************************** + +.. automodule:: kasa.modules + :members: + :inherited-members: + :undoc-members: diff --git a/docs/source/index.rst b/docs/source/index.rst index 2804757e2..711d1474a 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -8,6 +8,7 @@ cli discover smartdevice + design smartbulb smartplug smartdimmer diff --git a/docs/source/smartbulb.rst b/docs/source/smartbulb.rst index bf58ecbf5..3f2baa407 100644 --- a/docs/source/smartbulb.rst +++ b/docs/source/smartbulb.rst @@ -56,4 +56,5 @@ API documentation .. autoclass:: kasa.SmartBulb :members: + :inherited-members: :undoc-members: diff --git a/docs/source/smartdevice.rst b/docs/source/smartdevice.rst index 6b83c1a57..42b838568 100644 --- a/docs/source/smartdevice.rst +++ b/docs/source/smartdevice.rst @@ -1,12 +1,20 @@ +.. py:module:: kasa + Common API -====================== +========== The basic functionalities of all supported devices are accessible using the common :class:`SmartDevice` base class. -The property accesses use the data obtained before by awaiting :func:`update()`. +The property accesses use the data obtained before by awaiting :func:`SmartDevice.update()`. The values are cached until the next update call. In practice this means that property accesses do no I/O and are dependent, while I/O producing methods need to be awaited. +See :ref:`library_design` for more detailed information. + +.. note:: + The device instances share the communication socket in background to optimize I/O accesses. + This means that you need to use the same event loop for subsequent requests. + The library gives a warning ("Detected protocol reuse between different event loop") to hint if you are accessing the device incorrectly. -Methods changing the state of the device do not invalidate the cache (i.e., there is no implicit `update()`). +Methods changing the state of the device do not invalidate the cache (i.e., there is no implicit :func:`SmartDevice.update()` call made by the library). You can assume that the operation has succeeded if no exception is raised. These methods will return the device response, which can be useful for some use cases. @@ -22,28 +30,70 @@ Simple example script showing some functionality: async def main(): p = SmartPlug("127.0.0.1") - await p.update() - print(p.alias) + await p.update() # Request the update + print(p.alias) # Print out the alias + print(p.emeter_realtime) # Print out current emeter status + + await p.turn_off() # Turn the device off - await p.turn_off() + if __name__ == "__main__": + asyncio.run(main()) +If you want to perform updates in a loop, you need to make sure that the device accesses are done in the same event loop: + +.. code-block:: python + + import asyncio + from kasa import SmartPlug + + async def main(): + dev = SmartPlug("127.0.0.1") # We create the instance inside the main loop + while True: + await dev.update() # Request an update + print(dev.emeter_realtime) + await asyncio.sleep(0.5) # Sleep some time between updates if __name__ == "__main__": asyncio.run(main()) Refer to device type specific classes for more examples: +:class:`SmartPlug`, :class:`SmartBulb`, :class:`SmartStrip`, +:class:`SmartDimmer`, :class:`SmartLightStrip`. + +Energy Consumption and Usage Statistics +*************************************** + +.. note:: + In order to use the helper methods to calculate the statistics correctly, your devices need to have correct time set. + The devices use NTP and public servers from `NTP Pool Project `_ to synchronize their time. + +Energy Consumption +~~~~~~~~~~~~~~~~~~ + +The availability of energy consumption sensors depend on the device. +While most of the bulbs support it, only specific switches (e.g., HS110) or strips (e.g., HS300) support it. +You can use :attr:`~SmartDevice.has_emeter` to check for the availability. + + +Usage statistics +~~~~~~~~~~~~~~~~ + +You can use :attr:`~SmartDevice.on_since` to query for the time the device has been turned on. +Some devices also support reporting the usage statistics on daily or monthly basis. +You can access this information using through the usage module (:class:`kasa.modules.Usage`): + +.. code-block:: python -* :class:`SmartPlug` -* :class:`SmartBulb` -* :class:`SmartStrip` -* :class:`SmartDimmer` -* :class:`SmartLightStrip` + dev = SmartPlug("127.0.0.1") + usage = dev.modules["usage"] + print(f"Minutes on this month: {usage.usage_this_month}") + print(f"Minutes on today: {usage.usage_today}") API documentation -~~~~~~~~~~~~~~~~~ +***************** -.. autoclass:: kasa.SmartDevice +.. autoclass:: SmartDevice :members: :undoc-members: diff --git a/docs/source/smartdimmer.rst b/docs/source/smartdimmer.rst index fa4e1ece1..b44d8e0c8 100644 --- a/docs/source/smartdimmer.rst +++ b/docs/source/smartdimmer.rst @@ -10,4 +10,5 @@ API documentation .. autoclass:: kasa.SmartDimmer :members: + :inherited-members: :undoc-members: diff --git a/docs/source/smartlightstrip.rst b/docs/source/smartlightstrip.rst index 4d34efbbd..b961be5bb 100644 --- a/docs/source/smartlightstrip.rst +++ b/docs/source/smartlightstrip.rst @@ -10,4 +10,5 @@ API documentation .. autoclass:: kasa.SmartLightStrip :members: + :inherited-members: :undoc-members: diff --git a/docs/source/smartplug.rst b/docs/source/smartplug.rst index e9b8ccdfd..94305ca87 100644 --- a/docs/source/smartplug.rst +++ b/docs/source/smartplug.rst @@ -11,4 +11,5 @@ API documentation .. autoclass:: kasa.SmartPlug :members: + :inherited-members: :undoc-members: diff --git a/docs/source/smartstrip.rst b/docs/source/smartstrip.rst index edd4953bc..66d78f9c6 100644 --- a/docs/source/smartstrip.rst +++ b/docs/source/smartstrip.rst @@ -1,11 +1,6 @@ Smart strips ============ - -.. note:: - - The emeter feature is currently not implemented for smart strips. See https://github.com/python-kasa/python-kasa/issues/64 for details. - .. note:: Feel free to open a pull request to improve the documentation! @@ -34,4 +29,5 @@ API documentation .. autoclass:: kasa.SmartStrip :members: + :inherited-members: :undoc-members: diff --git a/poetry.lock b/poetry.lock index aa1231d90..4521b423d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -497,18 +497,19 @@ python-versions = "*" [[package]] name = "sphinx" -version = "3.5.4" +version = "4.5.0" description = "Python documentation generator" category = "main" optional = true -python-versions = ">=3.5" +python-versions = ">=3.6" [package.dependencies] alabaster = ">=0.7,<0.8" babel = ">=1.3" colorama = {version = ">=0.3.5", markers = "sys_platform == \"win32\""} -docutils = ">=0.12,<0.17" +docutils = ">=0.14,<0.18" imagesize = "*" +importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""} Jinja2 = ">=2.3" packaging = "*" Pygments = ">=2.0" @@ -516,14 +517,14 @@ requests = ">=2.5.0" snowballstemmer = ">=1.1" sphinxcontrib-applehelp = "*" sphinxcontrib-devhelp = "*" -sphinxcontrib-htmlhelp = "*" +sphinxcontrib-htmlhelp = ">=2.0.0" sphinxcontrib-jsmath = "*" sphinxcontrib-qthelp = "*" -sphinxcontrib-serializinghtml = "*" +sphinxcontrib-serializinghtml = ">=1.1.5" [package.extras] docs = ["sphinxcontrib-websupport"] -lint = ["flake8 (>=3.5.0)", "isort", "mypy (>=0.800)", "docutils-stubs"] +lint = ["flake8 (>=3.5.0)", "isort", "mypy (>=0.931)", "docutils-stubs", "types-typed-ast", "types-requests"] test = ["pytest", "pytest-cov", "html5lib", "cython", "typed-ast"] [[package]] @@ -754,7 +755,7 @@ docs = ["sphinx", "sphinx_rtd_theme", "m2r", "mistune", "sphinxcontrib-programou [metadata] lock-version = "1.1" python-versions = "^3.7" -content-hash = "cbc8eb721e3b498c25eef73c95b2aa309419fa075b878c18cac0b148113c25f9" +content-hash = "6577513a016c329bc825369761eae9971cb6a18a13c96ac0669c1f51ab3de87d" [metadata.files] alabaster = [ @@ -1074,8 +1075,8 @@ snowballstemmer = [ {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, ] sphinx = [ - {file = "Sphinx-3.5.4-py3-none-any.whl", hash = "sha256:2320d4e994a191f4b4be27da514e46b3d6b420f2ff895d064f52415d342461e8"}, - {file = "Sphinx-3.5.4.tar.gz", hash = "sha256:19010b7b9fa0dc7756a6e105b2aacd3a80f798af3c25c273be64d7beeb482cb1"}, + {file = "Sphinx-4.5.0-py3-none-any.whl", hash = "sha256:ebf612653238bcc8f4359627a9b7ce44ede6fdd75d9d30f68255c7383d3a6226"}, + {file = "Sphinx-4.5.0.tar.gz", hash = "sha256:7bf8ca9637a4ee15af412d1a1d9689fec70523a68ca9bb9127c2f3eeb344e2e6"}, ] sphinx-rtd-theme = [ {file = "sphinx_rtd_theme-0.5.2-py2.py3-none-any.whl", hash = "sha256:4a05bdbe8b1446d77a01e20a23ebc6777c74f43237035e76be89699308987d6f"}, diff --git a/pyproject.toml b/pyproject.toml index 43e9f6ab0..daf648b5e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,7 +22,7 @@ asyncclick = ">=8" pydantic = "^1" # required only for docs -sphinx = { version = "^3", optional = true } +sphinx = { version = "^4", optional = true } m2r = { version = "^0", optional = true } mistune = { version = "<2.0.0", optional = true } sphinx_rtd_theme = { version = "^0", optional = true } @@ -81,6 +81,10 @@ markers = [ "requires_dummy: test requires dummy data to pass, skipped on real devices", ] +[tool.doc8] +paths = ["docs"] +ignore = ["D001"] + [build-system] requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" From a39cef9a8cb597fd2ae1d8b7fcd498ddf17ecd14 Mon Sep 17 00:00:00 2001 From: Teemu R Date: Wed, 6 Apr 2022 01:41:08 +0200 Subject: [PATCH 12/17] Export modules & make sphinx happy (#334) --- docs/source/design.rst | 1 + kasa/modules/__init__.py | 17 ++++++++++++++++- kasa/smartbulb.py | 6 +++--- kasa/smartdimmer.py | 4 ++-- kasa/smartplug.py | 4 ++-- kasa/smartstrip.py | 4 ++-- 6 files changed, 26 insertions(+), 10 deletions(-) diff --git a/docs/source/design.rst b/docs/source/design.rst index b3d6b3591..140c83d8f 100644 --- a/docs/source/design.rst +++ b/docs/source/design.rst @@ -45,6 +45,7 @@ API documentation for modules ***************************** .. automodule:: kasa.modules + :noindex: :members: :inherited-members: :undoc-members: diff --git a/kasa/modules/__init__.py b/kasa/modules/__init__.py index e5cb83d66..8ad5088d5 100644 --- a/kasa/modules/__init__.py +++ b/kasa/modules/__init__.py @@ -1,4 +1,4 @@ -# flake8: noqa +"""Module for individual feature modules.""" from .ambientlight import AmbientLight from .antitheft import Antitheft from .cloud import Cloud @@ -10,3 +10,18 @@ from .schedule import Schedule from .time import Time from .usage import Usage + +__all__ = [ + "AmbientLight", + "Antitheft", + "Cloud", + "Countdown", + "Emeter", + "Module", + "Motion", + "Rule", + "RuleModule", + "Schedule", + "Time", + "Usage", +] diff --git a/kasa/smartbulb.py b/kasa/smartbulb.py index f941dcf1f..f060d2563 100644 --- a/kasa/smartbulb.py +++ b/kasa/smartbulb.py @@ -42,7 +42,7 @@ class HSV(NamedTuple): class SmartBulb(SmartDevice): - """Representation of a TP-Link Smart Bulb. + r"""Representation of a TP-Link Smart Bulb. To initialize, you have to await :func:`update()` at least once. This will allow accessing the properties using the exposed properties. @@ -50,7 +50,7 @@ class SmartBulb(SmartDevice): All changes to the device are done using awaitable methods, which will not change the cached values, but you must await :func:`update()` separately. - Errors reported by the device are raised as :class:`SmartDeviceException`s, + Errors reported by the device are raised as :class:`SmartDeviceException`\s, and should be handled by the user of the library. Examples: @@ -68,7 +68,7 @@ class SmartBulb(SmartDevice): >>> print(bulb.is_on) True - You can use the is_-prefixed properties to check for supported features + You can use the ``is_``-prefixed properties to check for supported features >>> bulb.is_dimmable True >>> bulb.is_color diff --git a/kasa/smartdimmer.py b/kasa/smartdimmer.py index 5c06b8b94..cb830d233 100644 --- a/kasa/smartdimmer.py +++ b/kasa/smartdimmer.py @@ -7,7 +7,7 @@ class SmartDimmer(SmartPlug): - """Representation of a TP-Link Smart Dimmer. + r"""Representation of a TP-Link Smart Dimmer. Dimmers work similarly to plugs, but provide also support for adjusting the brightness. This class extends :class:`SmartPlug` interface. @@ -18,7 +18,7 @@ class SmartDimmer(SmartPlug): All changes to the device are done using awaitable methods, which will not change the cached values, but you must await :func:`update()` separately. - Errors reported by the device are raised as :class:`SmartDeviceException`s, + Errors reported by the device are raised as :class:`SmartDeviceException`\s, and should be handled by the user of the library. Examples: diff --git a/kasa/smartplug.py b/kasa/smartplug.py index b636c3e11..d49e40542 100644 --- a/kasa/smartplug.py +++ b/kasa/smartplug.py @@ -9,7 +9,7 @@ class SmartPlug(SmartDevice): - """Representation of a TP-Link Smart Switch. + r"""Representation of a TP-Link Smart Switch. To initialize, you have to await :func:`update()` at least once. This will allow accessing the properties using the exposed properties. @@ -17,7 +17,7 @@ class SmartPlug(SmartDevice): All changes to the device are done using awaitable methods, which will not change the cached values, but you must await :func:`update()` separately. - Errors reported by the device are raised as :class:`SmartDeviceException`s, + Errors reported by the device are raised as :class:`SmartDeviceException`\s, and should be handled by the user of the library. Examples: diff --git a/kasa/smartstrip.py b/kasa/smartstrip.py index ba863d059..47ada6723 100755 --- a/kasa/smartstrip.py +++ b/kasa/smartstrip.py @@ -29,7 +29,7 @@ def merge_sums(dicts): class SmartStrip(SmartDevice): - """Representation of a TP-Link Smart Power Strip. + r"""Representation of a TP-Link Smart Power Strip. A strip consists of the parent device and its children. All methods of the parent act on all children, while the child devices @@ -41,7 +41,7 @@ class SmartStrip(SmartDevice): All changes to the device are done using awaitable methods, which will not change the cached values, but you must await :func:`update()` separately. - Errors reported by the device are raised as :class:`SmartDeviceException`s, + Errors reported by the device are raised as :class:`SmartDeviceException`\s, and should be handled by the user of the library. Examples: From 6e988bd9a953294450b63d0b547866bee6999bf7 Mon Sep 17 00:00:00 2001 From: Teemu R Date: Wed, 6 Apr 2022 02:25:47 +0200 Subject: [PATCH 13/17] Avoid discovery on --help (#335) --- kasa/cli.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/kasa/cli.py b/kasa/cli.py index c9cab4b50..5c1e18f64 100755 --- a/kasa/cli.py +++ b/kasa/cli.py @@ -1,6 +1,7 @@ """python-kasa cli tool.""" import asyncio import logging +import sys from pprint import pformat as pf from typing import cast @@ -66,6 +67,12 @@ @click.pass_context async def cli(ctx, host, alias, target, debug, bulb, plug, lightstrip, strip, type): """A tool for controlling TP-Link smart home devices.""" # noqa + # no need to perform any checks if we are just displaying the help + if sys.argv[-1] == "--help": + # Context object is required to avoid crashing on sub-groups + ctx.obj = SmartDevice(None) + return + if debug: logging.basicConfig(level=logging.DEBUG) else: From 631762b50c60c77e7dbe1d13d4e0c34d3f9e8b86 Mon Sep 17 00:00:00 2001 From: Teemu R Date: Wed, 6 Apr 2022 03:39:50 +0200 Subject: [PATCH 14/17] Drop deprecated, type-specific options in favor of --type (#336) * Drop deprecated, type-specific options in favor of --type * Fix tests --- kasa/cli.py | 25 +++---------------------- kasa/tests/test_cli.py | 16 ---------------- 2 files changed, 3 insertions(+), 38 deletions(-) diff --git a/kasa/cli.py b/kasa/cli.py index 5c1e18f64..40e84e77f 100755 --- a/kasa/cli.py +++ b/kasa/cli.py @@ -53,10 +53,6 @@ help="The broadcast address to be used for discovery.", ) @click.option("-d", "--debug", envvar="KASA_DEBUG", default=False, is_flag=True) -@click.option("--bulb", default=False, is_flag=True) -@click.option("--plug", default=False, is_flag=True) -@click.option("--lightstrip", default=False, is_flag=True) -@click.option("--strip", default=False, is_flag=True) @click.option( "--type", envvar="KASA_TYPE", @@ -65,7 +61,7 @@ ) @click.version_option(package_name="python-kasa") @click.pass_context -async def cli(ctx, host, alias, target, debug, bulb, plug, lightstrip, strip, type): +async def cli(ctx, host, alias, target, debug, type): """A tool for controlling TP-Link smart home devices.""" # noqa # no need to perform any checks if we are just displaying the help if sys.argv[-1] == "--help": @@ -95,19 +91,6 @@ async def cli(ctx, host, alias, target, debug, bulb, plug, lightstrip, strip, ty await ctx.invoke(discover) return - if bulb or plug or strip or lightstrip: - click.echo( - "Using --bulb, --plug, --strip, and --lightstrip is deprecated. Use --type instead to define the type" - ) - if bulb: - type = "bulb" - elif plug: - type = "plug" - elif strip: - type = "strip" - elif lightstrip: - type = "lightstrip" - if type is not None: dev = TYPE_TO_CLASS[type](host) else: @@ -158,9 +141,8 @@ async def join(dev: SmartDevice, ssid, password, keytype): @cli.command() @click.option("--timeout", default=3, required=False) -@click.option("--dump-raw", is_flag=True) @click.pass_context -async def discover(ctx, timeout, dump_raw): +async def discover(ctx, timeout): """Discover devices in the network.""" target = ctx.parent.params["target"] click.echo(f"Discovering devices on {target} for {timeout} seconds") @@ -201,8 +183,7 @@ async def sysinfo(dev): @cli.command() @pass_dev -@click.pass_context -async def state(ctx, dev: SmartDevice): +async def state(dev: SmartDevice): """Print out device state and versions.""" click.echo(click.style(f"== {dev.alias} - {dev.model} ==", bold=True)) click.echo(f"\tHost: {dev.host}") diff --git a/kasa/tests/test_cli.py b/kasa/tests/test_cli.py index ae4fa1759..289c5b58d 100644 --- a/kasa/tests/test_cli.py +++ b/kasa/tests/test_cli.py @@ -106,22 +106,6 @@ async def test_brightness(dev): assert "Brightness: 12" in res.output -def _generate_type_class_pairs(): - yield from TYPE_TO_CLASS.items() - - -@pytest.mark.parametrize("type_class", _generate_type_class_pairs()) -async def test_deprecated_type(dev, type_class, mocker): - """Make sure that using deprecated types yields a warning.""" - type, cls = type_class - if type == "dimmer": - return - runner = CliRunner() - with mocker.patch("kasa.SmartDevice.update"): - res = await runner.invoke(cli, ["--host", "127.0.0.2", f"--{type}"]) - assert "Using --bulb, --plug, --strip, and --lightstrip is deprecated" in res.output - - async def test_temperature(dev): pass From d2581bf07739e5ac5dc2883ec3d4806524d302a6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 13 Apr 2022 14:51:15 -1000 Subject: [PATCH 15/17] Add fixtures for kl420 (#339) * Add fixtures for kl420 * readme --- README.md | 1 + kasa/tests/conftest.py | 2 +- .../tests/fixtures/KL420L5(US)_1.0_1.0.2.json | 57 +++++++++++++++++++ 3 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 kasa/tests/fixtures/KL420L5(US)_1.0_1.0.2.json diff --git a/README.md b/README.md index 126b4afcc..9d62cb818 100644 --- a/README.md +++ b/README.md @@ -155,6 +155,7 @@ or the `parse_pcap.py` script contained inside the `devtools` directory. ### Light strips * KL400 +* KL420 * KL430 **Contributions (be it adding missing features, fixing bugs or improving documentation) are more than welcome, feel free to submit pull requests!** diff --git a/kasa/tests/conftest.py b/kasa/tests/conftest.py index f9fc917f6..93d374734 100644 --- a/kasa/tests/conftest.py +++ b/kasa/tests/conftest.py @@ -25,7 +25,7 @@ ) -LIGHT_STRIPS = {"KL400", "KL430"} +LIGHT_STRIPS = {"KL400", "KL430", "KL420"} VARIABLE_TEMP = {"LB120", "LB130", "KL120", "KL125", "KL130", "KL135", "KL430"} COLOR_BULBS = {"LB130", "KL125", "KL130", "KL135", *LIGHT_STRIPS} BULBS = { diff --git a/kasa/tests/fixtures/KL420L5(US)_1.0_1.0.2.json b/kasa/tests/fixtures/KL420L5(US)_1.0_1.0.2.json new file mode 100644 index 000000000..0d19e7949 --- /dev/null +++ b/kasa/tests/fixtures/KL420L5(US)_1.0_1.0.2.json @@ -0,0 +1,57 @@ +{ + "smartlife.iot.common.emeter": { + "get_realtime": { + "err_code": 0, + "power_mw": 1503, + "total_wh": 0 + } + }, + "system": { + "get_sysinfo": { + "LEF": 1, + "active_mode": "none", + "alias": "Kl420 test", + "ctrl_protocols": { + "name": "Linkie", + "version": "1.0" + }, + "description": "Kasa Smart Light Strip, Multicolor", + "dev_state": "normal", + "deviceId": "0000000000000000000000000000000000000000", + "disco_ver": "1.0", + "err_code": 0, + "hwId": "00000000000000000000000000000000", + "hw_ver": "1.0", + "is_color": 1, + "is_dimmable": 1, + "is_factory": false, + "is_variable_color_temp": 0, + "latitude_i": 0, + "length": 50, + "light_state": { + "brightness": 100, + "color_temp": 6500, + "hue": 0, + "mode": "normal", + "on_off": 1, + "saturation": 0 + }, + "lighting_effect_state": { + "brightness": 50, + "custom": 0, + "enable": 0, + "id": "", + "name": "station" + }, + "longitude_i": 0, + "mic_mac": "00:00:00:00:00:00", + "mic_type": "IOT.SMARTBULB", + "model": "KL420L5(US)", + "oemId": "00000000000000000000000000000000", + "preferred_state": [], + "rssi": -44, + "status": "new", + "sw_ver": "1.0.2 Build 211009 Rel.164949" + } + } +} From d908a5ab2a58e3883309b05303c779eee0092558 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 24 Apr 2022 07:38:42 -1000 Subject: [PATCH 16/17] Avoid retrying open_connection on unrecoverable errors (#340) * Avoid retrying open_connection on unrecoverable errors - We can retry so hard that we block the event loop Fixes ``` 2022-04-16 22:18:51 WARNING (MainThread) [asyncio] Executing exception=ConnectionRefusedError(61, "Connect call failed (192.168.107.200, 9999)") created at /opt/homebrew/Cellar/python@3.9/3.9.12/Frameworks/Python.framework/Versions/3.9/lib/python3.9/asyncio/tasks.py:460> took 1.001 seconds ``` * comment --- kasa/protocol.py | 23 +++++++++++++++++++++++ kasa/tests/test_protocol.py | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+) diff --git a/kasa/protocol.py b/kasa/protocol.py index 24c2cd056..b6d44be90 100755 --- a/kasa/protocol.py +++ b/kasa/protocol.py @@ -11,6 +11,7 @@ """ import asyncio import contextlib +import errno import json import logging import struct @@ -20,6 +21,7 @@ from .exceptions import SmartDeviceException _LOGGER = logging.getLogger(__name__) +_NO_RETRY_ERRORS = {errno.EHOSTDOWN, errno.EHOSTUNREACH, errno.ECONNREFUSED} class TPLinkSmartHomeProtocol: @@ -115,9 +117,30 @@ def _reset(self) -> None: async def _query(self, request: str, retry_count: int, timeout: int) -> Dict: """Try to query a device.""" + # + # Most of the time we will already be connected if the device is online + # and the connect call will do nothing and return right away + # + # However, if we get an unrecoverable error (_NO_RETRY_ERRORS and ConnectionRefusedError) + # we do not want to keep trying since many connection open/close operations + # in the same time frame can block the event loop. This is especially + # import when there are multiple tplink devices being polled. + # for retry in range(retry_count + 1): try: await self._connect(timeout) + except ConnectionRefusedError as ex: + await self.close() + raise SmartDeviceException( + f"Unable to connect to the device: {self.host}: {ex}" + ) + except OSError as ex: + await self.close() + if ex.errno in _NO_RETRY_ERRORS or retry >= retry_count: + raise SmartDeviceException( + f"Unable to connect to the device: {self.host}: {ex}" + ) + continue except Exception as ex: await self.close() if retry >= retry_count: diff --git a/kasa/tests/test_protocol.py b/kasa/tests/test_protocol.py index 5fe4763d5..f8931c11e 100644 --- a/kasa/tests/test_protocol.py +++ b/kasa/tests/test_protocol.py @@ -1,3 +1,4 @@ +import errno import json import logging import struct @@ -29,6 +30,39 @@ def aio_mock_writer(_, __): assert conn.call_count == retry_count + 1 +async def test_protocol_no_retry_on_unreachable(mocker): + conn = mocker.patch( + "asyncio.open_connection", + side_effect=OSError(errno.EHOSTUNREACH, "No route to host"), + ) + with pytest.raises(SmartDeviceException): + await TPLinkSmartHomeProtocol("127.0.0.1").query({}, retry_count=5) + + assert conn.call_count == 1 + + +async def test_protocol_no_retry_connection_refused(mocker): + conn = mocker.patch( + "asyncio.open_connection", + side_effect=ConnectionRefusedError, + ) + with pytest.raises(SmartDeviceException): + await TPLinkSmartHomeProtocol("127.0.0.1").query({}, retry_count=5) + + assert conn.call_count == 1 + + +async def test_protocol_retry_recoverable_error(mocker): + conn = mocker.patch( + "asyncio.open_connection", + side_effect=OSError(errno.ECONNRESET, "Connection reset by peer"), + ) + with pytest.raises(SmartDeviceException): + await TPLinkSmartHomeProtocol("127.0.0.1").query({}, retry_count=5) + + assert conn.call_count == 6 + + @pytest.mark.skipif(sys.version_info < (3, 8), reason="3.8 is first one with asyncmock") @pytest.mark.parametrize("retry_count", [1, 3, 5]) async def test_protocol_reconnect(mocker, retry_count): From 51fb908d8b64cc675a96abfcdea0847d50c2a1ba Mon Sep 17 00:00:00 2001 From: Teemu R Date: Mon, 25 Apr 2022 00:13:24 +0200 Subject: [PATCH 17/17] Prepare 0.5.0 (#342) * Prepare 0.5.0 * Add note about how to include release summary to changelog --- .github_changelog_generator | 3 ++ CHANGELOG.md | 76 +++++++++++++++++++++++++++++++++++-- RELEASING.md | 8 +++- pyproject.toml | 2 +- 4 files changed, 82 insertions(+), 7 deletions(-) diff --git a/.github_changelog_generator b/.github_changelog_generator index db89ad20c..0341d4088 100644 --- a/.github_changelog_generator +++ b/.github_changelog_generator @@ -1 +1,4 @@ +breaking_labels=breaking change +add-sections={"docs":{"prefix":"**Documentation updates:**","labels":["documentation"]}} +release_branch=master usernames-as-github-logins=true diff --git a/CHANGELOG.md b/CHANGELOG.md index 89058f1f8..57b921061 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,63 @@ # Changelog +## [0.5.0](https://github.com/python-kasa/python-kasa/tree/0.5.0) (2022-04-24) + +[Full Changelog](https://github.com/python-kasa/python-kasa/compare/0.4.3...0.5.0) + +This is the first release of 0.5 series which includes converting the code base towards more modular approach where device-exposed modules (e.g., emeter, antitheft, or schedule) are implemented in their separate python modules to decouple them from the device-specific classes. + +There should be no API breaking changes, but some previous issues hint that there may be as information from all supported modules are now requested during each update cycle (depending on the device type): +* Basic system info +* Emeter +* Time - properties (like `on_since`) use now time from the device for calculation to avoid jitter caused by different time between the host and the device +* Usage statistics - similar interface to emeter, but reports on-time statistics instead of energy consumption (new) +* Countdown (new) +* Antitheft (new) +* Schedule (new) +* Motion - for configuring motion settings on some dimmers (new) +* Ambientlight - for configuring brightness limits when motion sensor actuates on some dimmers (new) +* Cloud - information about cloud connectivity (new) + +For developers, the new functionalities are currently only exposed through the implementation modules accessible through `modules` property. +Pull requests improving the functionality of modules as well as adding better interfaces to device classes are welcome! + +**Breaking changes:** + +- Drop deprecated, type-specific options in favor of --type [\#336](https://github.com/python-kasa/python-kasa/pull/336) (@rytilahti) +- Convert the codebase to be more modular [\#299](https://github.com/python-kasa/python-kasa/pull/299) (@rytilahti) + +**Implemented enhancements:** + +- Improve HS220 support [\#44](https://github.com/python-kasa/python-kasa/issues/44) + +**Fixed bugs:** + +- Skip running discovery on --help on subcommands [\#122](https://github.com/python-kasa/python-kasa/issues/122) +- Avoid retrying open\_connection on unrecoverable errors [\#340](https://github.com/python-kasa/python-kasa/pull/340) (@bdraco) +- Avoid discovery on --help [\#335](https://github.com/python-kasa/python-kasa/pull/335) (@rytilahti) + +**Documentation updates:** + +- Trying to poll device every 5 seconds but getting asyncio errors [\#316](https://github.com/python-kasa/python-kasa/issues/316) +- Docs: Smart Strip - Emeter feature Note [\#257](https://github.com/python-kasa/python-kasa/issues/257) +- Documentation addition: Smartplug access to internet ntp server pool. [\#129](https://github.com/python-kasa/python-kasa/issues/129) +- Export modules & make sphinx happy [\#334](https://github.com/python-kasa/python-kasa/pull/334) (@rytilahti) +- Various documentation updates [\#333](https://github.com/python-kasa/python-kasa/pull/333) (@rytilahti) + +**Closed issues:** + +- "on since" changes [\#295](https://github.com/python-kasa/python-kasa/issues/295) +- How to access KP115 runtime data? [\#244](https://github.com/python-kasa/python-kasa/issues/244) +- How to resolve "Detected protocol reuse between different event loop" warning? [\#238](https://github.com/python-kasa/python-kasa/issues/238) +- Handle discovery where multiple LAN interfaces exist [\#104](https://github.com/python-kasa/python-kasa/issues/104) +- Hyper-V \(and probably virtualbox\) break UDP discovery [\#101](https://github.com/python-kasa/python-kasa/issues/101) +- Trying to get extended lightstrip functionality [\#100](https://github.com/python-kasa/python-kasa/issues/100) +- Can the HS105 be controlled without internet? [\#72](https://github.com/python-kasa/python-kasa/issues/72) + +**Merged pull requests:** + +- Add fixtures for kl420 [\#339](https://github.com/python-kasa/python-kasa/pull/339) (@bdraco) + ## [0.4.3](https://github.com/python-kasa/python-kasa/tree/0.4.3) (2022-04-05) [Full Changelog](https://github.com/python-kasa/python-kasa/compare/0.4.2...0.4.3) @@ -15,6 +73,7 @@ **Merged pull requests:** +- Release 0.4.3 [\#332](https://github.com/python-kasa/python-kasa/pull/332) (@rytilahti) - Update pre-commit hooks to fix black in CI [\#331](https://github.com/python-kasa/python-kasa/pull/331) (@rytilahti) - Fix test\_deprecated\_type stalling [\#325](https://github.com/python-kasa/python-kasa/pull/325) (@bdraco) @@ -167,6 +226,10 @@ - HS300 Children plugs have emeter [\#64](https://github.com/python-kasa/python-kasa/issues/64) - dump\_devinfo: handle latitude/longitude keys properly [\#175](https://github.com/python-kasa/python-kasa/pull/175) (@rytilahti) +**Documentation updates:** + +- Discover does not support specifying network interface [\#167](https://github.com/python-kasa/python-kasa/issues/167) + **Closed issues:** - Feature Request - Toggle Command [\#188](https://github.com/python-kasa/python-kasa/issues/188) @@ -175,7 +238,6 @@ - Help needed - awaiting game [\#179](https://github.com/python-kasa/python-kasa/issues/179) - Version inconsistency between CLI and pip [\#177](https://github.com/python-kasa/python-kasa/issues/177) - Release 0.4.0.dev3? [\#169](https://github.com/python-kasa/python-kasa/issues/169) -- Discover does not support specifying network interface [\#167](https://github.com/python-kasa/python-kasa/issues/167) - Can't command or query HS200 v5 switch [\#161](https://github.com/python-kasa/python-kasa/issues/161) **Merged pull requests:** @@ -202,6 +264,11 @@ - Simplify discovery query, refactor dump-devinfo [\#147](https://github.com/python-kasa/python-kasa/pull/147) (@rytilahti) - Return None instead of raising an exception on missing, valid emeter keys [\#146](https://github.com/python-kasa/python-kasa/pull/146) (@rytilahti) +**Documentation updates:** + +- Add ability to control individual sockets on KP400 [\#121](https://github.com/python-kasa/python-kasa/issues/121) +- Improve cli documentation for bulbs and power strips [\#123](https://github.com/python-kasa/python-kasa/pull/123) (@rytilahti) + **Closed issues:** - After installing, command `kasa` not found [\#165](https://github.com/python-kasa/python-kasa/issues/165) @@ -211,7 +278,6 @@ - Poetry returns error when installing dependencies [\#131](https://github.com/python-kasa/python-kasa/issues/131) - 'kasa wifi scan' raises RuntimeError [\#127](https://github.com/python-kasa/python-kasa/issues/127) - Runtime Error when I execute Kasa emeter command [\#124](https://github.com/python-kasa/python-kasa/issues/124) -- Add ability to control individual sockets on KP400 [\#121](https://github.com/python-kasa/python-kasa/issues/121) - HS105\(US\) HW 5.0/SW 1.0.2 Not Working [\#119](https://github.com/python-kasa/python-kasa/issues/119) - HS110\(UK\) not discoverable [\#113](https://github.com/python-kasa/python-kasa/issues/113) - Stopping Kasa SmartDevices from phoning home [\#111](https://github.com/python-kasa/python-kasa/issues/111) @@ -228,7 +294,6 @@ - Fix documentation on Smart strips [\#136](https://github.com/python-kasa/python-kasa/pull/136) (@flavio-fernandes) - add tapo link, fix tplink-smarthome-simulator link [\#133](https://github.com/python-kasa/python-kasa/pull/133) (@rytilahti) - Leverage data from UDP discovery to initialize device structure [\#132](https://github.com/python-kasa/python-kasa/pull/132) (@dlee1j1) -- Improve cli documentation for bulbs and power strips [\#123](https://github.com/python-kasa/python-kasa/pull/123) (@rytilahti) - Add HS220 hw 2.0 fixture [\#107](https://github.com/python-kasa/python-kasa/pull/107) (@appleguru) ## [0.4.0.dev2](https://github.com/python-kasa/python-kasa/tree/0.4.0.dev2) (2020-11-21) @@ -272,6 +337,10 @@ - Improve retry logic for discovery, messaging \(was: Handle empty responses\) [\#38](https://github.com/python-kasa/python-kasa/issues/38) - Add support for lightstrips \(KL430\) [\#74](https://github.com/python-kasa/python-kasa/pull/74) (@rytilahti) +**Documentation updates:** + +- Improve poetry usage documentation [\#60](https://github.com/python-kasa/python-kasa/issues/60) + **Closed issues:** - I don't python... how do I make this executable? [\#88](https://github.com/python-kasa/python-kasa/issues/88) @@ -279,7 +348,6 @@ - not able to pip install the library [\#82](https://github.com/python-kasa/python-kasa/issues/82) - Discover.discover\(\) add selecting network interface \[pull request\] [\#78](https://github.com/python-kasa/python-kasa/issues/78) - LB100 unable to turn on or off the lights [\#68](https://github.com/python-kasa/python-kasa/issues/68) -- Improve poetry usage documentation [\#60](https://github.com/python-kasa/python-kasa/issues/60) - sys\_info not None fails assertion [\#55](https://github.com/python-kasa/python-kasa/issues/55) - Upload pre-release to pypi for easier testing [\#17](https://github.com/python-kasa/python-kasa/issues/17) diff --git a/RELEASING.md b/RELEASING.md index adc8a05e5..37d2e4ca6 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -12,6 +12,12 @@ export NEW_RELEASE=0.4.0.dev4 poetry version $NEW_RELEASE ``` +3. Write a short and understandable summary for the release. + +* Create a new issue and label it with release-summary +* Create $NEW_RELEASE milestone in github, and assign the issue to that +* Close the issue + 3. Generate changelog ```bash @@ -21,8 +27,6 @@ export CHANGELOG_GITHUB_TOKEN=token github_changelog_generator --base HISTORY.md --user python-kasa --project python-kasa --since-tag $PREVIOUS_RELEASE --future-release $NEW_RELEASE -o CHANGELOG.md ``` -3. Write a short and understandable summary for the release. - 4. Commit the changed files ```bash diff --git a/pyproject.toml b/pyproject.toml index daf648b5e..688b80ab9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "python-kasa" -version = "0.5.0.dev0" +version = "0.5.0" description = "Python API for TP-Link Kasa Smarthome devices" license = "GPL-3.0-or-later" authors = ["Your Name "]