def _init_session(self, email, token, oath_token, password, session): if not session or not hasattr(session, 'authorized') \ or not session.authorized: # session is not an OAuth session that has been authorized, # so create a new Session. if not password and not token and not oath_token: raise ZenpyException( "password, token or oauth_token are required!") elif password and token: raise ZenpyException("password and token " "are mutually exclusive!") session = session if session else requests.Session() if password: session.auth = (email, password) elif token: session.auth = ('%s/token' % email, token) elif oath_token: session.headers.update( {'Authorization': 'Bearer %s' % oath_token}) else: raise ZenpyException("Invalid arguments to _init_session()!") headers = { 'Content-type': 'application/json', 'User-Agent': 'Zenpy/1.0.9' } session.headers.update(headers) return session
def post(self, fp, token=None, target_name=None, content_type=None): if hasattr(fp, 'read'): # File-like objects such as: # PY3: io.StringIO, io.TextIOBase, io.BufferedIOBase # PY2: file, io.StringIO, StringIO.StringIO, cStringIO.StringIO if not hasattr(fp, 'name') and not target_name: raise ZenpyException("upload requires a target file name") else: target_name = target_name or fp.name elif isinstance(fp, str): if os.path.isfile(fp): fp = open(fp, 'rb') target_name = target_name or fp.name elif not target_name: # Valid string, which is not a path, and without a target name raise ZenpyException("upload requires a target file name") elif not target_name: # Other serializable types accepted by requests (like dict) raise ZenpyException("upload requires a target file name") url = self.api._build_url( self.api.endpoint.upload(filename=target_name, token=token)) return self.api._post(url, data=fp, payload={}, content_type=content_type)
def _init_session(self, email, token, oath_token, password, session): if not session: session = requests.Session() # Workaround for possible race condition - https://github.com/kennethreitz/requests/issues/3661 session.mount('https://', HTTPAdapter(**self.http_adapter_kwargs())) if not hasattr(session, 'authorized') or not session.authorized: # session is not an OAuth session that has been authorized, so authorize the session. if not password and not token and not oath_token: raise ZenpyException( "password, token or oauth_token are required!") elif password and token: raise ZenpyException("password and token " "are mutually exclusive!") if password: session.auth = (email, password) elif token: session.auth = ('%s/token' % email, token) elif oath_token: session.headers.update( {'Authorization': 'Bearer %s' % oath_token}) else: raise ZenpyException("Invalid arguments to _init_session()!") headers = {'User-Agent': 'Zenpy/{}'.format(__version__)} session.headers.update(headers) return session
def __call__(self, sort_order=None, sort_by=None, **kwargs): if sort_order and sort_order not in ('asc', 'desc'): raise ZenpyException("sort_order must be one of (asc, desc)") if sort_by and sort_by not in ('alphabetical', 'created_at', 'updated_at', 'usage_1h', 'usage_24h', 'usage_7d'): raise ZenpyException( "sort_by is invalid - https://developer.zendesk.com/rest_api/docs/core/macros#available-parameters" ) if 'id' in kwargs: if len(kwargs) > 1: raise ZenpyException( "When specifying an id it must be the only parameter") params = dict() path = self.endpoint for key, value in kwargs.items(): if isinstance(value, bool): value = str(value).lower() if key == 'id': path += "/{}.json".format(value) else: params[key] = value if sort_order: params['sort_order'] = sort_order if sort_by: params['sort_by'] = sort_by if path == self.endpoint: path += '.json' return Url(path, params=params)
def __call__(self, sort_order=None, sort_by=None, **kwargs): kwargs.pop('sideload', None) if sort_order and sort_order not in ('asc', 'desc'): raise ZenpyException("sort_order must be one of (asc, desc)") if sort_by and sort_by not in ('alphabetical', 'created_at', 'updated_at', 'usage_1h', 'usage_24h', 'usage_7d'): raise ZenpyException( "sort_by is invalid - https://developer.zendesk.com/rest_api/docs/core/macros#available-parameters" ) if 'id' in kwargs: if len(kwargs) > 1: raise ZenpyException( "When specifying an id it must be the only parameter") url_out = '' else: url_out = self.endpoint + '?' for key, value in kwargs.items(): if isinstance(value, bool): value = str(value).lower() if key == 'id': url_out += self._single(self.endpoint, value) else: url_out += '&{}={}'.format(key, value) if sort_order: url_out += '&sort_order={}'.format(sort_order) if sort_by: url_out += '&sort_by={}'.format(sort_by) return url_out
def _check_type(self, items): # We don't want people passing, for example, a Group object to a Ticket endpoint. expected_class = self.object_manager.class_manager.class_for_type(self.object_type) if isinstance(items, list): if any((o.__class__ is not expected_class for o in items)): raise ZenpyException("Invalid type - expected %(expected_class)s" % locals()) else: if items.__class__ is not expected_class: raise ZenpyException("Invalid type - expected %(expected_class)s" % locals())
def format_between(self, key, values): if not isinstance(values, list) and not isinstance(values, tuple): raise ZenpyException("*_between requires a list or tuple!") elif not len(values) == 2: raise ZenpyException("*_between requires exactly 2 items!") elif not all([isinstance(d, datetime) for d in values]): raise ZenpyException("*_between only works with dates!") key = key.replace('_between', '') dates = [v.strftime(self.ZENDESK_DATE_FORMAT) for v in values] return "%s>%s %s<%s" % (key, dates[0], key, dates[1])
def format_between(self, key, values): if not is_iterable_but_not_string(values): raise ZenpyException("*_between requires an iterable (list, set, tuple etc)") elif not len(values) == 2: raise ZenpyException("*_between requires exactly 2 items!") elif not all([isinstance(d, datetime) for d in values]): raise ZenpyException("*_between only works with dates!") key = key.replace('_between', '') if values[0].tzinfo is None or values[1].tzinfo is None: dates = [v.strftime(self.ISO_8601_FORMAT) for v in values] else: dates = [str(v.replace(microsecond=0).isoformat()) for v in values] return "%s>%s %s<%s" % (key, dates[0], key, dates[1])
def post(self, fp, token=None, target_name=None, content_type=None, api_object=None): if hasattr(fp, 'read'): # File-like objects such as: # PY3: io.StringIO, io.TextIOBase, io.BufferedIOBase # PY2: file, io.StringIO, StringIO.StringIO, cStringIO.StringIO if not hasattr(fp, 'name') and not target_name: raise ZenpyException("upload requires a target file name") else: target_name = target_name or fp.name elif hasattr(fp, 'name'): # Path objects (pathlib.Path) if fp.name == '': raise ZenpyException("upload requires a target file name") target_name = target_name or fp.name # PathLike objects only compatible with python3.6 and above, so # we convert to string at this point # https://stackoverflow.com/a/42694113/4664727 fp = open(str(fp), 'rb') elif isinstance(fp, str): if os.path.isfile(fp): fp = open(fp, 'rb') target_name = target_name or fp.name elif not target_name: # Valid string, which is not a path, and without a target name raise ZenpyException("upload requires a target file name") elif not target_name: # Other serializable types accepted by requests (like dict) raise ZenpyException("upload requires a target file name") url = self.api._build_url( self.api.endpoint.upload(filename=target_name, token=token)) response = self.api._post(url, data=fp, payload={}, content_type=content_type) if hasattr(fp, "close"): fp.close() return response
def add_cache(self, object_type, cache_impl_name, maxsize, **kwargs): """ Add a new cache for the named object type and cache implementation """ if object_type not in ZendeskObjectMapping.class_mapping: raise ZenpyException("No such object type: %s" % object_type) cache_mapping[object_type] = ZenpyCache(cache_impl_name, maxsize, **kwargs)
def build(self, response): """ Deserialize the returned objects and return either a single Zenpy object, or a ResultGenerator in the case of multiple results. :param response: the requests Response object. """ response_json = response.json() zenpy_objects = self.deserialize(response_json) # Collection of objects (eg, users/tickets) plural_object_type = as_plural(self.api.object_type) if plural_object_type in zenpy_objects: return ZendeskResultGenerator(self, response_json) # Here the response matches the API object_type, seems legit. if self.api.object_type in zenpy_objects: return zenpy_objects[self.api.object_type] # Could be anything, if we know of this object then return it. for zenpy_object_name in self.api._object_mapping.class_mapping: if zenpy_object_name in zenpy_objects: return zenpy_objects[zenpy_object_name] # Maybe a collection of known objects? for zenpy_object_name in self.api._object_mapping.class_mapping: plural_zenpy_object_name = as_plural(zenpy_object_name) if plural_zenpy_object_name in zenpy_objects: return ZendeskResultGenerator( self, response_json, object_type=plural_zenpy_object_name) # Bummer, bail out with an informative message. raise ZenpyException("Unknown Response: " + str(response_json))
def post(self, endpoint, attachment, article=None, inline=False, file_name=None, content_type=None): if article: url = self.api._build_url(endpoint(id=article)) else: url = self.api._build_url(endpoint()) use_file_name = bool(file_name) and bool(content_type) if (bool(file_name) or bool(content_type)) and not use_file_name: raise ZenpyException( "Content_type and file_name are expected together!") if hasattr(attachment, 'read'): file = attachment if not use_file_name else (file_name, attachment, content_type) return self.api._post(url, payload={}, files=dict(inline=(None, inline), file=file)) elif os.path.isfile(attachment): with open(attachment, 'rb') as fp: file = fp if not use_file_name else (file_name, fp, content_type) return self.api._post(url, payload={}, files=dict(inline=(None, inline), file=file)) raise ValueError("Attachment is not a file-like object or valid path!")
def __call__(self, *args, **kwargs): params = list() if len(args) > 1: raise ZenpyException("Only query can be passed as an arg!") elif len(args) == 1: params.append("query={}".format(args[0])) params.extend(["{}={}".format(k, v) for k, v in kwargs.items()]) return self.endpoint + "&".join(params).lower()
def put(self, endpoint, help_centre_object_id, translation): if translation.locale is None: raise ZenpyException( "Locale can not be None when updating translation!") url = self.api._build_url( endpoint(help_centre_object_id, translation.locale)) payload = self.build_payload(translation) return self.api._put(url, payload=payload)
def build(self, response): """ Deserialize the returned objects and return either a single Zenpy object, or a ResultGenerator in the case of multiple results. :param response: the requests Response object. """ response_json = response.json() # Special case for incremental cursor based ticket audits export. if get_endpoint_path(self.api, response).startswith('/ticket_audits.json'): return TicketCursorGenerator(self, response_json, object_type="audit") # Special case for incremental cursor based tickets export. if get_endpoint_path( self.api, response).startswith('/incremental/tickets/cursor.json'): return TicketCursorGenerator(self, response_json, object_type="ticket") # Special case for Jira links. if get_endpoint_path(self.api, response).startswith('/services/jira/links'): return JiraLinkGenerator(self, response_json, response) zenpy_objects = self.deserialize(response_json) # Collection of objects (eg, users/tickets) plural_object_type = as_plural(self.api.object_type) if plural_object_type in zenpy_objects: return ZendeskResultGenerator( self, response_json, response_objects=zenpy_objects[plural_object_type]) # Here the response matches the API object_type, seems legit. if self.api.object_type in zenpy_objects: return zenpy_objects[self.api.object_type] # Could be anything, if we know of this object then return it. for zenpy_object_name in self.object_mapping.class_mapping: if zenpy_object_name in zenpy_objects: return zenpy_objects[zenpy_object_name] # Maybe a collection of known objects? for zenpy_object_name in self.object_mapping.class_mapping: plural_zenpy_object_name = as_plural(zenpy_object_name) if plural_zenpy_object_name in zenpy_objects: return ZendeskResultGenerator( self, response_json, object_type=plural_zenpy_object_name) # Bummer, bail out. raise ZenpyException("Unknown Response: " + str(response_json))
def add_cache(self, object_type, cache_impl_name, maxsize, **kwargs): """ Add a new cache for the named object type and cache implementation """ if object_type not in self.users.object_manager.class_manager.class_mapping: raise ZenpyException("No such object type: %s" % object_type) cache_mapping = self._get_cache_mapping() cache_mapping[object_type] = ZenpyCache(cache_impl_name, maxsize, **kwargs)
def perform(self, http_method, *args, **kwargs): http_method = http_method.lower() if http_method == 'put': return self.put(*args, **kwargs) elif http_method == 'post': return self.post(*args, **kwargs) elif http_method == 'delete': return self.delete(*args, **kwargs) raise ZenpyException("{} cannot handle HTTP method: {}".format(self.__class__.__name__, http_method))
def check_type(self, zenpy_objects): """ Ensure the passed type matches this API's object_type. """ expected_type = self.api._object_mapping.class_for_type(self.api.object_type) if not isinstance(zenpy_objects, collections.Iterable): zenpy_objects = [zenpy_objects] for zenpy_object in zenpy_objects: if type(zenpy_object) is not expected_type: raise ZenpyException( "Invalid type - expected {} found {}".format(expected_type, type(zenpy_object)) )
def build(self, response): response_json = response.json() response_objects = self.deserialize(response_json) if 'sla_policies' in response_objects: return ZendeskResultGenerator(self, response.json(), response_objects=response_objects['sla_policies']) elif 'sla_policy' in response_objects: return response_objects['sla_policy'] elif response_objects: return response_objects['definitions'] raise ZenpyException("Could not handle response: {}".format(response_json))
def __call__(self, *args, **kwargs): if not args: raise ZenpyException("You need to pass the query string as the first parameter") query = "query=%s" % args[0] result = [] for key, value in kwargs.items(): result.append("%s=%s" % (key, value)) query += '&' + "&".join(result) return self.endpoint + query
def __call__(self, **kwargs): query = "start_time=" if 'start_time' in kwargs: if isinstance(kwargs['start_time'], datetime): query += kwargs['start_time'].strftime(self.UNIX_TIME) else: query += str(kwargs['start_time']) return self.endpoint + query + self._format_sideload(self.sideload, seperator='&') raise ZenpyException("Incremental Endoint requires a start_time parameter!")
def deserialize(self, response_json): chats = list() if 'chats' in response_json: chat_list = response_json['chats'] elif 'docs' in response_json: chat_list = response_json['docs'].values() else: raise ZenpyException("Unexpected response: {}".format(response_json)) for chat in chat_list: chats.append(self.object_mapping.object_from_json('chat', chat)) return chats
def build(self, response): zenpy_objects = self.deserialize(response.json()) # JobStatus responses also include a ticket key so treat it specially. if 'job_status' in zenpy_objects: return zenpy_objects['job_status'] # TicketAudit responses are another special case containing both # a ticket and audit key. if 'ticket' and 'audit' in zenpy_objects: return zenpy_objects['ticket_audit'] raise ZenpyException("Could not process response: {}".format(response))
def format_between(self, key, values): if not is_iterable_but_not_string(values): raise ValueError("*_between requires an iterable (list, set, tuple etc)") elif not len(values) == 2: raise ZenpyException("*_between requires exactly 2 items!") for value in values: if not isinstance(value, datetime): raise ValueError("*_between only works with datetime objects!") elif value.tzinfo is not None and value.utcoffset().total_seconds() != 0: log.warning("search parameter '{}' requires UTC time, results likely incorrect.".format(key)) key = key.replace('_between', '') dates = [v.strftime(self.ISO_8601_FORMAT) for v in values] return "%s>%s %s<%s" % (key, dates[0], key, dates[1])
def __call__(self, score=None, sort_order=None, start_time=None, end_time=None): if sort_order and sort_order not in ('asc', 'desc'): raise ZenpyException("sort_order must be one of (asc, desc)") params = dict() if score: params['score'] = score if sort_order: params['sort_order'] = sort_order if start_time: params['start_time'] = to_unix_ts(start_time) if end_time: params['end_time'] = to_unix_ts(end_time) return Url(self.endpoint, params=params)
def __call__(self, score=None, sort_order=None): if sort_order not in ('asc', 'desc'): raise ZenpyException("sort_order must be one of (asc, desc)") base_url = self.endpoint + '?' if score: result = base_url + "score={}".format(score) else: result = base_url if sort_order: result += '&sort_order={}'.format(sort_order) return result
def __call__(self, start_time=None): if start_time is None: raise ZenpyException( "Incremental Endoint requires a start_time parameter!") elif isinstance(start_time, datetime): unix_time = to_unix_ts(start_time) else: unix_time = start_time query = "start_time=%s" % str(unix_time) return self.endpoint + query + self._format_sideload(self.sideload, seperator='&')
def upload(self, fp, token=None, target_name=None): """ Upload a file to Zendesk. :param fp: file object, StringIO instance, content, or file path to be uploaded :param token: upload token for uploading multiple files :param target_name: name of the file inside Zendesk :return: :class:`Upload` object containing a token and other information (see https://developer.zendesk.com/rest_api/docs/core/attachments#uploading-files) """ if hasattr(fp, 'read'): # File-like objects such as: # PY3: io.StringIO, io.TextIOBase, io.BufferedIOBase # PY2: file, io.StringIO, StringIO.StringIO, cStringIO.StringIO if not hasattr(fp, 'name') and not target_name: raise ZenpyException("upload requires a target file name") else: target_name = target_name or fp.name elif isinstance(fp, str): if os.path.isfile(fp): fp = open(fp, 'rb') target_name = target_name or fp.name elif not target_name: # Valid string, which is not a path, and without a target name raise ZenpyException("upload requires a target file name") elif not target_name: # Other serializable types accepted by requests (like dict) raise ZenpyException("upload requires a target file name") return self.post(self._get_url(self.endpoint.upload(filename=target_name, token=token)), data=fp, payload={})
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 __call__(self, start_time=None, include=None): if start_time is None: raise ZenpyException("Incremental Endpoint requires a start_time parameter!") elif isinstance(start_time, datetime): unix_time = to_unix_ts(start_time) else: unix_time = start_time params = dict(start_time=str(unix_time)) if include is not None: if is_iterable_but_not_string(include): params.update(dict(include=",".join(include))) else: params.update(dict(include=include)) return Url(self.endpoint, params=params)