From 34c694c20e84f827dc1343716fb2b94521adea4c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 9 Mar 2018 19:39:50 -0800 Subject: [PATCH 1/6] allow ios device tracker see calls to go through (#13020) --- .../components/device_tracker/__init__.py | 9 ++++++++- tests/components/device_tracker/test_init.py | 15 +++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index 196c11a614f..9fea2bc104d 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -111,6 +111,9 @@ SERVICE_SEE_PAYLOAD_SCHEMA = vol.Schema(vol.All( ATTR_ATTRIBUTES: dict, ATTR_SOURCE_TYPE: vol.In(SOURCE_TYPES), ATTR_CONSIDER_HOME: cv.time_period, + # Temp workaround for iOS app introduced in 0.65 + vol.Optional('battery_status'): str, + vol.Optional('hostname'): str, })) @@ -219,7 +222,11 @@ def async_setup(hass: HomeAssistantType, config: ConfigType): @asyncio.coroutine def async_see_service(call): """Service to see a device.""" - yield from tracker.async_see(**call.data) + # Temp workaround for iOS, introduced in 0.65 + data = dict(call.data) + data.pop('hostname', None) + data.pop('battery_status', None) + yield from tracker.async_see(**data) hass.services.async_register( DOMAIN, SERVICE_SEE, async_see_service, SERVICE_SEE_PAYLOAD_SCHEMA) diff --git a/tests/components/device_tracker/test_init.py b/tests/components/device_tracker/test_init.py index ebf568309ad..9d122fa17b6 100644 --- a/tests/components/device_tracker/test_init.py +++ b/tests/components/device_tracker/test_init.py @@ -730,3 +730,18 @@ async def test_old_style_track_new_is_skipped(mock_device_tracker_conf, hass): await hass.async_block_till_done() assert len(mock_device_tracker_conf) == 1 assert mock_device_tracker_conf[0].track is False + + +def test_see_schema_allowing_ios_calls(): + """Test SEE service schema allows extra keys. + + Temp work around because the iOS app sends incorrect data. + """ + device_tracker.SERVICE_SEE_PAYLOAD_SCHEMA({ + 'dev_id': 'Test', + "battery": 35, + "battery_status": 'Unplugged', + "gps": [10.0, 10.0], + "gps_accuracy": 300, + "hostname": 'beer', + }) From 8f807a3006d6e6822683a68c2a530873a5a38bd9 Mon Sep 17 00:00:00 2001 From: Johann Kellerman Date: Sat, 10 Mar 2018 01:52:21 +0200 Subject: [PATCH 2/6] Safe fix for #13015 (#13024) --- homeassistant/bootstrap.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 2f093f061d9..50d8502bbd1 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -113,7 +113,10 @@ def async_from_config_dict(config: Dict[str, Any], yield from hass.async_add_job(loader.prepare, hass) # Make a copy because we are mutating it. - config = OrderedDict(config) + new_config = OrderedDict() + for key, value in config.items(): + new_config[key] = value or {} + config = new_config # Merge packages conf_util.merge_packages_config( From 6ffc53b290ef9634f1133c76cdc9b8d88c0f73e2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 9 Mar 2018 19:38:51 -0800 Subject: [PATCH 3/6] Make Throttle async aware (#13027) * Make Throttle async aware * Lint --- .../components/media_player/bluesound.py | 22 ++++++++----------- .../components/media_player/volumio.py | 5 ++--- homeassistant/components/sensor/fido.py | 5 ++--- .../components/sensor/hydroquebec.py | 5 ++--- homeassistant/components/sensor/luftdaten.py | 13 ++++------- homeassistant/components/sensor/sabnzbd.py | 5 ++--- homeassistant/components/sensor/startca.py | 7 +++--- homeassistant/components/sensor/teksavvy.py | 7 +++--- .../components/sensor/wunderground.py | 7 +++--- homeassistant/util/__init__.py | 15 +++++++++++-- tests/util/test_init.py | 11 ++++++++++ 11 files changed, 54 insertions(+), 48 deletions(-) diff --git a/homeassistant/components/media_player/bluesound.py b/homeassistant/components/media_player/bluesound.py index d308b94e64c..a07e577c969 100644 --- a/homeassistant/components/media_player/bluesound.py +++ b/homeassistant/components/media_player/bluesound.py @@ -423,19 +423,17 @@ class BluesoundPlayer(MediaPlayerDevice): for player in self._hass.data[DATA_BLUESOUND]: yield from player.force_update_sync_status() - @asyncio.coroutine @Throttle(SYNC_STATUS_INTERVAL) - def async_update_sync_status(self, on_updated_cb=None, - raise_timeout=False): + async def async_update_sync_status(self, on_updated_cb=None, + raise_timeout=False): """Update sync status.""" - yield from self.force_update_sync_status( + await self.force_update_sync_status( on_updated_cb, raise_timeout=False) - @asyncio.coroutine @Throttle(UPDATE_CAPTURE_INTERVAL) - def async_update_captures(self): + async def async_update_captures(self): """Update Capture sources.""" - resp = yield from self.send_bluesound_command( + resp = await self.send_bluesound_command( 'RadioBrowse?service=Capture') if not resp: return @@ -459,11 +457,10 @@ class BluesoundPlayer(MediaPlayerDevice): return self._capture_items - @asyncio.coroutine @Throttle(UPDATE_PRESETS_INTERVAL) - def async_update_presets(self): + async def async_update_presets(self): """Update Presets.""" - resp = yield from self.send_bluesound_command('Presets') + resp = await self.send_bluesound_command('Presets') if not resp: return self._preset_items = [] @@ -488,11 +485,10 @@ class BluesoundPlayer(MediaPlayerDevice): return self._preset_items - @asyncio.coroutine @Throttle(UPDATE_SERVICES_INTERVAL) - def async_update_services(self): + async def async_update_services(self): """Update Services.""" - resp = yield from self.send_bluesound_command('Services') + resp = await self.send_bluesound_command('Services') if not resp: return self._services_items = [] diff --git a/homeassistant/components/media_player/volumio.py b/homeassistant/components/media_player/volumio.py index 84b957533fe..0a940c0aa9d 100644 --- a/homeassistant/components/media_player/volumio.py +++ b/homeassistant/components/media_player/volumio.py @@ -253,8 +253,7 @@ class Volumio(MediaPlayerDevice): return self.send_volumio_msg('commands', params={'cmd': 'clearQueue'}) - @asyncio.coroutine @Throttle(PLAYLIST_UPDATE_INTERVAL) - def _async_update_playlists(self, **kwargs): + async def _async_update_playlists(self, **kwargs): """Update available Volumio playlists.""" - self._playlists = yield from self.send_volumio_msg('listplaylists') + self._playlists = await self.send_volumio_msg('listplaylists') diff --git a/homeassistant/components/sensor/fido.py b/homeassistant/components/sensor/fido.py index 4fc79745b99..25a104bf259 100644 --- a/homeassistant/components/sensor/fido.py +++ b/homeassistant/components/sensor/fido.py @@ -157,13 +157,12 @@ class FidoData(object): REQUESTS_TIMEOUT, httpsession) self.data = {} - @asyncio.coroutine @Throttle(MIN_TIME_BETWEEN_UPDATES) - def async_update(self): + async def async_update(self): """Get the latest data from Fido.""" from pyfido.client import PyFidoError try: - yield from self.client.fetch_data() + await self.client.fetch_data() except PyFidoError as exp: _LOGGER.error("Error on receive last Fido data: %s", exp) return False diff --git a/homeassistant/components/sensor/hydroquebec.py b/homeassistant/components/sensor/hydroquebec.py index e10abc14ff1..3678ac9268f 100644 --- a/homeassistant/components/sensor/hydroquebec.py +++ b/homeassistant/components/sensor/hydroquebec.py @@ -182,13 +182,12 @@ class HydroquebecData(object): return self.client.get_contracts() return [] - @asyncio.coroutine @Throttle(MIN_TIME_BETWEEN_UPDATES) - def _fetch_data(self): + async def _fetch_data(self): """Fetch latest data from HydroQuebec.""" from pyhydroquebec.client import PyHydroQuebecError try: - yield from self.client.fetch_data() + await self.client.fetch_data() except PyHydroQuebecError as exp: _LOGGER.error("Error on receive last Hydroquebec data: %s", exp) return False diff --git a/homeassistant/components/sensor/luftdaten.py b/homeassistant/components/sensor/luftdaten.py index 72ee8a7ce93..c5e0b12b0e0 100644 --- a/homeassistant/components/sensor/luftdaten.py +++ b/homeassistant/components/sensor/luftdaten.py @@ -133,13 +133,9 @@ class LuftdatenSensor(Entity): except KeyError: return - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Get the latest data from luftdaten.info and update the state.""" - try: - yield from self.luftdaten.async_update() - except TypeError: - pass + await self.luftdaten.async_update() class LuftdatenData(object): @@ -150,12 +146,11 @@ class LuftdatenData(object): self.data = data @Throttle(MIN_TIME_BETWEEN_UPDATES) - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Get the latest data from luftdaten.info.""" from luftdaten.exceptions import LuftdatenError try: - yield from self.data.async_get_data() + await self.data.async_get_data() except LuftdatenError: _LOGGER.error("Unable to retrieve data from luftdaten.info") diff --git a/homeassistant/components/sensor/sabnzbd.py b/homeassistant/components/sensor/sabnzbd.py index 632e1ed5c1d..c5dd09e0ccc 100644 --- a/homeassistant/components/sensor/sabnzbd.py +++ b/homeassistant/components/sensor/sabnzbd.py @@ -75,15 +75,14 @@ def setup_sabnzbd(base_url, apikey, name, config, for variable in monitored]) -@asyncio.coroutine @Throttle(MIN_TIME_BETWEEN_UPDATES) -def async_update_queue(sab_api): +async def async_update_queue(sab_api): """ Throttled function to update SABnzbd queue. This ensures that the queue info only gets updated once for all sensors """ - yield from sab_api.refresh_queue() + await sab_api.refresh_queue() def request_configuration(host, name, hass, config, async_add_devices, diff --git a/homeassistant/components/sensor/startca.py b/homeassistant/components/sensor/startca.py index a5908812b6c..aefbc2d4626 100644 --- a/homeassistant/components/sensor/startca.py +++ b/homeassistant/components/sensor/startca.py @@ -140,21 +140,20 @@ class StartcaData(object): """ return float(value) * 10 ** -9 - @asyncio.coroutine @Throttle(MIN_TIME_BETWEEN_UPDATES) - def async_update(self): + async def async_update(self): """Get the Start.ca bandwidth data from the web service.""" import xmltodict _LOGGER.debug("Updating Start.ca usage data") url = 'https://www.start.ca/support/usage/api?key=' + \ self.api_key with async_timeout.timeout(REQUEST_TIMEOUT, loop=self.loop): - req = yield from self.websession.get(url) + req = await self.websession.get(url) if req.status != 200: _LOGGER.error("Request failed with status: %u", req.status) return False - data = yield from req.text() + data = await req.text() try: xml_data = xmltodict.parse(data) except ExpatError: diff --git a/homeassistant/components/sensor/teksavvy.py b/homeassistant/components/sensor/teksavvy.py index 9c4263422ff..0bf1ef4caff 100644 --- a/homeassistant/components/sensor/teksavvy.py +++ b/homeassistant/components/sensor/teksavvy.py @@ -132,22 +132,21 @@ class TekSavvyData(object): self.data = {"limit": self.bandwidth_cap} if self.bandwidth_cap > 0 \ else {"limit": float('inf')} - @asyncio.coroutine @Throttle(MIN_TIME_BETWEEN_UPDATES) - def async_update(self): + async def async_update(self): """Get the TekSavvy bandwidth data from the web service.""" headers = {"TekSavvy-APIKey": self.api_key} _LOGGER.debug("Updating TekSavvy data") url = "https://api.teksavvy.com/"\ "web/Usage/UsageSummaryRecords?$filter=IsCurrent%20eq%20true" with async_timeout.timeout(REQUEST_TIMEOUT, loop=self.loop): - req = yield from self.websession.get(url, headers=headers) + req = await self.websession.get(url, headers=headers) if req.status != 200: _LOGGER.error("Request failed with status: %u", req.status) return False try: - data = yield from req.json() + data = await req.json() for (api, ha_name) in API_HA_MAP: self.data[ha_name] = float(data["value"][0][api]) on_peak_download = self.data["onpeak_download"] diff --git a/homeassistant/components/sensor/wunderground.py b/homeassistant/components/sensor/wunderground.py index edcc1c92bf9..0375bb1344c 100644 --- a/homeassistant/components/sensor/wunderground.py +++ b/homeassistant/components/sensor/wunderground.py @@ -777,14 +777,13 @@ class WUndergroundData(object): return url + '.json' - @asyncio.coroutine @Throttle(MIN_TIME_BETWEEN_UPDATES) - def async_update(self): + async def async_update(self): """Get the latest data from WUnderground.""" try: with async_timeout.timeout(10, loop=self._hass.loop): - response = yield from self._session.get(self._build_url()) - result = yield from response.json() + response = await self._session.get(self._build_url()) + result = await response.json() if "error" in result['response']: raise ValueError(result['response']["error"]["description"]) self.data = result diff --git a/homeassistant/util/__init__.py b/homeassistant/util/__init__.py index 75721a37466..a869251dc3c 100644 --- a/homeassistant/util/__init__.py +++ b/homeassistant/util/__init__.py @@ -1,4 +1,5 @@ """Helper methods for various modules.""" +import asyncio from collections.abc import MutableSet from itertools import chain import threading @@ -276,6 +277,16 @@ class Throttle(object): is_func = (not hasattr(method, '__self__') and '.' not in method.__qualname__.split('..')[-1]) + # Make sure we return a coroutine if the method is async. + if asyncio.iscoroutinefunction(method): + async def throttled_value(): + """Stand-in function for when real func is being throttled.""" + return None + else: + def throttled_value(): + """Stand-in function for when real func is being throttled.""" + return None + @wraps(method) def wrapper(*args, **kwargs): """Wrap that allows wrapped to be called only once per min_time. @@ -298,7 +309,7 @@ class Throttle(object): throttle = host._throttle[id(self)] if not throttle[0].acquire(False): - return None + return throttled_value() # Check if method is never called or no_throttle is given force = kwargs.pop('no_throttle', False) or not throttle[1] @@ -309,7 +320,7 @@ class Throttle(object): throttle[1] = utcnow() return result - return None + return throttled_value() finally: throttle[0].release() diff --git a/tests/util/test_init.py b/tests/util/test_init.py index 2902cb62517..5493843c246 100644 --- a/tests/util/test_init.py +++ b/tests/util/test_init.py @@ -280,3 +280,14 @@ class TestUtil(unittest.TestCase): mock_random.SystemRandom.return_value = generator assert util.get_random_string(length=3) == 'ABC' + + +async def test_throttle_async(): + """Test Throttle decorator with async method.""" + @util.Throttle(timedelta(seconds=2)) + async def test_method(): + """Only first call should return a value.""" + return True + + assert (await test_method()) is True + assert (await test_method()) is None From 3c41c0c46e6f9d8d3fdff91507f0875eae2d4a0e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 9 Mar 2018 19:38:33 -0800 Subject: [PATCH 4/6] Add support for input boolean to Google Assistant (#13030) --- .../components/google_assistant/trait.py | 2 + .../components/google_assistant/test_trait.py | 40 ++++++++++++++++++- 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index dd7b761e782..c78d70e21e6 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -5,6 +5,7 @@ from homeassistant.components import ( cover, group, fan, + input_boolean, media_player, light, scene, @@ -182,6 +183,7 @@ class OnOffTrait(_Trait): """Test if state is supported.""" return domain in ( group.DOMAIN, + input_boolean.DOMAIN, switch.DOMAIN, fan.DOMAIN, light.DOMAIN, diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py index 90dd5d33581..4ffb273662e 100644 --- a/tests/components/google_assistant/test_trait.py +++ b/tests/components/google_assistant/test_trait.py @@ -9,8 +9,9 @@ from homeassistant.components import ( climate, cover, fan, - media_player, + input_boolean, light, + media_player, scene, script, switch, @@ -138,6 +139,43 @@ async def test_onoff_group(hass): } +async def test_onoff_input_boolean(hass): + """Test OnOff trait support for input_boolean domain.""" + assert trait.OnOffTrait.supported(media_player.DOMAIN, 0) + + trt_on = trait.OnOffTrait(State('input_boolean.bla', STATE_ON)) + + assert trt_on.sync_attributes() == {} + + assert trt_on.query_attributes() == { + 'on': True + } + + trt_off = trait.OnOffTrait(State('input_boolean.bla', STATE_OFF)) + assert trt_off.query_attributes() == { + 'on': False + } + + on_calls = async_mock_service(hass, input_boolean.DOMAIN, SERVICE_TURN_ON) + await trt_on.execute(hass, trait.COMMAND_ONOFF, { + 'on': True + }) + assert len(on_calls) == 1 + assert on_calls[0].data == { + ATTR_ENTITY_ID: 'input_boolean.bla', + } + + off_calls = async_mock_service(hass, input_boolean.DOMAIN, + SERVICE_TURN_OFF) + await trt_on.execute(hass, trait.COMMAND_ONOFF, { + 'on': False + }) + assert len(off_calls) == 1 + assert off_calls[0].data == { + ATTR_ENTITY_ID: 'input_boolean.bla', + } + + async def test_onoff_switch(hass): """Test OnOff trait support for switch domain.""" assert trait.OnOffTrait.supported(media_player.DOMAIN, 0) From a58d8fc68b1020ea5b2cbddca37110f029ebadd6 Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Sat, 10 Mar 2018 04:41:59 +0100 Subject: [PATCH 5/6] HomeKit Bugfix: names (#13031) * Fix display_names, changed default port (+1) * Revert port change --- .../components/homekit/accessories.py | 9 +++++---- homeassistant/components/homekit/const.py | 1 + tests/components/homekit/test_accessories.py | 20 ++++++++++--------- 3 files changed, 17 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index 689bcb3377c..1cd94070289 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -5,16 +5,17 @@ from pyhap.accessory import Accessory, Bridge, Category from .const import ( SERV_ACCESSORY_INFO, SERV_BRIDGING_STATE, MANUFACTURER, - CHAR_MODEL, CHAR_MANUFACTURER, CHAR_SERIAL_NUMBER) + CHAR_MODEL, CHAR_MANUFACTURER, CHAR_NAME, CHAR_SERIAL_NUMBER) _LOGGER = logging.getLogger(__name__) -def set_accessory_info(acc, model, manufacturer=MANUFACTURER, +def set_accessory_info(acc, name, model, manufacturer=MANUFACTURER, serial_number='0000'): """Set the default accessory information.""" service = acc.get_service(SERV_ACCESSORY_INFO) + service.get_characteristic(CHAR_NAME).set_value(name) service.get_characteristic(CHAR_MODEL).set_value(model) service.get_characteristic(CHAR_MANUFACTURER).set_value(manufacturer) service.get_characteristic(CHAR_SERIAL_NUMBER).set_value(serial_number) @@ -49,7 +50,7 @@ class HomeAccessory(Accessory): def __init__(self, display_name, model, category='OTHER', **kwargs): """Initialize a Accessory object.""" super().__init__(display_name, **kwargs) - set_accessory_info(self, model) + set_accessory_info(self, display_name, model) self.category = getattr(Category, category, Category.OTHER) def _set_services(self): @@ -62,7 +63,7 @@ class HomeBridge(Bridge): def __init__(self, display_name, model, pincode, **kwargs): """Initialize a Bridge object.""" super().__init__(display_name, pincode=pincode, **kwargs) - set_accessory_info(self, model) + set_accessory_info(self, display_name, model) def _set_services(self): add_preload_service(self, SERV_ACCESSORY_INFO) diff --git a/homeassistant/components/homekit/const.py b/homeassistant/components/homekit/const.py index 35bd25eabd3..73dfbf69049 100644 --- a/homeassistant/components/homekit/const.py +++ b/homeassistant/components/homekit/const.py @@ -22,6 +22,7 @@ CHAR_HEATING_THRESHOLD_TEMPERATURE = 'HeatingThresholdTemperature' CHAR_LINK_QUALITY = 'LinkQuality' CHAR_MANUFACTURER = 'Manufacturer' CHAR_MODEL = 'Model' +CHAR_NAME = 'Name' CHAR_ON = 'On' CHAR_POSITION_STATE = 'PositionState' CHAR_REACHABLE = 'Reachable' diff --git a/tests/components/homekit/test_accessories.py b/tests/components/homekit/test_accessories.py index a45aa82d981..6f39a8c792b 100644 --- a/tests/components/homekit/test_accessories.py +++ b/tests/components/homekit/test_accessories.py @@ -13,7 +13,7 @@ from homeassistant.components.homekit.accessories import ( HomeAccessory, HomeBridge) from homeassistant.components.homekit.const import ( SERV_ACCESSORY_INFO, SERV_BRIDGING_STATE, - CHAR_MODEL, CHAR_MANUFACTURER, CHAR_SERIAL_NUMBER) + CHAR_MODEL, CHAR_MANUFACTURER, CHAR_NAME, CHAR_SERIAL_NUMBER) from tests.mock.homekit import ( get_patch_paths, mock_preload_service, @@ -69,21 +69,23 @@ def test_override_properties(): def test_set_accessory_info(): """Test setting of basic accessory information with MockAccessory.""" acc = MockAccessory('Accessory') - set_accessory_info(acc, 'model', 'manufacturer', '0000') + set_accessory_info(acc, 'name', 'model', 'manufacturer', '0000') assert len(acc.services) == 1 serv = acc.services[0] assert serv.display_name == SERV_ACCESSORY_INFO - assert len(serv.characteristics) == 3 + assert len(serv.characteristics) == 4 chars = serv.characteristics - assert chars[0].display_name == CHAR_MODEL - assert chars[0].value == 'model' - assert chars[1].display_name == CHAR_MANUFACTURER - assert chars[1].value == 'manufacturer' - assert chars[2].display_name == CHAR_SERIAL_NUMBER - assert chars[2].value == '0000' + assert chars[0].display_name == CHAR_NAME + assert chars[0].value == 'name' + assert chars[1].display_name == CHAR_MODEL + assert chars[1].value == 'model' + assert chars[2].display_name == CHAR_MANUFACTURER + assert chars[2].value == 'manufacturer' + assert chars[3].display_name == CHAR_SERIAL_NUMBER + assert chars[3].value == '0000' @patch(PATH_ACC, side_effect=mock_preload_service) From bf430ad14b685c0c5a2648c50e2bffddf6203048 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 9 Mar 2018 19:42:52 -0800 Subject: [PATCH 6/6] Version bump to 0.65.1 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index d8f7e00959c..d2a0bc43a8d 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 65 -PATCH_VERSION = '0' +PATCH_VERSION = '1' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3)