summaryrefslogtreecommitdiff
path: root/src/leap/common
diff options
context:
space:
mode:
authorTomás Touceda <chiiph@leap.se>2014-04-04 16:19:22 -0300
committerTomás Touceda <chiiph@leap.se>2014-04-04 16:19:22 -0300
commit75309e8bd6b762cad41eaf7d6bf8d4a3696105d2 (patch)
tree4a72d5764657a3e5df9bab7e545876e476a6360e /src/leap/common
parent25ef3a640bb6e7877487a6300c065635092c92c0 (diff)
parent5b09a916bc83d92d5c0ecc62ee3f9788e1a56077 (diff)
Merge branch 'release-0.3.7'0.3.7
Diffstat (limited to 'src/leap/common')
-rw-r--r--src/leap/common/decorators.py160
-rw-r--r--src/leap/common/events/events.proto1
-rw-r--r--src/leap/common/events/events_pb2.py17
-rw-r--r--src/leap/common/mail.py9
-rw-r--r--src/leap/common/tests/test_check.py1
-rw-r--r--src/leap/common/tests/test_memoize.py76
6 files changed, 253 insertions, 11 deletions
diff --git a/src/leap/common/decorators.py b/src/leap/common/decorators.py
new file mode 100644
index 0000000..2ef6711
--- /dev/null
+++ b/src/leap/common/decorators.py
@@ -0,0 +1,160 @@
+# -*- coding: utf-8 -*-
+# decorators.py
+# Copyright (C) 2013 LEAP
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+"""
+Useful decorators.
+"""
+import collections
+import datetime
+import functools
+import logging
+
+logger = logging.getLogger(__name__)
+
+
+class _memoized(object):
+ """
+ Decorator.
+
+ Caches a function's return value each time it is called.
+ If called later with the same arguments, the cached value is returned
+ (not reevaluated).
+ """
+
+ # cache invalidation time, in seconds
+ CACHE_INVALIDATION_DELTA = 1800
+
+ def __init__(self, func, ignore_kwargs=None, is_method=False,
+ invalidation=None):
+
+ """
+ :param ignore_kwargs: If True, ignore all kwargs.
+ If tuple, ignore those kwargs.
+ :type ignore_kwargs: bool, tuple or None
+ :param is_method: whether the decorated function is a method.
+ (ignores the self argument if so).
+ :type is_method: True
+ """
+ self.ignore_kwargs = ignore_kwargs if ignore_kwargs else []
+ self.is_method = is_method
+ self.func = func
+
+ if invalidation:
+ self.CACHE_INVALIDATION_DELTA = invalidation
+
+ # TODO should put bounds to the cache dict so we do not
+ # consume a huge amount of memory.
+ self.cache = {}
+ self.cache_ts = {}
+
+ def __call__(self, *args, **kwargs):
+ """
+ Executes the call.
+
+ :tyoe args: tuple
+ :type kwargs: dict
+ """
+ def ret_or_raise(value):
+ """
+ Returns the value except if it is an exception,
+ in which case it's raised.
+ """
+ if isinstance(value, Exception):
+ raise value
+ return value
+
+ if self.is_method:
+ # forget about `self` as key
+ key_args = args[1:]
+ else:
+ key_args = args
+
+ if self.ignore_kwargs is True:
+ key = key_args
+ else:
+ key = (key_args, frozenset(
+ [(k, v) for k, v in kwargs.items()
+ if k not in self.ignore_kwargs]))
+
+ if not isinstance(key, collections.Hashable):
+ # uncacheable. a list, for instance.
+ # better to not cache than blow up.
+ logger.warning("Key is not hashable, bailing out!")
+ return self.func(*args, **kwargs)
+
+ if key in self.cache:
+ if self._is_cache_still_valid(key):
+ value = self.cache[key]
+ logger.debug("Got value from cache...")
+ return ret_or_raise(value)
+ else:
+ logger.debug("Cache is invalid, evaluating again...")
+
+ # no cache, or cache invalid
+ try:
+ value = self.func(*args, **kwargs)
+ except Exception as exc:
+ logger.error("Exception while calling function: %r" % (exc,))
+ value = exc
+ self.cache[key] = value
+ self.cache_ts[key] = datetime.datetime.now()
+ return ret_or_raise(value)
+
+ def _is_cache_still_valid(self, key, now=datetime.datetime.now):
+ """
+ Returns True if the cache value is still valid, False otherwise.
+
+ For now, this happen if less than CACHE_INVALIDATION_DELTA seconds
+ have passed from the time in which we recorded the cached value.
+
+ :param key: the key to lookup in the cache
+ :type key: hashable
+ :param now: a callable that returns a datetime object. override
+ for dependency injection during testing.
+ :type now: callable
+ :rtype: bool
+ """
+ cached_ts = self.cache_ts[key]
+ delta = datetime.timedelta(seconds=self.CACHE_INVALIDATION_DELTA)
+ return (now() - cached_ts) < delta
+
+ def __repr__(self):
+ """
+ Return the function's docstring.
+ """
+ return self.func.__doc__
+
+ def __get__(self, obj, objtype):
+ """
+ Support instance methods.
+ """
+ return functools.partial(self.__call__, obj)
+
+
+def memoized_method(function=None, ignore_kwargs=None):
+ """
+ Wrap _memoized to allow for deferred calling
+
+ :type function: callable, or None.
+ :type ignore_kwargs: None, True or tuple.
+ """
+ if function:
+ return _memoized(function, is_method=True)
+ else:
+ def wrapper(function):
+ return _memoized(
+ function, ignore_kwargs=ignore_kwargs, is_method=True)
+ return wrapper
diff --git a/src/leap/common/events/events.proto b/src/leap/common/events/events.proto
index 2708b93..a8eb82a 100644
--- a/src/leap/common/events/events.proto
+++ b/src/leap/common/events/events.proto
@@ -63,6 +63,7 @@ enum Event {
KEYMANAGER_STARTED_KEY_GENERATION = 43;
KEYMANAGER_FINISHED_KEY_GENERATION = 44;
KEYMANAGER_DONE_UPLOADING_KEYS = 45;
+ SOLEDAD_INVALID_AUTH_TOKEN = 46;
}
diff --git a/src/leap/common/events/events_pb2.py b/src/leap/common/events/events_pb2.py
index 93f6f0b..f0b05f7 100644
--- a/src/leap/common/events/events_pb2.py
+++ b/src/leap/common/events/events_pb2.py
@@ -13,7 +13,7 @@ from google.protobuf import descriptor_pb2
DESCRIPTOR = descriptor.FileDescriptor(
name='events.proto',
package='leap.common.events',
- serialized_pb='\n\x0c\x65vents.proto\x12\x12leap.common.events\"\x97\x01\n\rSignalRequest\x12(\n\x05\x65vent\x18\x01 \x02(\x0e\x32\x19.leap.common.events.Event\x12\x0f\n\x07\x63ontent\x18\x02 \x02(\t\x12\x12\n\nmac_method\x18\x03 \x02(\t\x12\x0b\n\x03mac\x18\x04 \x02(\x0c\x12\x12\n\nenc_method\x18\x05 \x01(\t\x12\x16\n\x0e\x65rror_occurred\x18\x06 \x01(\x08\"j\n\x0fRegisterRequest\x12(\n\x05\x65vent\x18\x01 \x02(\x0e\x32\x19.leap.common.events.Event\x12\x0c\n\x04port\x18\x02 \x02(\x05\x12\x12\n\nmac_method\x18\x03 \x02(\t\x12\x0b\n\x03mac\x18\x04 \x02(\x0c\"l\n\x11UnregisterRequest\x12(\n\x05\x65vent\x18\x01 \x02(\x0e\x32\x19.leap.common.events.Event\x12\x0c\n\x04port\x18\x02 \x02(\x05\x12\x12\n\nmac_method\x18\x03 \x02(\t\x12\x0b\n\x03mac\x18\x04 \x02(\x0c\"\r\n\x0bPingRequest\"\x82\x01\n\rEventResponse\x12\x38\n\x06status\x18\x01 \x02(\x0e\x32(.leap.common.events.EventResponse.Status\x12\x0e\n\x06result\x18\x02 \x01(\t\"\'\n\x06Status\x12\x06\n\x02OK\x10\x01\x12\n\n\x06UNAUTH\x10\x02\x12\t\n\x05\x45RROR\x10\x03*\xc0\t\n\x05\x45vent\x12\x15\n\x11\x43LIENT_SESSION_ID\x10\x01\x12\x0e\n\nCLIENT_UID\x10\x02\x12\x19\n\x15SOLEDAD_CREATING_KEYS\x10\x03\x12\x1e\n\x1aSOLEDAD_DONE_CREATING_KEYS\x10\x04\x12\x1a\n\x16SOLEDAD_UPLOADING_KEYS\x10\x05\x12\x1f\n\x1bSOLEDAD_DONE_UPLOADING_KEYS\x10\x06\x12\x1c\n\x18SOLEDAD_DOWNLOADING_KEYS\x10\x07\x12!\n\x1dSOLEDAD_DONE_DOWNLOADING_KEYS\x10\x08\x12\x1c\n\x18SOLEDAD_NEW_DATA_TO_SYNC\x10\t\x12\x1a\n\x16SOLEDAD_DONE_DATA_SYNC\x10\n\x12\x17\n\x13UPDATER_NEW_UPDATES\x10\x0b\x12\x19\n\x15UPDATER_DONE_UPDATING\x10\x0c\x12\x10\n\x0cRAISE_WINDOW\x10\r\x12\x18\n\x14SMTP_SERVICE_STARTED\x10\x0e\x12 \n\x1cSMTP_SERVICE_FAILED_TO_START\x10\x0f\x12%\n!SMTP_RECIPIENT_ACCEPTED_ENCRYPTED\x10\x10\x12\'\n#SMTP_RECIPIENT_ACCEPTED_UNENCRYPTED\x10\x11\x12\x1b\n\x17SMTP_RECIPIENT_REJECTED\x10\x12\x12\x1f\n\x1bSMTP_START_ENCRYPT_AND_SIGN\x10\x13\x12\x1d\n\x19SMTP_END_ENCRYPT_AND_SIGN\x10\x14\x12\x13\n\x0fSMTP_START_SIGN\x10\x15\x12\x11\n\rSMTP_END_SIGN\x10\x16\x12\x1b\n\x17SMTP_SEND_MESSAGE_START\x10\x17\x12\x1d\n\x19SMTP_SEND_MESSAGE_SUCCESS\x10\x18\x12\x1b\n\x17SMTP_SEND_MESSAGE_ERROR\x10\x19\x12\x18\n\x14SMTP_CONNECTION_LOST\x10\x1a\x12\x18\n\x14IMAP_SERVICE_STARTED\x10\x1e\x12 \n\x1cIMAP_SERVICE_FAILED_TO_START\x10\x1f\x12\x15\n\x11IMAP_CLIENT_LOGIN\x10 \x12\x19\n\x15IMAP_FETCHED_INCOMING\x10!\x12\x17\n\x13IMAP_MSG_PROCESSING\x10\"\x12\x16\n\x12IMAP_MSG_DECRYPTED\x10#\x12\x1a\n\x16IMAP_MSG_SAVED_LOCALLY\x10$\x12\x1d\n\x19IMAP_MSG_DELETED_INCOMING\x10%\x12\x18\n\x14IMAP_UNHANDLED_ERROR\x10&\x12\x14\n\x10IMAP_UNREAD_MAIL\x10\'\x12\x1e\n\x1aKEYMANAGER_LOOKING_FOR_KEY\x10(\x12\x18\n\x14KEYMANAGER_KEY_FOUND\x10)\x12\x1c\n\x18KEYMANAGER_KEY_NOT_FOUND\x10*\x12%\n!KEYMANAGER_STARTED_KEY_GENERATION\x10+\x12&\n\"KEYMANAGER_FINISHED_KEY_GENERATION\x10,\x12\"\n\x1eKEYMANAGER_DONE_UPLOADING_KEYS\x10-2\xdd\x02\n\x13\x45ventsServerService\x12J\n\x04ping\x12\x1f.leap.common.events.PingRequest\x1a!.leap.common.events.EventResponse\x12R\n\x08register\x12#.leap.common.events.RegisterRequest\x1a!.leap.common.events.EventResponse\x12V\n\nunregister\x12%.leap.common.events.UnregisterRequest\x1a!.leap.common.events.EventResponse\x12N\n\x06signal\x12!.leap.common.events.SignalRequest\x1a!.leap.common.events.EventResponse2\xb1\x01\n\x13\x45ventsClientService\x12J\n\x04ping\x12\x1f.leap.common.events.PingRequest\x1a!.leap.common.events.EventResponse\x12N\n\x06signal\x12!.leap.common.events.SignalRequest\x1a!.leap.common.events.EventResponseB\x03\x90\x01\x01')
+ serialized_pb='\n\x0c\x65vents.proto\x12\x12leap.common.events\"\x97\x01\n\rSignalRequest\x12(\n\x05\x65vent\x18\x01 \x02(\x0e\x32\x19.leap.common.events.Event\x12\x0f\n\x07\x63ontent\x18\x02 \x02(\t\x12\x12\n\nmac_method\x18\x03 \x02(\t\x12\x0b\n\x03mac\x18\x04 \x02(\x0c\x12\x12\n\nenc_method\x18\x05 \x01(\t\x12\x16\n\x0e\x65rror_occurred\x18\x06 \x01(\x08\"j\n\x0fRegisterRequest\x12(\n\x05\x65vent\x18\x01 \x02(\x0e\x32\x19.leap.common.events.Event\x12\x0c\n\x04port\x18\x02 \x02(\x05\x12\x12\n\nmac_method\x18\x03 \x02(\t\x12\x0b\n\x03mac\x18\x04 \x02(\x0c\"l\n\x11UnregisterRequest\x12(\n\x05\x65vent\x18\x01 \x02(\x0e\x32\x19.leap.common.events.Event\x12\x0c\n\x04port\x18\x02 \x02(\x05\x12\x12\n\nmac_method\x18\x03 \x02(\t\x12\x0b\n\x03mac\x18\x04 \x02(\x0c\"\r\n\x0bPingRequest\"\x82\x01\n\rEventResponse\x12\x38\n\x06status\x18\x01 \x02(\x0e\x32(.leap.common.events.EventResponse.Status\x12\x0e\n\x06result\x18\x02 \x01(\t\"\'\n\x06Status\x12\x06\n\x02OK\x10\x01\x12\n\n\x06UNAUTH\x10\x02\x12\t\n\x05\x45RROR\x10\x03*\xe0\t\n\x05\x45vent\x12\x15\n\x11\x43LIENT_SESSION_ID\x10\x01\x12\x0e\n\nCLIENT_UID\x10\x02\x12\x19\n\x15SOLEDAD_CREATING_KEYS\x10\x03\x12\x1e\n\x1aSOLEDAD_DONE_CREATING_KEYS\x10\x04\x12\x1a\n\x16SOLEDAD_UPLOADING_KEYS\x10\x05\x12\x1f\n\x1bSOLEDAD_DONE_UPLOADING_KEYS\x10\x06\x12\x1c\n\x18SOLEDAD_DOWNLOADING_KEYS\x10\x07\x12!\n\x1dSOLEDAD_DONE_DOWNLOADING_KEYS\x10\x08\x12\x1c\n\x18SOLEDAD_NEW_DATA_TO_SYNC\x10\t\x12\x1a\n\x16SOLEDAD_DONE_DATA_SYNC\x10\n\x12\x17\n\x13UPDATER_NEW_UPDATES\x10\x0b\x12\x19\n\x15UPDATER_DONE_UPDATING\x10\x0c\x12\x10\n\x0cRAISE_WINDOW\x10\r\x12\x18\n\x14SMTP_SERVICE_STARTED\x10\x0e\x12 \n\x1cSMTP_SERVICE_FAILED_TO_START\x10\x0f\x12%\n!SMTP_RECIPIENT_ACCEPTED_ENCRYPTED\x10\x10\x12\'\n#SMTP_RECIPIENT_ACCEPTED_UNENCRYPTED\x10\x11\x12\x1b\n\x17SMTP_RECIPIENT_REJECTED\x10\x12\x12\x1f\n\x1bSMTP_START_ENCRYPT_AND_SIGN\x10\x13\x12\x1d\n\x19SMTP_END_ENCRYPT_AND_SIGN\x10\x14\x12\x13\n\x0fSMTP_START_SIGN\x10\x15\x12\x11\n\rSMTP_END_SIGN\x10\x16\x12\x1b\n\x17SMTP_SEND_MESSAGE_START\x10\x17\x12\x1d\n\x19SMTP_SEND_MESSAGE_SUCCESS\x10\x18\x12\x1b\n\x17SMTP_SEND_MESSAGE_ERROR\x10\x19\x12\x18\n\x14SMTP_CONNECTION_LOST\x10\x1a\x12\x18\n\x14IMAP_SERVICE_STARTED\x10\x1e\x12 \n\x1cIMAP_SERVICE_FAILED_TO_START\x10\x1f\x12\x15\n\x11IMAP_CLIENT_LOGIN\x10 \x12\x19\n\x15IMAP_FETCHED_INCOMING\x10!\x12\x17\n\x13IMAP_MSG_PROCESSING\x10\"\x12\x16\n\x12IMAP_MSG_DECRYPTED\x10#\x12\x1a\n\x16IMAP_MSG_SAVED_LOCALLY\x10$\x12\x1d\n\x19IMAP_MSG_DELETED_INCOMING\x10%\x12\x18\n\x14IMAP_UNHANDLED_ERROR\x10&\x12\x14\n\x10IMAP_UNREAD_MAIL\x10\'\x12\x1e\n\x1aKEYMANAGER_LOOKING_FOR_KEY\x10(\x12\x18\n\x14KEYMANAGER_KEY_FOUND\x10)\x12\x1c\n\x18KEYMANAGER_KEY_NOT_FOUND\x10*\x12%\n!KEYMANAGER_STARTED_KEY_GENERATION\x10+\x12&\n\"KEYMANAGER_FINISHED_KEY_GENERATION\x10,\x12\"\n\x1eKEYMANAGER_DONE_UPLOADING_KEYS\x10-\x12\x1e\n\x1aSOLEDAD_INVALID_AUTH_TOKEN\x10.2\xdd\x02\n\x13\x45ventsServerService\x12J\n\x04ping\x12\x1f.leap.common.events.PingRequest\x1a!.leap.common.events.EventResponse\x12R\n\x08register\x12#.leap.common.events.RegisterRequest\x1a!.leap.common.events.EventResponse\x12V\n\nunregister\x12%.leap.common.events.UnregisterRequest\x1a!.leap.common.events.EventResponse\x12N\n\x06signal\x12!.leap.common.events.SignalRequest\x1a!.leap.common.events.EventResponse2\xb1\x01\n\x13\x45ventsClientService\x12J\n\x04ping\x12\x1f.leap.common.events.PingRequest\x1a!.leap.common.events.EventResponse\x12N\n\x06signal\x12!.leap.common.events.SignalRequest\x1a!.leap.common.events.EventResponseB\x03\x90\x01\x01')
_EVENT = descriptor.EnumDescriptor(
name='Event',
@@ -189,11 +189,15 @@ _EVENT = descriptor.EnumDescriptor(
name='KEYMANAGER_DONE_UPLOADING_KEYS', index=41, number=45,
options=None,
type=None),
+ descriptor.EnumValueDescriptor(
+ name='SOLEDAD_INVALID_AUTH_TOKEN', index=42, number=46,
+ options=None,
+ type=None),
],
containing_type=None,
options=None,
serialized_start=557,
- serialized_end=1773,
+ serialized_end=1805,
)
@@ -239,6 +243,7 @@ KEYMANAGER_KEY_NOT_FOUND = 42
KEYMANAGER_STARTED_KEY_GENERATION = 43
KEYMANAGER_FINISHED_KEY_GENERATION = 44
KEYMANAGER_DONE_UPLOADING_KEYS = 45
+SOLEDAD_INVALID_AUTH_TOKEN = 46
_EVENTRESPONSE_STATUS = descriptor.EnumDescriptor(
@@ -532,8 +537,8 @@ _EVENTSSERVERSERVICE = descriptor.ServiceDescriptor(
file=DESCRIPTOR,
index=0,
options=None,
- serialized_start=1776,
- serialized_end=2125,
+ serialized_start=1808,
+ serialized_end=2157,
methods=[
descriptor.MethodDescriptor(
name='ping',
@@ -587,8 +592,8 @@ _EVENTSCLIENTSERVICE = descriptor.ServiceDescriptor(
file=DESCRIPTOR,
index=1,
options=None,
- serialized_start=2128,
- serialized_end=2305,
+ serialized_start=2160,
+ serialized_end=2337,
methods=[
descriptor.MethodDescriptor(
name='ping',
diff --git a/src/leap/common/mail.py b/src/leap/common/mail.py
index 2f2146d..b630c90 100644
--- a/src/leap/common/mail.py
+++ b/src/leap/common/mail.py
@@ -20,26 +20,25 @@ Utility functions for email.
import email
import re
-from leap.common.check import leap_assert_type
-
def get_email_charset(content, default="utf-8"):
"""
Mini parser to retrieve the charset of an email.
:param content: mail contents
- :type content: unicode
+ :type content: unicode or str
:param default: optional default value for encoding
:type default: str or None
:returns: the charset as parsed from the contents
:rtype: str
"""
- leap_assert_type(content, unicode)
+ if isinstance(content, unicode):
+ content.encode("utf-8", "replace")
charset = default
try:
- em = email.message_from_string(content.encode("utf-8", "replace"))
+ em = email.message_from_string(content)
# Miniparser for: Content-Type: <something>; charset=<charset>
charset_re = r'''charset=(?P<charset>[\w|\d|-]*)'''
charset = re.findall(charset_re, em["Content-Type"])[0]
diff --git a/src/leap/common/tests/test_check.py b/src/leap/common/tests/test_check.py
index 6ce8493..cd488ff 100644
--- a/src/leap/common/tests/test_check.py
+++ b/src/leap/common/tests/test_check.py
@@ -27,6 +27,7 @@ import mock
from leap.common import check
+
class CheckTests(unittest.TestCase):
def test_raises_on_false_condition(self):
with self.assertRaises(AssertionError):
diff --git a/src/leap/common/tests/test_memoize.py b/src/leap/common/tests/test_memoize.py
new file mode 100644
index 0000000..c923fc5
--- /dev/null
+++ b/src/leap/common/tests/test_memoize.py
@@ -0,0 +1,76 @@
+# -*- coding: utf-8 -*-
+# test_check.py
+# Copyright (C) 2013 LEAP
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+"""
+Tests for:
+ * leap/common/decorators._memoized
+"""
+try:
+ import unittest2 as unittest
+except ImportError:
+ import unittest
+
+from time import sleep
+
+import mock
+
+from leap.common.decorators import _memoized
+
+
+class MemoizeTests(unittest.TestCase):
+
+ def test_memoized_call(self):
+ """
+ Test that a memoized function only calls once.
+ """
+ witness = mock.Mock()
+
+ @_memoized
+ def callmebaby():
+ return witness()
+
+ for i in range(10):
+ callmebaby()
+ witness.assert_called_once_with()
+
+ def test_cache_invalidation(self):
+ """
+ Test that time makes the cache invalidation expire.
+ """
+ witness = mock.Mock()
+
+ cache_with_alzheimer = _memoized
+ cache_with_alzheimer.CACHE_INVALIDATION_DELTA = 1
+
+ @cache_with_alzheimer
+ def callmebaby(*args):
+ return witness(*args)
+
+ for i in range(10):
+ callmebaby()
+ witness.assert_called_once_with()
+
+ sleep(2)
+ callmebaby("onemoretime")
+
+ expected = [mock.call(), mock.call("onemoretime")]
+ self.assertEqual(
+ witness.call_args_list,
+ expected)
+
+
+if __name__ == "__main__":
+ unittest.main()