diff --git a/.circleci/config.yml b/.circleci/config.yml index 8fe10b5..bd09816 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -4,13 +4,22 @@ aliases: - &with-requests environment: REQUESTS: requests + - &lint + steps: + - checkout + - run: make ci-lint - &job-defaults steps: - checkout - run: pip install $REQUESTS - - run: python setup.py test + - run: make test jobs: + lint-sdk: + docker: + - image: circleci/python:3.9 + <<: *with-requests + <<: *lint python-3_4: docker: - image: circleci/python:3.4 @@ -36,12 +45,19 @@ jobs: - image: circleci/python:3.8 <<: *with-requests <<: *job-defaults + python-3_9: + docker: + - image: circleci/python:3.9 + <<: *with-requests + <<: *job-defaults workflows: main: jobs: + - lint-sdk - python-3_4 - python-3_5 - python-3_6 - python-3_7 - python-3_8 + - python-3_9 diff --git a/.python-version b/.python-version index d2577d9..a5c4c76 100644 --- a/.python-version +++ b/.python-version @@ -1 +1 @@ -3.7.7 +3.9.0 diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..c422f5e --- /dev/null +++ b/Makefile @@ -0,0 +1,35 @@ +PIP = pip3 +PYTHON = python3 + +.PHONY = help ci-lint coverage lint pre-lint setup test +.DEFAULT_GOAL = help + +help: + @echo "---------------HELP-----------------" + @echo "To check the project coverage type make coverage" + @echo "To lint the project type make lint" + @echo "To setup the project type make setup" + @echo "To test the project type make test" + @echo "------------------------------------" + +coverage: + ${PIP} install coverage + coverage run setup.py test + +ci-lint: pre-lint lint + +pre-lint: + ${PIP} install pylint + ${PIP} install setuptools-lint + ${PIP} install --upgrade pep8 + ${PIP} install --upgrade autopep8 + +lint: + ${PYTHON} setup.py lint + autopep8 --in-place -r castle + +setup: + ${PYTHON} setup.py install + +test: + ${PYTHON} setup.py test diff --git a/castle/apis/request.py b/castle/apis/request.py index bab8556..9f40616 100644 --- a/castle/apis/request.py +++ b/castle/apis/request.py @@ -4,6 +4,7 @@ HTTPS_SCHEME = 'https' + class ApisRequest(object): def __init__(self, headers=None): self.headers = headers or dict() diff --git a/castle/client.py b/castle/client.py index c60adeb..a7fa34b 100644 --- a/castle/client.py +++ b/castle/client.py @@ -1,15 +1,15 @@ import warnings from castle.configuration import configuration from castle.api import Api -from castle.context.default import ContextDefault -from castle.context.merger import ContextMerger +from castle.context.get_default import ContextGetDefault +from castle.context.merge import ContextMerge from castle.commands.authenticate import CommandsAuthenticate from castle.commands.identify import CommandsIdentify from castle.commands.impersonate import CommandsImpersonate from castle.commands.track import CommandsTrack from castle.exceptions import InternalServerError, RequestError, ImpersonationFailed from castle.failover_response import FailoverResponse -from castle.utils import timestamp as generate_timestamp +from castle.utils.timestamp import UtilsTimestamp as generate_timestamp class Client(object): @@ -27,15 +27,15 @@ def from_request(cls, request, options=None): def to_context(request, options=None): if options is None: options = {} - default_context = ContextDefault( + default_context = ContextGetDefault( request, options.get('cookies')).call() - return ContextMerger.call(default_context, options.get('context', {})) + return ContextMerge.call(default_context, options.get('context', {})) @staticmethod def to_options(options=None): if options is None: options = {} - options.setdefault('timestamp', generate_timestamp()) + options.setdefault('timestamp', generate_timestamp.call()) if 'traits' in options: warnings.warn('use user_traits instead of traits key', DeprecationWarning) diff --git a/castle/commands/authenticate.py b/castle/commands/authenticate.py index 58c5e1d..b997613 100644 --- a/castle/commands/authenticate.py +++ b/castle/commands/authenticate.py @@ -1,7 +1,7 @@ from castle.command import Command -from castle.utils import timestamp -from castle.context.merger import ContextMerger -from castle.context.sanitizer import ContextSanitizer +from castle.utils.timestamp import UtilsTimestamp as generate_timestamp +from castle.context.merge import ContextMerge +from castle.context.sanitize import ContextSanitize from castle.validators.present import ValidatorsPresent @@ -11,10 +11,10 @@ def __init__(self, context): def build(self, options): ValidatorsPresent.call(options, 'event') - context = ContextMerger.call(self.context, options.get('context')) - context = ContextSanitizer.call(context) + context = ContextMerge.call(self.context, options.get('context')) + context = ContextSanitize.call(context) if context: options.update({'context': context}) - options.update({'sent_at': timestamp()}) + options.update({'sent_at': generate_timestamp.call()}) return Command(method='post', path='authenticate', data=options) diff --git a/castle/commands/identify.py b/castle/commands/identify.py index debe9aa..370b5f6 100644 --- a/castle/commands/identify.py +++ b/castle/commands/identify.py @@ -1,7 +1,7 @@ from castle.command import Command -from castle.utils import timestamp -from castle.context.merger import ContextMerger -from castle.context.sanitizer import ContextSanitizer +from castle.utils.timestamp import UtilsTimestamp as generate_timestamp +from castle.context.merge import ContextMerge +from castle.context.sanitize import ContextSanitize from castle.validators.not_supported import ValidatorsNotSupported @@ -11,10 +11,10 @@ def __init__(self, context): def build(self, options): ValidatorsNotSupported.call(options, 'properties') - context = ContextMerger.call(self.context, options.get('context')) - context = ContextSanitizer.call(context) + context = ContextMerge.call(self.context, options.get('context')) + context = ContextSanitize.call(context) if context: options.update({'context': context}) - options.update({'sent_at': timestamp()}) + options.update({'sent_at': generate_timestamp.call()}) return Command(method='post', path='identify', data=options) diff --git a/castle/commands/impersonate.py b/castle/commands/impersonate.py index 6df3bfd..f8662db 100644 --- a/castle/commands/impersonate.py +++ b/castle/commands/impersonate.py @@ -1,7 +1,7 @@ from castle.command import Command -from castle.utils import timestamp -from castle.context.merger import ContextMerger -from castle.context.sanitizer import ContextSanitizer +from castle.utils.timestamp import UtilsTimestamp as generate_timestamp +from castle.context.merge import ContextMerge +from castle.context.sanitize import ContextSanitize from castle.validators.present import ValidatorsPresent @@ -12,13 +12,13 @@ def __init__(self, context): def build(self, options): ValidatorsPresent.call(options, 'user_id') - context = ContextMerger.call(self.context, options.get('context')) - context = ContextSanitizer.call(context) + context = ContextMerge.call(self.context, options.get('context')) + context = ContextSanitize.call(context) ValidatorsPresent.call(context, 'user_agent', 'ip') if context: options.update({'context': context}) - options.update({'sent_at': timestamp()}) + options.update({'sent_at': generate_timestamp.call()}) method = ('delete' if options.get('reset', False) else 'post') diff --git a/castle/commands/track.py b/castle/commands/track.py index bc66705..5bbcf21 100644 --- a/castle/commands/track.py +++ b/castle/commands/track.py @@ -1,7 +1,7 @@ from castle.command import Command -from castle.utils import timestamp -from castle.context.merger import ContextMerger -from castle.context.sanitizer import ContextSanitizer +from castle.utils.timestamp import UtilsTimestamp as generate_timestamp +from castle.context.merge import ContextMerge +from castle.context.sanitize import ContextSanitize from castle.validators.present import ValidatorsPresent @@ -11,10 +11,10 @@ def __init__(self, context): def build(self, options): ValidatorsPresent.call(options, 'event') - context = ContextMerger.call(self.context, options.get('context')) - context = ContextSanitizer.call(context) + context = ContextMerge.call(self.context, options.get('context')) + context = ContextSanitize.call(context) if context: options.update({'context': context}) - options.update({'sent_at': timestamp()}) + options.update({'sent_at': generate_timestamp.call()}) return Command(method='post', path='track', data=options) diff --git a/castle/configuration.py b/castle/configuration.py index f1bb397..209cd18 100644 --- a/castle/configuration.py +++ b/castle/configuration.py @@ -42,6 +42,7 @@ \Aunix\Z| \Aunix:"""] + class Configuration(object): def __init__(self): self.request_timeout = REQUEST_TIMEOUT diff --git a/castle/context/default.py b/castle/context/get_default.py similarity index 98% rename from castle/context/default.py rename to castle/context/get_default.py index 167aa96..7674f92 100644 --- a/castle/context/default.py +++ b/castle/context/get_default.py @@ -7,7 +7,7 @@ __version__ = VERSION -class ContextDefault(object): +class ContextGetDefault(object): def __init__(self, request, cookies): self.cookies = self._fetch_cookies(request, cookies) self.pre_headers = HeadersFilter(request).call() diff --git a/castle/context/merge.py b/castle/context/merge.py new file mode 100644 index 0000000..85595fe --- /dev/null +++ b/castle/context/merge.py @@ -0,0 +1,11 @@ +from castle.utils.merge import UtilsMerge +from castle.utils.clone import UtilsClone + + +class ContextMerge(object): + + @staticmethod + def call(initial_context, request_context): + source_copy = UtilsClone.call(initial_context) + UtilsMerge.call(source_copy, request_context) + return source_copy diff --git a/castle/context/merger.py b/castle/context/merger.py deleted file mode 100644 index e4e33e9..0000000 --- a/castle/context/merger.py +++ /dev/null @@ -1,10 +0,0 @@ -from castle.utils import clone, deep_merge - - -class ContextMerger(object): - - @staticmethod - def call(initial_context, request_context): - source_copy = clone(initial_context) - deep_merge(source_copy, request_context) - return source_copy diff --git a/castle/context/sanitizer.py b/castle/context/sanitize.py similarity index 94% rename from castle/context/sanitizer.py rename to castle/context/sanitize.py index 49ec5aa..2bd7aa2 100644 --- a/castle/context/sanitizer.py +++ b/castle/context/sanitize.py @@ -1,4 +1,4 @@ -class ContextSanitizer(object): +class ContextSanitize(object): @classmethod def call(cls, context): diff --git a/castle/test/__init__.py b/castle/test/__init__.py index 05aea48..9df0ef6 100644 --- a/castle/test/__init__.py +++ b/castle/test/__init__.py @@ -11,9 +11,9 @@ 'castle.test.api_test', 'castle.test.client_test', 'castle.test.configuration_test', - 'castle.test.context.default_test', - 'castle.test.context.merger_test', - 'castle.test.context.sanitizer_test', + 'castle.test.context.get_default_test', + 'castle.test.context.merge_test', + 'castle.test.context.sanitize_test', 'castle.test.command_test', 'castle.test.commands.authenticate_test', 'castle.test.commands.identify_test', @@ -29,7 +29,9 @@ 'castle.test.secure_mode_test', 'castle.test.validators.not_supported_test', 'castle.test.validators.present_test', - 'castle.test.utils_test' + 'castle.test.utils.clone_test', + 'castle.test.utils.merge_test', + 'castle.test.utils.timestamp_test', ] # pylint: disable=redefined-builtin diff --git a/castle/test/client_test.py b/castle/test/client_test.py index 7fdb922..bfcc0f6 100644 --- a/castle/test/client_test.py +++ b/castle/test/client_test.py @@ -22,7 +22,7 @@ def request(): class ClientTestCase(unittest.TestCase): def setUp(self): # patch timestamp to return a known value - timestamp_patcher = mock.patch('castle.client.generate_timestamp') + timestamp_patcher = mock.patch('castle.client.generate_timestamp.call') self.mock_timestamp = timestamp_patcher.start() self.mock_timestamp.return_value = '2018-01-02T03:04:05.678' self.addCleanup(timestamp_patcher.stop) diff --git a/castle/test/commands/authenticate_test.py b/castle/test/commands/authenticate_test.py index d437aad..daf1ac8 100644 --- a/castle/test/commands/authenticate_test.py +++ b/castle/test/commands/authenticate_test.py @@ -2,7 +2,7 @@ from castle.command import Command from castle.commands.authenticate import CommandsAuthenticate from castle.exceptions import InvalidParametersError -from castle.utils import clone +from castle.utils.clone import UtilsClone def default_options(): @@ -31,7 +31,7 @@ class CommandsAuthenticateTestCase(unittest.TestCase): def setUp(self): # patch timestamp to return a known value timestamp_patcher = mock.patch( - 'castle.commands.authenticate.timestamp') + 'castle.commands.authenticate.generate_timestamp.call') self.mock_timestamp = timestamp_patcher.start() self.mock_timestamp.return_value = mock.sentinel.timestamp self.addCleanup(timestamp_patcher.stop) @@ -46,7 +46,7 @@ def test_build(self): options = default_options_plus(context={'spam': True}) # expect the original context to have been merged with the context specified in the options - expected_data = clone(options) + expected_data = UtilsClone.call(options) expected_data.update(context={'test': '1', 'spam': True}) expected = default_command_with_data(**expected_data) diff --git a/castle/test/commands/identify_test.py b/castle/test/commands/identify_test.py index 8cb837d..a9b2aac 100644 --- a/castle/test/commands/identify_test.py +++ b/castle/test/commands/identify_test.py @@ -2,7 +2,7 @@ from castle.command import Command from castle.commands.identify import CommandsIdentify from castle.exceptions import InvalidParametersError -from castle.utils import clone +from castle.utils.clone import UtilsClone def default_options(): @@ -29,7 +29,7 @@ def default_command_with_data(**data): class CommandsIdentifyTestCase(unittest.TestCase): def setUp(self): # patch timestamp to return a known value - timestamp_patcher = mock.patch('castle.commands.identify.timestamp') + timestamp_patcher = mock.patch('castle.commands.identify.generate_timestamp.call') self.mock_timestamp = timestamp_patcher.start() self.mock_timestamp.return_value = mock.sentinel.timestamp self.addCleanup(timestamp_patcher.stop) @@ -44,7 +44,7 @@ def test_build(self): options = default_options_plus(context={'color': 'blue'}) # expect the original context to have been merged with the context specified in the options - expected_data = clone(options) + expected_data = UtilsClone.call(options) expected_data.update(context={'test': '1', 'color': 'blue'}) expected = default_command_with_data(**expected_data) diff --git a/castle/test/commands/impersonate_test.py b/castle/test/commands/impersonate_test.py index 9647cb8..fe4331a 100644 --- a/castle/test/commands/impersonate_test.py +++ b/castle/test/commands/impersonate_test.py @@ -2,7 +2,7 @@ from castle.command import Command from castle.commands.impersonate import CommandsImpersonate from castle.exceptions import InvalidParametersError -from castle.utils import clone +from castle.utils.clone import UtilsClone def default_options(): @@ -39,7 +39,7 @@ def default_reset_command_with_data(**data): class CommandsImpersonateTestCase(unittest.TestCase): def setUp(self): # patch timestamp to return a known value - timestamp_patcher = mock.patch('castle.commands.impersonate.timestamp') + timestamp_patcher = mock.patch('castle.commands.impersonate.generate_timestamp.call') self.mock_timestamp = timestamp_patcher.start() self.mock_timestamp.return_value = mock.sentinel.timestamp self.addCleanup(timestamp_patcher.stop) @@ -56,7 +56,7 @@ def test_build(self): ) # expect the original context to have been merged with the context specified in the options - expected_data = clone(options) + expected_data = UtilsClone.call(options) expected_data.update( context={'lang': 'es', 'local time': '8:53pm', 'ip': '127.0.0.1', 'user_agent': 'Chrome'} @@ -74,7 +74,7 @@ def test_reset_build(self): ) # expect the original context to have been merged with the context specified in the options - expected_data = clone(options) + expected_data = UtilsClone.call(options) expected_data.update( context={'lang': 'es', 'local time': '8:53pm', 'ip': '127.0.0.1', 'user_agent': 'Chrome'} diff --git a/castle/test/commands/track_test.py b/castle/test/commands/track_test.py index d89233d..a2f4de1 100644 --- a/castle/test/commands/track_test.py +++ b/castle/test/commands/track_test.py @@ -2,7 +2,7 @@ from castle.command import Command from castle.commands.track import CommandsTrack from castle.exceptions import InvalidParametersError -from castle.utils import clone +from castle.utils.clone import UtilsClone def default_options(): @@ -29,7 +29,7 @@ def default_command_with_data(**data): class CommandsTrackTestCase(unittest.TestCase): def setUp(self): # patch timestamp to return a known value - timestamp_patcher = mock.patch('castle.commands.track.timestamp') + timestamp_patcher = mock.patch('castle.commands.track.generate_timestamp.call') self.mock_timestamp = timestamp_patcher.start() self.mock_timestamp.return_value = mock.sentinel.timestamp self.addCleanup(timestamp_patcher.stop) @@ -44,7 +44,7 @@ def test_build(self): options = default_options_plus(context={'local time': '8:53pm'}) # expect the original context to have been merged with the context specified in the options - expected_data = clone(options) + expected_data = UtilsClone.call(options) expected_data.update(context={'lang': 'es', 'local time': '8:53pm'}) expected = default_command_with_data(**expected_data) diff --git a/castle/test/configuration_test.py b/castle/test/configuration_test.py index 4f4851e..88eab72 100644 --- a/castle/test/configuration_test.py +++ b/castle/test/configuration_test.py @@ -3,6 +3,7 @@ from castle.exceptions import ConfigurationError from castle.configuration import Configuration + class ConfigurationTestCase(unittest.TestCase): def test_default_values(self): config = Configuration() diff --git a/castle/test/context/default_test.py b/castle/test/context/get_default_test.py similarity index 89% rename from castle/test/context/default_test.py rename to castle/test/context/get_default_test.py index d16f2f2..8af2c56 100644 --- a/castle/test/context/default_test.py +++ b/castle/test/context/get_default_test.py @@ -1,7 +1,7 @@ from castle.test import unittest, mock from castle.version import VERSION as __version__ -from castle.context.default import ContextDefault +from castle.context.get_default import ContextGetDefault def client_id(): @@ -32,10 +32,10 @@ def request(env): return req -class ContextDefaultTestCase(unittest.TestCase): +class ContextGetDefaultTestCase(unittest.TestCase): def test_default_context(self): - context = ContextDefault( + context = ContextGetDefault( request(environ()), cookies()).call() self.assertEqual(context['client_id'], client_id()) self.assertEqual(context['active'], True) diff --git a/castle/test/context/merger_test.py b/castle/test/context/merge_test.py similarity index 70% rename from castle/test/context/merger_test.py rename to castle/test/context/merge_test.py index ed8eeff..024177d 100644 --- a/castle/test/context/merger_test.py +++ b/castle/test/context/merge_test.py @@ -1,13 +1,13 @@ from castle.test import unittest -from castle.context.merger import ContextMerger +from castle.context.merge import ContextMerge -class ContextMergerTestCase(unittest.TestCase): +class ContextMergeTestCase(unittest.TestCase): def test_call(self): params = {'foo': {'foo': 'bar', 'nonfoo': 'nonbar'}, 'to_remove': 'ok'} self.assertEqual( - ContextMerger.call( + ContextMerge.call( params, {'foo': {'foo': 'foo'}, 'to_remove': None}), {'foo': {'foo': 'foo', 'nonfoo': 'nonbar'}} ) diff --git a/castle/test/context/sanitizer_test.py b/castle/test/context/sanitize_test.py similarity index 54% rename from castle/test/context/sanitizer_test.py rename to castle/test/context/sanitize_test.py index 09eba44..ee80ea9 100644 --- a/castle/test/context/sanitizer_test.py +++ b/castle/test/context/sanitize_test.py @@ -1,22 +1,22 @@ from castle.test import unittest -from castle.context.sanitizer import ContextSanitizer +from castle.context.sanitize import ContextSanitize -class ContextSanitizerTestCase(unittest.TestCase): +class ContextSanitizeTestCase(unittest.TestCase): def test_call_when_no_context(self): context = None - self.assertEqual(ContextSanitizer.call(context), {}) + self.assertEqual(ContextSanitize.call(context), {}) def test_call_when_no_active_context(self): context = {'foo': 'bar'} - self.assertEqual(ContextSanitizer.call(context), {'foo': 'bar'}) + self.assertEqual(ContextSanitize.call(context), {'foo': 'bar'}) def test_call_when_no_active_is_string(self): context = {'foo': 'bar', 'active': 'true'} - self.assertEqual(ContextSanitizer.call(context), {'foo': 'bar'}) + self.assertEqual(ContextSanitize.call(context), {'foo': 'bar'}) def test_call_when_have_active_flag(self): context = {'foo': 'bar', 'active': True} - self.assertEqual(ContextSanitizer.call(context), + self.assertEqual(ContextSanitize.call(context), {'foo': 'bar', 'active': True}) diff --git a/castle/test/utils/__init__.py b/castle/test/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/castle/test/utils/clone_test.py b/castle/test/utils/clone_test.py new file mode 100644 index 0000000..9449064 --- /dev/null +++ b/castle/test/utils/clone_test.py @@ -0,0 +1,10 @@ +from castle.test import unittest +from castle.utils.clone import UtilsClone + + +class UtilsCloneTestCase(unittest.TestCase): + def test_clone(self): + params = {'foo': 'bar'} + new_params = UtilsClone.call(params) + self.assertEqual(params, new_params) + self.assertIsNot(new_params, params) diff --git a/castle/test/utils_test.py b/castle/test/utils/merge_test.py similarity index 72% rename from castle/test/utils_test.py rename to castle/test/utils/merge_test.py index 44af8a0..c8d1416 100644 --- a/castle/test/utils_test.py +++ b/castle/test/utils/merge_test.py @@ -1,22 +1,12 @@ -from datetime import datetime - -from castle.test import mock, unittest -from castle.utils import clone, deep_merge, timestamp - - -class UtilsTestCase(unittest.TestCase): - def test_clone(self): - params = {'foo': 'bar'} - new_params = clone(params) - self.assertEqual(params, new_params) - self.assertIsNot(new_params, params) +from castle.test import unittest +from castle.utils.merge import UtilsMerge class DeepMergeTestCase(unittest.TestCase): def test_simple_merge(self): a = {'key': 'value'} b = {'otherkey': 'othervalue'} - deep_merge(a, b) + UtilsMerge.call(a, b) expected = {'key': 'value', 'otherkey': 'othervalue'} self.assertEqual(a, expected) @@ -26,61 +16,61 @@ def test_merge_list(self): # combine them. a = {'key': ['original']} b = {'key': ['new']} - deep_merge(a, b) + UtilsMerge.call(a, b) self.assertEqual(a, {'key': ['new']}) def test_merge_number(self): # The value from b is always taken a = {'key': 10} b = {'key': 45} - deep_merge(a, b) + UtilsMerge.call(a, b) self.assertEqual(a, {'key': 45}) a = {'key': 45} b = {'key': 10} - deep_merge(a, b) + UtilsMerge.call(a, b) self.assertEqual(a, {'key': 10}) def test_merge_boolean(self): # The value from b is always taken a = {'key': False} b = {'key': True} - deep_merge(a, b) + UtilsMerge.call(a, b) self.assertEqual(a, {'key': True}) a = {'key': True} b = {'key': False} - deep_merge(a, b) + UtilsMerge.call(a, b) self.assertEqual(a, {'key': False}) def test_merge_string(self): a = {'key': 'value'} b = {'key': 'othervalue'} - deep_merge(a, b) + UtilsMerge.call(a, b) self.assertEqual(a, {'key': 'othervalue'}) def test_merge_when_no_extra(self): a = {'key': 'value'} b = None - deep_merge(a, b) + UtilsMerge.call(a, b) self.assertEqual(a, {'key': 'value'}) def test_merge_none_deletes_from_base(self): a = {'key': 'value', 'other': 'value'} b = {'other': None} - deep_merge(a, b) + UtilsMerge.call(a, b) self.assertEqual(a, {'key': 'value'}) def test_merge_overrides_value(self): # The value from b is always taken, even when it's a different type a = {'key': 'original'} b = {'key': {'newkey': 'newvalue'}} - deep_merge(a, b) + UtilsMerge.call(a, b) self.assertEqual(a, {'key': {'newkey': 'newvalue'}}) a = {'key': {'anotherkey': 'value'}} b = {'key': 'newvalue'} - deep_merge(a, b) + UtilsMerge.call(a, b) self.assertEqual(a, {'key': 'newvalue'}) def test_deep_merge(self): @@ -101,7 +91,7 @@ def test_deep_merge(self): } } } - deep_merge(a, b) + UtilsMerge.call(a, b) expected = { 'first': { @@ -114,13 +104,3 @@ def test_deep_merge(self): } } self.assertEqual(a, expected) - - -class TimestampTestCase(unittest.TestCase): - - @mock.patch('castle.utils.datetime') - def test_it_should_use_iso_format(self, mock_datetime): - mock_datetime.utcnow.return_value = datetime( - 2018, 1, 2, 3, 4, 5, 678901) - expected = '2018-01-02T03:04:05.678901' - self.assertEqual(timestamp(), expected) diff --git a/castle/test/utils/timestamp_test.py b/castle/test/utils/timestamp_test.py new file mode 100644 index 0000000..299a72e --- /dev/null +++ b/castle/test/utils/timestamp_test.py @@ -0,0 +1,14 @@ +from datetime import datetime + +from castle.test import mock, unittest +from castle.utils.timestamp import UtilsTimestamp + + +class UtilsTimestampTestCase(unittest.TestCase): + + @mock.patch('castle.utils.timestamp.datetime') + def test_it_should_use_iso_format(self, mock_datetime): + mock_datetime.utcnow.return_value = datetime( + 2018, 1, 2, 3, 4, 5, 678901) + expected = '2018-01-02T03:04:05.678901' + self.assertEqual(UtilsTimestamp.call(), expected) diff --git a/castle/utils.py b/castle/utils.py deleted file mode 100644 index 09c3c66..0000000 --- a/castle/utils.py +++ /dev/null @@ -1,34 +0,0 @@ -import copy -from datetime import datetime - - -def clone(dict_object): - return copy.deepcopy(dict_object) - - -def deep_merge(base, extra): - """ - Deeply merge two dictionaries, overriding existing keys in the base. - - :param base: The base dictionary which will be merged into. - :param extra: The dictionary to merge into the base. Keys from this - dictionary will take precedence. - """ - if extra is None: - return - - for key, value in extra.items(): - if value is None: - if key in base: - del base[key] - # If the key represents a dict on both given dicts, merge the sub-dicts - elif isinstance(base.get(key), dict) and isinstance(value, dict): - deep_merge(base[key], value) - else: - # Otherwise, set the key on the base to be the value of the extra. - base[key] = value - - -def timestamp(): - """Return an ISO8601 timestamp representing the current datetime in UTC.""" - return datetime.utcnow().isoformat() diff --git a/castle/utils/__init__.py b/castle/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/castle/utils/clone.py b/castle/utils/clone.py new file mode 100644 index 0000000..8ef115c --- /dev/null +++ b/castle/utils/clone.py @@ -0,0 +1,8 @@ +import copy + + +class UtilsClone(object): + + @staticmethod + def call(dict_object): + return copy.deepcopy(dict_object) diff --git a/castle/utils/merge.py b/castle/utils/merge.py new file mode 100644 index 0000000..2cd0720 --- /dev/null +++ b/castle/utils/merge.py @@ -0,0 +1,24 @@ +class UtilsMerge(object): + + @staticmethod + def call(base, extra): + """ + Deeply merge two dictionaries, overriding existing keys in the base. + + :param base: The base dictionary which will be merged into. + :param extra: The dictionary to merge into the base. Keys from this + dictionary will take precedence. + """ + if extra is None: + return + + for key, value in extra.items(): + if value is None: + if key in base: + del base[key] + # If the key represents a dict on both given dicts, merge the sub-dicts + elif isinstance(base.get(key), dict) and isinstance(value, dict): + __class__.call(base[key], value) + else: + # Otherwise, set the key on the base to be the value of the extra. + base[key] = value diff --git a/castle/utils/timestamp.py b/castle/utils/timestamp.py new file mode 100644 index 0000000..9faf1d3 --- /dev/null +++ b/castle/utils/timestamp.py @@ -0,0 +1,9 @@ +from datetime import datetime + + +class UtilsTimestamp(object): + + @staticmethod + def call(): + """Return an ISO8601 timestamp representing the current datetime in UTC.""" + return datetime.utcnow().isoformat() diff --git a/setup.py b/setup.py index 17b2b88..605b363 100644 --- a/setup.py +++ b/setup.py @@ -33,6 +33,8 @@ 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', ], install_requires=install_requires, tests_require=test_require,