class Api(object): """ Base class for API. """ subdomain = None def __init__(self, subdomain, session, endpoint, object_type, timeout, ratelimit): self.subdomain = subdomain self.session = session self.timeout = timeout self.ratelimit = ratelimit self.endpoint = endpoint self.object_type = object_type self.protocol = 'https' self.version = 'v2' self.object_manager = ObjectManager(self) self.callsafety = { 'lastcalltime': None, 'lastlimitremaining': None } def _ratelimit(self, http_method, url, **kwargs): def time_since_last_call(): if self.callsafety['lastcalltime'] is not None: return int(time() - self.callsafety['lastcalltime']) else: return None lastlimitremaining = self.callsafety['lastlimitremaining'] if time_since_last_call() is None or time_since_last_call() >= 10 or lastlimitremaining >= self.ratelimit: response = http_method(url, **kwargs) else: # We hit our limit floor and aren't quite at 10 seconds yet.. log.warn( "Safety Limit Reached of %s remaining calls and time since last call is under 10 seconds" % self.ratelimit) while time_since_last_call() < 10: remaining_sleep = int(10 - time_since_last_call()) log.debug(" -> sleeping: %s more seconds" % remaining_sleep) sleep(1) response = http_method(url, **kwargs) self.callsafety['lastcalltime'] = time() if 'X-Rate-Limit-Remaining' in response.headers: self.callsafety['lastlimitremaining'] = int(response.headers['X-Rate-Limit-Remaining']) else: self.callsafety['lastlimitremaining'] = 0 return response def post(self, url, payload, data=None): log.debug("POST: %s - %s" % (url, str(payload))) headers = None if data: headers = {'Content-Type': 'application/octet-stream'} response = self.session.post(url, json=serialize(payload), data=data, headers=headers, timeout=self.timeout) self._check_and_cache_response(response) return self._build_response(response.json()) def _put(self, url, payload): return self._call_api(self.session.put, url, json=serialize(payload), timeout=self.timeout) def _delete(self, url, payload=None): return self._call_api(self.session.delete, url, json=payload, timeout=self.timeout) def _get(self, url): return self._call_api(self.session.get, url, timeout=self.timeout) def _call_api(self, http_method, url, **kwargs): log.debug("{}: {} - {}".format(http_method.__name__.upper(), url, kwargs)) if self.ratelimit is not None: # This path indicates we're taking a proactive approach to not hit the rate limit response = self._ratelimit(http_method=http_method, url=url, **kwargs) else: response = http_method(url, **kwargs) # If we are being rate-limited, wait the required period before trying again. while 'retry-after' in response.headers and int(response.headers['retry-after']) > 0: retry_after_seconds = int(response.headers['retry-after']) log.warn( "Waiting for requested retry-after period: %s seconds" % retry_after_seconds) while retry_after_seconds > 0: retry_after_seconds -= 1 log.debug(" -> sleeping: %s more seconds" % retry_after_seconds) sleep(1) response = http_method(url, **kwargs) if self.ratelimit is not None: self.callsafety['lastcalltime'] = time() self.callsafety['lastlimitremaining'] = int(response.headers['X-Rate-Limit-Remaining']) return self._check_and_cache_response(response) def _get_items(self, endpoint, object_type, *args, **kwargs): sideload = 'sideload' not in kwargs or ('sideload' in kwargs and kwargs['sideload']) # If an ID is present a single object has been requested if 'id' in kwargs: return self._get_item(kwargs['id'], endpoint, object_type, sideload) if 'ids' in kwargs: cached_objects = [] # Check to see if we have all objects in the cache. # If we are missing even one we need to request them all again. for _id in kwargs['ids']: obj = self.object_manager.query_cache(object_type, _id) if obj: cached_objects.append(obj) else: return self._get_paginated(endpoint, object_type, *args, **kwargs) return cached_objects return self._get_paginated(endpoint, object_type, *args, **kwargs) def _get_item(self, _id, endpoint, object_type, sideload=True, skip_cache=False): if not skip_cache: # Check if we already have this item in the cache item = self.object_manager.query_cache(object_type, _id) if item: return item _json = self._query(endpoint=endpoint(id=_id, sideload=sideload)) # If the result is paginated return a generator if 'next_page' in _json: return ResultGenerator(self, object_type, _json) # Annoyingly, tags is always plural. if 'tags' in _json: return self.object_manager.object_from_json(object_type, _json[object_type + 's']) else: return self.object_manager.object_from_json(object_type, _json[object_type]) def _get_paginated(self, endpoint, object_type, *args, **kwargs): _json = self._query(endpoint=endpoint(*args, **kwargs)) return ResultGenerator(self, object_type, _json) def _query(self, endpoint): response = self._get(self._get_url(endpoint=endpoint)) return response.json() def _build_response(self, response_json): # When updating and deleting API objects various responses can be returned # We can figure out what we have by the keys in the returned JSON if 'ticket' and 'audit' in response_json: return self.object_manager.object_from_json('ticket_audit', response_json) elif 'tags' in response_json: return response_json['tags'] known_objects = ('ticket', 'user', 'job_status', 'group', 'satisfaction_rating', 'request', 'organization', 'organization_membership', 'upload', 'result') for object_type in known_objects: if object_type in response_json: return self.object_manager.object_from_json(object_type, response_json[object_type]) raise ZenpyException("Unknown Response: " + str(response_json)) def _check_and_cache_response(self, response): if response.status_code > 299 or response.status_code < 200: log.debug("Received response code [%s] - headers: %s" % (response.status_code, str(response.headers))) # If it's just a RecordNotFound error raise the right exception, # otherwise try and get a nice error message. if 'application/json' in response.headers['content-type']: try: _json = response.json() if 'error' in _json and _json['error'] == 'RecordNotFound': raise RecordNotFoundException(json.dumps(_json)) else: raise APIException(json.dumps(_json)) except ValueError: pass # No can do, just raise the correct Exception. response.raise_for_status() else: try: self.object_manager.update_caches(response.json()) except ValueError: pass return response def _object_from_json(self, object_type, object_json): return self.object_manager.object_from_json(object_type, object_json) def _query_cache(self, object_type, _id): return self.object_manager.query_cache(object_type, _id) def _get_url(self, endpoint=''): return "%(protocol)s://%(subdomain)s.zendesk.com/api/%(version)s/" % self.__dict__ + endpoint def __call__(self, *args, **kwargs): """ Retrieve API objects. If called with no arguments returns a ResultGenerator of all retrievable items. Alternatively, can be called with an id to only return that item. """ return self._get_items(self.endpoint, self.object_type, *args, **kwargs) def _get_user(self, _id): return self._get_item(_id, endpoint=Endpoint.users, object_type='user', sideload=True) def _get_users(self, _ids): return self._get_items(endpoint=Endpoint.users, object_type='user', ids=_ids) def _get_comment(self, _id): return self._get_item(_id, endpoint=Endpoint.tickets.comments, object_type='comment', sideload=True) def _get_organization(self, _id): return self._get_item(_id, endpoint=Endpoint.organizations, object_type='organization', sideload=True) def _get_group(self, _id): return self._get_item(_id, endpoint=Endpoint.groups, object_type='group', sideload=True) def _get_brand(self, _id): return self._get_item(_id, endpoint=Endpoint.brands, object_type='brand', sideload=True) def _get_ticket(self, _id, skip_cache=False): return self._get_item(_id, endpoint=Endpoint.tickets, object_type='ticket', sideload=False, skip_cache=skip_cache) def _get_actions(self, actions): for action in actions: yield self._object_from_json('action', action) def _get_events(self, events): for event in events: yield self._object_from_json(event['type'].lower(), event) def _get_via(self, via): return self._object_from_json('via', via) def _get_source(self, source): return self._object_from_json('source', source) def _get_attachments(self, attachments): for attachment in attachments: yield self._object_from_json('attachment', attachment) def _get_thumbnails(self, thumbnails): for thumbnail in thumbnails: yield self._object_from_json('thumbnail', thumbnail) def _get_satisfaction_rating(self, satisfaction_rating): return self._object_from_json('satisfaction_rating', satisfaction_rating) def _get_sharing_agreements(self, sharing_agreement_ids): sharing_agreements = [] for _id in sharing_agreement_ids: sharing_agreement = self._get_item(_id, Endpoint.sharing_agreements, 'sharing_agreement') if sharing_agreement: sharing_agreements.append(sharing_agreement) return sharing_agreements def _get_ticket_metric_item(self, metric_item): return self._object_from_json('ticket_metric_item', metric_item) def _get_metadata(self, metadata): return self._object_from_json('metadata', metadata) def _get_system(self, system): return self._object_from_json('system', system) def _get_problem(self, problem_id): return self._get_item(problem_id, Endpoint.tickets, 'ticket') # This will be deprecated soon - https://developer.zendesk.com/rest_api/docs/web-portal/forums def _get_forum(self, forum_id): return forum_id def _get_user_fields(self, user_fields): return user_fields def _get_organization_fields(self, organization_fields): return organization_fields # TODO implement this with Enterprise def _get_custom_fields(self, custom_fields): return custom_fields # This is ticket fields, hopefully it doesn't conflict with another field type def _get_fields(self, fields): return fields def _get_upload(self, upload): return self._object_from_json('upload', upload) def _get_attachment(self, attachment): return self._object_from_json('attachment', attachment) def _get_child_events(self, child_events): return child_events
class BaseApi(object): """ Base class for API. """ subdomain = None def __init__(self, subdomain, session): self.subdomain = subdomain self.protocol = 'https' self.version = 'v2' self.object_manager = ObjectManager(self) self.session = session def _post(self, url, payload): log.debug("POST: %s - %s" % (url, str(payload))) payload = json.loads(json.dumps(payload, cls=ApiObjectEncoder)) response = self.session.post(url, json=payload) self._check_and_cache_response(response) return self._build_response(response.json()) def _put(self, url, payload): log.debug("PUT: " + url) payload = json.loads(json.dumps(payload, cls=ApiObjectEncoder)) response = self.session.put(url, json=payload) self._check_and_cache_response(response) return self._build_response(response.json()) def _delete(self, url, payload=None): log.debug("DELETE: " + url) if payload: response = self.session.delete(url, json=payload) else: response = self.session.delete(url) return self._check_and_cache_response(response) def _get(self, url, stream=False): log.debug("GET: " + url) response = self.session.get(url, stream=stream) # If we are being rate-limited, wait the required period before trying again. while 'retry-after' in response.headers and int( response.headers['retry-after']) > 0: retry_after_seconds = int(response.headers['retry-after']) log.warn( "APIRateLimitExceeded - sleeping for requested retry-after period: %s seconds" % retry_after_seconds) while retry_after_seconds > 0: retry_after_seconds -= 1 log.debug("APIRateLimitExceeded - sleeping: %s more seconds" % retry_after_seconds) sleep(1) response = self.session.get(url, stream=stream) return self._check_and_cache_response(response) def _get_items(self, endpoint, object_type, *args, **kwargs): sideload = 'sideload' not in kwargs or ('sideload' in kwargs and kwargs['sideload']) # If an ID is present a single object has been requested if 'id' in kwargs: return self._get_item(kwargs['id'], endpoint, object_type, sideload) if 'ids' in kwargs: cached_objects = [] # Check to see if we have all objects in the cache. # If we are missing even one we need to request them all again. for _id in kwargs['ids']: obj = self.object_manager.query_cache(object_type, _id) if obj: cached_objects.append(obj) else: return self._get_paginated(endpoint, object_type, *args, **kwargs) return cached_objects return self._get_paginated(endpoint, object_type, *args, **kwargs) def _get_item(self, _id, endpoint, object_type, sideload=True, skip_cache=False): if not skip_cache: # Check if we already have this item in the cache item = self.object_manager.query_cache(object_type, _id) if item: return item _json = self._query(endpoint=endpoint(id=_id, sideload=sideload)) # If the result is paginated return a generator if 'next_page' in _json: return ResultGenerator(self, object_type, _json) # Annoyingly, tags is always plural. if 'tags' in _json: return self.object_manager.object_from_json( object_type, _json[object_type + 's']) else: return self.object_manager.object_from_json( object_type, _json[object_type]) def _get_paginated(self, endpoint, object_type, *args, **kwargs): _json = self._query(endpoint=endpoint(*args, **kwargs)) return ResultGenerator(self, object_type, _json) def _query(self, endpoint): response = self._get(self._get_url(endpoint=endpoint)) return response.json() def _build_response(self, response_json): # When updating and deleting API objects various responses can be returned # We can figure out what we have by the keys in the returned JSON if 'ticket' and 'audit' in response_json: return self.object_manager.object_from_json( 'ticket_audit', response_json) elif 'tags' in response_json: return response_json['tags'] for object_type in ('ticket', 'user', 'job_status', 'group', 'satisfaction_rating', 'request', 'organization'): if object_type in response_json: return self.object_manager.object_from_json( object_type, response_json[object_type]) raise ZenpyException("Unknown Response: " + str(response_json)) def _check_and_cache_response(self, response): if response.status_code > 299 or response.status_code < 200: # If it's just a RecordNotFound error raise the right exception, # otherwise try and get a nice error message. if 'application/json' in response.headers['content-type']: try: _json = response.json() if 'error' in _json and _json['error'] == 'RecordNotFound': raise RecordNotFoundException(json.dumps(_json)) else: raise APIException(json.dumps(_json)) except ValueError: pass # No can do, just raise the correct Exception. response.raise_for_status() else: try: self.object_manager.update_caches(response.json()) except ValueError: pass return response def _get_url(self, endpoint=''): return "%(protocol)s://%(subdomain)s.zendesk.com/api/%(version)s/" % self.__dict__ + endpoint
class BaseApi(object): """ Base class for API. """ subdomain = None def __init__(self, subdomain, session): self.subdomain = subdomain self.protocol = 'https' self.version = 'v2' self.object_manager = ObjectManager(self) self.session = session def _post(self, url, payload): log.debug("POST: %s - %s" % (url, str(payload))) payload = json.loads(json.dumps(payload, cls=ApiObjectEncoder)) response = self.session.post(url, json=payload) self._check_and_cache_response(response) return self._build_response(response.json()) def _put(self, url, payload): log.debug("PUT: " + url) payload = json.loads(json.dumps(payload, cls=ApiObjectEncoder)) response = self.session.put(url, json=payload) self._check_and_cache_response(response) return self._build_response(response.json()) def _delete(self, url, payload=None): log.debug("DELETE: " + url) if payload: response = self.session.delete(url, json=payload) else: response = self.session.delete(url) return self._check_and_cache_response(response) def _get(self, url, stream=False): log.debug("GET: " + url) response = self.session.get(url, stream=stream) # If we are being rate-limited, wait the required period before trying again. while 'retry-after' in response.headers and int(response.headers['retry-after']) > 0: retry_after_seconds = int(response.headers['retry-after']) log.warn( "APIRateLimitExceeded - sleeping for requested retry-after period: %s seconds" % retry_after_seconds) while retry_after_seconds > 0: retry_after_seconds -= 1 log.debug("APIRateLimitExceeded - sleeping: %s more seconds" % retry_after_seconds) sleep(1) response = self.session.get(url, stream=stream) return self._check_and_cache_response(response) def _get_items(self, endpoint, object_type, *args, **kwargs): sideload = 'sideload' not in kwargs or ('sideload' in kwargs and kwargs['sideload']) # If an ID is present a single object has been requested if 'id' in kwargs: return self._get_item(kwargs['id'], endpoint, object_type, sideload) if 'ids' in kwargs: cached_objects = [] # Check to see if we have all objects in the cache. # If we are missing even one we need to request them all again. for _id in kwargs['ids']: obj = self.object_manager.query_cache(object_type, _id) if obj: cached_objects.append(obj) else: return self._get_paginated(endpoint, object_type, *args, **kwargs) return cached_objects return self._get_paginated(endpoint, object_type, *args, **kwargs) def _get_item(self, _id, endpoint, object_type, sideload=True, skip_cache=False): if not skip_cache: # Check if we already have this item in the cache item = self.object_manager.query_cache(object_type, _id) if item: return item _json = self._query(endpoint=endpoint(id=_id, sideload=sideload)) # If the result is paginated return a generator if 'next_page' in _json: return ResultGenerator(self, object_type, _json) # Annoyingly, tags is always plural. if 'tags' in _json: return self.object_manager.object_from_json(object_type, _json[object_type + 's']) else: return self.object_manager.object_from_json(object_type, _json[object_type]) def _get_paginated(self, endpoint, object_type, *args, **kwargs): _json = self._query(endpoint=endpoint(*args, **kwargs)) return ResultGenerator(self, object_type, _json) def _query(self, endpoint): response = self._get(self._get_url(endpoint=endpoint)) return response.json() def _build_response(self, response_json): # When updating and deleting API objects various responses can be returned # We can figure out what we have by the keys in the returned JSON if 'ticket' and 'audit' in response_json: return self.object_manager.object_from_json('ticket_audit', response_json) elif 'tags' in response_json: return response_json['tags'] for object_type in ('ticket', 'user', 'job_status', 'group', 'satisfaction_rating', 'request', 'organization'): if object_type in response_json: return self.object_manager.object_from_json(object_type, response_json[object_type]) raise ZenpyException("Unknown Response: " + str(response_json)) def _check_and_cache_response(self, response): if response.status_code > 299 or response.status_code < 200: # If it's just a RecordNotFound error raise the right exception, # otherwise try and get a nice error message. if 'application/json' in response.headers['content-type']: try: _json = response.json() if 'error' in _json and _json['error'] == 'RecordNotFound': raise RecordNotFoundException(json.dumps(_json)) else: raise APIException(json.dumps(_json)) except ValueError: pass # No can do, just raise the correct Exception. response.raise_for_status() else: try: self.object_manager.update_caches(response.json()) except ValueError: pass return response def _get_url(self, endpoint=''): return "%(protocol)s://%(subdomain)s.zendesk.com/api/%(version)s/" % self.__dict__ + endpoint