Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[TDL-15656] Added Support for dev mode #41

Open
wants to merge 24 commits into
base: master
Choose a base branch
from

Conversation

somethingmorerelevant
Copy link
Member

@somethingmorerelevant somethingmorerelevant commented Sep 6, 2022

Description of change

Add support for -dev / --dev cli param,

  • prevent refresh_token expiry while running in dev-mode
  • stores the current access_token and expiry in config
  • exits tap if existing access_token is not available
  • Added util to write back to config file
  • Added Unit tests for dev-mode
  • Updated Singer-Python Dependancy to 5.13 (dev mode support)
  • Updated CirlceCi Config yaml

Manual QA steps

  • First run the tap normally using the standard params
  • Check the config file for added keys access_token & expires_in
  • Run the tap using the --dev or '-dev' param
  • Check the loglife for dev mode related logs

Risks (Low)

P.R Dependant on #158

  • Tap will exit for existing connections when run in dev mode if access_token is not available

Rollback steps

  • revert this branch

@somethingmorerelevant somethingmorerelevant marked this pull request as ready for review September 13, 2022 03:10
tap_eloqua/utils.py Outdated Show resolved Hide resolved
tap_eloqua/__init__.py Outdated Show resolved Hide resolved
tap_eloqua/client.py Outdated Show resolved Hide resolved
tap_eloqua/client.py Outdated Show resolved Hide resolved
tap_eloqua/client.py Outdated Show resolved Hide resolved
tap_eloqua/sync.py Outdated Show resolved Hide resolved
tap_eloqua/client.py Outdated Show resolved Hide resolved
Co-authored-by: Rushikesh Todkar <98420315+RushiT0122@users.noreply.github.com>
tap_eloqua/client.py Outdated Show resolved Hide resolved
@RushiT0122 RushiT0122 self-requested a review January 23, 2023 09:53
Comment on lines +49 to +50
with open(self.tmp_config_path, "w") as ff:
json.dump(self.base_config, ff)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will strongly suggest deleting the temp config once the unit test is executed. If we don't delete it, I think it will keep on accumulating the temp configs (as the everyday job runs) within circleci server.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agreed, but as the cci runner is temporary provisioned and the files created during testing are not preserved.
it will be autodeleted

Server5xxError,
max_tries=5,
factor=2)
@backoff.on_exception(backoff.expo,Server5xxError,5,factor=2)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
@backoff.on_exception(backoff.expo,Server5xxError,5,factor=2)
@backoff.on_exception(backoff.expo,
Server5xxError,
max_tries=5,
factor=2)

Comment on lines 44 to 47
except KeyError as err:
raise Exception("Unable to locate key in config") from err
if not self.__access_token or self.__expires < now():
raise Exception("Access Token in config is expired, unable to authenticate in dev mode")

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use either ex or e as a convention for exception.

Suggested change
except KeyError as err:
raise Exception("Unable to locate key in config") from err
if not self.__access_token or self.__expires < now():
raise Exception("Access Token in config is expired, unable to authenticate in dev mode")
except KeyError as ex:
raise Exception("Unable to locate key in config") from ex
if not self.__access_token or self.__expires < now():
raise Exception("Access Token in config is expired, unable to authenticate in dev mode")

@@ -135,3 +128,4 @@ def get(self, path, **kwargs):

def post(self, path, **kwargs):
return self.request('POST', path=path, **kwargs)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove this extra line.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

expires_seconds = data['expires_in'] - 10 # pad by 10 seconds
self.__expires = datetime.utcnow() + timedelta(seconds=expires_seconds)
if not self.dev_mode:
update_config_keys = {"refresh_token":self.__refresh_token,"access_token":self.__access_token,"expires_in": strftime(self.__expires)}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reduce the statement length.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
update_config_keys = {"refresh_token":self.__refresh_token,"access_token":self.__access_token,"expires_in": strftime(self.__expires)}
update_config_keys = {"refresh_token": self.__refresh_token,
"access_token": self.__access_token,
"expires_in": strftime(self.__expires)}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

@@ -377,12 +377,12 @@ def sync_activity_stream(client,
activity_type):
finished = False
sync_start = pendulum.now('UTC')
end_date = sync_start
end_date, last_sync_date = sync_start, None
Copy link

@RushiT0122 RushiT0122 Jan 23, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these changes in sync.py dev-mode related? If not can we keep it separate from dev-mode changes.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And if these are required changes then update the description on this PR.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@RushiT0122
These are not dev mode related changes, but if you see the code from Line:380 - Line:403
last_sync_date was declared on Line:385 in the try statement, but accessed outside its declared scope in the except block, hence it was introduced here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm pretty sure this is a problem in other languages, but not python

>>> for i in range(3):
...   try:
...     print("About to initialize `my_variable`")
...     my_variable = i
...     print(f"my_variable: {my_variable}")
...     raise RuntimeError(f"Problem occured with {my_variable}")
...   except Exception as err:
...     print(f"Caught {type(err)} with message {str(err)}")
...     print(f"Current value of my_variable {my_variable}")
...
About to initialize `my_variable`
my_variable: 0
Caught <class 'RuntimeError'> with message Problem occured with 0
Current value of my_variable 0
About to initialize `my_variable`
my_variable: 1
Caught <class 'RuntimeError'> with message Problem occured with 1
Current value of my_variable 1
About to initialize `my_variable`
my_variable: 2
Caught <class 'RuntimeError'> with message Problem occured with 2
Current value of my_variable 2

LOGGER = get_logger()


def write_config(config_path,data :Dict) -> Dict:
Copy link

@RushiT0122 RushiT0122 Jan 23, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please check this comment on tap-deputy: singer-io/tap-deputy#3 (comment)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

made required changes

Comment on lines 67 to 72
try:
client = EloquaClient(config_path=self.tmp_config_path,dev_mode=True,config=self.base_config)

client.get_access_token()
except Exception as error:
self.assertEqual(str(error), "Unable to locate key in config")

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use with self.assertRaises() unittest construct here.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't use assertRaises here, because we can only check the "type" of the exception with it.
But in this case, all exceptions are raised with "Exception" class and I want to validate the message-string that is raised with it.

self.assertEqual(str(error), "Unable to locate key in config")

@patch("requests.Session.post", side_effect=mocked_auth_post)
def test_client_valid_token(self, mocked_auth_post):

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
def test_client_valid_token(self, mocked_auth_post):
def test_client_valid_access_token(self, mocked_auth_post):

client.get_access_token()

@patch("requests.Session.post", side_effect=mocked_auth_post)
def test_client_invalid_token(self, mocked_auth_post):

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
def test_client_invalid_token(self, mocked_auth_post):
def test_client_invalid_access_token(self, mocked_auth_post):

Comment on lines 101 to 105
try:
client = EloquaClient(config_path=self.tmp_config_path,dev_mode=True,config=config_sample)
client.get_access_token()
except Exception as error:
self.assertEqual(str(error), "Access Token in config is expired, unable to authenticate in dev mode")

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use with self.assertRaises() unittest construct here.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't use assertRaises here, because we can only check the "type" of the exception with it.
But in this case, all exceptions are raised with "Exception" class and I want to validate the message-string that is raised with it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Docs for assertRaises. These docs show how you can use a context manager with assertRaises. And they show that the exception object is a property on the manager, which means we can do exactly what you want to do

Here's how you use it

$ python3
Python 3.10.9 (main, Dec 15 2022, 18:25:35) [Clang 14.0.0 (clang-1400.0.29.202)] on darwin
Type "help", "copyright", "credits" or "license" for more information.

# start setup

>>> import unittest
>>> class some_test(unittest.TestCase):
...   pass
...
>>> my_test = some_test()


# Prove assertRaises works

>>> with my_test.assertRaises(Exception):
...   1+1
...
2
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/local/Cellar/python@3.10/3.10.9/Frameworks/Python.framework/Versions/3.10/lib/python3.10/unittest/case.py", line 226, in __exit__
    self._raiseFailure("{} not raised".format(exc_name))
  File "/usr/local/Cellar/python@3.10/3.10.9/Frameworks/Python.framework/Versions/3.10/lib/python3.10/unittest/case.py", line 163, in _raiseFailure
    raise self.test_case.failureException(msg)
AssertionError: Exception not raised


# Prove the test can pass

>>> with my_test.assertRaises(Exception) as context_manager:
...   raise Exception("this is a test")
...


# Show we have the exception

>>> actual_exception = context_manager.exception
>>> actual_exception
Exception('this is a test')
>>> str(actual_exception)
'this is a test'

self.assertEqual(True, "access_token" in config)
self.assertEqual(True, "refresh_token" in config)

def test_devmode_accesstoken_absent(self, *args, **kwargs):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like *args and **kwargs is unused

Copy link
Contributor

@luandy64 luandy64 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall, there are a lot of little changes to make. Functionally, the code seems fine and a lot like other dev-mode implementations.

But because code is written once and read over and over, I think it's very important that we think about cleaning up the style once the logic is complete. It's nice to reference any of the style guides out on the internet (they all basically say the same thing).

name: 'pylint'
command: |
source /usr/local/share/virtualenvs/tap-eloqua/bin/activate
pylint tap_eloqua -d C,R,W
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we use the standard list of disables?

@@ -377,12 +377,12 @@ def sync_activity_stream(client,
activity_type):
finished = False
sync_start = pendulum.now('UTC')
end_date = sync_start
end_date, last_sync_date = sync_start, None
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm pretty sure this is a problem in other languages, but not python

>>> for i in range(3):
...   try:
...     print("About to initialize `my_variable`")
...     my_variable = i
...     print(f"my_variable: {my_variable}")
...     raise RuntimeError(f"Problem occured with {my_variable}")
...   except Exception as err:
...     print(f"Caught {type(err)} with message {str(err)}")
...     print(f"Current value of my_variable {my_variable}")
...
About to initialize `my_variable`
my_variable: 0
Caught <class 'RuntimeError'> with message Problem occured with 0
Current value of my_variable 0
About to initialize `my_variable`
my_variable: 1
Caught <class 'RuntimeError'> with message Problem occured with 1
Current value of my_variable 1
About to initialize `my_variable`
my_variable: 2
Caught <class 'RuntimeError'> with message Problem occured with 2
Current value of my_variable 2

@@ -0,0 +1,18 @@
from typing import Dict
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is unused

@patch("requests.Session.post", side_effect=mocked_auth_post)
def test_client_without_dev_mode(self, mocked_auth_post):
"""checks if the client can write refresh token and expiry to
config."""
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this line wrapped if line 23 isn't?

Comment on lines +62 to +64
self.assertEqual(True, "expires_in" in config)
self.assertEqual(True, "access_token" in config)
self.assertEqual(True, "refresh_token" in config)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there are 2 better options here:

  1. "expires_in" in config is a boolean. So these are equivalent
            self.assertEqual(True, "expires_in" in config)
            self.assert("expires_in" in config)
  1. https://docs.python.org/3/library/unittest.html#unittest.TestCase.assertIn

if self.dev_mode:
try:
self.__access_token = self.config['access_token']
self.__expires=strptime_to_utc(self.config['expires_in'])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
self.__expires=strptime_to_utc(self.config['expires_in'])
self.__expires = strptime_to_utc(self.config['expires_in'])

Comment on lines +86 to +88
"refresh_token":self.__refresh_token,
"access_token":self.__access_token,
"expires_in": strftime(self.__expires)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"refresh_token":self.__refresh_token,
"access_token":self.__access_token,
"expires_in": strftime(self.__expires)
"refresh_token": self.__refresh_token,
"access_token": self.__access_token,
"expires_in": strftime(self.__expires)

"access_token":self.__access_token,
"expires_in": strftime(self.__expires)
}
self.config = write_config(self.__config_path,update_config_keys)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
self.config = write_config(self.__config_path,update_config_keys)
self.config = write_config(self.__config_path, update_config_keys)

data = self.request('GET',
url='https://login.eloqua.com/id',
endpoint='base_url')
data = self.request('GET',url='https://login.eloqua.com/id',endpoint='base_url')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
data = self.request('GET',url='https://login.eloqua.com/id',endpoint='base_url')
data = self.request('GET', url='https://login.eloqua.com/id', endpoint='base_url')

json.dump(self.base_config, ff)


client = EloquaClient(config_path=self.tmp_config_path,dev_mode=False,config=self.base_config)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
client = EloquaClient(config_path=self.tmp_config_path,dev_mode=False,config=self.base_config)
client = EloquaClient(config_path=self.tmp_config_path, dev_mode=False, config=self.base_config)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants