Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ jobs:
source /usr/local/share/virtualenvs/dev_env.sh
# BUG https://jira.talendforge.org/browse/TDL-15395
echo "pylint is skipping the following: $PYLINT_DISABLE_LIST"
pylint tap_jira -d "$PYLINT_DISABLE_LIST,unsupported-assignment-operation,unsupported-membership-test,unsubscriptable-object,dangerous-default-value,too-many-instance-attributes"
pylint tap_jira -d "$PYLINT_DISABLE_LIST,unsupported-assignment-operation,unsupported-membership-test,unsubscriptable-object,dangerous-default-value,too-many-instance-attributes,unspecified-encoding"
- slack/notify-on-failure:
only_for_branches: master

Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Changelog

## [v2.2.0]
* Add support for dev mode [#104](https://github.com/singer-io/tap-jira/pull/104)
## [v2.1.5]
* Skipped the record for out of range date values [#87](https://github.com/singer-io/tap-jira/pull/87)
## [v2.1.4]
Expand Down
4 changes: 2 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@
from setuptools import setup, find_packages

setup(name="tap-jira",
version="2.1.5",
version="2.2.0",
description="Singer.io tap for extracting data from the Jira API",
author="Stitch",
url="http://singer.io",
classifiers=["Programming Language :: Python :: 3 :: Only"],
py_modules=["tap_jira"],
install_requires=[
"singer-python==5.12.1",
"singer-python==5.13.0",
"requests==2.20.0",
"dateparser"
],
Expand Down
5 changes: 3 additions & 2 deletions tap_jira/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
REQUIRED_CONFIG_KEYS_CLOUD = ["start_date",
"user_agent",
"cloud_id",
"access_token",
"refresh_token",
"oauth_client_id",
"oauth_client_secret"]
Expand Down Expand Up @@ -113,10 +112,12 @@ def sync():
@singer.utils.handle_top_exception(LOGGER)
def main():
args = get_args()
if args.dev:
LOGGER.warning("Executing Tap in Dev mode")

jira_config = args.config
# jira client instance
jira_client = Client(jira_config)
jira_client = Client(args.config_path, jira_config, args.dev)

# Setup Context
Context.client = jira_client
Expand Down
25 changes: 23 additions & 2 deletions tap_jira/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import time
import threading
import re
import json
from requests.exceptions import (HTTPError, Timeout)
from requests.auth import HTTPBasicAuth
import requests
Expand Down Expand Up @@ -157,13 +158,14 @@ def get_request_timeout(config):
return request_timeout

class Client():
def __init__(self, config):
def __init__(self, config_path, config, dev_mode = False):
self.is_cloud = 'oauth_client_id' in config.keys()
self.session = requests.Session()
self.next_request_at = datetime.now()
self.user_agent = config.get("user_agent")
self.login_timer = None
self.timeout = get_request_timeout(config)
self.config_path = config_path

# Assign False for cloud Jira instance
self.is_on_prem_instance = False
Expand All @@ -178,10 +180,14 @@ def __init__(self, config):
self.oauth_client_id = config.get('oauth_client_id')
self.oauth_client_secret = config.get('oauth_client_secret')

if dev_mode and not self.access_token:
raise Exception("Access token config property is missing")

# Only appears to be needed once for any 6 hour period. If
# running the tap for more than 6 hours is needed this will
# likely need to be more complicated.
self.refresh_credentials()
if not dev_mode:
self.refresh_credentials()
self.test_credentials_are_authorized()
else:
LOGGER.info("Using Basic Auth API authentication")
Expand Down Expand Up @@ -262,6 +268,8 @@ def refresh_credentials(self):
timeout=self.timeout)
resp.raise_for_status()
self.access_token = resp.json()['access_token']
self.refresh_token = resp.json()['refresh_token']
self._write_config()
except Exception as ex:
error_message = str(ex)
if resp:
Expand All @@ -284,6 +292,19 @@ def test_basic_credentials_are_authorized(self):
# Assign True value to is_on_prem_instance property for on-prem Jira instance
self.is_on_prem_instance = self.request("users","GET","/rest/api/2/serverInfo").get('deploymentType') == "Server"

def _write_config(self):
LOGGER.info("Credentials Refreshed")

# Update config at config_path
with open(self.config_path) as file:
config = json.load(file)

config['refresh_token'] = self.refresh_token
config['access_token'] = self.access_token

with open(self.config_path, 'w') as file:
json.dump(config, file, indent=2)

class Paginator():
def __init__(self, client, page_num=0, order_by=None, items_key="values"):
self.client = client
Expand Down
2 changes: 2 additions & 0 deletions tests/unittests/test_basic_auth_in_discover.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ def __init__(self):
self.properties = False
self.config = {}
self.state = False
self.dev = False
self.config_path = "mock_config.json"

# Mock response
def get_mock_http_response(status_code, content={}):
Expand Down
74 changes: 74 additions & 0 deletions tests/unittests/test_dev_mode.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import json
import os
import unittest
from unittest.mock import patch, MagicMock

import singer

from tap_jira.http import Client


LOGGER = singer.get_logger()


class TestClientDevMode(unittest.TestCase):
"""Test the dev mode functionality."""

def setUp(self):
"""Creates a sample config for test execution"""
# Data to be written
self.mock_config = {
"oauth_client_secret": "sample_client_secret",
"user_agent": "test_user_agent",
"oauth_client_id": "sample_client_id",
"access_token": "sample_access_token",
"cloud_id": "1234567890",
"refresh_token": "sample_refresh_token",
"start_date": "2017-12-04T19:19:32Z",
"request_timeout": 300,
"groups": "jira-administrators, site-admins, jira-software-users",
}

self.tmp_config_filename = "sample_jira_config.json"

# Serializing json
json_object = json.dumps(self.mock_config, indent=4)
# Writing to sample_quickbooks_config.json
with open(self.tmp_config_filename, "w") as outfile:
outfile.write(json_object)

def tearDown(self):
"""Deletes the sample config"""
if os.path.isfile(self.tmp_config_filename):
os.remove(self.tmp_config_filename)

@patch("tap_jira.http.Client.request", return_value=MagicMock(status_code=200))
@patch("requests.Session.post", return_value=MagicMock(status_code=200))
@patch("tap_jira.http.Client._write_config")
def test_client_with_dev_mode(
self, mock_write_config, mock_post_request, mock_request
):
"""Checks the dev mode implementation and verifies write config functionality is
not called"""
Client(
config_path=self.tmp_config_filename, config=self.mock_config, dev_mode=True
)

# _write_config function should never be called as it will update the config
self.assertEqual(mock_write_config.call_count, 0)

@patch("tap_jira.http.Client.request", return_value=MagicMock(status_code=200))
@patch("requests.Session.post", side_effect=Exception())
def test_client_dev_mode_missing_access_token(
self, mock_post_request, mock_request
):
"""Exception should be raised if missing access token"""

del self.mock_config["access_token"]

with self.assertRaises(Exception):
Client(
config_path=self.tmp_config_filename,
config=self.mock_config,
dev_mode=True,
)
52 changes: 39 additions & 13 deletions tests/unittests/test_error_handling.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,9 @@ def test_request_with_handling_for_400_exceptin_handling(self,mock_send):
try:
tap_stream_id = "tap_jira"
mock_config = {"username":"mock_username","password":"mock_password","base_url": "mock_base_url"}
mock_client = http.Client(mock_config)
mock_config_path = "mock_config.json"
mock_dev_mode = False
mock_client = http.Client(mock_config_path, mock_config, mock_dev_mode)
mock_client.request(tap_stream_id)
except http.JiraBadRequestError as e:
expected_error_message = "HTTP-error-code: 400, Error: A validation exception has occurred."
Expand All @@ -85,7 +87,9 @@ def test_request_with_handling_for_401_exceptin_handling(self,mock_send):
try:
tap_stream_id = "tap_jira"
mock_config = {"username":"mock_username","password":"mock_password","base_url": "mock_base_url"}
mock_client = http.Client(mock_config)
mock_config_path = "mock_config.json"
mock_dev_mode = False
mock_client = http.Client(mock_config_path, mock_config, mock_dev_mode)
mock_client.request(tap_stream_id)
except http.JiraUnauthorizedError as e:
expected_error_message = "HTTP-error-code: 401, Error: Invalid authorization credentials."
Expand All @@ -98,7 +102,9 @@ def test_request_with_handling_for_403_exceptin_handling(self,mock_send):
try:
tap_stream_id = "tap_jira"
mock_config = {"username":"mock_username","password":"mock_password","base_url": "mock_base_url"}
mock_client = http.Client(mock_config)
mock_config_path = "mock_config.json"
mock_dev_mode = False
mock_client = http.Client(mock_config_path, mock_config, mock_dev_mode)
mock_client.request(tap_stream_id)
except http.JiraForbiddenError as e:
expected_error_message = "HTTP-error-code: 403, Error: User does not have permission to access the resource."
Expand All @@ -112,7 +118,9 @@ def test_request_with_handling_for_404_exceptin_handling(self,mock_send):
try:
tap_stream_id = "tap_jira"
mock_config = {"username":"mock_username","password":"mock_password","base_url": "mock_base_url"}
mock_client = http.Client(mock_config)
mock_config_path = "mock_config.json"
mock_dev_mode = False
mock_client = http.Client(mock_config_path, mock_config, mock_dev_mode)
mock_client.request(tap_stream_id)
except http.JiraNotFoundError as e:
expected_error_message = "HTTP-error-code: 404, Error: The resource you have specified cannot be found."
Expand All @@ -124,7 +132,9 @@ def test_request_with_handling_for_409_exceptin_handling(self,mock_send):
try:
tap_stream_id = "tap_jira"
mock_config = {"username":"mock_username","password":"mock_password","base_url": "mock_base_url"}
mock_client = http.Client(mock_config)
mock_config_path = "mock_config.json"
mock_dev_mode = False
mock_client = http.Client(mock_config_path, mock_config, mock_dev_mode)
mock_client.request(tap_stream_id)
except http.JiraConflictError as e:
expected_error_message = "HTTP-error-code: 409, Error: The request does not match our state in some way."
Expand All @@ -136,7 +146,9 @@ def test_request_with_handling_for_429_exceptin_handling(self,mock_send):
try:
tap_stream_id = "tap_jira"
mock_config = {"username":"mock_username","password":"mock_password","base_url": "mock_base_url"}
mock_client = http.Client(mock_config)
mock_config_path = "mock_config.json"
mock_dev_mode = False
mock_client = http.Client(mock_config_path, mock_config, mock_dev_mode)
mock_client.request(tap_stream_id)
except http.JiraRateLimitError as e:
expected_error_message = "HTTP-error-code: 429, Error: The API rate limit for your organisation/application pairing has been exceeded."
Expand All @@ -149,7 +161,9 @@ def test_request_with_handling_for_449_exceptin_handling(self,mock_send):
try:
tap_stream_id = "tap_jira"
mock_config = {"username":"mock_username","password":"mock_password","base_url": "mock_base_url"}
mock_client = http.Client(mock_config)
mock_config_path = "mock_config.json"
mock_dev_mode = False
mock_client = http.Client(mock_config_path, mock_config, mock_dev_mode)
mock_client.request(tap_stream_id)
except http.JiraSubRequestFailedError as e:
expected_error_message = "HTTP-error-code: 449, Error: The API was unable to process every part of the request."
Expand All @@ -162,7 +176,9 @@ def test_request_with_handling_for_500_exceptin_handling(self,mock_send):
try:
tap_stream_id = "tap_jira"
mock_config = {"username":"mock_username","password":"mock_password","base_url": "mock_base_url"}
mock_client = http.Client(mock_config)
mock_config_path = "mock_config.json"
mock_dev_mode = False
mock_client = http.Client(mock_config_path, mock_config, mock_dev_mode)
mock_client.request(tap_stream_id)
except http.JiraInternalServerError as e:
expected_error_message = "HTTP-error-code: 500, Error: The server encountered an unexpected condition which prevented it from fulfilling the request."
Expand All @@ -175,7 +191,9 @@ def test_request_with_handling_for_501_exceptin_handling(self,mock_send):
try:
tap_stream_id = "tap_jira"
mock_config = {"username":"mock_username","password":"mock_password","base_url": "mock_base_url"}
mock_client = http.Client(mock_config)
mock_config_path = "mock_config.json"
mock_dev_mode = False
mock_client = http.Client(mock_config_path, mock_config, mock_dev_mode)
mock_client.request(tap_stream_id)
except http.JiraNotImplementedError as e:
expected_error_message = "HTTP-error-code: 501, Error: The server does not support the functionality required to fulfill the request."
Expand All @@ -188,7 +206,9 @@ def test_request_with_handling_for_502_exceptin_handling(self,mock_send):
try:
tap_stream_id = "tap_jira"
mock_config = {"username":"mock_username","password":"mock_password","base_url": "mock_base_url"}
mock_client = http.Client(mock_config)
mock_config_path = "mock_config.json"
mock_dev_mode = False
mock_client = http.Client(mock_config_path, mock_config, mock_dev_mode)
mock_client.request(tap_stream_id)
except http.JiraBadGatewayError as e:
expected_error_message = "HTTP-error-code: 502, Error: Server received an invalid response."
Expand All @@ -201,7 +221,9 @@ def test_request_with_handling_for_503_exceptin_handling(self,mock_send):
try:
tap_stream_id = "tap_jira"
mock_config = {"username":"mock_username","password":"mock_password","base_url": "mock_base_url"}
mock_client = http.Client(mock_config)
mock_config_path = "mock_config.json"
mock_dev_mode = False
mock_client = http.Client(mock_config_path, mock_config, mock_dev_mode)
mock_client.request(tap_stream_id)
except http.JiraServiceUnavailableError as e:
expected_error_message = "HTTP-error-code: 503, Error: API service is currently unavailable."
Expand All @@ -214,7 +236,9 @@ def test_request_with_handling_for_504_exceptin_handling(self,mock_send):
try:
tap_stream_id = "tap_jira"
mock_config = {"username":"mock_username","password":"mock_password","base_url": "mock_base_url"}
mock_client = http.Client(mock_config)
mock_config_path = "mock_config.json"
mock_dev_mode = False
mock_client = http.Client(mock_config_path, mock_config, mock_dev_mode)
mock_client.request(tap_stream_id)
except http.JiraGatewayTimeoutError as e:
expected_error_message = "HTTP-error-code: 504, Error: API service time out, please check Jira server."
Expand All @@ -227,7 +251,9 @@ def test_request_with_handling_for_505_exceptin_handling(self,mock_send):
try:
tap_stream_id = "tap_jira"
mock_config = {"username":"mock_username","password":"mock_password","base_url": "mock_base_url"}
mock_client = http.Client(mock_config)
mock_config_path = "mock_config.json"
mock_dev_mode = False
mock_client = http.Client(mock_config_path, mock_config, mock_dev_mode)
mock_client.request(tap_stream_id)
except http.JiraError as e:
expected_error_message = "HTTP-error-code: 505, Error: Unknown Error"
Expand Down
Loading