Compare commits

...
37 Commits
Author SHA1 Message Date
Kevin FronczakandGitHub f4480da276 Merge pull request #180 from fronzbot/dev
0.14.0
2019-05-23 16:56:46 -04:00
Kevin FronczakandGitHub 7767d3af65 Merge pull request #182 from fronzbot/update-changes
Update CHANGES.rst
2019-05-23 16:51:54 -04:00
Kevin FronczakandGitHub 79e699ba78 Update CHANGES.rst 2019-05-23 16:42:12 -04:00
Kevin FronczakandGitHub b28d6f4eb4 Merge pull request #181 from fronzbot/version-bump
Version bump
2019-05-23 16:40:14 -04:00
Kevin FronczakandGitHub d021f48c67 Update CHANGES.rst 2019-05-23 16:15:07 -04:00
Kevin Fronczak 1e71af28dd Version bump 2019-05-23 16:06:56 -04:00
Kevin FronczakandGitHub cb5867d430 Merge pull request #179 from fronzbot/bugfix
Blink instantiation bugfix
2019-05-23 14:45:26 -04:00
Kevin Fronczak bb887936b6 Blink instantiation bugfix 2019-05-23 14:35:27 -04:00
Kevin FronczakandGitHub 720d520398 Merge pull request #175 from fronzbot/update-subdomains
Change subdomain from rest.region to rest-region
2019-05-21 23:56:25 -04:00
Kevin FronczakandGitHub 8fb1116641 Merge branch 'dev' into update-subdomains 2019-05-21 23:37:00 -04:00
Kevin FronczakandGitHub c319762152 Merge pull request #178 from fronzbot/dev-bump
dev version bump
2019-05-21 23:35:25 -04:00
Kevin Fronczak 726d8b8c65 dev version bump 2019-05-21 23:14:26 -04:00
Kevin FronczakandGitHub 683650e2c2 Merge pull request #177 from fronzbot/fix-motion-detect
Fix motion detect
2019-05-21 23:13:15 -04:00
Kevin Fronczak 50b1a35168 Update tests 2019-05-21 22:46:11 -04:00
Kevin Fronczak b88a5feddf Change default interval to one minute 2019-05-21 22:38:49 -04:00
Kevin Fronczak 93472e38de Added in motion interval to allow for looking at historic motion events.
- Good debug utility
- Helps iron out rapid motion events, or missed events due to quick
calls to refresh method
2019-05-21 22:36:46 -04:00
Kevin Fronczak 85c14ede8d Move constants into helpers/constants.py 2019-05-21 22:23:31 -04:00
Kevin Fronczak 58ce109518 Use UTC for time conversions 2019-05-21 22:09:36 -04:00
Kevin Fronczak cf65648293 Add legacy flag to be able to use old 'rest.region' subdomain 2019-05-20 21:15:08 -04:00
Kevin FronczakandGitHub d46e7ed96d Merge pull request #176 from fronzbot/slug
Slugify filenames in video download to ensure OS interoperability
2019-05-20 20:43:28 -04:00
Kevin Fronczak 00395b3825 Slugify filenames in video download to ensure OS interoperability 2019-05-20 11:04:58 -04:00
Kevin FronczakandGitHub f67f0dd6a2 Dev version bump 2019-05-20 00:00:57 -04:00
Kevin FronczakandGitHub d90cd3309f Update README.rst 2019-05-19 18:00:42 -04:00
Kevin Fronczak 130aa8382a Change subdomain from rest.region to rest-region 2019-05-18 18:23:46 -04:00
Kevin FronczakandGitHub e836a6fa58 Merge pull request #174 from fronzbot/fix-battery-voltage
Change battery percentage to state
2019-05-18 18:17:38 -04:00
Kevin Fronczak 92d89b9fe5 Change battery percentage to state 2019-05-18 12:42:01 -04:00
Kevin FronczakandGitHub afdd73118d Merge pull request #165 from fronzbot/dev
0.13.1
2019-03-01 21:59:29 -05:00
Kevin FronczakandGitHub 0262f14a53 Merge pull request #163 from fronzbot/dev
0.13.0
2019-03-01 21:04:36 -05:00
Kevin FronczakandGitHub ff8f091a74 Merge pull request #154 from fronzbot/dev
0.12.1
2019-01-31 18:22:09 -08:00
Kevin FronczakandGitHub 77e200f6a5 Merge pull request #152 from fronzbot/dev
0.12.0
2019-01-31 17:05:03 -08:00
Kevin FronczakandGitHub f3a8036649 Merge pull request #141 from fronzbot/dev
0.11.2
2019-01-23 08:05:05 -08:00
Kevin FronczakandGitHub 853ccbce79 Merge pull request #136 from fronzbot/dev
0.11.1
2019-01-02 09:19:08 -08:00
Kevin FronczakandGitHub 1a24e1896f Merge pull request #129 from fronzbot/dev
0.11.0
2018-11-23 20:27:40 -05:00
Kevin FronczakandGitHub ae946dbb53 Merge pull request #121 from fronzbot/dev
0.10.3
2018-11-20 21:47:42 -05:00
Kevin FronczakandGitHub 80c6444c2e Merge pull request #112 from fronzbot/dev
0.10.2
2018-10-30 20:22:08 -04:00
Kevin FronczakandGitHub e872fc9ce5 Merge pull request #105 from fronzbot/dev
0.10.1
2018-10-18 13:19:49 -04:00
Kevin FronczakandGitHub 4e6957a772 Merge pull request #100 from fronzbot/dev
Release 0.10.0
2018-10-16 17:43:21 -04:00
11 changed files with 100 additions and 29 deletions
+27 -5
View File
@@ -3,6 +3,28 @@ Changelog
A list of changes between each release A list of changes between each release
0.14.0 (2019-05-23)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
**Breaking Changes:**
- ``BlinkCamera.battery`` no longer reports a percentage, instead it returns a string representing the state of the battery.
- Previous logic for calculating percentage was incorrect
- raw battery voltage can be accessed via ``BlinkCamera.battery_voltage``
**Bug Fixes:**
- Updated video endpoint (fixes broken motion detection)
- Removed throttling from critical api methods which prevented proper operation of multi-sync unit setups
- Slugify downloaded video names to allow for OS interoperability
- Added one minute offset (``Blink.motion_interval``) when checking for recent motion to allow time for events to propagate to server prior to refresh call.
**Everything else:**
- Changed all urls to use ``rest-region`` rather than ``rest.region``. Ability to revert to old method is enabled by instantiating ``Blink()`` with the ``legacy_subdomain`` variable set to ``True``.
- Added debug mode to ``blinkpy.download_videos`` routine to simply print the videos prepped for download, rather than actually saving them.
- Use UTC for time conversions, rather than local timezone
0.13.1 (2019-03-01) 0.13.1 (2019-03-01)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
- Remove throttle decorator from network status request - Remove throttle decorator from network status request
@@ -10,10 +32,10 @@ A list of changes between each release
0.13.0 (2019-03-01) 0.13.0 (2019-03-01)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
**Breaking change:** **Breaking change:**
Wifi status reported in dBm again, instead of bars (which is great). Also, the old `get_camera_info` method has changed and requires a `camera_id` parameter. Wifi status reported in dBm again, instead of bars (which is great). Also, the old ``get_camera_info`` method has changed and requires a ``camera_id`` parameter.
- Adds throttle decorator - Adds throttle decorator
- Decorate following functions with 4s throttle (call method with `force=True` to override): - Decorate following functions with 4s throttle (call method with ``force=True`` to override):
- request_network_status - request_network_status
- request_syncmodule - request_syncmodule
- request_system_arm - request_system_arm
@@ -28,7 +50,7 @@ Wifi status reported in dBm again, instead of bars (which is great). Also, the
- request_motion_detection_enable - request_motion_detection_enable
- request_motion_detection_disable - request_motion_detection_disable
- Use the updated homescreen api endpoint to retrieve camera information. The old method to retrieve all cameras at once seems to not exist, and this was the only solution I could figure out and confirm to work. - Use the updated homescreen api endpoint to retrieve camera information. The old method to retrieve all cameras at once seems to not exist, and this was the only solution I could figure out and confirm to work.
- Adds throttle decorator to refresh function to prevent too many frequent calls with `force_cache` flag set to `True`. This additional throttle can be overridden with the `force=True` argument passed to the refresh function. - Adds throttle decorator to refresh function to prevent too many frequent calls with ``force_cache`` flag set to ``True``. This additional throttle can be overridden with the ``force=True`` argument passed to the refresh function.
- Add ability to cycle through login api endpoints to anticipate future endpoint deprecation - Add ability to cycle through login api endpoints to anticipate future endpoint deprecation
@@ -70,7 +92,7 @@ Wifi status reported in dBm again, instead of bars (which is great). Also, the
0.10.2 (2018-10-30) 0.10.2 (2018-10-30)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
- Set minimum required version of the requests library to 2.20.0 due to vulnerability in earlier releases. - Set minimum required version of the requests library to 2.20.0 due to vulnerability in earlier releases.
- When multiple networks detected, changed log level to 'warning' from 'error' - When multiple networks detected, changed log level to ``warning`` from ``error``
0.10.1 (2018-10-18) 0.10.1 (2018-10-18)
@@ -98,7 +120,7 @@ Wifi status reported in dBm again, instead of bars (which is great). Also, the
- Use session for http requests - Use session for http requests
**Breaking change:** **Breaking change:**
- Cameras now accessed through sync module Blink.sync.cameras - Cameras now accessed through sync module ``Blink.sync.cameras``
0.8.1 (2018-09-24) 0.8.1 (2018-09-24)
+7
View File
@@ -2,6 +2,10 @@ blinkpy |Build Status| |Coverage Status| |Docs| |PyPi Version| |Python Version|
================================================================================ ================================================================================
A Python library for the Blink Camera system A Python library for the Blink Camera system
Like the library? Consider buying me a cup of coffee!
|Donate|
Disclaimer: Disclaimer:
~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~
Published under the MIT license - See LICENSE file for more details. Published under the MIT license - See LICENSE file for more details.
@@ -106,3 +110,6 @@ Example usage, which downloads all videos recorded since July 4th, 2018 at 9:34a
:target: http://blinkpy.readthedocs.io/en/latest/?badge=latest :target: http://blinkpy.readthedocs.io/en/latest/?badge=latest
.. |Python Version| image:: https://img.shields.io/pypi/pyversions/blinkpy.svg .. |Python Version| image:: https://img.shields.io/pypi/pyversions/blinkpy.svg
:target: https://img.shields.io/pypi/pyversions/blinkpy.svg :target: https://img.shields.io/pypi/pyversions/blinkpy.svg
.. |Donate| image:: https://www.paypalobjects.com/en_US/i/btn/btn_donateCC_LG.gif
:target: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=UR6Z2B8GXYUCC
+21 -11
View File
@@ -21,6 +21,7 @@ from shutil import copyfileobj
from requests.structures import CaseInsensitiveDict from requests.structures import CaseInsensitiveDict
from dateutil.parser import parse from dateutil.parser import parse
from slugify import slugify
from blinkpy import api from blinkpy import api
from blinkpy.sync_module import BlinkSyncModule from blinkpy.sync_module import BlinkSyncModule
@@ -29,14 +30,10 @@ from blinkpy.helpers.util import (
create_session, merge_dicts, get_time, BlinkURLHandler, create_session, merge_dicts, get_time, BlinkURLHandler,
BlinkAuthenticationException, Throttle) BlinkAuthenticationException, Throttle)
from blinkpy.helpers.constants import ( from blinkpy.helpers.constants import (
BLINK_URL, LOGIN_URL, OLD_LOGIN_URL, LOGIN_BACKUP_URL) BLINK_URL, LOGIN_URL, OLD_LOGIN_URL, LOGIN_BACKUP_URL,
DEFAULT_MOTION_INTERVAL, DEFAULT_REFRESH, MIN_THROTTLE_TIME)
from blinkpy.helpers.constants import __version__ from blinkpy.helpers.constants import __version__
REFRESH_RATE = 30
# Prevents rapid calls to blink.refresh()
# with the force_cache flag set to True
MIN_THROTTLE_TIME = 2
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@@ -45,7 +42,9 @@ class Blink():
"""Class to initialize communication.""" """Class to initialize communication."""
def __init__(self, username=None, password=None, def __init__(self, username=None, password=None,
refresh_rate=REFRESH_RATE): refresh_rate=DEFAULT_REFRESH,
motion_interval=DEFAULT_MOTION_INTERVAL,
legacy_subdomain=False):
""" """
Initialize Blink system. Initialize Blink system.
@@ -53,6 +52,13 @@ class Blink():
:param password: Blink password :param password: Blink password
:param refresh_rate: Refresh rate of blink information. :param refresh_rate: Refresh rate of blink information.
Defaults to 15 (seconds) Defaults to 15 (seconds)
:param motion_interval: How far back to register motion in minutes.
Defaults to last refresh time.
Useful for preventing motion_detected property
from de-asserting too quickly.
:param legacy_subdomain: Set to TRUE to use old 'rest.region'
endpoints (only use if you are having
api issues).
""" """
self._username = username self._username = username
self._password = password self._password = password
@@ -72,7 +78,9 @@ class Blink():
self.cameras = CaseInsensitiveDict({}) self.cameras = CaseInsensitiveDict({})
self.video_list = CaseInsensitiveDict({}) self.video_list = CaseInsensitiveDict({})
self._login_url = LOGIN_URL self._login_url = LOGIN_URL
self.motion_interval = motion_interval
self.version = __version__ self.version = __version__
self.legacy = legacy_subdomain
@property @property
def auth_header(self): def auth_header(self):
@@ -136,7 +144,7 @@ class Blink():
self._auth_header = {'Host': self._host, self._auth_header = {'Host': self._host,
'TOKEN_AUTH': self._token} 'TOKEN_AUTH': self._token}
self.urls = BlinkURLHandler(self.region_id) self.urls = BlinkURLHandler(self.region_id, legacy=self.legacy)
return self._auth_header return self._auth_header
@@ -310,7 +318,8 @@ class Blink():
continue continue
clip_address = "{}{}".format(self.urls.base_url, address) clip_address = "{}{}".format(self.urls.base_url, address)
filename = "{}_{}.mp4".format(camera_name, created_at) filename = "{}-{}".format(camera_name, created_at)
filename = "{}.mp4".format(slugify(filename))
filename = os.path.join(path, filename) filename = os.path.join(path, filename)
if not debug: if not debug:
@@ -325,5 +334,6 @@ class Blink():
_LOGGER.info("Downloaded video to %s", filename) _LOGGER.info("Downloaded video to %s", filename)
else: else:
print("Camera: {}, Timestamp: {}, Address: {}".format( print(("Camera: {}, Timestamp: {}, "
camera_name, created_at, address)) "Address: {}, Filename: {}").format(
camera_name, created_at, address, filename))
+3 -2
View File
@@ -41,6 +41,7 @@ class BlinkCamera():
'temperature_c': self.temperature_c, 'temperature_c': self.temperature_c,
'temperature_calibrated': self.temperature_calibrated, 'temperature_calibrated': self.temperature_calibrated,
'battery': self.battery, 'battery': self.battery,
'battery_voltage': self.battery_voltage,
'thumbnail': self.thumbnail, 'thumbnail': self.thumbnail,
'video': self.clip, 'video': self.clip,
'motion_enabled': self.motion_enabled, 'motion_enabled': self.motion_enabled,
@@ -54,8 +55,8 @@ class BlinkCamera():
@property @property
def battery(self): def battery(self):
"""Return battery level as percentage.""" """Return battery as string."""
return round(self.battery_voltage / 180 * 100) return self.battery_state
@property @property
def temperature_c(self): def temperature_c(self):
+5 -1
View File
@@ -4,7 +4,7 @@ import os
MAJOR_VERSION = 0 MAJOR_VERSION = 0
MINOR_VERSION = 14 MINOR_VERSION = 14
PATCH_VERSION = '0.dev0' PATCH_VERSION = 0
__version__ = '{}.{}.{}'.format(MAJOR_VERSION, MINOR_VERSION, PATCH_VERSION) __version__ = '{}.{}.{}'.format(MAJOR_VERSION, MINOR_VERSION, PATCH_VERSION)
@@ -60,3 +60,7 @@ ONLINE = {'online': True, 'offline': False}
OTHER OTHER
''' '''
TIMESTAMP_FORMAT = '%Y-%m-%dT%H:%M:%S%Z' TIMESTAMP_FORMAT = '%Y-%m-%dT%H:%M:%S%Z'
DEFAULT_MOTION_INTERVAL = 1
DEFAULT_REFRESH = 30
MIN_THROTTLE_TIME = 2
+6 -3
View File
@@ -15,7 +15,7 @@ def get_time(time_to_convert=None):
"""Create blink-compatible timestamp.""" """Create blink-compatible timestamp."""
if time_to_convert is None: if time_to_convert is None:
time_to_convert = time.time() time_to_convert = time.time()
return time.strftime(TIMESTAMP_FORMAT, time.localtime(time_to_convert)) return time.strftime(TIMESTAMP_FORMAT, time.gmtime(time_to_convert))
def merge_dicts(dict_a, dict_b): def merge_dicts(dict_a, dict_b):
@@ -111,9 +111,12 @@ class BlinkAuthenticationException(BlinkException):
class BlinkURLHandler(): class BlinkURLHandler():
"""Class that handles Blink URLS.""" """Class that handles Blink URLS."""
def __init__(self, region_id): def __init__(self, region_id, legacy=False):
"""Initialize the urls.""" """Initialize the urls."""
self.base_url = "https://rest.{}.{}".format(region_id, BLINK_URL) self.subdomain = 'rest-{}'.format(region_id)
if legacy:
self.subdomain = 'rest.{}'.format(region_id)
self.base_url = "https://{}.{}".format(self.subdomain, BLINK_URL)
self.home_url = "{}/homescreen".format(self.base_url) self.home_url = "{}/homescreen".format(self.base_url)
self.event_url = "{}/events/network".format(self.base_url) self.event_url = "{}/events/network".format(self.base_url)
self.network_url = "{}/network".format(self.base_url) self.network_url = "{}/network".format(self.base_url)
+9 -1
View File
@@ -33,6 +33,7 @@ class BlinkSyncModule():
self.network_info = None self.network_info = None
self.events = [] self.events = []
self.cameras = CaseInsensitiveDict({}) self.cameras = CaseInsensitiveDict({})
self.motion_interval = blink.motion_interval
self.motion = {} self.motion = {}
self.last_record = {} self.last_record = {}
self.camera_list = camera_list self.camera_list = camera_list
@@ -161,8 +162,15 @@ class BlinkSyncModule():
def check_new_videos(self): def check_new_videos(self):
"""Check if new videos since last refresh.""" """Check if new videos since last refresh."""
try:
interval = self.blink.last_refresh - self.motion_interval*60
except TypeError:
# This is the first start, so refresh hasn't happened yet.
# No need to check for motion.
return False
resp = api.request_videos(self.blink, resp = api.request_videos(self.blink,
time=self.blink.last_refresh, time=interval,
page=1) page=1)
for camera in self.cameras.keys(): for camera in self.cameras.keys():
+1
View File
@@ -1,3 +1,4 @@
python-dateutil==2.7.5 python-dateutil==2.7.5
requests>=2.20.0 requests>=2.20.0
python-slugify==3.0.2
testtools==2.3.0 testtools==2.3.0
+4 -4
View File
@@ -87,15 +87,15 @@ class TestBlinkCameraSetup(unittest.TestCase):
self.assertEqual(self.camera.network_id, '5678') self.assertEqual(self.camera.network_id, '5678')
self.assertEqual(self.camera.serial, '12345678') self.assertEqual(self.camera.serial, '12345678')
self.assertEqual(self.camera.motion_enabled, False) self.assertEqual(self.camera.motion_enabled, False)
self.assertEqual(self.camera.battery, 50) self.assertEqual(self.camera.battery, 'ok')
self.assertEqual(self.camera.temperature, 68) self.assertEqual(self.camera.temperature, 68)
self.assertEqual(self.camera.temperature_c, 20) self.assertEqual(self.camera.temperature_c, 20)
self.assertEqual(self.camera.temperature_calibrated, 71) self.assertEqual(self.camera.temperature_calibrated, 71)
self.assertEqual(self.camera.wifi_strength, 4) self.assertEqual(self.camera.wifi_strength, 4)
self.assertEqual(self.camera.thumbnail, self.assertEqual(self.camera.thumbnail,
'https://rest.test.immedia-semi.com/thumb.jpg') 'https://rest-test.immedia-semi.com/thumb.jpg')
self.assertEqual(self.camera.clip, self.assertEqual(self.camera.clip,
'https://rest.test.immedia-semi.com/test.mp4') 'https://rest-test.immedia-semi.com/test.mp4')
self.assertEqual(self.camera.image_from_cache, 'test') self.assertEqual(self.camera.image_from_cache, 'test')
self.assertEqual(self.camera.video_from_cache, 'foobar') self.assertEqual(self.camera.video_from_cache, 'foobar')
@@ -136,7 +136,7 @@ class TestBlinkCameraSetup(unittest.TestCase):
} }
self.camera.update(config) self.camera.update(config)
self.assertEqual(self.camera.thumbnail, self.assertEqual(self.camera.thumbnail,
'https://rest.test.immedia-semi.com/new/thumb.jpg') 'https://rest-test.immedia-semi.com/new/thumb.jpg')
def test_no_thumbnails(self, mock_sess): def test_no_thumbnails(self, mock_sess):
"""Tests that thumbnail is 'None' if none found.""" """Tests that thumbnail is 'None' if none found."""
+9 -1
View File
@@ -17,12 +17,14 @@ class TestBlinkSyncModule(unittest.TestCase):
def setUp(self): def setUp(self):
"""Set up Blink module.""" """Set up Blink module."""
self.blink = blinkpy.Blink(username=USERNAME, self.blink = blinkpy.Blink(username=USERNAME,
password=PASSWORD) password=PASSWORD,
motion_interval=0)
# pylint: disable=protected-access # pylint: disable=protected-access
self.blink._auth_header = { self.blink._auth_header = {
'Host': 'test.url.tld', 'Host': 'test.url.tld',
'TOKEN_AUTH': 'foobar123' 'TOKEN_AUTH': 'foobar123'
} }
self.blink.last_refresh = 0
self.blink.urls = blinkpy.BlinkURLHandler('test') self.blink.urls = blinkpy.BlinkURLHandler('test')
self.blink.sync['test'] = BlinkSyncModule(self.blink, self.blink.sync['test'] = BlinkSyncModule(self.blink,
'test', 'test',
@@ -59,6 +61,12 @@ class TestBlinkSyncModule(unittest.TestCase):
self.assertEqual(self.blink.sync['test'].get_camera_info('1234'), self.assertEqual(self.blink.sync['test'].get_camera_info('1234'),
'foobar') 'foobar')
def test_check_new_videos_startup(self, mock_resp):
"""Test that check_new_videos does not block startup."""
sync_module = self.blink.sync['test']
self.blink.last_refresh = None
self.assertFalse(sync_module.check_new_videos())
def test_check_new_videos(self, mock_resp): def test_check_new_videos(self, mock_resp):
"""Test recent video response.""" """Test recent video response."""
mock_resp.return_value = { mock_resp.return_value = {
+8 -1
View File
@@ -3,7 +3,7 @@
import unittest import unittest
from unittest import mock from unittest import mock
import time import time
from blinkpy.helpers.util import Throttle from blinkpy.helpers.util import Throttle, BlinkURLHandler
class TestUtil(unittest.TestCase): class TestUtil(unittest.TestCase):
@@ -98,3 +98,10 @@ class TestUtil(unittest.TestCase):
with mock.patch('time.time', return_value=now_plus_6): with mock.patch('time.time', return_value=now_plus_6):
self.assertEqual(tester.test1(), None) self.assertEqual(tester.test1(), None)
self.assertEqual(tester.test2(), True) self.assertEqual(tester.test2(), True)
def test_legacy_subdomains(self):
"""Test that subdomain can be set to legacy mode."""
urls = BlinkURLHandler('test')
self.assertEqual(urls.subdomain, 'rest-test')
urls = BlinkURLHandler('test', legacy=True)
self.assertEqual(urls.subdomain, 'rest.test')