-
Notifications
You must be signed in to change notification settings - Fork 39
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 14803 check api access in discovery mode #74
base: master
Are you sure you want to change the base?
Changes from 6 commits
21edee7
bc279a5
7663264
475719c
7a5fd9b
f8cf58b
1b9d70a
88a3760
080b2ad
3b71bea
125a051
7bfdd1e
e409fae
46855f1
95e4dee
f10650e
15d5c64
961fcb1
75363da
c47824f
ee826c5
3313e58
543b960
db3c697
3af90b6
bd4771e
5eaf72e
5770b07
17757a0
b376cde
0f7968c
c3fb796
c379ad2
081060e
929f5d7
b168865
89be5f4
b509cf6
269ca0c
d08b811
bd1fd48
57daa7b
0c4b706
d87d99e
8cd80ff
ef85e74
ef19a71
33ac8d7
d630fca
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,11 @@ | ||
import os | ||
import json | ||
import singer | ||
import zenpy | ||
from tap_zendesk.streams import STREAMS | ||
from tap_zendesk.http import ZendeskForbiddenError | ||
|
||
LOGGER = singer.get_logger() | ||
|
||
def get_abs_path(path): | ||
return os.path.join(os.path.dirname(os.path.realpath(__file__)), path) | ||
|
@@ -20,12 +24,37 @@ def load_shared_schema_refs(): | |
|
||
return shared_schema_refs | ||
|
||
def discover_streams(client): | ||
def discover_streams(client, config): | ||
streams = [] | ||
error_list = [] | ||
refs = load_shared_schema_refs() | ||
|
||
|
||
for s in STREAMS.values(): | ||
s = s(client) | ||
s = s(client, config) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @prijendev Can you please give understandable variable name instead of 's' There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In existing code they have used 's'. Change in name will reflect lot off changes in code as it is used in many place. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @prijendev Please do the variable name changes. If it means change will reflect lot off changes that's fine There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @prijendev Add Comments to the code There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Added comments to the code. And you can find detailed comments in the |
||
schema = singer.resolve_schema_references(s.load_schema(), refs) | ||
try: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add comments here to explain the code. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added |
||
s.check_access() | ||
except ZendeskForbiddenError as e: | ||
error_list.append(s.name) | ||
except zenpy.lib.exception.APIException as e: | ||
err = json.loads(e.args[0]).get('error') | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @prijendev Since There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Updated |
||
|
||
if isinstance(err, dict): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @prijendev Can you add same comment here as it is added in this PR - https://github.com/singer-io/tap-zendesk/pull/69/files#diff-89887ca52395ce152e9723c5595655edcd41793204060336e88ea9805c274433R142 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Updated |
||
if err.get('message', None) == "You do not have access to this page. Please contact the account owner of this help desk for further help.": | ||
error_list.append(s.name) | ||
elif json.loads(e.args[0]).get('description') == "You are missing the following required scopes: read": | ||
error_list.append(s.name) | ||
else: | ||
raise e | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @prijendev Raise an exception from None. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Raised an exception from None. |
||
|
||
streams.append({'stream': s.name, 'tap_stream_id': s.name, 'schema': schema, 'metadata': s.load_metadata()}) | ||
|
||
if error_list: | ||
streams_name = ", ".join(error_list) | ||
message = "HTTP-error-code: 403, Error: You are missing the following required scopes: read. "\ | ||
f"The account credentials supplied do not have read access for the following stream(s): {streams_name}" | ||
raise ZendeskForbiddenError(message) | ||
|
||
|
||
return streams |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,6 +7,95 @@ | |
LOGGER = singer.get_logger() | ||
|
||
|
||
class ZendeskError(Exception): | ||
def __init__(self, message=None, response=None): | ||
super().__init__(message) | ||
self.message = message | ||
self.response = response | ||
|
||
class ZendeskBackoffError(ZendeskError): | ||
pass | ||
|
||
class ZendeskBadRequestError(ZendeskError): | ||
pass | ||
|
||
class ZendeskUnauthorizedError(ZendeskError): | ||
pass | ||
|
||
class ZendeskForbiddenError(ZendeskError): | ||
pass | ||
|
||
class ZendeskNotFoundError(ZendeskError): | ||
pass | ||
|
||
class ZendeskConflictError(ZendeskError): | ||
pass | ||
|
||
class ZendeskUnprocessableEntityError(ZendeskError): | ||
pass | ||
|
||
class ZendeskRateLimitError(ZendeskBackoffError): | ||
pass | ||
|
||
class ZendeskInternalServerError(ZendeskError): | ||
pass | ||
|
||
class ZendeskNotImplementedError(ZendeskError): | ||
pass | ||
|
||
class ZendeskBadGatewayError(ZendeskError): | ||
pass | ||
|
||
class ZendeskServiceUnavailableError(ZendeskBackoffError): | ||
pass | ||
|
||
ERROR_CODE_EXCEPTION_MAPPING = { | ||
400: { | ||
"raise_exception": ZendeskBadRequestError, | ||
"message": "A validation exception has occurred." | ||
}, | ||
401: { | ||
"raise_exception": ZendeskUnauthorizedError, | ||
"message": "The access token provided is expired, revoked, malformed or invalid for other reasons." | ||
}, | ||
403: { | ||
"raise_exception": ZendeskForbiddenError, | ||
"message": "You are missing the following required scopes: read" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Will it be each time There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes. Because we are calling just GET API for each stream. So, it will return the same message of read scopes. |
||
}, | ||
404: { | ||
"raise_exception": ZendeskNotFoundError, | ||
"message": "There is no help desk configured at this address. This means that the address is available and that you can claim it at http://www.zendesk.com/signup" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same as above if it is not the fixed message for 404 each time then please put a generalized message. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Updated to generalized message. |
||
}, | ||
409: { | ||
"raise_exception": ZendeskConflictError, | ||
"message": "The request does not match our state in some way." | ||
}, | ||
422: { | ||
"raise_exception": ZendeskUnprocessableEntityError, | ||
"message": "The request content itself is not processable by the server." | ||
}, | ||
429: { | ||
"raise_exception": ZendeskRateLimitError, | ||
"message": "The API rate limit for your organisation/application pairing has been exceeded." | ||
}, | ||
500: { | ||
"raise_exception": ZendeskInternalServerError, | ||
"message": "The server encountered an unexpected condition which prevented" \ | ||
" it from fulfilling the request." | ||
}, | ||
501: { | ||
"raise_exception": ZendeskNotImplementedError, | ||
"message": "The server does not support the functionality required to fulfill the request." | ||
}, | ||
502: { | ||
"raise_exception": ZendeskBadGatewayError, | ||
"message": "Server received an invalid response." | ||
}, | ||
503: { | ||
"raise_exception": ZendeskServiceUnavailableError, | ||
"message": "API service is currently unavailable." | ||
} | ||
} | ||
def is_fatal(exception): | ||
status_code = exception.response.status_code | ||
|
||
|
@@ -15,16 +104,36 @@ def is_fatal(exception): | |
LOGGER.info("Caught HTTP 429, retrying request in %s seconds", sleep_time) | ||
sleep(sleep_time) | ||
return False | ||
|
||
elif status_code == 503: | ||
sleep_time = int(exception.response.headers['Retry-After']) | ||
LOGGER.info("Caught HTTP 503, retrying request in %s seconds", sleep_time) | ||
sleep(sleep_time) | ||
return False | ||
return 400 <=status_code < 500 | ||
|
||
def check_status(response): | ||
# Forming a response message for raising custom exception | ||
try: | ||
response_json = response.json() | ||
except Exception: # pylint: disable=broad-except | ||
response_json = {} | ||
if response.status_code != 200: | ||
message = "HTTP-error-code: {}, Error: {}".format( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use f-strings instead of format. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Replaced format with f-string. |
||
response.status_code, | ||
response_json.get("errorMessages", [ERROR_CODE_EXCEPTION_MAPPING.get( | ||
response.status_code, {}).get("message", "Unknown Error")])[0] | ||
) | ||
exc = ERROR_CODE_EXCEPTION_MAPPING.get( | ||
response.status_code, {}).get("raise_exception", ZendeskError) | ||
raise exc(message, response) from None | ||
|
||
@backoff.on_exception(backoff.expo, | ||
requests.exceptions.HTTPError, | ||
ZendeskBackoffError, | ||
max_tries=10, | ||
giveup=is_fatal) | ||
def call_api(url, params, headers): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please add comments here There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added. |
||
response = requests.get(url, params=params, headers=headers) | ||
response.raise_for_status() | ||
check_status(response) | ||
return response | ||
|
||
|
||
|
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -16,6 +16,7 @@ | |||||||||||||||||||||
LOGGER = singer.get_logger() | ||||||||||||||||||||||
KEY_PROPERTIES = ['id'] | ||||||||||||||||||||||
|
||||||||||||||||||||||
START_DATE = datetime.datetime.strftime(datetime.datetime.utcnow() - datetime.timedelta(days=1), "%Y-%m-%dT00:00:00Z") | ||||||||||||||||||||||
CUSTOM_TYPES = { | ||||||||||||||||||||||
'text': 'string', | ||||||||||||||||||||||
'textarea': 'string', | ||||||||||||||||||||||
|
@@ -114,13 +115,33 @@ def load_metadata(self): | |||||||||||||||||||||
def is_selected(self): | ||||||||||||||||||||||
return self.stream is not None | ||||||||||||||||||||||
|
||||||||||||||||||||||
def check_access(self): | ||||||||||||||||||||||
''' | ||||||||||||||||||||||
Check whether permission given to access stream resource or not. | ||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Updated. |
||||||||||||||||||||||
''' | ||||||||||||||||||||||
url = self.endpoint.format(self.config['subdomain']) | ||||||||||||||||||||||
|
||||||||||||||||||||||
headers = { | ||||||||||||||||||||||
'Content-Type': 'application/json', | ||||||||||||||||||||||
'Accept': 'application/json', | ||||||||||||||||||||||
'Authorization': 'Bearer {}'.format(self.config['access_token']) | ||||||||||||||||||||||
} | ||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Replaced format with f-string. |
||||||||||||||||||||||
|
||||||||||||||||||||||
http.call_api(url, params={'page[size]': 1}, headers=headers) | ||||||||||||||||||||||
|
||||||||||||||||||||||
|
||||||||||||||||||||||
def raise_or_log_zenpy_apiexception(schema, stream, e): | ||||||||||||||||||||||
# There are multiple tiers of Zendesk accounts. Some of them have | ||||||||||||||||||||||
# access to `custom_fields` and some do not. This is the specific | ||||||||||||||||||||||
# error that appears to be return from the API call in the event that | ||||||||||||||||||||||
# it doesn't have access. | ||||||||||||||||||||||
if not isinstance(e, zenpy.lib.exception.APIException): | ||||||||||||||||||||||
raise ValueError("Called with a bad exception type") from e | ||||||||||||||||||||||
#If read permission not available in oauth access_token, then it returns below error. | ||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Updated. |
||||||||||||||||||||||
if json.loads(e.args[0]).get('description') == "You are missing the following required scopes: read": | ||||||||||||||||||||||
LOGGER.warning("The account credentials supplied do not have access to `%s` custom fields.", | ||||||||||||||||||||||
stream) | ||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Resolved |
||||||||||||||||||||||
return schema | ||||||||||||||||||||||
if json.loads(e.args[0])['error']['message'] == "You do not have access to this page. Please contact the account owner of this help desk for further help.": | ||||||||||||||||||||||
LOGGER.warning("The account credentials supplied do not have access to `%s` custom fields.", | ||||||||||||||||||||||
stream) | ||||||||||||||||||||||
|
@@ -158,6 +179,8 @@ def sync(self, state): | |||||||||||||||||||||
self.update_bookmark(state, organization.updated_at) | ||||||||||||||||||||||
yield (self.stream, organization) | ||||||||||||||||||||||
|
||||||||||||||||||||||
def check_access(self): | ||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please add comment for this function There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added |
||||||||||||||||||||||
self.client.organizations.incremental(start_time=START_DATE) | ||||||||||||||||||||||
|
||||||||||||||||||||||
class Users(Stream): | ||||||||||||||||||||||
name = "users" | ||||||||||||||||||||||
|
@@ -235,6 +258,8 @@ def sync(self, state): | |||||||||||||||||||||
start = end - datetime.timedelta(seconds=1) | ||||||||||||||||||||||
end = start + datetime.timedelta(seconds=search_window_size) | ||||||||||||||||||||||
|
||||||||||||||||||||||
def check_access(self): | ||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please add a comment for this function There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added |
||||||||||||||||||||||
self.client.search("", updated_after=START_DATE, updated_before='2000-01-02T00:00:00Z', type="user") | ||||||||||||||||||||||
|
||||||||||||||||||||||
class Tickets(Stream): | ||||||||||||||||||||||
name = "tickets" | ||||||||||||||||||||||
|
@@ -338,6 +363,9 @@ def emit_sub_stream_metrics(sub_stream): | |||||||||||||||||||||
emit_sub_stream_metrics(comments_stream) | ||||||||||||||||||||||
singer.write_state(state) | ||||||||||||||||||||||
|
||||||||||||||||||||||
def check_access(self): | ||||||||||||||||||||||
self.client.tickets.incremental(start_time=START_DATE, paginate_by_time=False) | ||||||||||||||||||||||
|
||||||||||||||||||||||
class TicketAudits(Stream): | ||||||||||||||||||||||
name = "ticket_audits" | ||||||||||||||||||||||
replication_method = "INCREMENTAL" | ||||||||||||||||||||||
|
@@ -349,6 +377,9 @@ def sync(self, ticket_id): | |||||||||||||||||||||
self.count += 1 | ||||||||||||||||||||||
yield (self.stream, ticket_audit) | ||||||||||||||||||||||
|
||||||||||||||||||||||
def check_access(self): | ||||||||||||||||||||||
self.client.tickets.audits(ticket=None) | ||||||||||||||||||||||
|
||||||||||||||||||||||
class TicketMetrics(Stream): | ||||||||||||||||||||||
name = "ticket_metrics" | ||||||||||||||||||||||
replication_method = "INCREMENTAL" | ||||||||||||||||||||||
|
@@ -359,6 +390,9 @@ def sync(self, ticket_id): | |||||||||||||||||||||
self.count += 1 | ||||||||||||||||||||||
yield (self.stream, ticket_metric) | ||||||||||||||||||||||
|
||||||||||||||||||||||
def check_access(self): | ||||||||||||||||||||||
self.client.tickets.metrics(ticket=None) | ||||||||||||||||||||||
|
||||||||||||||||||||||
class TicketComments(Stream): | ||||||||||||||||||||||
name = "ticket_comments" | ||||||||||||||||||||||
replication_method = "INCREMENTAL" | ||||||||||||||||||||||
|
@@ -370,6 +404,9 @@ def sync(self, ticket_id): | |||||||||||||||||||||
self.count += 1 | ||||||||||||||||||||||
yield (self.stream, ticket_comment) | ||||||||||||||||||||||
|
||||||||||||||||||||||
def check_access(self): | ||||||||||||||||||||||
self.client.tickets.comments(ticket=None) | ||||||||||||||||||||||
|
||||||||||||||||||||||
class SatisfactionRatings(Stream): | ||||||||||||||||||||||
name = "satisfaction_ratings" | ||||||||||||||||||||||
replication_method = "INCREMENTAL" | ||||||||||||||||||||||
|
@@ -475,6 +512,9 @@ def sync(self, state): | |||||||||||||||||||||
self.update_bookmark(state, form.updated_at) | ||||||||||||||||||||||
yield (self.stream, form) | ||||||||||||||||||||||
|
||||||||||||||||||||||
def check_access(self): | ||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please add a comment for this function There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added |
||||||||||||||||||||||
self.client.ticket_forms() | ||||||||||||||||||||||
|
||||||||||||||||||||||
class GroupMemberships(Stream): | ||||||||||||||||||||||
name = "group_memberships" | ||||||||||||||||||||||
replication_method = "INCREMENTAL" | ||||||||||||||||||||||
|
@@ -512,6 +552,9 @@ def sync(self, state): # pylint: disable=unused-argument | |||||||||||||||||||||
for policy in self.client.sla_policies(): | ||||||||||||||||||||||
yield (self.stream, policy) | ||||||||||||||||||||||
|
||||||||||||||||||||||
def check_access(self): | ||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please add a comment for this function There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added |
||||||||||||||||||||||
self.client.sla_policies() | ||||||||||||||||||||||
|
||||||||||||||||||||||
STREAMS = { | ||||||||||||||||||||||
"tickets": Tickets, | ||||||||||||||||||||||
"groups": Groups, | ||||||||||||||||||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please add comments to the code changes
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added comments