From beaf6b0a1561b30e3fd58756d5cbc0f62aad0c90 Mon Sep 17 00:00:00 2001 From: brephophagist Date: Thu, 4 Aug 2016 17:47:45 -0700 Subject: [PATCH 01/46] Allow clients to specify empty filter name Permits passing the kwarg filter_name=None to the api.TicketAPI.list_tickets() methods in both the v1 and v2 modules. In v2 of the FD API, there is no 'all_tickets' filter as there is in v1; one gets the same behavior by omitting the 'filter' argument from the URL's GET parameters. To maintain parity between the two versions' modules, passing a filter_name=None kwarg to v1.api.TicketAPI.list_tickets() selects the default 'all_tickets' filter (the same behavior as omitting the kwarg). Adds relevant tests. --- freshdesk/v1/api.py | 7 ++++--- freshdesk/v1/test.py | 6 ++++++ freshdesk/v2/api.py | 14 +++++++++++--- freshdesk/v2/test.py | 7 +++++++ 4 files changed, 28 insertions(+), 6 deletions(-) diff --git a/freshdesk/v1/api.py b/freshdesk/v1/api.py index 5849141..2c8dd72 100644 --- a/freshdesk/v1/api.py +++ b/freshdesk/v1/api.py @@ -16,14 +16,15 @@ def list_tickets(self, **kwargs): """List all tickets, optionally filtered by a view. Specify filters as keyword arguments, such as: - filter_name = one of ['all_tickets', 'new_my_open', 'spam', 'deleted'] - (defaults to 'all_tickets') + filter_name = one of ['all_tickets', 'new_my_open', 'spam', 'deleted', + None] + (defaults to 'all_tickets'; passing None uses the default) Multiple filters are AND'd together. """ filter_name = 'all_tickets' - if 'filter_name' in kwargs: + if 'filter_name' in kwargs and kwargs['filter_name'] is not None: filter_name = kwargs['filter_name'] del kwargs['filter_name'] diff --git a/freshdesk/v1/test.py b/freshdesk/v1/test.py index b1d57d2..8cdad66 100644 --- a/freshdesk/v1/test.py +++ b/freshdesk/v1/test.py @@ -146,6 +146,12 @@ def test_default_filter_name(self): self.assertEqual(len(tickets), 1) self.assertEqual(tickets[0].display_id, self.ticket.display_id) + def test_none_filter_name(self): + tickets = self.api.tickets.list_tickets(filter_name=None) + self.assertIsInstance(tickets, list) + self.assertEqual(len(tickets), 1) + self.assertEqual(tickets[0].display_id, self.ticket.display_id) + class TestComment(TestCase): @classmethod diff --git a/freshdesk/v2/api.py b/freshdesk/v2/api.py index a376380..c5a3d6b 100644 --- a/freshdesk/v2/api.py +++ b/freshdesk/v2/api.py @@ -43,8 +43,12 @@ def list_tickets(self, **kwargs): """List all tickets, optionally filtered by a view. Specify filters as keyword arguments, such as: - filter_name = one of ['new_and_my_open', 'watching', 'spam', 'deleted'] + filter_name = one of ['new_and_my_open', 'watching', 'spam', 'deleted', + None] (defaults to 'new_and_my_open') + Passing None means that no named filter will be passed to + Freshdesk, which mimics the behavior of the 'all_tickets' filter + in v1 of the API. Multiple filters are AND'd together. """ @@ -54,14 +58,18 @@ def list_tickets(self, **kwargs): filter_name = kwargs['filter_name'] del kwargs['filter_name'] - url = 'tickets?filter=%s' % filter_name + url = 'tickets' + if filter_name is not None: + url += '?filter=%s&' % filter_name + else: + url += '?' page = 1 per_page = 100 tickets = [] # Skip pagination by looping over each page and adding tickets while True: - this_page = self._api._get(url + '&page=%d&per_page=%d' + this_page = self._api._get(url + 'page=%d&per_page=%d' % (page, per_page), kwargs) tickets += this_page if len(this_page) < per_page: diff --git a/freshdesk/v2/test.py b/freshdesk/v2/test.py index b6aa20d..c36c401 100644 --- a/freshdesk/v2/test.py +++ b/freshdesk/v2/test.py @@ -26,6 +26,7 @@ def __init__(self, *args): re.compile(r'tickets\?filter=deleted&page=1&per_page=100'): self.read_test_file('all_tickets.json'), re.compile(r'tickets\?filter=spam&page=1&per_page=100'): self.read_test_file('all_tickets.json'), re.compile(r'tickets\?filter=watching&page=1&per_page=100'): self.read_test_file('all_tickets.json'), + re.compile(r'tickets\?page=1&per_page=100'): self.read_test_file('all_tickets.json'), re.compile(r'tickets/1$'): self.read_test_file('ticket_1.json'), re.compile(r'tickets/1/conversations'): self.read_test_file('conversations.json'), re.compile(r'contacts/1$'): self.read_test_file('contact.json'), @@ -199,6 +200,12 @@ def test_default_filter_name(self): self.assertEqual(len(tickets), 1) self.assertEqual(tickets[0].id, self.ticket.id) + def test_none_filter_name(self): + tickets = self.api.tickets.list_tickets(filter_name=None) + self.assertIsInstance(tickets, list) + self.assertEqual(len(tickets), 1) + self.assertEqual(tickets[0].id, self.ticket.id) + class TestComment(TestCase): @classmethod From 95fcaf642466548e3c77bd1e668287db1498ef58 Mon Sep 17 00:00:00 2001 From: Sam Morrison Date: Mon, 17 Apr 2017 12:20:27 +1000 Subject: [PATCH 02/46] Add support for creating outbound emails See https://developer.freshdesk.com/api/#create_outbound_email --- freshdesk/v2/api.py | 16 ++++++++++++++++ freshdesk/v2/test.py | 31 +++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/freshdesk/v2/api.py b/freshdesk/v2/api.py index a376380..c9e0839 100644 --- a/freshdesk/v2/api.py +++ b/freshdesk/v2/api.py @@ -28,6 +28,22 @@ def create_ticket(self, subject, **kwargs): ticket = self._api._post(url, data=json.dumps(data)) return Ticket(**ticket) + def create_outbound_email(self, subject, description, email, + email_config_id, **kwargs): + """Creates an outbound email""" + url = 'tickets/outbound_email' + priority = kwargs.get('priority', 1) + data = { + 'subject': subject, + 'description': description, + 'priority': priority, + 'email': email, + 'email_config_id': email_config_id, + } + data.update(kwargs) + ticket = self._api._post(url, data=json.dumps(data)) + return Ticket(**ticket) + def update_ticket(self, ticket_id, **kwargs): """Updates a ticket from a given ticket ID""" url = 'tickets/%d' % ticket_id diff --git a/freshdesk/v2/test.py b/freshdesk/v2/test.py index b6aa20d..2c33011 100644 --- a/freshdesk/v2/test.py +++ b/freshdesk/v2/test.py @@ -122,6 +122,37 @@ def test_create_ticket(self): self.assertIn('foo', ticket.tags) self.assertIn('bar', ticket.tags) + @responses.activate + def test_create_outbound_email(self): + j = self.ticket_json.copy() + values = { + 'subject': 'This is a sample outbound_email', + 'description_text': 'This is a sample outbound, feel free to delete it.', + 'status': 5, + 'email_config_id': 5000054536, + } + j.update(values) + responses.add(responses.POST, + 'https://{}/api/v2/tickets/outbound_email'.format(DOMAIN), + status=200, content_type='application/json', + json=j) + + ticket = self.api.tickets.create_outbound_email('This is a sample outbound_email', + description='This is a sample outbound, feel free to delete it.', + email='test@example.com', + email_config_id=5000054536, + priority=1, + tags=['foo', 'bar'], + cc_emails=['test2@example.com']) + self.assertIsInstance(ticket, Ticket) + self.assertEqual(ticket.subject, 'This is a sample outbound_email') + self.assertEqual(ticket.description_text, 'This is a sample outbound, feel free to delete it.') + self.assertEqual(ticket.priority, 'low') + self.assertEqual(ticket.status, 'closed') + self.assertEqual(ticket.cc_emails, ['test2@example.com']) + self.assertIn('foo', ticket.tags) + self.assertIn('bar', ticket.tags) + @responses.activate def test_update_ticket(self): j = self.ticket_json.copy() From 930ccdd56e181d63d387efc2252b1fc3dc5fb296 Mon Sep 17 00:00:00 2001 From: Sam Kingston Date: Sat, 26 Aug 2017 07:12:15 +1000 Subject: [PATCH 03/46] Version bump to 1.0.1 --- CHANGELOG.md | 4 ++++ freshdesk/__init__.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ef3e070..2babca1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ Changelog ========= +v1.0.1 - 2017-08-26 + + * Allow clients to specify empty filter name (@brephophagist) + v1.0.0 - 2016-06-16 * Add support for version 2 of the Freskdesk API while maintaining support for diff --git a/freshdesk/__init__.py b/freshdesk/__init__.py index 1f356cc..cd7ca49 100644 --- a/freshdesk/__init__.py +++ b/freshdesk/__init__.py @@ -1 +1 @@ -__version__ = '1.0.0' +__version__ = '1.0.1' From 61b6fac236a92d450d860c98d5b9bae81f9cc6f1 Mon Sep 17 00:00:00 2001 From: Robert Armstrong Date: Fri, 5 Jan 2018 08:44:02 -0800 Subject: [PATCH 04/46] Fix string output for Group --- freshdesk/v2/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freshdesk/v2/models.py b/freshdesk/v2/models.py index c3ddd8f..ec3f08a 100644 --- a/freshdesk/v2/models.py +++ b/freshdesk/v2/models.py @@ -51,7 +51,7 @@ def source(self): class Group(FreshdeskModel): def __str__(self): - return self.body + return self.name def __repr__(self): return ''.format(self.name) From 20049f68587da201c9ab7096bf2a3e2f342018eb Mon Sep 17 00:00:00 2001 From: Robert Armstrong Date: Fri, 5 Jan 2018 08:47:01 -0800 Subject: [PATCH 05/46] Add Company to API --- freshdesk/v2/models.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/freshdesk/v2/models.py b/freshdesk/v2/models.py index ec3f08a..23c2392 100644 --- a/freshdesk/v2/models.py +++ b/freshdesk/v2/models.py @@ -93,3 +93,11 @@ def __str__(self): def __repr__(self): return ''.format(self.name) + +class Company(FreshdeskModel): + def __str__(self): + return self.name + + def __repr__(self): + return ''.format(self.name) + From 40f5b5f0ce16088ff080558a5f8b0850c83adac2 Mon Sep 17 00:00:00 2001 From: Robert Armstrong Date: Fri, 5 Jan 2018 08:52:20 -0800 Subject: [PATCH 06/46] Add Company to API --- freshdesk/v2/api.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/freshdesk/v2/api.py b/freshdesk/v2/api.py index c5a3d6b..994d582 100644 --- a/freshdesk/v2/api.py +++ b/freshdesk/v2/api.py @@ -151,6 +151,13 @@ def get_customer(self, company_id): def get_customer_from_contact(self, contact): return self.get_customer(contact.customer_id) +class CompanyAPI(object): + def __init__(self, api): + self._api = api + + def get_company(self, company_id): + url = 'company/%s' % company_id + return self.get_company(company.company_id) class API(object): def __init__(self, domain, api_key): From b2fffe58bb3ca8e68e81106510ef7cf16e29e932 Mon Sep 17 00:00:00 2001 From: Robert Armstrong Date: Fri, 5 Jan 2018 09:18:42 -0800 Subject: [PATCH 07/46] fix company add in api --- freshdesk/__init__.py | 2 +- freshdesk/v2/api.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/freshdesk/__init__.py b/freshdesk/__init__.py index cd7ca49..a6221b3 100644 --- a/freshdesk/__init__.py +++ b/freshdesk/__init__.py @@ -1 +1 @@ -__version__ = '1.0.1' +__version__ = '1.0.2' diff --git a/freshdesk/v2/api.py b/freshdesk/v2/api.py index 994d582..bb9849d 100644 --- a/freshdesk/v2/api.py +++ b/freshdesk/v2/api.py @@ -1,7 +1,7 @@ import requests from requests.exceptions import HTTPError import json -from freshdesk.v2.models import Ticket, Comment, Customer, Contact, Group +from freshdesk.v2.models import Ticket, Comment, Customer, Contact, Group, Company class TicketAPI(object): @@ -179,6 +179,7 @@ def __init__(self, domain, api_key): self.tickets = TicketAPI(self) self.comments = CommentAPI(self) self.contacts = ContactAPI(self) + self.company = CompanyAPI(self) self.groups = GroupAPI(self) self.customers = CustomerAPI(self) From 9555eac30c12033a4ad8bb5e06132fc7c0284a85 Mon Sep 17 00:00:00 2001 From: Robert Armstrong Date: Fri, 5 Jan 2018 10:48:31 -0800 Subject: [PATCH 08/46] clean up CompanyAPI --- freshdesk/v2/api.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/freshdesk/v2/api.py b/freshdesk/v2/api.py index bb9849d..48aac8c 100644 --- a/freshdesk/v2/api.py +++ b/freshdesk/v2/api.py @@ -156,8 +156,8 @@ def __init__(self, api): self._api = api def get_company(self, company_id): - url = 'company/%s' % company_id - return self.get_company(company.company_id) + url = 'companies/%s' % company_id + return Company(**self._api._get(url)) class API(object): def __init__(self, domain, api_key): @@ -179,7 +179,7 @@ def __init__(self, domain, api_key): self.tickets = TicketAPI(self) self.comments = CommentAPI(self) self.contacts = ContactAPI(self) - self.company = CompanyAPI(self) + self.companies = CompanyAPI(self) self.groups = GroupAPI(self) self.customers = CustomerAPI(self) From 9badc63d2100ccf2c2f0031e27a9bf33cece9a20 Mon Sep 17 00:00:00 2001 From: Sam Kingston Date: Mon, 8 Jan 2018 10:50:08 +1000 Subject: [PATCH 09/46] Version bump to 1.1.0 --- CHANGELOG.md | 6 ++++++ README.md | 1 + freshdesk/__init__.py | 2 +- 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2babca1..3150f3c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,12 @@ Changelog ========= +v1.1.0 - 2018-01-08 + + * #13: Fixed group representation (@helix90) + * #14: Add support for Company API (@helix90) + * #11: Add support for creating outbound emails (@sorrison) + v1.0.1 - 2017-08-26 * Allow clients to specify empty filter name (@brephophagist) diff --git a/README.md b/README.md index 517f0c6..70f2c2e 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ Support for the v2 API includes the following features: * [Groups](http://developer.freshdesk.com/api/#groups) - [List](http://developer.freshdesk.com/api/#list_all_groups) - [Get](http://developer.freshdesk.com/api/#view_group) +* [Company](https://developers.freshdesk.com/api/#companies) ## Installation diff --git a/freshdesk/__init__.py b/freshdesk/__init__.py index a6221b3..1a72d32 100644 --- a/freshdesk/__init__.py +++ b/freshdesk/__init__.py @@ -1 +1 @@ -__version__ = '1.0.2' +__version__ = '1.1.0' From 33aeeba3e5fa1e1045ad5076539986d2655a5187 Mon Sep 17 00:00:00 2001 From: prenit-coverfox Date: Wed, 30 May 2018 18:59:18 +0530 Subject: [PATCH 10/46] added Agent model and api --- freshdesk/v2/api.py | 70 ++++++++++++++++++++++++++++++++++++++++-- freshdesk/v2/models.py | 6 ++++ 2 files changed, 74 insertions(+), 2 deletions(-) diff --git a/freshdesk/v2/api.py b/freshdesk/v2/api.py index f204a99..06c923f 100644 --- a/freshdesk/v2/api.py +++ b/freshdesk/v2/api.py @@ -1,7 +1,7 @@ import requests from requests.exceptions import HTTPError import json -from freshdesk.v2.models import Ticket, Comment, Customer, Contact, Group, Company +from freshdesk.v2.models import Ticket, Comment, Customer, Contact, Group, Company, Agent class TicketAPI(object): @@ -59,7 +59,7 @@ def list_tickets(self, **kwargs): """List all tickets, optionally filtered by a view. Specify filters as keyword arguments, such as: - filter_name = one of ['new_and_my_open', 'watching', 'spam', 'deleted', + filter_name = one of ['new_and_my_open', 'watching', 'spam', 'deleted',, Agent None] (defaults to 'new_and_my_open') Passing None means that no named filter will be passed to @@ -175,6 +175,71 @@ def get_company(self, company_id): url = 'companies/%s' % company_id return Company(**self._api._get(url)) + +class AgentAPI(object): + def __init__(self, api): + self._api = api + + def list_agents(self, **kwargs): + """List all agents, optionally filtered by a view. Specify filters as + keyword arguments, such as: + + { + email='abc@xyz.com', + phone=873902, + mobile=56523, + state='fulltime' + } + + Passing None means that no named filter will be passed to + Freshdesk, which returns list of all agents + + Multiple filters are AND'd together. + """ + + url = 'agents?' + if kwargs: + for filter_name, filter_value in kwargs.items(): + url = url + "{}={}&".format(filter_name, filter_value) + del kwargs[filter_name] + + page = 1 + per_page = 100 + agents = [] + + # Skip pagination by looping over each page and adding tickets + while True: + this_page = self._api._get(url + 'page=%d&per_page=%d' + % (page, per_page), kwargs) + agents += this_page + if len(this_page) < per_page: + break + page += 1 + + return [Agent(**a) for a in agents] + + def get_agent(self, agent_id): + """Fetches the agent for the given agent ID""" + url = 'agents/%s' % agent_id + return Agent(**self._api._get(url)) + + def update_agent(self, agent_id, **kwargs): + """Updates an agent""" + url = 'agents/%s' % agent_id + agent = self._api._put(url, data=json.dumps(kwargs)) + return Agent(**agent) + + def delete_agent(self, agent_id): + """Delete the agent for the given agent ID""" + url = 'agents/%d' % agent_id + self._api._delete(url) + + def currently_authenticated_agent(self): + """Fetches currently logged in agent""" + url = 'agents/me' + return Agent(**self._api._get(url)) + + class API(object): def __init__(self, domain, api_key): """Creates a wrapper to perform API actions. @@ -198,6 +263,7 @@ def __init__(self, domain, api_key): self.companies = CompanyAPI(self) self.groups = GroupAPI(self) self.customers = CustomerAPI(self) + self.agents = AgentAPI(self) if domain.find('freshdesk.com') < 0: raise AttributeError('Freshdesk v2 API works only via Freshdesk' diff --git a/freshdesk/v2/models.py b/freshdesk/v2/models.py index 23c2392..0300a2a 100644 --- a/freshdesk/v2/models.py +++ b/freshdesk/v2/models.py @@ -101,3 +101,9 @@ def __str__(self): def __repr__(self): return ''.format(self.name) +class Agent(FreshdeskModel): + def __str__(self): + return self.contact.name + + def __repr__(self): + return ''.format(self.contact.name) From 1d49bc386fa8f8bf28a5a59da12cdafe6c7e88a0 Mon Sep 17 00:00:00 2001 From: prenit-coverfox Date: Wed, 30 May 2018 19:04:00 +0530 Subject: [PATCH 11/46] fixed typos in api.py file --- freshdesk/v2/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freshdesk/v2/api.py b/freshdesk/v2/api.py index 06c923f..5eb1e40 100644 --- a/freshdesk/v2/api.py +++ b/freshdesk/v2/api.py @@ -59,7 +59,7 @@ def list_tickets(self, **kwargs): """List all tickets, optionally filtered by a view. Specify filters as keyword arguments, such as: - filter_name = one of ['new_and_my_open', 'watching', 'spam', 'deleted',, Agent + filter_name = one of ['new_and_my_open', 'watching', 'spam', 'deleted', None] (defaults to 'new_and_my_open') Passing None means that no named filter will be passed to From 29e9c856a6997504754cd579cfdf8abac7123d50 Mon Sep 17 00:00:00 2001 From: prenit-coverfox Date: Thu, 31 May 2018 15:11:02 +0530 Subject: [PATCH 12/46] added Role model and api support --- freshdesk/v2/api.py | 19 ++++++++++++++++++- freshdesk/v2/models.py | 9 +++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/freshdesk/v2/api.py b/freshdesk/v2/api.py index 5eb1e40..3e02403 100644 --- a/freshdesk/v2/api.py +++ b/freshdesk/v2/api.py @@ -1,7 +1,7 @@ import requests from requests.exceptions import HTTPError import json -from freshdesk.v2.models import Ticket, Comment, Customer, Contact, Group, Company, Agent +from freshdesk.v2.models import Ticket, Comment, Customer, Contact, Group, Company, Agent, Role class TicketAPI(object): @@ -176,6 +176,22 @@ def get_company(self, company_id): return Company(**self._api._get(url)) +class RoleAPI(object): + def __init__(self, api): + self._api = api + + def list_roles(self): + url = 'roles' + roles = [] + for r in self._api._get(url): + roles.append(Role(**r)) + return roles + + def get_role(self, role_id): + url = 'roles/%s' % role_id + return Role(**self._api._get(url)) + + class AgentAPI(object): def __init__(self, api): self._api = api @@ -264,6 +280,7 @@ def __init__(self, domain, api_key): self.groups = GroupAPI(self) self.customers = CustomerAPI(self) self.agents = AgentAPI(self) + self.roles = RoleAPI(self) if domain.find('freshdesk.com') < 0: raise AttributeError('Freshdesk v2 API works only via Freshdesk' diff --git a/freshdesk/v2/models.py b/freshdesk/v2/models.py index 0300a2a..fb374ad 100644 --- a/freshdesk/v2/models.py +++ b/freshdesk/v2/models.py @@ -107,3 +107,12 @@ def __str__(self): def __repr__(self): return ''.format(self.contact.name) + + +class Role(FreshdeskModel): + def __str__(self): + return self.name + + def __repr__(self): + return ''.format(self.name) + From 021fc01d43e775efc7ea4f1396faeac1dadbf985 Mon Sep 17 00:00:00 2001 From: prenit-coverfox Date: Thu, 31 May 2018 16:58:21 +0530 Subject: [PATCH 13/46] added test cases for role and agent model and api --- freshdesk/v2/models.py | 6 +- freshdesk/v2/sample_json_data/agent_1.json | 23 +++++ freshdesk/v2/sample_json_data/agents.json | 49 +++++++++ freshdesk/v2/sample_json_data/role_1.json | 11 ++ freshdesk/v2/sample_json_data/roles.json | 24 +++++ freshdesk/v2/test.py | 114 ++++++++++++++++++++- 6 files changed, 223 insertions(+), 4 deletions(-) create mode 100644 freshdesk/v2/sample_json_data/agent_1.json create mode 100644 freshdesk/v2/sample_json_data/agents.json create mode 100644 freshdesk/v2/sample_json_data/role_1.json create mode 100644 freshdesk/v2/sample_json_data/roles.json diff --git a/freshdesk/v2/models.py b/freshdesk/v2/models.py index fb374ad..1d966fc 100644 --- a/freshdesk/v2/models.py +++ b/freshdesk/v2/models.py @@ -103,10 +103,10 @@ def __repr__(self): class Agent(FreshdeskModel): def __str__(self): - return self.contact.name + return self.contact['name'] def __repr__(self): - return ''.format(self.contact.name) + return ''.format(self.id, self.contact['name']) class Role(FreshdeskModel): @@ -114,5 +114,5 @@ def __str__(self): return self.name def __repr__(self): - return ''.format(self.name) + return ''.format(self.name) diff --git a/freshdesk/v2/sample_json_data/agent_1.json b/freshdesk/v2/sample_json_data/agent_1.json new file mode 100644 index 0000000..96827d6 --- /dev/null +++ b/freshdesk/v2/sample_json_data/agent_1.json @@ -0,0 +1,23 @@ +{ + "available":true, + "occasional":false, + "signature":null, + "id":1, + "ticket_scope":1, + "created_at":"2015-08-18T16:18:05Z", + "updated_at":"2015-08-18T16:18:05Z", + "available_since":null, + "contact":{ + "active":true, + "email":"abc@xyz.com", + "job_title":null, + "language":"en", + "last_login_at":"2015-08-21T14:54:46+05:30", + "mobile":1234, + "name":"Support", + "phone":5678, + "time_zone":"Chennai", + "created_at":"2015-08-18T16:18:05Z", + "updated_at":"2015-08-25T08:50:20Z" + } +} diff --git a/freshdesk/v2/sample_json_data/agents.json b/freshdesk/v2/sample_json_data/agents.json new file mode 100644 index 0000000..e679a7b --- /dev/null +++ b/freshdesk/v2/sample_json_data/agents.json @@ -0,0 +1,49 @@ +[ + { + "available":true, + "occasional":false, + "signature":null, + "id":1, + "ticket_scope":1, + "created_at":"2015-08-18T16:18:05Z", + "updated_at":"2015-08-18T16:18:05Z", + "available_since":null, + "contact":{ + "active":true, + "email":"abc@xyz.com", + "job_title":null, + "language":"en", + "last_login_at":"2015-08-21T14:54:46+05:30", + "mobile":1234, + "name":"Support", + "phone":5678, + "time_zone":"Chennai", + "created_at":"2015-08-18T16:18:05Z", + "updated_at":"2015-08-25T08:50:20Z" + } + }, + { + "available":true, + "occasional":false, + "signature":null, + "signature":null, + "id":432, + "ticket_scope":1, + "created_at":"2015-08-28T11:47:58Z", + "updated_at":"2015-08-28T11:47:58Z", + "available_since":null, + "contact":{ + "active":false, + "email":"superman@freshdesk.com", + "job_title":"Journalist", + "language":"en", + "last_login_at":null, + "mobile":null, + "name":"Clark Kent", + "phone":null, + "time_zone":"Chennai", + "created_at":"2015-08-28T09:08:16Z", + "updated_at":"2015-08-28T11:47:58Z" + } + } +] diff --git a/freshdesk/v2/sample_json_data/role_1.json b/freshdesk/v2/sample_json_data/role_1.json new file mode 100644 index 0000000..39297ea --- /dev/null +++ b/freshdesk/v2/sample_json_data/role_1.json @@ -0,0 +1,11 @@ +{ + "id": 1, + "name": "Agent", + "description": "Can log, view, reply, update and resolve tickets and manage contacts.", + "business_hour_id": null, + "escalate_to": 1, + "unassigned_for": "30m", + "auto_ticket_assign": true, + "created_at": "2014-01-08T07:53:41+05:30", + "updated_at": "2014-01-08T07:53:41+05:30" +} diff --git a/freshdesk/v2/sample_json_data/roles.json b/freshdesk/v2/sample_json_data/roles.json new file mode 100644 index 0000000..2888503 --- /dev/null +++ b/freshdesk/v2/sample_json_data/roles.json @@ -0,0 +1,24 @@ +[ + { + "id": 1, + "name": "Agent", + "description": "Can log, view, reply, update and resolve tickets and manage contacts.", + "business_hour_id": null, + "escalate_to": 1, + "unassigned_for": "30m", + "auto_ticket_assign": true, + "created_at": "2014-01-08T07:53:41+05:30", + "updated_at": "2014-01-08T07:53:41+05:30" + }, + { + "id": 2, + "name": "Administrator", + "description": "Can configure all features through the Admin tab, but is restricted from viewing Account or Billing related information.", + "business_hour_id": null, + "escalate_to": 1, + "unassigned_for": "30m", + "auto_ticket_assign": true, + "created_at": "2014-01-08T07:53:41+05:30", + "updated_at": "2014-01-08T07:53:41+05:30" + } +] diff --git a/freshdesk/v2/test.py b/freshdesk/v2/test.py index 020a8fc..d030f95 100644 --- a/freshdesk/v2/test.py +++ b/freshdesk/v2/test.py @@ -6,7 +6,7 @@ from unittest import TestCase from freshdesk.v2.api import API -from freshdesk.v2.models import Ticket, Comment, Contact, Customer, Group +from freshdesk.v2.models import Ticket, Comment, Contact, Customer, Group, Agent, Role """ Test suite for python-freshdesk. @@ -33,6 +33,14 @@ def __init__(self, *args): re.compile(r'customers/1$'): self.read_test_file('customer.json'), re.compile(r'groups$'): self.read_test_file('groups.json'), re.compile(r'groups/1$'): self.read_test_file('group_1.json'), + re.compile(r'roles$'): self.read_test_file('roles.json'), + re.compile(r'roles/1$'): self.read_test_file('role_1.json'), + re.compile(r'agents\?email=abc@xyz.com&page=1&per_page=100'): self.read_test_file('agent_1.json'), + re.compile(r'agents\?mobile=1234&page=1&per_page=100'): self.read_test_file('agent_1.json'), + re.compile(r'agents\?phone=5678&page=1&per_page=100'): self.read_test_file('agent_1.json'), + re.compile(r'agents\?state=fulltime&page=1&per_page=100'): self.read_test_file('agent_1.json'), + re.compile(r'agents\?page=1&per_page=100'): self.read_test_file('agents.json'), + re.compile(r'agents/1$'): self.read_test_file('agent_1.json'), } super(MockedAPI, self).__init__(*args) @@ -359,3 +367,107 @@ def test_group_datetime(self): def test_group_repr(self): self.assertEqual(repr(self.group), '') + +class TestRole(TestCase): + @classmethod + def setUpClass(cls): + cls.api = MockedAPI(DOMAIN, API_KEY) + cls.role = cls.api.roles.get_role(1) + + def test_list_roles(self): + roles = self.api.roles.list_roles() + self.assertIsInstance(roles, list) + self.assertEqual(len(roles), 2) + self.assertEqual(roles[0].id, self.role.id) + + def test_role(self): + self.assertIsInstance(self.role, Role) + self.assertEqual(self.role.name, 'Agent') + self.assertEqual(self.role.description, 'Can log, view, reply, update and resolve tickets and manage contacts.') + + def test_group_datetime(self): + self.assertIsInstance(self.role.created_at, datetime.datetime) + self.assertIsInstance(self.role.updated_at, datetime.datetime) + + def test_group_repr(self): + self.assertEqual(repr(self.role), '') + + +class TestAgent(TestCase): + + @classmethod + def setUpClass(cls): + cls.api = MockedAPI(DOMAIN, API_KEY) + cls.agent = cls.api.agents.get_agent(1) + cls.agent_json = json.loads(open(os.path.join(os.path.dirname(__file__), + 'sample_json_data', + 'agent_1.json')).read()) + + def test_str(self): + self.assertEqual(str(self.agent), 'Support') + + def test_repr(self): + self.assertEqual(repr(self.agent), '') + + def test_get_agent(self): + self.assertIsInstance(self.agent, Agent) + self.assertEqual(self.agent.id, 1) + self.assertEqual(self.agent.contact['name'], 'Support') + self.assertEqual(self.agent.contact['email'], 'abc@xyz.com') + self.assertEqual(self.agent.contact['mobile'], 1234) + self.assertEqual(self.agent.contact['phone'], 5678) + self.assertEqual(self.agent.occasional, False) + + @responses.activate + def test_update_agent(self): + a = self.agent_json.copy() + + responses.add(responses.GET, + 'https://{}/api/v2/agents/1'.format(DOMAIN), + status=200, content_type='application/json', json=a) + + values = { + 'occasional': True, + 'contact': { + 'name': 'Updated Name' + } + } + + b = a.copy() + b.update(values) + + responses.add(responses.PUT, + 'https://{}/api/v2/agents/1'.format(DOMAIN), + status=200, content_type='application/json', json=b) + + agent = self.api.agents.update_agent(a['id'], **values) + + self.assertEqual(agent.occasional, True) + self.assertEqual(agent.contact['name'], 'Updated Name') + + @responses.activate + def test_delete_agent(self): + responses.add(responses.DELETE, + 'https://{}/api/v2/agents/1'.format(DOMAIN), + status=204) + self.api.agents.delete_agent(1) + + def test_agent_name(self): + self.assertEqual(self.agent.contact['name'], 'Support') + + def test_agent_mobile(self): + self.assertEqual(self.agent.contact['mobile'], 1234) + + def test_agent_state(self): + self.assertEqual(self.agent.available, True) + self.assertEqual(self.agent.occasional, False) + + def test_agent_datetime(self): + self.assertIsInstance(self.agent.created_at, datetime.datetime) + self.assertIsInstance(self.agent.updated_at, datetime.datetime) + + def test_none_filter_name(self): + agents = self.api.agents.list_agents() + self.assertIsInstance(agents, list) + self.assertEqual(len(agents), 2) + self.assertEqual(agents[0].id, self.agent.id) From 03aa2412363d2deae602b760278b6c2dd14db289 Mon Sep 17 00:00:00 2001 From: prenit-coverfox Date: Mon, 4 Jun 2018 13:07:23 +0530 Subject: [PATCH 14/46] added ticket --- freshdesk/v2/api.py | 15 ++++++++++++++- freshdesk/v2/models.py | 7 +++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/freshdesk/v2/api.py b/freshdesk/v2/api.py index 3e02403..0f89218 100644 --- a/freshdesk/v2/api.py +++ b/freshdesk/v2/api.py @@ -1,7 +1,7 @@ import requests from requests.exceptions import HTTPError import json -from freshdesk.v2.models import Ticket, Comment, Customer, Contact, Group, Company, Agent, Role +from freshdesk.v2.models import Ticket, Comment, Customer, Contact, Group, Company, Agent, Role, TicketField class TicketAPI(object): @@ -192,6 +192,18 @@ def get_role(self, role_id): return Role(**self._api._get(url)) +class TicketFieldAPI(object): + def __init__(self, api): + self._api = api + + def list_ticket_fields(self, ): + url = 'ticket_fields' + ticket_fields = [] + for tt in self._api._get(url): + ticket_fields.append(TicketField(**tt)) + return ticket_fields + + class AgentAPI(object): def __init__(self, api): self._api = api @@ -281,6 +293,7 @@ def __init__(self, domain, api_key): self.customers = CustomerAPI(self) self.agents = AgentAPI(self) self.roles = RoleAPI(self) + self.ticket_fields = TicketFieldAPI(self) if domain.find('freshdesk.com') < 0: raise AttributeError('Freshdesk v2 API works only via Freshdesk' diff --git a/freshdesk/v2/models.py b/freshdesk/v2/models.py index 1d966fc..7e0bf8b 100644 --- a/freshdesk/v2/models.py +++ b/freshdesk/v2/models.py @@ -22,6 +22,13 @@ def _to_timestamp(self, timestamp_str): return dateutil.parser.parse(timestamp_str) +class TicketField(FreshdeskModel): + def __str__(self): + return self.name + + def __repr__(self): + return ''.format(self.name, self.description) + class Ticket(FreshdeskModel): def __str__(self): return self.subject From 9d5dff0b3f2fe6a2b308ec76b974e42d681c82e1 Mon Sep 17 00:00:00 2001 From: prenit-coverfox Date: Mon, 4 Jun 2018 13:31:37 +0530 Subject: [PATCH 15/46] added type filter to ticket_fields api --- freshdesk/v2/api.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/freshdesk/v2/api.py b/freshdesk/v2/api.py index 0f89218..8b37e4c 100644 --- a/freshdesk/v2/api.py +++ b/freshdesk/v2/api.py @@ -196,9 +196,13 @@ class TicketFieldAPI(object): def __init__(self, api): self._api = api - def list_ticket_fields(self, ): + def list_ticket_fields(self, type=None): url = 'ticket_fields' - ticket_fields = [] + ticket_fields = [] + + if type: + url = "{}?type={}".format(url, type) + for tt in self._api._get(url): ticket_fields.append(TicketField(**tt)) return ticket_fields From 0f53d11a77a4be06ec6ccf0356a546e853ee36f4 Mon Sep 17 00:00:00 2001 From: prenit-coverfox Date: Mon, 4 Jun 2018 14:53:25 +0530 Subject: [PATCH 16/46] changed list_ticket_fields method argument --- freshdesk/v2/api.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/freshdesk/v2/api.py b/freshdesk/v2/api.py index 8b37e4c..4d67035 100644 --- a/freshdesk/v2/api.py +++ b/freshdesk/v2/api.py @@ -196,15 +196,15 @@ class TicketFieldAPI(object): def __init__(self, api): self._api = api - def list_ticket_fields(self, type=None): + def list_ticket_fields(self, **kwargs): url = 'ticket_fields' ticket_fields = [] - if type: - url = "{}?type={}".format(url, type) + if kwargs.has_key('type'): + url = "{}?type={}".format(url, kwargs['type']) - for tt in self._api._get(url): - ticket_fields.append(TicketField(**tt)) + for tf in self._api._get(url): + ticket_fields.append(TicketField(**tf)) return ticket_fields From 732324decad127347a569118494994fff4888422 Mon Sep 17 00:00:00 2001 From: Sam Kingston Date: Mon, 4 Jun 2018 20:50:23 +1000 Subject: [PATCH 17/46] Version bump --- .gitignore | 1 + CHANGELOG.md | 4 ++++ README.md | 3 +++ freshdesk/__init__.py | 2 +- 4 files changed, 9 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 34357dc..ba6d13d 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ dist/ /.coverage /cover .tox +build/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 3150f3c..4a27453 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ Changelog ========= +v1.1.1 - 2018-06-04 + + * #16: Add support for Role, Ticket fields and Agent API (@prenit-coverfox) + v1.1.0 - 2018-01-08 * #13: Fixed group representation (@helix90) diff --git a/README.md b/README.md index 70f2c2e..9169dde 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ Support for the v2 API includes the following features: - [Create](http://developer.freshdesk.com/api/#create_ticket) - [Update](http://developer.freshdesk.com/api/#update_ticket) - [Delete](http://developer.freshdesk.com/api/#delete_a_ticket) + - Custom ticket fields (as of 1.1.1) * [Comments](http://developer.freshdesk.com/api/#conversations) (known as Conversations in Freshdesk) - [List](http://developer.freshdesk.com/api/#list_all_ticket_notes) - [Create note](http://developer.freshdesk.com/api/#add_note_to_a_ticket) @@ -28,6 +29,8 @@ Support for the v2 API includes the following features: - [List](http://developer.freshdesk.com/api/#list_all_groups) - [Get](http://developer.freshdesk.com/api/#view_group) * [Company](https://developers.freshdesk.com/api/#companies) +* [Roles](https://developers.freshdesk.com/api/#roles) - from 1.1.1 +* [Agents](https://developers.freshdesk.com/api/#agents) - from 1.1.1 ## Installation diff --git a/freshdesk/__init__.py b/freshdesk/__init__.py index 1a72d32..b3ddbc4 100644 --- a/freshdesk/__init__.py +++ b/freshdesk/__init__.py @@ -1 +1 @@ -__version__ = '1.1.0' +__version__ = '1.1.1' From 112ad23ce1946e0a29266150120869c6d25076bf Mon Sep 17 00:00:00 2001 From: Sam Kingston Date: Mon, 4 Jun 2018 20:51:56 +1000 Subject: [PATCH 18/46] Version bump to fix release stuff up --- CHANGELOG.md | 4 ++++ freshdesk/__init__.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a27453..873ce0e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ Changelog ========= +v1.1.2 - 2018-06-04 + + * No code changes: fix release stuff up (@sjkingo) + v1.1.1 - 2018-06-04 * #16: Add support for Role, Ticket fields and Agent API (@prenit-coverfox) diff --git a/freshdesk/__init__.py b/freshdesk/__init__.py index b3ddbc4..7b344ec 100644 --- a/freshdesk/__init__.py +++ b/freshdesk/__init__.py @@ -1 +1 @@ -__version__ = '1.1.1' +__version__ = '1.1.2' From 2b462f415b544e17bc142cde08c67f3bdf958e67 Mon Sep 17 00:00:00 2001 From: prenit-coverfox <37108931+prenit-coverfox@users.noreply.github.com> Date: Wed, 27 Jun 2018 07:07:30 +0530 Subject: [PATCH 19/46] Expand on v1 and v2 of API (#18) * added create ticket api wrapper in v1 * added create_contact method in v1 and v2 * added make_agent method to contact api v2 * changed test classes and added tests for create ticket and agent * tests added for Contact, Agent, Role, Group api methods in v2 * tests added for Contact, Agent api methods in v1 * changed list_contacts filter comment --- freshdesk/v1/api.py | 204 +++++++++++++- freshdesk/v1/models.py | 9 + freshdesk/v1/sample_json_data/agent_1.json | 36 +++ .../v1/sample_json_data/agent_1_updated.json | 36 +++ freshdesk/v1/sample_json_data/agents.json | 74 +++++ freshdesk/v1/sample_json_data/contact.json | 28 +- .../sample_json_data/contact5004272350.json | 1 - freshdesk/v1/sample_json_data/contacts.json | 56 ++++ freshdesk/v1/sample_json_data/ticket_1.json | 80 +++++- freshdesk/v1/test.py | 207 ++++++++++++-- freshdesk/v2/api.py | 91 ++++++- freshdesk/v2/sample_json_data/agent_1.json | 4 +- .../v2/sample_json_data/agent_1_updated.json | 23 ++ freshdesk/v2/sample_json_data/contacts.json | 49 ++++ freshdesk/v2/sample_json_data/note_1.json | 18 ++ .../v2/sample_json_data/outbound_email_1.json | 42 +++ freshdesk/v2/sample_json_data/reply_1.json | 20 ++ .../v2/sample_json_data/ticket_1_updated.json | 42 +++ freshdesk/v2/test.py | 252 ++++++++++-------- 19 files changed, 1114 insertions(+), 158 deletions(-) create mode 100644 freshdesk/v1/sample_json_data/agent_1.json create mode 100644 freshdesk/v1/sample_json_data/agent_1_updated.json create mode 100644 freshdesk/v1/sample_json_data/agents.json delete mode 100644 freshdesk/v1/sample_json_data/contact5004272350.json create mode 100644 freshdesk/v1/sample_json_data/contacts.json create mode 100644 freshdesk/v2/sample_json_data/agent_1_updated.json create mode 100644 freshdesk/v2/sample_json_data/contacts.json create mode 100644 freshdesk/v2/sample_json_data/note_1.json create mode 100644 freshdesk/v2/sample_json_data/outbound_email_1.json create mode 100644 freshdesk/v2/sample_json_data/reply_1.json create mode 100644 freshdesk/v2/sample_json_data/ticket_1_updated.json diff --git a/freshdesk/v1/api.py b/freshdesk/v1/api.py index 2c8dd72..1492668 100644 --- a/freshdesk/v1/api.py +++ b/freshdesk/v1/api.py @@ -1,12 +1,31 @@ import requests +import json from requests.exceptions import HTTPError -from freshdesk.v1.models import Ticket, Contact, Customer, TimeEntry +from freshdesk.v1.models import Ticket, Contact, Agent, Customer, TimeEntry class TicketAPI(object): def __init__(self, api): self._api = api + def create_ticket(self, subject, **kwargs): + url = 'helpdesk/tickets.json' + status = kwargs.get('status', 2) + priority = kwargs.get('priority', 1) + cc_emails = ','.join(kwargs.get('cc_emails', [])) + ticket_data = { + 'subject': subject, + 'status': status, + 'priority': priority, + } + ticket_data.update(kwargs) + data = { + 'helpdesk_ticket': ticket_data, + 'cc_emails': cc_emails, + } + + return Ticket(**self._api._post(url, data=data)['helpdesk_ticket']) + def get_ticket(self, ticket_id): """Fetches the ticket for the given ticket ID""" url = 'helpdesk/tickets/%d.json' % ticket_id @@ -59,10 +78,118 @@ class ContactAPI(object): def __init__(self, api): self._api = api + def list_contacts(self, **kwargs): + """ + List all contacts, optionally filtered by a query. Specify filters as + query keyword argument, such as: + + query= email is abc@xyz.com, + query= mobile is 1234567890, + query= phone is 1234567890, + + contacts can be filtered by name such as; + + letter=Prenit + + Passing None means that no named filter will be passed to + Freshdesk, which returns list of all contacts + + """ + + url = 'contacts.json?' + if 'query' in kwargs.keys(): + filter_query = kwargs.pop('query') + url = url + "query={}".format(filter_query) + + if 'state' in kwargs.keys(): + state_query = kwargs.pop('state') + url = url + "state={}".format(state_query) + + if 'letter' in kwargs.keys(): + name_query = kwargs.pop('letter') + url = url + "letter={}".format(name_query) + + contacts = self._api._get(url) + return [Contact(**c['user']) for c in contacts] + + def create_contact(self, *args, **kwargs): + """Creates a contact""" + url = 'contacts.json' + contact_data = { + 'active': True, + 'helpdesk_agent': False, + 'description': 'Freshdesk Contact' + } + contact_data.update(kwargs) + payload = { + 'user': contact_data + } + + return Contact(**self._api._post(url, data=payload)['user']) + + def make_agent(self, contact_id): + url = 'contacts/%d/make_agent.json' % contact_id + agent = self._api._put(url)['agent'] + return self._api.agents.get_agent(agent['id']) + def get_contact(self, contact_id): - url = 'contacts/%s.json' % contact_id + url = 'contacts/%d.json' % contact_id return Contact(**self._api._get(url)['user']) + def delete_contact(self, contact_id): + url = 'contacts/%d.json' % contact_id + self._api._delete(url) + + +class AgentAPI(object): + def __init__(self, api): + self._api = api + + def list_agents(self, **kwargs): + """List all agents, optionally filtered by a query. Specify filters as + query keyword argument, such as: + + query= email is abc@xyz.com, + query= mobile is 1234567890, + query= phone is 1234567890, + + agents can be filtered by state such as: + + state=active/occasional + + Passing None means that no named filter will be passed to + Freshdesk, which returns list of all agents + + """ + + url = 'agents.json?' + if 'query' in kwargs.keys(): + filter_query = kwargs.pop('query') + url = url + "query={}".format(filter_query) + + if 'state' in kwargs.keys(): + state_query = kwargs.pop('state') + url = url + "state={}".format(state_query) + + agents = self._api._get(url) + return [Agent(**a['agent']) for a in agents] + + def get_agent(self, agent_id): + """Fetches the agent for the given agent ID""" + url = 'agents/%s.json' % agent_id + return Agent(**self._api._get(url)['agent']) + + def update_agent(self, agent_id, **kwargs): + """Updates an agent""" + url = 'agents/%s.json' % agent_id + agent = self._api._put(url, data=json.dumps(kwargs))['agent'] + return Agent(**agent) + + def delete_agent(self, agent_id): + """Delete the agent for the given agent ID""" + url = 'agents/%d.json' % agent_id + self._api._delete(url) + class CustomerAPI(object): def __init__(self, api): @@ -115,24 +242,77 @@ def __init__(self, domain, api_key): .tickets: the Ticket API """ - self._api_prefix = 'http://{}/'.format(domain.rstrip('/')) - self._session = requests.Session() - self._session.auth = (api_key, 'unused_with_api_key') - self._session.headers = {'Content-Type': 'application/json'} + self._api_prefix = 'https://{}/'.format(domain.rstrip('/')) + self.auth = (api_key, 'X') + self.headers = {'Content-Type': 'application/json'} self.tickets = TicketAPI(self) self.contacts = ContactAPI(self) + self.agents = AgentAPI(self) self.timesheets = TimeAPI(self) self.customers = CustomerAPI(self) def _get(self, url, params={}): """Wrapper around request.get() to use the API prefix. Returns a JSON response.""" - r = self._session.get(self._api_prefix + url, params=params) - r.raise_for_status() - if 'Retry-After' in r.headers: + r = requests.get(self._api_prefix + url, + params=params, + headers=self.headers, + auth=self.auth, + ) + return self._action(r) + + def _post(self, url, data={}): + """Wrapper around request.post() to use the API prefix. Returns a JSON response.""" + r = requests.post(self._api_prefix + url, + data=json.dumps(data), + headers=self.headers, + auth=self.auth, + allow_redirects=False, + ) + return self._action(r) + + def _put(self, url, data={}): + """Wrapper around request.put() to use the API prefix. Returns a JSON response.""" + r = requests.put(self._api_prefix + url, + data=json.dumps(data), + headers=self.headers, + auth=self.auth, + allow_redirects=False, + ) + return self._action(r) + + def _delete(self, url): + """Wrapper around request.delete() to use the API prefix. Returns a JSON response.""" + r = requests.delete(self._api_prefix + url, + headers=self.headers, + auth=self.auth, + allow_redirects=False, + ) + return self._action(r) + + def _action(self, res): + """Returns JSON response or raise exception if errors are present""" + try: + j = res.json() + except: + res.raise_for_status() + j = {} + + if 'Retry-After' in res.headers: raise HTTPError('403 Forbidden: API rate-limit has been reached until {}.' - 'See http://freshdesk.com/api#ratelimit'.format(r.headers['Retry-After'])) - j = r.json() + 'See http://freshdesk.com/api#ratelimit'.format(res.headers['Retry-After'])) + if 'require_login' in j: raise HTTPError('403 Forbidden: API key is incorrect for this domain') - return r.json() + + if 'error' in j: + raise HTTPError('{}: {}'.format(j.get('description'), + j.get('errors'))) + + # Catch any other errors + try: + res.raise_for_status() + except Exception as e: + raise HTTPError("{}: {}".format(e, j)) + + return j diff --git a/freshdesk/v1/models.py b/freshdesk/v1/models.py index 7fb600a..825a630 100644 --- a/freshdesk/v1/models.py +++ b/freshdesk/v1/models.py @@ -13,6 +13,7 @@ def __init__(self, **kwargs): k = '_' + k setattr(self, k, v) self._keys.add(k) + self.created_at = self._to_timestamp(self.created_at) self.updated_at = self._to_timestamp(self.updated_at) @@ -68,6 +69,14 @@ def __repr__(self): return ''.format(self.name) +class Agent(FreshdeskModel): + def __str__(self): + return self.user['name'] + + def __repr__(self): + return ''.format(self.id, self.user['name']) + + class TimeEntry(FreshdeskModel): def __str__(self): return str(self.id) diff --git a/freshdesk/v1/sample_json_data/agent_1.json b/freshdesk/v1/sample_json_data/agent_1.json new file mode 100644 index 0000000..0c01eef --- /dev/null +++ b/freshdesk/v1/sample_json_data/agent_1.json @@ -0,0 +1,36 @@ +{ + "agent":{ + "active_since":null, + "available":true, + "created_at":"2015-01-02T22:56:39-10:00", + "id":1, + "occasional":false, + "points":0, + "scoreboard_level_id":1, + "signature":null, + "signature_html":"\u003Cp\u003E\u003Cbr\u003E\u003C/p\u003E\r\n", + "ticket_permission":1, + "updated_at":"2015-01-04T23:09:52-10:00", + "user":{ + "active":false, + "address":null, + "created_at":"2014-12-30T18:49:39-10:00", + "deleted":false, + "description":null, + "email":"rachel@freshdesk.com", + "external_id":null, + "fb_profile_id":null, + "helpdesk_agent":true, + "id":1, + "job_title":"Agent", + "language":"en", + "mobile":1234, + "name":"Rachel", + "phone":5678, + "time_zone":"Chennai", + "twitter_id":null, + "updated_at":"2015-01-04T23:09:51-10:00" + }, + "user_id":1 + } +} diff --git a/freshdesk/v1/sample_json_data/agent_1_updated.json b/freshdesk/v1/sample_json_data/agent_1_updated.json new file mode 100644 index 0000000..e2e8a9b --- /dev/null +++ b/freshdesk/v1/sample_json_data/agent_1_updated.json @@ -0,0 +1,36 @@ +{ + "agent":{ + "active_since":null, + "available":true, + "created_at":"2015-01-02T22:56:39-10:00", + "id":1, + "occasional":true, + "points":0, + "scoreboard_level_id":1, + "signature":null, + "signature_html":"\u003Cp\u003E\u003Cbr\u003E\u003C/p\u003E\r\n", + "ticket_permission":1, + "updated_at":"2015-01-04T23:09:52-10:00", + "user":{ + "active":false, + "address":null, + "created_at":"2014-12-30T18:49:39-10:00", + "deleted":false, + "description":null, + "email":"rachel@freshdesk.com", + "external_id":null, + "fb_profile_id":null, + "helpdesk_agent":true, + "id":1, + "job_title":"Agent", + "language":"en", + "mobile":1234, + "name":"Updated Name", + "phone":5678, + "time_zone":"Chennai", + "twitter_id":null, + "updated_at":"2015-01-04T23:09:51-10:00" + }, + "user_id":1 + } +} diff --git a/freshdesk/v1/sample_json_data/agents.json b/freshdesk/v1/sample_json_data/agents.json new file mode 100644 index 0000000..a513fe6 --- /dev/null +++ b/freshdesk/v1/sample_json_data/agents.json @@ -0,0 +1,74 @@ +[ + { + "agent":{ + "active_since":null, + "available":true, + "created_at":"2015-01-02T22:56:39-10:00", + "id":1, + "occasional":false, + "points":0, + "scoreboard_level_id":1, + "signature":null, + "signature_html":"\u003Cp\u003E\u003Cbr\u003E\u003C/p\u003E\r\n", + "ticket_permission":1, + "updated_at":"2015-01-04T23:09:52-10:00", + "user":{ + "active":false, + "address":null, + "created_at":"2014-12-30T18:49:39-10:00", + "deleted":false, + "description":null, + "email":"rachel@freshdesk.com", + "external_id":null, + "fb_profile_id":null, + "helpdesk_agent":true, + "id":1, + "job_title":"Agent", + "language":"en", + "mobile":1234, + "name":"Rachel", + "phone":5678, + "time_zone":"Chennai", + "twitter_id":null, + "updated_at":"2015-01-04T23:09:51-10:00" + }, + "user_id":1 + } + }, + { + "agent":{ + "active_since":null, + "available":true, + "created_at":"2015-01-02T22:56:39-10:00", + "id":2, + "occasional":false, + "points":0, + "scoreboard_level_id":1, + "signature":null, + "signature_html":"\u003Cp\u003E\u003Cbr\u003E\u003C/p\u003E\r\n", + "ticket_permission":1, + "updated_at":"2015-01-04T23:09:52-10:00", + "user":{ + "active":false, + "address":null, + "created_at":"2014-12-30T18:49:39-10:00", + "deleted":false, + "description":null, + "email":"sam@freshdesk.com", + "external_id":null, + "fb_profile_id":null, + "helpdesk_agent":true, + "id":2, + "job_title":"Agent", + "language":"en", + "mobile":"", + "name":"Sam", + "phone":"", + "time_zone":"Chennai", + "twitter_id":null, + "updated_at":"2015-01-04T23:09:51-10:00" + }, + "user_id":2 + } + } +] diff --git a/freshdesk/v1/sample_json_data/contact.json b/freshdesk/v1/sample_json_data/contact.json index 2aacd04..388f973 100644 --- a/freshdesk/v1/sample_json_data/contact.json +++ b/freshdesk/v1/sample_json_data/contact.json @@ -1 +1,27 @@ -{"user":{"active":false,"address":null,"created_at":"2014-12-31T12:27:09+10:00","customer_id":1,"deleted":false,"description":null,"email":"rachel@freshdesk.com","external_id":null,"fb_profile_id":null,"helpdesk_agent":false,"id":5004272351,"job_title":null,"language":"en","mobile":null,"name":"Rachel","phone":null,"time_zone":"Brisbane","twitter_id":null,"updated_at":"2014-12-31T12:27:09+10:00","company_id":null,"custom_field":{}}} +{ + "user":{ + "active":false, + "address":null, + "created_at":"2014-12-31T12:27:09+10:00", + "customer_id":1, + "deleted":false, + "description":null, + "email":"rachel@freshdesk.com", + "external_id":null, + "fb_profile_id":null, + "helpdesk_agent":false, + "id":1, + "job_title":null, + "language":"en", + "mobile":null, + "name":"Rachel", + "phone":null, + "time_zone":"Brisbane", + "twitter_id":null, + "updated_at":"2014-12-31T12:27:09+10:00", + "company_id":null, + "custom_field":{ + + } + } +} diff --git a/freshdesk/v1/sample_json_data/contact5004272350.json b/freshdesk/v1/sample_json_data/contact5004272350.json deleted file mode 100644 index 35da56d..0000000 --- a/freshdesk/v1/sample_json_data/contact5004272350.json +++ /dev/null @@ -1 +0,0 @@ -{"user":{"active":false,"address":null,"created_at":"2014-12-31T12:27:09+10:00","customer_id":null,"deleted":false,"description":null,"email":"william@freshdesk.com","external_id":null,"fb_profile_id":null,"helpdesk_agent":true,"id":5004272350,"job_title":"Helpdesk Engineer","language":"en","mobile":null,"name":"William","phone":null,"time_zone":"Amsterdam","twitter_id":null,"updated_at":"2014-12-31T12:27:09+10:00","company_id":null,"custom_field":{}}} diff --git a/freshdesk/v1/sample_json_data/contacts.json b/freshdesk/v1/sample_json_data/contacts.json new file mode 100644 index 0000000..5565162 --- /dev/null +++ b/freshdesk/v1/sample_json_data/contacts.json @@ -0,0 +1,56 @@ +[ + { + "user":{ + "active":false, + "address":null, + "created_at":"2014-12-31T12:27:09+10:00", + "customer_id":1, + "deleted":false, + "description":null, + "email":"rachel@freshdesk.com", + "external_id":null, + "fb_profile_id":null, + "helpdesk_agent":false, + "id":1, + "job_title":null, + "language":"en", + "mobile":null, + "name":"Rachel", + "phone":null, + "time_zone":"Brisbane", + "twitter_id":null, + "updated_at":"2014-12-31T12:27:09+10:00", + "company_id":null, + "custom_field":{ + + } + } + }, + { + "user":{ + "active":false, + "address":null, + "created_at":"2014-12-31T12:27:09+10:00", + "customer_id":2, + "deleted":false, + "description":null, + "email":"sam@freshdesk.com", + "external_id":null, + "fb_profile_id":null, + "helpdesk_agent":false, + "id":2, + "job_title":null, + "language":"en", + "mobile":null, + "name":"Sam", + "phone":null, + "time_zone":"Brisbane", + "twitter_id":null, + "updated_at":"2014-12-31T12:27:09+10:00", + "company_id":null, + "custom_field":{ + + } + } + } +] diff --git a/freshdesk/v1/sample_json_data/ticket_1.json b/freshdesk/v1/sample_json_data/ticket_1.json index d5a2615..ba27666 100644 --- a/freshdesk/v1/sample_json_data/ticket_1.json +++ b/freshdesk/v1/sample_json_data/ticket_1.json @@ -1 +1,79 @@ -{"helpdesk_ticket":{"cc_email":{"cc_emails":[],"fwd_emails":[],"reply_cc":[]},"created_at":"2014-12-31T12:27:09+10:00","deleted":false,"delta":true,"description":"This is a sample ticket, feel free to delete it.","description_html":"\u003Cdiv\u003EThis is a sample ticket, feel free to delete it.\u003C/div\u003E","display_id":1,"due_by":"2015-01-05T12:27:09+10:00","email_config_id":null,"frDueBy":"2015-01-01T12:27:09+10:00","fr_escalated":false,"group_id":null,"id":5007268642,"isescalated":false,"notes":[{"note":{"body":"This is a reply.","body_html":"\u003Cdiv style=\"font-size: 13px; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;\"\u003E\n\u003Cdiv\u003E\u003C/div\u003E\n\u003Cdiv\u003E\u003Cdiv\u003EThis is a reply.\u003C/div\u003E\u003C/div\u003E\r\n\u003C/div\u003E","created_at":"2015-01-01T10:58:36+10:00","deleted":false,"id":5006713943,"incoming":false,"private":false,"source":0,"updated_at":"2015-01-01T10:58:36+10:00","user_id":5004272350,"attachments":[],"support_email":"support@pythonfreshdesk.freshdesk.com"}}],"owner_id":null,"priority":1,"requester_id":5004272351,"responder_id":5004272350,"source":2,"spam":false,"status":2,"subject":"This is a sample ticket","ticket_type":"Question","to_email":null,"trained":false,"updated_at":"2015-01-01T10:58:39+10:00","urgent":false,"status_name":"Open","requester_status_name":"Being Processed","priority_name":"Low","source_name":"Portal","requester_name":"Rachel","responder_name":"William","to_emails":null,"product_id":null,"attachments":[],"custom_field":{},"tags":[]}} +{ + "helpdesk_ticket": { + "cc_email": { + "cc_emails": [ + "test2@example.com" + ], + "fwd_emails": [ + + ], + "reply_cc": [ + + ] + }, + "created_at": "2014-12-31T12:27:09+10:00", + "deleted": false, + "delta": true, + "description": "This is a sample ticket, feel free to delete it.", + "description_html": "
This is a sample ticket, feel free to delete it.<\/div>", + "display_id": 1, + "due_by": "2015-01-05T12:27:09+10:00", + "email_config_id": null, + "frDueBy": "2015-01-01T12:27:09+10:00", + "fr_escalated": false, + "group_id": null, + "id": 5007268642, + "isescalated": false, + "notes": [ + { + "note": { + "body": "This is a reply.", + "body_html": "
\n
<\/div>\n
This is a reply.<\/div><\/div>\r\n<\/div>", + "created_at": "2015-01-01T10:58:36+10:00", + "deleted": false, + "id": 5006713943, + "incoming": false, + "private": false, + "source": 0, + "updated_at": "2015-01-01T10:58:36+10:00", + "user_id": 5004272350, + "attachments": [ + + ], + "support_email": "support@pythonfreshdesk.freshdesk.com" + } + } + ], + "owner_id": null, + "priority": 1, + "requester_id": 5004272351, + "responder_id": 5004272350, + "source": 2, + "spam": false, + "status": 2, + "subject": "This is a sample ticket", + "ticket_type": "Question", + "to_email": null, + "trained": false, + "updated_at": "2015-01-01T10:58:39+10:00", + "urgent": false, + "status_name": "Open", + "requester_status_name": "Being Processed", + "priority_name": "Low", + "source_name": "Portal", + "requester_name": "Rachel", + "responder_name": "William", + "to_emails": null, + "product_id": null, + "attachments": [ + + ], + "custom_field": { + + }, + "tags": [ + "foo", + "bar" + ] + } +} diff --git a/freshdesk/v1/test.py b/freshdesk/v1/test.py index 8cdad66..953cff1 100644 --- a/freshdesk/v1/test.py +++ b/freshdesk/v1/test.py @@ -6,8 +6,7 @@ from unittest import TestCase from freshdesk.v1.api import API -from freshdesk.v1.models import Ticket, Comment, Contact, Customer, TimeEntry - +from freshdesk.v1.models import Ticket, Comment, Contact, Customer, TimeEntry, Agent """ Test suite for python-freshdesk. @@ -23,20 +22,42 @@ class MockedAPI(API): def __init__(self, *args): self.resolver = { - re.compile(r'helpdesk/tickets/filter/all_tickets\?format=json&page=1'): self.read_test_file('all_tickets.json'), - re.compile(r'helpdesk/tickets/filter/new_my_open\?format=json&page=1'): self.read_test_file('all_tickets.json'), - re.compile(r'helpdesk/tickets/filter/spam\?format=json&page=1'): [], - re.compile(r'helpdesk/tickets/filter/deleted\?format=json&page=1'): [], - re.compile(r'helpdesk/tickets/1/time_sheets.json'): self.read_test_file('timeentries_ticket_1.json'), - re.compile(r'helpdesk/tickets/1.json'): self.read_test_file('ticket_1.json'), - re.compile(r'.*&page=2'): [], - re.compile(r'contacts/5004272351.json'): self.read_test_file('contact.json'), - re.compile(r'contacts/5004272350.json'): self.read_test_file('contact5004272350.json'), - re.compile(r'customers/1.json'): self.read_test_file('customer.json'), - re.compile(r'helpdesk/time_sheets.json'): self.read_test_file('timeentries_ticket_1.json'), - re.compile(r'helpdesk/time_sheets.json\?agent_id='): self.read_test_file('timeentries_ticket_1.json'), - re.compile(r'helpdesk/time_sheets.json'): self.read_test_file('timeentries_ticket_1.json'), + 'get': { + re.compile(r'helpdesk/tickets/filter/all_tickets\?format=json&page=1'): self.read_test_file( + 'all_tickets.json'), + re.compile(r'helpdesk/tickets/filter/new_my_open\?format=json&page=1'): self.read_test_file( + 'all_tickets.json'), + re.compile(r'helpdesk/tickets/filter/spam\?format=json&page=1'): [], + re.compile(r'helpdesk/tickets/filter/deleted\?format=json&page=1'): [], + re.compile(r'helpdesk/tickets/1/time_sheets.json'): self.read_test_file('timeentries_ticket_1.json'), + re.compile(r'helpdesk/tickets/1.json'): self.read_test_file('ticket_1.json'), + re.compile(r'.*&page=2'): [], + re.compile(r'contacts.json'): self.read_test_file('contacts.json'), + re.compile(r'contacts/1.json'): self.read_test_file('contact.json'), + re.compile(r'contacts/1.json'): self.read_test_file('contact.json'), + re.compile(r'agents.json\?$'): self.read_test_file('agents.json'), + re.compile(r'agents/1.json$'): self.read_test_file('agent_1.json'), + re.compile(r'customers/1.json'): self.read_test_file('customer.json'), + re.compile(r'helpdesk/time_sheets.json'): self.read_test_file('timeentries_ticket_1.json'), + re.compile(r'helpdesk/time_sheets.json\?agent_id='): self.read_test_file('timeentries_ticket_1.json'), + re.compile(r'helpdesk/time_sheets.json'): self.read_test_file('timeentries_ticket_1.json'), + }, + 'post': { + re.compile(r'helpdesk/tickets.json'): self.read_test_file('ticket_1.json'), + re.compile(r'contacts.json'): self.read_test_file('contact.json'), + re.compile(r'agents/1.json$'): self.read_test_file('agent_1.json'), + }, + 'put': { + re.compile(r'contacts/1/make_agent.json'): self.read_test_file('agent_1.json'), + re.compile(r'agents/1.json$'): self.read_test_file('agent_1_updated.json'), + }, + 'delete': { + re.compile(r'helpdesk/tickets/1.json'): None, + re.compile(r'contacts/1.json'): None, + re.compile(r'agents/1.json$'): None, + } } + super(MockedAPI, self).__init__(*args) def read_test_file(self, filename): @@ -44,7 +65,7 @@ def read_test_file(self, filename): return json.loads(open(path, 'r').read()) def _get(self, url, *args, **kwargs): - for pattern, j in self.resolver.items(): + for pattern, j in self.resolver['get'].items(): if pattern.match(url): return j @@ -52,18 +73,45 @@ def _get(self, url, *args, **kwargs): from requests.exceptions import HTTPError raise HTTPError('404: mocked_api_get() has no pattern for \'{}\''.format(url)) + def _post(self, url, *args, **kwargs): + for pattern, data in self.resolver['post'].items(): + if pattern.match(url): + return data + + # No match found, raise 404 + from requests.exceptions import HTTPError + raise HTTPError('404: mocked_api_post() has no pattern for \'{}\''.format(url)) + + def _put(self, url, *args, **kwargs): + for pattern, data in self.resolver['put'].items(): + if pattern.match(url): + return data + + # No match found, raise 404 + from requests.exceptions import HTTPError + raise HTTPError('404: mocked_api_put() has no pattern for \'{}\''.format(url)) + + def _delete(self, url, *args, **kwargs): + for pattern, data in self.resolver['delete'].items(): + if pattern.match(url): + return data + + # No match found, raise 404 + from requests.exceptions import HTTPError + raise HTTPError('404: mocked_api_delete() has no pattern for \'{}\''.format(url)) + class TestAPIClass(TestCase): def test_api_prefix(self): api = API('test_domain', 'test_key') - self.assertEqual(api._api_prefix, 'http://test_domain/') + self.assertEqual(api._api_prefix, 'https://test_domain/') api = API('test_domain/', 'test_key') - self.assertEqual(api._api_prefix, 'http://test_domain/') + self.assertEqual(api._api_prefix, 'https://test_domain/') @responses.activate def test_403_error(self): responses.add(responses.GET, - 'http://{}/helpdesk/tickets/1.json'.format(DOMAIN), + 'https://{}/helpdesk/tickets/1.json'.format(DOMAIN), status=403) api = API(DOMAIN, 'invalid_api_key') @@ -75,7 +123,7 @@ def test_403_error(self): def test_404_error(self): DOMAIN_404 = 'google.com' responses.add(responses.GET, - 'http://{}/helpdesk/tickets/1.json'.format(DOMAIN_404), + 'https://{}/helpdesk/tickets/1.json'.format(DOMAIN_404), status=404) api = API(DOMAIN_404, 'invalid_api_key') @@ -89,6 +137,9 @@ class TestTicket(TestCase): def setUpClass(cls): cls.api = MockedAPI(DOMAIN, API_KEY) cls.ticket = cls.api.tickets.get_ticket(1) + cls.ticket_json = json.loads(open(os.path.join(os.path.dirname(__file__), + 'sample_json_data', + 'ticket_1.json')).read()) def test_str(self): self.assertEqual(str(self.ticket), 'This is a sample ticket') @@ -96,6 +147,22 @@ def test_str(self): def test_repr(self): self.assertEqual(repr(self.ticket), '') + def test_create_ticket(self): + ticket = self.api.tickets.create_ticket('This is a sample ticket', + description='This is a sample ticket, feel free to delete it.', + email='test@example.com', + priority=1, status=2, + tags=['foo', 'bar'], + cc_emails=['test2@example.com']) + self.assertIsInstance(ticket, Ticket) + self.assertEqual(ticket.subject, 'This is a sample ticket') + self.assertEqual(ticket.description, 'This is a sample ticket, feel free to delete it.') + self.assertEqual(ticket.priority, 'low') + self.assertEqual(ticket.status, 'open') + self.assertEqual(ticket.cc_email['cc_emails'], ['test2@example.com']) + self.assertIn('foo', ticket.tags) + self.assertIn('bar', ticket.tags) + def test_get_ticket(self): self.assertIsInstance(self.ticket, Ticket) self.assertEqual(self.ticket.display_id, 1) @@ -175,7 +242,7 @@ class TestContact(TestCase): @classmethod def setUpClass(cls): cls.api = MockedAPI(DOMAIN, API_KEY) - cls.contact = cls.api.contacts.get_contact('5004272351') + cls.contact = cls.api.contacts.get_contact(1) def test_get_contact(self): self.assertIsInstance(self.contact, Contact) @@ -184,6 +251,39 @@ def test_get_contact(self): self.assertEqual(self.contact.helpdesk_agent, False) self.assertEqual(self.contact.customer_id, 1) + def test_list_contacts(self): + contacts = self.api.contacts.list_contacts() + self.assertIsInstance(contacts, list) + self.assertEquals(len(contacts), 2) + self.assertIsInstance(contacts[0], Contact) + self.assertEquals(contacts[0].id, self.contact.id) + self.assertEquals(contacts[0].email, self.contact.email) + self.assertEquals(contacts[0].name, self.contact.name) + + def test_create_contact(self): + contact_data = { + 'name': 'Rachel', + 'email': 'rachel@freshdesk.com' + } + contact = self.api.contacts.create_contact(contact_data) + self.assertIsInstance(contact, Contact) + self.assertEquals(contact.id, self.contact.id) + self.assertEquals(contact.email, self.contact.email) + self.assertEquals(contact.name, self.contact.name) + + def test_make_agent(self): + agent = self.api.contacts.make_agent(self.contact.id) + self.assertIsInstance(agent, Agent) + self.assertEquals(agent.available, True) + self.assertEquals(agent.occasional, False) + self.assertEquals(agent.id, 1) + self.assertEquals(agent.user_id, self.contact.id) + self.assertEquals(agent.user['email'], self.contact.email) + self.assertEquals(agent.user['name'], self.contact.name) + + def test_delete_contact(self): + self.assertEquals(self.api.contacts.delete_contact(1), None) + def test_contact_datetime(self): self.assertIsInstance(self.contact.created_at, datetime.datetime) self.assertIsInstance(self.contact.updated_at, datetime.datetime) @@ -199,8 +299,8 @@ class TestCustomer(TestCase): @classmethod def setUpClass(cls): cls.api = MockedAPI(DOMAIN, API_KEY) - cls.customer = cls.api.customers.get_customer('1') - cls.contact = cls.api.contacts.get_contact('5004272351') + cls.customer = cls.api.customers.get_customer(1) + cls.contact = cls.api.contacts.get_contact(1) def test_customer(self): self.assertIsInstance(self.customer, Customer) @@ -248,3 +348,64 @@ def test_get_all_timesheets(self): self.test_timesheet() self.timesheet = self.api.timesheets.get_all_timesheets(filter_name="agent_id", filter_value="5004272350") self.test_timesheet() + + +class TestAgent(TestCase): + + @classmethod + def setUpClass(cls): + cls.api = MockedAPI(DOMAIN, API_KEY) + cls.agent = cls.api.agents.get_agent(1) + cls.agent_json = json.loads(open(os.path.join(os.path.dirname(__file__), + 'sample_json_data', + 'agent_1.json')).read()) + + def test_str(self): + self.assertEqual(str(self.agent), 'Rachel') + + def test_repr(self): + self.assertEqual(repr(self.agent), '') + + def test_list_agents(self): + agents = self.api.agents.list_agents() + self.assertIsInstance(agents, list) + self.assertEqual(len(agents), 2) + self.assertEqual(agents[0].id, self.agent.id) + + def test_get_agent(self): + self.assertIsInstance(self.agent, Agent) + self.assertEqual(self.agent.id, 1) + self.assertEqual(self.agent.user['name'], 'Rachel') + self.assertEqual(self.agent.user['email'], 'rachel@freshdesk.com') + self.assertEqual(self.agent.user['mobile'], 1234) + self.assertEqual(self.agent.user['phone'], 5678) + self.assertEqual(self.agent.occasional, False) + + def test_update_agent(self): + values = { + 'occasional': True, + 'contact': { + 'name': 'Updated Name' + } + } + agent = self.api.agents.update_agent(1, **values) + + self.assertEqual(agent.occasional, True) + self.assertEqual(agent.user['name'], 'Updated Name') + + def test_delete_agent(self): + self.assertEquals(self.api.agents.delete_agent(1), None) + + def test_agent_name(self): + self.assertEqual(self.agent.user['name'], 'Rachel') + + def test_agent_mobile(self): + self.assertEqual(self.agent.user['mobile'], 1234) + + def test_agent_state(self): + self.assertEqual(self.agent.available, True) + self.assertEqual(self.agent.occasional, False) + + def test_agent_datetime(self): + self.assertIsInstance(self.agent.created_at, datetime.datetime) + self.assertIsInstance(self.agent.updated_at, datetime.datetime) diff --git a/freshdesk/v2/api.py b/freshdesk/v2/api.py index 4d67035..7400abc 100644 --- a/freshdesk/v2/api.py +++ b/freshdesk/v2/api.py @@ -151,10 +151,83 @@ class ContactAPI(object): def __init__(self, api): self._api = api + def list_contacts(self, **kwargs): + """ + List all contacts, optionally filtered by a query. Specify filters as + query keyword argument, such as: + + email=abc@xyz.com, + mobile=1234567890, + phone=1234567890, + + contacts can be filtered by state and company_id such as: + + state=[blocked/deleted/unverified/verified] + company_id=1234 + + contacts updated after a timestamp can be filtered such as; + + _updated_since=2018-01-19T02:00:00Z + + Passing None means that no named filter will be passed to + Freshdesk, which returns list of all contacts + + """ + + url = 'contacts?' + if kwargs: + for filter_name, filter_value in kwargs.items(): + url = url + "{}={}&".format(filter_name, filter_value) + del kwargs[filter_name] + + page = 1 if not 'page' in kwargs else kwargs['page'] + per_page = 10 if not 'per_page' in kwargs else kwargs['per_page'] + contacts = [] + + # Skip pagination by looping over each page and adding contacts + while True: + this_page = self._api._get(url + 'page=%d&per_page=%d' + % (page, per_page), kwargs) + contacts += this_page + if len(this_page) < per_page or 'page' in kwargs: + break + + page += 1 + + return [Contact(**c) for c in contacts] + + def create_contact(self, *args, **kwargs): + """Creates a contact""" + url = 'contacts' + data = { + 'view_all_tickets': False, + 'description': 'Freshdesk Contact' + } + data.update(kwargs) + return Contact(**self._api._post(url, data=json.dumps(data))) + def get_contact(self, contact_id): - url = 'contacts/%s' % contact_id + url = 'contacts/%d' % contact_id return Contact(**self._api._get(url)) + def soft_delete_contact(self, contact_id): + url = 'contacts/%d' % contact_id + self._api._delete(url) + + def permanently_delete_contact(self, contact_id, force=True): + url = 'contacts/%d/hard_delete?force=%r' % (contact_id, force) + self._api._delete(url) + + def make_agent(self, contact_id, **kwargs): + url = 'contacts/%d/make_agent' % contact_id + data = { + 'occasional': False, + 'ticket_scope': 2, + } + data.update(kwargs) + agent = self._api._put(url, data=json.dumps(data)) + return self._api.agents.get_agent(agent['id']) + class CustomerAPI(object): def __init__(self, api): @@ -202,7 +275,7 @@ def list_ticket_fields(self, **kwargs): if kwargs.has_key('type'): url = "{}?type={}".format(url, kwargs['type']) - + for tf in self._api._get(url): ticket_fields.append(TicketField(**tf)) return ticket_fields @@ -215,7 +288,7 @@ def __init__(self, api): def list_agents(self, **kwargs): """List all agents, optionally filtered by a view. Specify filters as keyword arguments, such as: - + { email='abc@xyz.com', phone=873902, @@ -224,7 +297,7 @@ def list_agents(self, **kwargs): } Passing None means that no named filter will be passed to - Freshdesk, which returns list of all agents + Freshdesk, which returns list of all agents Multiple filters are AND'd together. """ @@ -249,12 +322,12 @@ def list_agents(self, **kwargs): page += 1 return [Agent(**a) for a in agents] - + def get_agent(self, agent_id): - """Fetches the agent for the given agent ID""" + """Fetches the agent for the given agent ID""" url = 'agents/%s' % agent_id - return Agent(**self._api._get(url)) - + return Agent(**self._api._get(url)) + def update_agent(self, agent_id, **kwargs): """Updates an agent""" url = 'agents/%s' % agent_id @@ -270,7 +343,7 @@ def currently_authenticated_agent(self): """Fetches currently logged in agent""" url = 'agents/me' return Agent(**self._api._get(url)) - + class API(object): def __init__(self, domain, api_key): diff --git a/freshdesk/v2/sample_json_data/agent_1.json b/freshdesk/v2/sample_json_data/agent_1.json index 96827d6..685f47d 100644 --- a/freshdesk/v2/sample_json_data/agent_1.json +++ b/freshdesk/v2/sample_json_data/agent_1.json @@ -9,12 +9,12 @@ "available_since":null, "contact":{ "active":true, - "email":"abc@xyz.com", + "email":"rachel@freshdesk.com", "job_title":null, "language":"en", "last_login_at":"2015-08-21T14:54:46+05:30", "mobile":1234, - "name":"Support", + "name":"Rachel", "phone":5678, "time_zone":"Chennai", "created_at":"2015-08-18T16:18:05Z", diff --git a/freshdesk/v2/sample_json_data/agent_1_updated.json b/freshdesk/v2/sample_json_data/agent_1_updated.json new file mode 100644 index 0000000..6b74842 --- /dev/null +++ b/freshdesk/v2/sample_json_data/agent_1_updated.json @@ -0,0 +1,23 @@ +{ + "available":true, + "occasional":true, + "signature":null, + "id":1, + "ticket_scope":1, + "created_at":"2015-08-18T16:18:05Z", + "updated_at":"2015-08-18T16:18:05Z", + "available_since":null, + "contact":{ + "active":true, + "email":"abc@xyz.com", + "job_title":null, + "language":"en", + "last_login_at":"2015-08-21T14:54:46+05:30", + "mobile":1234, + "name":"Updated Name", + "phone":5678, + "time_zone":"Chennai", + "created_at":"2015-08-18T16:18:05Z", + "updated_at":"2015-08-25T08:50:20Z" + } +} diff --git a/freshdesk/v2/sample_json_data/contacts.json b/freshdesk/v2/sample_json_data/contacts.json new file mode 100644 index 0000000..615af7e --- /dev/null +++ b/freshdesk/v2/sample_json_data/contacts.json @@ -0,0 +1,49 @@ +[ + { + "active": false, + "address": null, + "created_at": "2014-12-31T12:27:09+10:00", + "customer_id": 1, + "deleted": false, + "description": null, + "email": "rachel@freshdesk.com", + "external_id": null, + "fb_profile_id": null, + "helpdesk_agent": false, + "id": 1, + "job_title": null, + "language": "en", + "mobile": null, + "name": "Rachel", + "phone": null, + "time_zone": "Brisbane", + "twitter_id": null, + "updated_at": "2014-12-31T12:27:09+10:00", + "company_id": null, + "custom_field": {} + }, + { + "active": false, + "address": null, + "created_at": "2014-12-31T12:27:09+10:00", + "customer_id": 1, + "deleted": false, + "description": null, + "email": "rachel@freshdesk.com", + "external_id": null, + "fb_profile_id": null, + "helpdesk_agent": false, + "id": 1, + "job_title": null, + "language": "en", + "mobile": null, + "name": "Rachel", + "phone": null, + "time_zone": "Brisbane", + "twitter_id": null, + "updated_at": "2014-12-31T12:27:09+10:00", + "company_id": null, + "custom_field": {} + } + +] diff --git a/freshdesk/v2/sample_json_data/note_1.json b/freshdesk/v2/sample_json_data/note_1.json new file mode 100644 index 0000000..6f8e3dc --- /dev/null +++ b/freshdesk/v2/sample_json_data/note_1.json @@ -0,0 +1,18 @@ +{ + "body": "
This is a private note
", + "body_text": "This is a private note", + "id": 1, + "incoming": false, + "private": true, + "user_id": 1, + "support_email": null, + "source": 2, + "ticket_id": 1, + "to_emails": [], + "from_email": null, + "cc_emails": null, + "bcc_emails": null, + "created_at": "2016-05-19T05:36:12Z", + "updated_at": "2016-05-19T05:36:12Z", + "attachments": [] +} diff --git a/freshdesk/v2/sample_json_data/outbound_email_1.json b/freshdesk/v2/sample_json_data/outbound_email_1.json new file mode 100644 index 0000000..4be3f39 --- /dev/null +++ b/freshdesk/v2/sample_json_data/outbound_email_1.json @@ -0,0 +1,42 @@ +{ + "cc_emails": [ + "test2@example.com" + ], + "fwd_emails": [], + "reply_cc_emails": [ + "test2@example.com" + ], + "email_config_id": 5000054536, + "fr_escalated": false, + "group_id": null, + "priority": 1, + "priority_name": "Low", + "requester_id": 6012691534, + "responder_id": 6010396333, + "source": 2, + "spam": false, + "status": 2, + "subject": "This is a sample outbound email", + "type": "Question", + "company_id": null, + "id": 1, + "to_emails": null, + "product_id": null, + "created_at": "2016-05-18T04:36:04Z", + "updated_at": "2016-05-18T23:42:50Z", + "due_by": "2016-05-18T23:36:04Z", + "fr_due_by": "2016-05-18T05:36:04Z", + "is_escalated": true, + "description_text": "This is a sample outbound email, feel free to delete it.", + "description": "
This is a sample outbound email, feel free to delete it.
", + "custom_fields": { + "parent_or_child": null, + "parent_ticket_id": null, + "child_ticket_id": null + }, + "tags": [ + "foo", + "bar" + ], + "attachments": [] +} diff --git a/freshdesk/v2/sample_json_data/reply_1.json b/freshdesk/v2/sample_json_data/reply_1.json new file mode 100644 index 0000000..bc785cf --- /dev/null +++ b/freshdesk/v2/sample_json_data/reply_1.json @@ -0,0 +1,20 @@ +{ + "body": "
This is a reply
", + "body_text": "This is a reply", + "id": 2, + "incoming": false, + "private": false, + "user_id": 1, + "support_email": null, + "source": 0, + "ticket_id": 1, + "to_emails": [ + "test@example.com" + ], + "from_email": "Rachel ", + "cc_emails": null, + "bcc_emails": null, + "created_at": "2016-05-19T05:36:14Z", + "updated_at": "2016-05-19T05:36:14Z", + "attachments": [] +} diff --git a/freshdesk/v2/sample_json_data/ticket_1_updated.json b/freshdesk/v2/sample_json_data/ticket_1_updated.json new file mode 100644 index 0000000..ccb5747 --- /dev/null +++ b/freshdesk/v2/sample_json_data/ticket_1_updated.json @@ -0,0 +1,42 @@ +{ + "cc_emails": [ + "test2@example.com" + ], + "fwd_emails": [], + "reply_cc_emails": [ + "test2@example.com" + ], + "email_config_id": null, + "fr_escalated": false, + "group_id": null, + "priority": 3, + "priority_name": "Low", + "requester_id": 6012691534, + "responder_id": 6010396333, + "source": 2, + "spam": false, + "status": 4, + "subject": "Test subject update", + "type": "Question", + "company_id": null, + "id": 1, + "to_emails": null, + "product_id": null, + "created_at": "2016-05-18T04:36:04Z", + "updated_at": "2016-05-18T23:42:50Z", + "due_by": "2016-05-18T23:36:04Z", + "fr_due_by": "2016-05-18T05:36:04Z", + "is_escalated": true, + "description_text": "This is a sample ticket, feel free to delete it.", + "description": "
This is a sample ticket, feel free to delete it.
", + "custom_fields": { + "parent_or_child": null, + "parent_ticket_id": null, + "child_ticket_id": null + }, + "tags": [ + "hello", + "world" + ], + "attachments": [] +} diff --git a/freshdesk/v2/test.py b/freshdesk/v2/test.py index d030f95..11677fa 100644 --- a/freshdesk/v2/test.py +++ b/freshdesk/v2/test.py @@ -2,6 +2,7 @@ import json import re import os.path + import responses from unittest import TestCase @@ -22,26 +23,48 @@ class MockedAPI(API): def __init__(self, *args): self.resolver = { - re.compile(r'tickets\?filter=new_and_my_open&page=1&per_page=100'): self.read_test_file('all_tickets.json'), - re.compile(r'tickets\?filter=deleted&page=1&per_page=100'): self.read_test_file('all_tickets.json'), - re.compile(r'tickets\?filter=spam&page=1&per_page=100'): self.read_test_file('all_tickets.json'), - re.compile(r'tickets\?filter=watching&page=1&per_page=100'): self.read_test_file('all_tickets.json'), - re.compile(r'tickets\?page=1&per_page=100'): self.read_test_file('all_tickets.json'), - re.compile(r'tickets/1$'): self.read_test_file('ticket_1.json'), - re.compile(r'tickets/1/conversations'): self.read_test_file('conversations.json'), - re.compile(r'contacts/1$'): self.read_test_file('contact.json'), - re.compile(r'customers/1$'): self.read_test_file('customer.json'), - re.compile(r'groups$'): self.read_test_file('groups.json'), - re.compile(r'groups/1$'): self.read_test_file('group_1.json'), - re.compile(r'roles$'): self.read_test_file('roles.json'), - re.compile(r'roles/1$'): self.read_test_file('role_1.json'), - re.compile(r'agents\?email=abc@xyz.com&page=1&per_page=100'): self.read_test_file('agent_1.json'), - re.compile(r'agents\?mobile=1234&page=1&per_page=100'): self.read_test_file('agent_1.json'), - re.compile(r'agents\?phone=5678&page=1&per_page=100'): self.read_test_file('agent_1.json'), - re.compile(r'agents\?state=fulltime&page=1&per_page=100'): self.read_test_file('agent_1.json'), - re.compile(r'agents\?page=1&per_page=100'): self.read_test_file('agents.json'), - re.compile(r'agents/1$'): self.read_test_file('agent_1.json'), + 'get': { + re.compile(r'tickets\?filter=new_and_my_open&page=1&per_page=100'): self.read_test_file('all_tickets.json'), + re.compile(r'tickets\?filter=deleted&page=1&per_page=100'): self.read_test_file('all_tickets.json'), + re.compile(r'tickets\?filter=spam&page=1&per_page=100'): self.read_test_file('all_tickets.json'), + re.compile(r'tickets\?filter=watching&page=1&per_page=100'): self.read_test_file('all_tickets.json'), + re.compile(r'tickets\?page=1&per_page=100'): self.read_test_file('all_tickets.json'), + re.compile(r'tickets/1$'): self.read_test_file('ticket_1.json'), + re.compile(r'tickets/1/conversations'): self.read_test_file('conversations.json'), + re.compile(r'contacts\?page=1&per_page=10$'): self.read_test_file('contacts.json'), + re.compile(r'contacts/1$'): self.read_test_file('contact.json'), + re.compile(r'customers/1$'): self.read_test_file('customer.json'), + re.compile(r'groups$'): self.read_test_file('groups.json'), + re.compile(r'groups/1$'): self.read_test_file('group_1.json'), + re.compile(r'roles$'): self.read_test_file('roles.json'), + re.compile(r'roles/1$'): self.read_test_file('role_1.json'), + re.compile(r'agents\?email=abc@xyz.com&page=1&per_page=100'): self.read_test_file('agent_1.json'), + re.compile(r'agents\?mobile=1234&page=1&per_page=100'): self.read_test_file('agent_1.json'), + re.compile(r'agents\?phone=5678&page=1&per_page=100'): self.read_test_file('agent_1.json'), + re.compile(r'agents\?state=fulltime&page=1&per_page=100'): self.read_test_file('agent_1.json'), + re.compile(r'agents\?page=1&per_page=100'): self.read_test_file('agents.json'), + re.compile(r'agents/1$'): self.read_test_file('agent_1.json'), + }, + 'post': { + re.compile(r'tickets$'): self.read_test_file('ticket_1.json'), + re.compile(r'tickets/outbound_email$'): self.read_test_file('outbound_email_1.json'), + re.compile(r'tickets/1/notes$'): self.read_test_file('note_1.json'), + re.compile(r'tickets/1/reply$'): self.read_test_file('reply_1.json'), + re.compile(r'contacts$'): self.read_test_file('contact.json'), + }, + 'put': { + re.compile(r'tickets/1$'): self.read_test_file('ticket_1_updated.json'), + re.compile(r'contacts/1/make_agent$'): self.read_test_file('agent_1.json'), + re.compile(r'agents/1$'): self.read_test_file('agent_1_updated.json'), + }, + 'delete': { + re.compile(r'tickets/1$'): None, + re.compile(r'agents/1$'): None, + re.compile(r'contacts/1$'): None, + re.compile(r'contacts/1/hard_delete\?force=True$'): None, + } } + super(MockedAPI, self).__init__(*args) def read_test_file(self, filename): @@ -49,7 +72,7 @@ def read_test_file(self, filename): return json.loads(open(path, 'r').read()) def _get(self, url, *args, **kwargs): - for pattern, data in self.resolver.items(): + for pattern, data in self.resolver['get'].items(): if pattern.match(url): return data @@ -57,6 +80,33 @@ def _get(self, url, *args, **kwargs): from requests.exceptions import HTTPError raise HTTPError('404: mocked_api_get() has no pattern for \'{}\''.format(url)) + def _post(self, url, *args, **kwargs): + for pattern, data in self.resolver['post'].items(): + if pattern.match(url): + return data + + # No match found, raise 404 + from requests.exceptions import HTTPError + raise HTTPError('404: mocked_api_post() has no pattern for \'{}\''.format(url)) + + def _put(self, url, *args, **kwargs): + for pattern, data in self.resolver['put'].items(): + if pattern.match(url): + return data + + # No match found, raise 404 + from requests.exceptions import HTTPError + raise HTTPError('404: mocked_api_put() has no pattern for \'{}\''.format(url)) + + def _delete(self, url, *args, **kwargs): + for pattern, data in self.resolver['delete'].items(): + if pattern.match(url): + return data + + # No match found, raise 404 + from requests.exceptions import HTTPError + raise HTTPError('404: mocked_api_delete() has no pattern for \'{}\''.format(url)) + class TestAPIClass(TestCase): @@ -93,6 +143,9 @@ def setUpClass(cls): cls.ticket_json = json.loads(open(os.path.join(os.path.dirname(__file__), 'sample_json_data', 'ticket_1.json')).read()) + cls.outbound_email_json = json.loads(open(os.path.join(os.path.dirname(__file__), + 'sample_json_data', + 'outbound_email_1.json')).read()) def test_str(self): self.assertEqual(str(self.ticket), 'This is a sample ticket') @@ -109,13 +162,7 @@ def test_get_ticket(self): self.assertIn('foo', self.ticket.tags) self.assertIn('bar', self.ticket.tags) - @responses.activate def test_create_ticket(self): - responses.add(responses.POST, - 'https://{}/api/v2/tickets'.format(DOMAIN), - status=200, content_type='application/json', - json=self.ticket_json) - ticket = self.api.tickets.create_ticket('This is a sample ticket', description='This is a sample ticket, feel free to delete it.', email='test@example.com', @@ -131,38 +178,34 @@ def test_create_ticket(self): self.assertIn('foo', ticket.tags) self.assertIn('bar', ticket.tags) - @responses.activate def test_create_outbound_email(self): - j = self.ticket_json.copy() + j = self.outbound_email_json.copy() + email = 'test@example.com' + subject = 'This is a sample outbound email' + description = 'This is a sample outbound email, feel free to delete it.' + email_config_id = 5000054536 values = { - 'subject': 'This is a sample outbound_email', - 'description_text': 'This is a sample outbound, feel free to delete it.', 'status': 5, - 'email_config_id': 5000054536, + 'priority': 1, + 'tags': ['foo', 'bar'], + 'cc_emails': ['test2@example.com'] } - j.update(values) - responses.add(responses.POST, - 'https://{}/api/v2/tickets/outbound_email'.format(DOMAIN), - status=200, content_type='application/json', - json=j) - - ticket = self.api.tickets.create_outbound_email('This is a sample outbound_email', - description='This is a sample outbound, feel free to delete it.', - email='test@example.com', - email_config_id=5000054536, - priority=1, - tags=['foo', 'bar'], - cc_emails=['test2@example.com']) - self.assertIsInstance(ticket, Ticket) - self.assertEqual(ticket.subject, 'This is a sample outbound_email') - self.assertEqual(ticket.description_text, 'This is a sample outbound, feel free to delete it.') - self.assertEqual(ticket.priority, 'low') - self.assertEqual(ticket.status, 'closed') - self.assertEqual(ticket.cc_emails, ['test2@example.com']) - self.assertIn('foo', ticket.tags) - self.assertIn('bar', ticket.tags) - @responses.activate + email = self.api.tickets.create_outbound_email( + subject, + description, + email, + email_config_id, + **values + ) + + self.assertEqual(email.description_text, j['description_text']) + self.assertEqual(email._priority, j['priority']) + self.assertEqual(email._status, j['status']) + self.assertEqual(email.cc_emails, j['cc_emails']) + self.assertIn('foo', email.tags) + self.assertIn('bar', email.tags) + def test_update_ticket(self): j = self.ticket_json.copy() values = { @@ -173,14 +216,6 @@ def test_update_ticket(self): } j.update(values) - responses.add(responses.GET, - 'https://{}/api/v2/tickets/1'.format(DOMAIN), - status=200, content_type='application/json', json=j) - - responses.add(responses.PUT, - 'https://{}/api/v2/tickets/1'.format(DOMAIN), - status=200, content_type='application/json', json=j) - ticket = self.api.tickets.update_ticket(j['id'], **values) self.assertEqual(ticket.subject, 'Test subject update') self.assertEqual(ticket.status, 'resolved') @@ -188,12 +223,8 @@ def test_update_ticket(self): self.assertIn('hello', ticket.tags) self.assertIn('world', ticket.tags) - @responses.activate def test_delete_ticket(self): - responses.add(responses.DELETE, - 'https://{}/api/v2/tickets/1'.format(DOMAIN), - status=204) - self.api.tickets.delete_ticket(1) + self.assertEquals(self.api.tickets.delete_ticket(1), None) def test_ticket_priority(self): self.assertEqual(self.ticket._priority, 1) @@ -267,25 +298,13 @@ def test_comment_str(self): def test_comment_repr(self): self.assertEqual(repr(self.comments[0]), '') - @responses.activate def test_create_note(self): - responses.add(responses.POST, - 'https://{}/api/v2/tickets/1/notes'.format(DOMAIN), - status=200, content_type='application/json', - json=self.comments_json[0]) - comment = self.api.comments.create_note(1, 'This is a private note') self.assertIsInstance(comment, Comment) self.assertEqual(comment.body_text, 'This is a private note') self.assertEqual(comment.source, 'note') - @responses.activate def test_create_reply(self): - responses.add(responses.POST, - 'https://{}/api/v2/tickets/1/reply'.format(DOMAIN), - status=200, content_type='application/json', - json=self.comments_json[1]) - comment = self.api.comments.create_reply(1, 'This is a reply') self.assertIsInstance(comment, Comment) self.assertEqual(comment.body_text, 'This is a reply') @@ -296,7 +315,7 @@ class TestContact(TestCase): @classmethod def setUpClass(cls): cls.api = MockedAPI(DOMAIN, API_KEY) - cls.contact = cls.api.contacts.get_contact('1') + cls.contact = cls.api.contacts.get_contact(1) def test_get_contact(self): self.assertIsInstance(self.contact, Contact) @@ -305,6 +324,37 @@ def test_get_contact(self): self.assertEqual(self.contact.helpdesk_agent, False) self.assertEqual(self.contact.customer_id, 1) + def test_list_contact(self): + contacts = self.api.contacts.list_contacts() + self.assertIsInstance(contacts, list) + self.assertIsInstance(contacts[0], Contact) + self.assertEquals(len(contacts), 2) + self.assertEquals(contacts[0].__dict__, self.contact.__dict__) + + def test_create_contact(self): + contact_data = { + 'name': 'Rachel', + 'email': 'rachel@freshdesk.com' + } + contact = self.api.contacts.create_contact(contact_data) + self.assertIsInstance(contact, Contact) + self.assertEquals(contact.email, self.contact.email) + self.assertEquals(contact.name, self.contact.name) + + def test_soft_delete_contact(self): + self.assertEquals(self.api.contacts.soft_delete_contact(1), None) + + def test_permanently_delete_contact(self): + self.assertEquals(self.api.contacts.permanently_delete_contact(1), None) + + def test_make_agent(self): + agent = self.api.contacts.make_agent(self.contact.id) + self.assertIsInstance(agent, Agent) + self.assertEquals(agent.available, True) + self.assertEquals(agent.occasional, False) + self.assertEquals(agent.contact['email'], self.contact.email) + self.assertEquals(agent.contact['name'], self.contact.name) + def test_contact_datetime(self): self.assertIsInstance(self.contact.created_at, datetime.datetime) self.assertIsInstance(self.contact.updated_at, datetime.datetime) @@ -321,7 +371,7 @@ class TestCustomer(TestCase): def setUpClass(cls): cls.api = MockedAPI(DOMAIN, API_KEY) cls.customer = cls.api.customers.get_customer('1') - cls.contact = cls.api.contacts.get_contact('1') + cls.contact = cls.api.contacts.get_contact(1) def test_customer(self): self.assertIsInstance(self.customer, Customer) @@ -329,14 +379,14 @@ def test_customer(self): self.assertEqual(self.customer.domains, 'acme.com') self.assertEqual(self.customer.cf_custom_key, 'custom_value') - def test_contact_datetime(self): + def test_customer_datetime(self): self.assertIsInstance(self.customer.created_at, datetime.datetime) self.assertIsInstance(self.customer.updated_at, datetime.datetime) - def test_contact_str(self): + def test_customer_str(self): self.assertEqual(str(self.customer), 'ACME Corp.') - def test_contact_repr(self): + def test_customer_repr(self): self.assertEqual(repr(self.customer), '') def test_get_customer_from_contact(self): @@ -365,6 +415,9 @@ def test_group_datetime(self): self.assertIsInstance(self.group.created_at, datetime.datetime) self.assertIsInstance(self.group.updated_at, datetime.datetime) + def test_group_str(self): + self.assertEqual(str(self.group), 'Entertainers') + def test_group_repr(self): self.assertEqual(repr(self.group), '') @@ -404,56 +457,37 @@ def setUpClass(cls): 'agent_1.json')).read()) def test_str(self): - self.assertEqual(str(self.agent), 'Support') + self.assertEqual(str(self.agent), 'Rachel') def test_repr(self): - self.assertEqual(repr(self.agent), '') + self.assertEqual(repr(self.agent), '') def test_get_agent(self): self.assertIsInstance(self.agent, Agent) self.assertEqual(self.agent.id, 1) - self.assertEqual(self.agent.contact['name'], 'Support') - self.assertEqual(self.agent.contact['email'], 'abc@xyz.com') + self.assertEqual(self.agent.contact['name'], 'Rachel') + self.assertEqual(self.agent.contact['email'], 'rachel@freshdesk.com') self.assertEqual(self.agent.contact['mobile'], 1234) self.assertEqual(self.agent.contact['phone'], 5678) self.assertEqual(self.agent.occasional, False) - @responses.activate def test_update_agent(self): - a = self.agent_json.copy() - - responses.add(responses.GET, - 'https://{}/api/v2/agents/1'.format(DOMAIN), - status=200, content_type='application/json', json=a) - values = { 'occasional': True, 'contact': { 'name': 'Updated Name' } } - - b = a.copy() - b.update(values) - - responses.add(responses.PUT, - 'https://{}/api/v2/agents/1'.format(DOMAIN), - status=200, content_type='application/json', json=b) - - agent = self.api.agents.update_agent(a['id'], **values) + agent = self.api.agents.update_agent(1, **values) self.assertEqual(agent.occasional, True) self.assertEqual(agent.contact['name'], 'Updated Name') - @responses.activate def test_delete_agent(self): - responses.add(responses.DELETE, - 'https://{}/api/v2/agents/1'.format(DOMAIN), - status=204) - self.api.agents.delete_agent(1) + self.assertEquals(self.api.agents.delete_agent(1), None) def test_agent_name(self): - self.assertEqual(self.agent.contact['name'], 'Support') + self.assertEqual(self.agent.contact['name'], 'Rachel') def test_agent_mobile(self): self.assertEqual(self.agent.contact['mobile'], 1234) From 8ae0794cbcecba6c8233a54a89a4897dbc8a9dd7 Mon Sep 17 00:00:00 2001 From: Sam Kingston Date: Wed, 27 Jun 2018 11:39:44 +1000 Subject: [PATCH 20/46] Version bump to 1.2.0 --- CHANGELOG.md | 4 ++++ freshdesk/__init__.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 873ce0e..3162fef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ Changelog ========= +v1.2.0 - 2018-06-27 + + * Filled out v1 and v2 API's and expanded test cases (@prenit-coverfox) + v1.1.2 - 2018-06-04 * No code changes: fix release stuff up (@sjkingo) diff --git a/freshdesk/__init__.py b/freshdesk/__init__.py index 7b344ec..58d478a 100644 --- a/freshdesk/__init__.py +++ b/freshdesk/__init__.py @@ -1 +1 @@ -__version__ = '1.1.2' +__version__ = '1.2.0' From e82aaf0341146cd2c02dd078b0626140a6aa6930 Mon Sep 17 00:00:00 2001 From: Sam Kingston Date: Thu, 5 Jul 2018 16:35:48 +1000 Subject: [PATCH 21/46] Fixed set delcaration bug - closes #21 --- CHANGELOG.md | 4 ++++ freshdesk/__init__.py | 2 +- freshdesk/v1/models.py | 3 ++- freshdesk/v2/models.py | 4 +++- 4 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3162fef..9d2bf10 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ Changelog ========= +v1.2.1 - 2018-07-05 + + * #21: Fixed set declaration bug in base class (@sjkingo) + v1.2.0 - 2018-06-27 * Filled out v1 and v2 API's and expanded test cases (@prenit-coverfox) diff --git a/freshdesk/__init__.py b/freshdesk/__init__.py index 58d478a..3f262a6 100644 --- a/freshdesk/__init__.py +++ b/freshdesk/__init__.py @@ -1 +1 @@ -__version__ = '1.2.0' +__version__ = '1.2.1' diff --git a/freshdesk/v1/models.py b/freshdesk/v1/models.py index 825a630..ea2df76 100644 --- a/freshdesk/v1/models.py +++ b/freshdesk/v1/models.py @@ -2,9 +2,10 @@ class FreshdeskModel(object): - _keys = set() + _keys = None def __init__(self, **kwargs): + self._keys = set() if "custom_field" in kwargs.keys() and len(kwargs["custom_field"]) > 0: custom_fields = kwargs.pop("custom_field") kwargs.update(custom_fields) diff --git a/freshdesk/v2/models.py b/freshdesk/v2/models.py index 7e0bf8b..9b9ea9b 100644 --- a/freshdesk/v2/models.py +++ b/freshdesk/v2/models.py @@ -2,9 +2,11 @@ class FreshdeskModel(object): - _keys = set() + _keys = None def __init__(self, **kwargs): + self._keys = set() + if "custom_field" in kwargs.keys() and len(kwargs["custom_field"]) > 0: custom_fields = kwargs.pop("custom_field") kwargs.update(custom_fields) From b640317ffa65d97a89cbce1ac72eb9fb5aa02235 Mon Sep 17 00:00:00 2001 From: prenit-coverfox <37108931+prenit-coverfox@users.noreply.github.com> Date: Mon, 9 Jul 2018 06:17:43 +0530 Subject: [PATCH 22/46] Changes from #20 including v1 deprecation * defaults to v2 api. fixed pagination issues * fixed version error message * Added error handling for V2 api calls --- freshdesk/api.py | 18 ++++++++++++++++++ freshdesk/v2/api.py | 42 ++++++++++++++++++++++-------------------- freshdesk/v2/test.py | 2 +- 3 files changed, 41 insertions(+), 21 deletions(-) diff --git a/freshdesk/api.py b/freshdesk/api.py index 1b76905..6745423 100644 --- a/freshdesk/api.py +++ b/freshdesk/api.py @@ -25,5 +25,23 @@ def API(domain, api_key, version=1, **kwargs): except AttributeError: pass + if version == 1: + deprecation_message = """ + Freshdesk has deprecated their V1 API from 1st July, 2018. + For more info, visit https://support.freshdesk.com/support/solutions/articles/231955-important-deprecation-of-api-v1 + + For more info about freshdesk V2 API, visit https://developers.freshdesk.com/api/ + + Now python-freshdesk library will by default return V2 API client. You need to migrate your project accordingly. + + + """ + print(deprecation_message) + version = 2 + + if version != 2: + print("Freshdesk V%d API is not released yet. Returning default V2 API client\n" % version) + + version = 2 client_class = _VERSIONS[version] return client_class(domain, api_key, **kwargs) diff --git a/freshdesk/v2/api.py b/freshdesk/v2/api.py index 7400abc..dee0a8f 100644 --- a/freshdesk/v2/api.py +++ b/freshdesk/v2/api.py @@ -79,16 +79,17 @@ def list_tickets(self, **kwargs): url += '?filter=%s&' % filter_name else: url += '?' - page = 1 - per_page = 100 + page = 1 if not 'page' in kwargs else kwargs['page'] + per_page = 100 if not 'per_page' in kwargs else kwargs['per_page'] tickets = [] - # Skip pagination by looping over each page and adding tickets + # Skip pagination by looping over each page and adding tickets if 'page' key is not in kwargs. + # else return the requested page and break the loop while True: this_page = self._api._get(url + 'page=%d&per_page=%d' % (page, per_page), kwargs) tickets += this_page - if len(this_page) < per_page: + if len(this_page) < per_page or 'page' in kwargs: break page += 1 @@ -175,16 +176,13 @@ def list_contacts(self, **kwargs): """ url = 'contacts?' - if kwargs: - for filter_name, filter_value in kwargs.items(): - url = url + "{}={}&".format(filter_name, filter_value) - del kwargs[filter_name] - page = 1 if not 'page' in kwargs else kwargs['page'] - per_page = 10 if not 'per_page' in kwargs else kwargs['per_page'] + per_page = 100 if not 'per_page' in kwargs else kwargs['per_page'] + contacts = [] - # Skip pagination by looping over each page and adding contacts + # Skip pagination by looping over each page and adding tickets if 'page' key is not in kwargs. + # else return the requested page and break the loop while True: this_page = self._api._get(url + 'page=%d&per_page=%d' % (page, per_page), kwargs) @@ -303,21 +301,18 @@ def list_agents(self, **kwargs): """ url = 'agents?' - if kwargs: - for filter_name, filter_value in kwargs.items(): - url = url + "{}={}&".format(filter_name, filter_value) - del kwargs[filter_name] + page = 1 if not 'page' in kwargs else kwargs['page'] + per_page = 100 if not 'per_page' in kwargs else kwargs['per_page'] - page = 1 - per_page = 100 agents = [] - # Skip pagination by looping over each page and adding tickets + # Skip pagination by looping over each page and adding tickets if 'page' key is not in kwargs. + # else return the requested page and break the loop while True: this_page = self._api._get(url + 'page=%d&per_page=%d' % (page, per_page), kwargs) agents += this_page - if len(this_page) < per_page: + if len(this_page) < per_page or 'page' in kwargs: break page += 1 @@ -384,7 +379,14 @@ def _action(self, req): req.raise_for_status() j = {} - if 'error' in j: + if 'Retry-After' in req.headers: + raise HTTPError('429 Rate Limit Exceeded: API rate-limit has been reached until {} seconds.' + 'See http://freshdesk.com/api#ratelimit'.format(req.headers['Retry-After'])) + + if 'code' in j and j['code'] == "invalid_credentials": + raise HTTPError('401 Unauthorized: Please login with correct credentials') + + if 'errors' in j: raise HTTPError('{}: {}'.format(j.get('description'), j.get('errors'))) diff --git a/freshdesk/v2/test.py b/freshdesk/v2/test.py index 11677fa..0d16bc6 100644 --- a/freshdesk/v2/test.py +++ b/freshdesk/v2/test.py @@ -31,7 +31,7 @@ def __init__(self, *args): re.compile(r'tickets\?page=1&per_page=100'): self.read_test_file('all_tickets.json'), re.compile(r'tickets/1$'): self.read_test_file('ticket_1.json'), re.compile(r'tickets/1/conversations'): self.read_test_file('conversations.json'), - re.compile(r'contacts\?page=1&per_page=10$'): self.read_test_file('contacts.json'), + re.compile(r'contacts\?page=1&per_page=100$'): self.read_test_file('contacts.json'), re.compile(r'contacts/1$'): self.read_test_file('contact.json'), re.compile(r'customers/1$'): self.read_test_file('customer.json'), re.compile(r'groups$'): self.read_test_file('groups.json'), From 697edf562c4fea5978c05eb4cb08a6c42e7b1dea Mon Sep 17 00:00:00 2001 From: Sam Kingston Date: Mon, 9 Jul 2018 10:54:13 +1000 Subject: [PATCH 23/46] Version bump --- CHANGELOG.md | 4 ++++ freshdesk/__init__.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d2bf10..cc9f4fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ Changelog ========= +v1.2.2 - 2018-07-09 + + * #20: Changes from #20 including v1 deprecation (@prenit-coverfox) + v1.2.1 - 2018-07-05 * #21: Fixed set declaration bug in base class (@sjkingo) diff --git a/freshdesk/__init__.py b/freshdesk/__init__.py index 3f262a6..923b987 100644 --- a/freshdesk/__init__.py +++ b/freshdesk/__init__.py @@ -1 +1 @@ -__version__ = '1.2.1' +__version__ = '1.2.2' From 09b0600d666d14ce89fc1fab1c0c5aab79e0991b Mon Sep 17 00:00:00 2001 From: prenit-coverfox Date: Sun, 15 Jul 2018 23:46:37 +0530 Subject: [PATCH 24/46] fetch all pages in list_groups. support for pagination --- freshdesk/v2/api.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/freshdesk/v2/api.py b/freshdesk/v2/api.py index dee0a8f..a35c243 100644 --- a/freshdesk/v2/api.py +++ b/freshdesk/v2/api.py @@ -136,12 +136,21 @@ class GroupAPI(object): def __init__(self, api): self._api = api - def list_groups(self): - url = 'groups' + def list_groups(self, **kwargs): + url = 'groups?' + page = 1 if not 'page' in kwargs else kwargs['page'] + per_page = 100 if not 'per_page' in kwargs else kwargs['per_page'] + groups = [] - for g in self._api._get(url): - groups.append(Group(**g)) - return groups + while True: + this_page = self._api._get(url + 'page=%d&per_page=%d' + % (page, per_page), kwargs) + groups += this_page + if len(this_page) < per_page or 'page' in kwargs: + break + page += 1 + + return [Group(**g) for g in groups] def get_group(self, group_id): url = 'groups/%s' % group_id @@ -238,6 +247,7 @@ def get_customer(self, company_id): def get_customer_from_contact(self, contact): return self.get_customer(contact.customer_id) + class CompanyAPI(object): def __init__(self, api): self._api = api From 202e0f7a30dbf1fb6a9ea48756ff4c0f04f4396a Mon Sep 17 00:00:00 2001 From: prenit-coverfox Date: Sun, 15 Jul 2018 23:51:28 +0530 Subject: [PATCH 25/46] fixed test cases for group --- freshdesk/v2/test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freshdesk/v2/test.py b/freshdesk/v2/test.py index 0d16bc6..17b3424 100644 --- a/freshdesk/v2/test.py +++ b/freshdesk/v2/test.py @@ -34,7 +34,7 @@ def __init__(self, *args): re.compile(r'contacts\?page=1&per_page=100$'): self.read_test_file('contacts.json'), re.compile(r'contacts/1$'): self.read_test_file('contact.json'), re.compile(r'customers/1$'): self.read_test_file('customer.json'), - re.compile(r'groups$'): self.read_test_file('groups.json'), + re.compile(r'groups\?page=1&per_page=100$'): self.read_test_file('groups.json'), re.compile(r'groups/1$'): self.read_test_file('group_1.json'), re.compile(r'roles$'): self.read_test_file('roles.json'), re.compile(r'roles/1$'): self.read_test_file('role_1.json'), From 9a0e226a28b3528612483542456dc7bd63400506 Mon Sep 17 00:00:00 2001 From: Sam Kingston Date: Fri, 17 Aug 2018 07:02:14 +1000 Subject: [PATCH 26/46] License project as BSD --- LICENSE | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..5772ea2 --- /dev/null +++ b/LICENSE @@ -0,0 +1,23 @@ + +Copyright (c) 2014-2018 Sam Kingston and others. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. From acc1a5812842d879979367e1fb5bf42814f60919 Mon Sep 17 00:00:00 2001 From: prenit-coverfox Date: Wed, 29 Aug 2018 17:07:59 +0530 Subject: [PATCH 27/46] fixed make agent api --- freshdesk/v2/api.py | 4 +- .../v2/sample_json_data/contact_1_agent.json | 39 +++++++++++++++++++ freshdesk/v2/test.py | 2 +- 3 files changed, 42 insertions(+), 3 deletions(-) create mode 100644 freshdesk/v2/sample_json_data/contact_1_agent.json diff --git a/freshdesk/v2/api.py b/freshdesk/v2/api.py index a35c243..5beca47 100644 --- a/freshdesk/v2/api.py +++ b/freshdesk/v2/api.py @@ -232,8 +232,8 @@ def make_agent(self, contact_id, **kwargs): 'ticket_scope': 2, } data.update(kwargs) - agent = self._api._put(url, data=json.dumps(data)) - return self._api.agents.get_agent(agent['id']) + contact = self._api._put(url, data=json.dumps(data)) + return self._api.agents.get_agent(contact['agent']['id']) class CustomerAPI(object): diff --git a/freshdesk/v2/sample_json_data/contact_1_agent.json b/freshdesk/v2/sample_json_data/contact_1_agent.json new file mode 100644 index 0000000..3090b05 --- /dev/null +++ b/freshdesk/v2/sample_json_data/contact_1_agent.json @@ -0,0 +1,39 @@ +{ + "active": false, + "address": null, + "created_at": "2014-12-31T12:27:09+10:00", + "customer_id": 1, + "deleted": false, + "description": null, + "email": "rachel@freshdesk.com", + "external_id": null, + "fb_profile_id": null, + "helpdesk_agent": false, + "id": 1, + "job_title": null, + "language": "en", + "mobile": null, + "name": "Rachel", + "phone": null, + "time_zone": "Brisbane", + "twitter_id": null, + "updated_at": "2014-12-31T12:27:09+10:00", + "company_id": null, + "custom_field": {}, + "agent": { + "available_since": null, + "available": true, + "occasional": false, + "signature": null, + "group_ids" : [ + 1 + ], + "role_ids" : [ + 1 + ], + "id": 1, + "ticket_scope": 1, + "created_at": "2015-08-28T11:47:58Z", + "updated_at": "2015-08-28T11:47:58Z" + } +} diff --git a/freshdesk/v2/test.py b/freshdesk/v2/test.py index 17b3424..fa4beda 100644 --- a/freshdesk/v2/test.py +++ b/freshdesk/v2/test.py @@ -54,7 +54,7 @@ def __init__(self, *args): }, 'put': { re.compile(r'tickets/1$'): self.read_test_file('ticket_1_updated.json'), - re.compile(r'contacts/1/make_agent$'): self.read_test_file('agent_1.json'), + re.compile(r'contacts/1/make_agent$'): self.read_test_file('contact_1_agent.json'), re.compile(r'agents/1$'): self.read_test_file('agent_1_updated.json'), }, 'delete': { From 36a8d7ec39850ebae620a6927e186fb38015b5ae Mon Sep 17 00:00:00 2001 From: Diederik van der Boor Date: Thu, 13 Sep 2018 11:49:46 +0200 Subject: [PATCH 28/46] Add the update_contact() call to the ContactAPI --- freshdesk/v2/api.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/freshdesk/v2/api.py b/freshdesk/v2/api.py index dee0a8f..a72c30c 100644 --- a/freshdesk/v2/api.py +++ b/freshdesk/v2/api.py @@ -208,6 +208,10 @@ def get_contact(self, contact_id): url = 'contacts/%d' % contact_id return Contact(**self._api._get(url)) + def update_contact(self, contact_id, **data): + url = 'contacts/%d' % contact_id + return Contact(**self._api._put(url, data=json.dumps(data))) + def soft_delete_contact(self, contact_id): url = 'contacts/%d' % contact_id self._api._delete(url) From 8d3ab29fe93127a6cbd4a8dba3c5f1f7fd61acdd Mon Sep 17 00:00:00 2001 From: Diederik van der Boor Date: Thu, 13 Sep 2018 12:21:07 +0200 Subject: [PATCH 29/46] Also add restore_contact() call --- freshdesk/v2/api.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/freshdesk/v2/api.py b/freshdesk/v2/api.py index a72c30c..8ce1f93 100644 --- a/freshdesk/v2/api.py +++ b/freshdesk/v2/api.py @@ -216,6 +216,10 @@ def soft_delete_contact(self, contact_id): url = 'contacts/%d' % contact_id self._api._delete(url) + def restore_contact(self, contact_id): + url = 'contacts/%d/restore' % contact_id + self._api._put(url) + def permanently_delete_contact(self, contact_id, force=True): url = 'contacts/%d/hard_delete?force=%r' % (contact_id, force) self._api._delete(url) From 02d110c8b21dda2e9eedf83fde1a62abdd9876f0 Mon Sep 17 00:00:00 2001 From: prenit-coverfox Date: Sat, 22 Sep 2018 22:09:07 +0530 Subject: [PATCH 30/46] added support to create ticket with attachments --- freshdesk/v2/api.py | 35 ++++++++++++++++++++++++++++++----- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/freshdesk/v2/api.py b/freshdesk/v2/api.py index 5beca47..a34713f 100644 --- a/freshdesk/v2/api.py +++ b/freshdesk/v2/api.py @@ -15,7 +15,13 @@ def get_ticket(self, ticket_id): return Ticket(**ticket) def create_ticket(self, subject, **kwargs): - """Creates a ticket""" + """ + Creates a ticket + To create ticket with attachments, + pass a key 'attachments' with value as list of fully qualified file paths in string format. + ex: attachments = ('/path/to/attachment1', '/path/to/attachment2') + """ + url = 'tickets' status = kwargs.get('status', 2) priority = kwargs.get('priority', 1) @@ -25,11 +31,26 @@ def create_ticket(self, subject, **kwargs): 'priority': priority, } data.update(kwargs) + if 'attachments' in data: + ticket = self._create_ticket_with_attachment(url, data) + return Ticket(**ticket) + ticket = self._api._post(url, data=json.dumps(data)) return Ticket(**ticket) - def create_outbound_email(self, subject, description, email, - email_config_id, **kwargs): + def _create_ticket_with_attachment(self, url, data): + attachments = data['attachments'] + del data['attachments'] + multipart_data = [] + + for attachment in attachments: + file_name = attachment.split("/")[-1:][0] + multipart_data.append(('attachments[]', (file_name, open(attachment), None))) + + ticket = self._api._post(url, data=data, files=multipart_data) + return ticket + + def create_outbound_email(self, subject, description, email, email_config_id, **kwargs): """Creates an outbound email""" url = 'tickets/outbound_email' priority = kwargs.get('priority', 1) @@ -413,9 +434,13 @@ def _get(self, url, params={}): req = self._session.get(self._api_prefix + url, params=params) return self._action(req) - def _post(self, url, data={}): + def _post(self, url, data={}, **kwargs): """Wrapper around request.post() to use the API prefix. Returns a JSON response.""" - req = self._session.post(self._api_prefix + url, data=data) + if 'files' in kwargs: + req = requests.post(self._api_prefix + url, auth=self._session.auth, data=data, **kwargs) + return self._action(req) + + req = self._session.post(self._api_prefix + url, data=data, **kwargs) return self._action(req) def _put(self, url, data={}): From bc8a6d000d843338d1123ecd4205c3e4f1071dc8 Mon Sep 17 00:00:00 2001 From: prenit-coverfox Date: Sat, 22 Sep 2018 22:31:54 +0530 Subject: [PATCH 31/46] Added Test Cases For Create ticket with attachments method --- freshdesk/v2/sample_json_data/attachment.txt | 1 + freshdesk/v2/sample_json_data/ticket_1.json | 12 +++++++++++- freshdesk/v2/test.py | 4 +++- 3 files changed, 15 insertions(+), 2 deletions(-) create mode 100644 freshdesk/v2/sample_json_data/attachment.txt diff --git a/freshdesk/v2/sample_json_data/attachment.txt b/freshdesk/v2/sample_json_data/attachment.txt new file mode 100644 index 0000000..47f20bb --- /dev/null +++ b/freshdesk/v2/sample_json_data/attachment.txt @@ -0,0 +1 @@ +test attachment file diff --git a/freshdesk/v2/sample_json_data/ticket_1.json b/freshdesk/v2/sample_json_data/ticket_1.json index 8ea487a..cc97a19 100644 --- a/freshdesk/v2/sample_json_data/ticket_1.json +++ b/freshdesk/v2/sample_json_data/ticket_1.json @@ -38,5 +38,15 @@ "foo", "bar" ], - "attachments": [] + "attachments": [ + { + "id":1, + "content_type":"text/plain", + "file_size":44115, + "name":"attachment.txt", + "attachment_url":"https://cdn.freshdesk.com/data/helpdesk/attachments/production/4004881085/original/attachment.txt", + "created_at":"2014-07-28T16:20:03+05:30", + "updated_at":"2014-07-28T16:20:03+05:30" + } + ] } diff --git a/freshdesk/v2/test.py b/freshdesk/v2/test.py index fa4beda..52aa62a 100644 --- a/freshdesk/v2/test.py +++ b/freshdesk/v2/test.py @@ -163,12 +163,14 @@ def test_get_ticket(self): self.assertIn('bar', self.ticket.tags) def test_create_ticket(self): + attachment_path = os.path.join(os.path.dirname(__file__), 'sample_json_data', 'attachment.txt') ticket = self.api.tickets.create_ticket('This is a sample ticket', description='This is a sample ticket, feel free to delete it.', email='test@example.com', priority=1, status=2, tags=['foo', 'bar'], - cc_emails=['test2@example.com']) + cc_emails=['test2@example.com'], + attachments=(attachment_path,)) self.assertIsInstance(ticket, Ticket) self.assertEqual(ticket.subject, 'This is a sample ticket') self.assertEqual(ticket.description_text, 'This is a sample ticket, feel free to delete it.') From e1aac4bb0bdaa70b8e802f6e1e1927824ca5d503 Mon Sep 17 00:00:00 2001 From: prenit-coverfox Date: Sat, 22 Sep 2018 23:13:04 +0530 Subject: [PATCH 32/46] Added Test Cases For Update, Restore Contact --- .../v2/sample_json_data/contact_updated.json | 23 +++++++++++++++++++ freshdesk/v2/test.py | 16 +++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 freshdesk/v2/sample_json_data/contact_updated.json diff --git a/freshdesk/v2/sample_json_data/contact_updated.json b/freshdesk/v2/sample_json_data/contact_updated.json new file mode 100644 index 0000000..e907408 --- /dev/null +++ b/freshdesk/v2/sample_json_data/contact_updated.json @@ -0,0 +1,23 @@ +{ + "active": false, + "address": null, + "created_at": "2014-12-31T12:27:09+10:00", + "customer_id": 1, + "deleted": false, + "description": null, + "email": "rachel@freshdesk.com", + "external_id": null, + "fb_profile_id": null, + "helpdesk_agent": false, + "id": 1, + "job_title": null, + "language": "en", + "mobile": null, + "name": "New Name", + "phone": null, + "time_zone": "Brisbane", + "twitter_id": null, + "updated_at": "2014-12-31T12:27:09+10:00", + "company_id": null, + "custom_field": {} +} diff --git a/freshdesk/v2/test.py b/freshdesk/v2/test.py index 52aa62a..5ab6f7b 100644 --- a/freshdesk/v2/test.py +++ b/freshdesk/v2/test.py @@ -54,6 +54,8 @@ def __init__(self, *args): }, 'put': { re.compile(r'tickets/1$'): self.read_test_file('ticket_1_updated.json'), + re.compile(r'contacts/1$'): self.read_test_file('contact_updated.json'), + re.compile(r'contacts/1/restore$'): self.read_test_file('contact.json'), re.compile(r'contacts/1/make_agent$'): self.read_test_file('contact_1_agent.json'), re.compile(r'agents/1$'): self.read_test_file('agent_1_updated.json'), }, @@ -343,12 +345,26 @@ def test_create_contact(self): self.assertEquals(contact.email, self.contact.email) self.assertEquals(contact.name, self.contact.name) + def test_update_contact(self): + contact_data = { + 'name': 'New Name' + } + contact = self.api.contacts.update_contact(1, **contact_data) + self.assertIsInstance(contact, Contact) + self.assertEquals(contact.name, 'New Name') + def test_soft_delete_contact(self): self.assertEquals(self.api.contacts.soft_delete_contact(1), None) def test_permanently_delete_contact(self): self.assertEquals(self.api.contacts.permanently_delete_contact(1), None) + def test_restore_contact(self): + self.api.contacts.restore_contact(1) + contact = self.api.contacts.get_contact(1) + self.assertIsInstance(contact, Contact) + self.assertEquals(contact.deleted, False) + def test_make_agent(self): agent = self.api.contacts.make_agent(self.contact.id) self.assertIsInstance(agent, Agent) From 7469bbc6e225445f1e4b5452d9bfa99881481ca3 Mon Sep 17 00:00:00 2001 From: prenit-coverfox Date: Sun, 23 Sep 2018 00:26:37 +0530 Subject: [PATCH 33/46] Version Bump. Updated README.md --- CHANGELOG.md | 7 +++ README.md | 113 ++++++++++++++++++++++++++++++++++++------ freshdesk/__init__.py | 2 +- 3 files changed, 105 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cc9f4fc..4f2c43c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,13 @@ Changelog ========= +v1.2.3- 2018-09-23 + + + * #28: Changes from #28 create ticket with attachments (@prenit-coverfox) + * #25: Changes from #25 update_contact, restore_contact (@vdboor) + * #22: Changes from #22 pagination in list_groups(@prenit-coverfox) + v1.2.2 - 2018-07-09 * #20: Changes from #20 including v1 deprecation (@prenit-coverfox) diff --git a/README.md b/README.md index 9169dde..8d23976 100644 --- a/README.md +++ b/README.md @@ -8,19 +8,19 @@ A library for the [Freshdesk](http://freshdesk.com/) helpdesk system for Python There is support for a limited subset of features, using either Freshdesk API v1 or v2. -Support for the v1 API includes the following features: -* Getting a [Ticket](http://freshdesk.com/api#view_a_ticket) and filtering ticket lists -* Getting a [Contact/User/Customer](http://freshdesk.com/api#view_user) -* Viewing [timesheets](http://freshdesk.com/api#view_all_time_entry) +After the deprication of Freshdesk V1 API, library uses V2 API only. Support for the v2 API includes the following features: * [Tickets](http://developer.freshdesk.com/api/#tickets) - - [List](http://developer.freshdesk.com/api/#list_all_tickets) - [Get](http://developer.freshdesk.com/api/#view_a_ticket) - [Create](http://developer.freshdesk.com/api/#create_ticket) - [Update](http://developer.freshdesk.com/api/#update_ticket) - [Delete](http://developer.freshdesk.com/api/#delete_a_ticket) + - [Create OutBound Email](http://developer.freshdesk.com/api/#create_outbound_email) + - [List](http://developer.freshdesk.com/api/#list_all_tickets) - Custom ticket fields (as of 1.1.1) +* [Ticket Fields](http://developer.freshdesk.com/api/#ticket_fields) + - [List](http://developer.freshdesk.com/api/#list_all_ticket_fields) * [Comments](http://developer.freshdesk.com/api/#conversations) (known as Conversations in Freshdesk) - [List](http://developer.freshdesk.com/api/#list_all_ticket_notes) - [Create note](http://developer.freshdesk.com/api/#add_note_to_a_ticket) @@ -28,9 +28,25 @@ Support for the v2 API includes the following features: * [Groups](http://developer.freshdesk.com/api/#groups) - [List](http://developer.freshdesk.com/api/#list_all_groups) - [Get](http://developer.freshdesk.com/api/#view_group) +* [Contacts](http://developer.freshdesk.com/api/#contacts) + - [Get](http://developer.freshdesk.com/api/#view_contact) + - [List](http://developer.freshdesk.com/api/#list_all_contacts) + - [Create](http://developer.freshdesk.com/api/#create_contact) + - [Update](http://developer.freshdesk.com/api/#update_contact) - from 1.2.3 + - [Delete](http://developer.freshdesk.com/api/#delete_contact) + - [Restore](http://developer.freshdesk.com/api/#restore_contact) - from 1.2.3 + - [Make agent](http://developer.freshdesk.com/api/#make_agent) + * [Company](https://developers.freshdesk.com/api/#companies) + - [Get](http://developer.freshdesk.com/api/#view_company) * [Roles](https://developers.freshdesk.com/api/#roles) - from 1.1.1 + - [Get](http://developer.freshdesk.com/api/#view_role) + - [List](http://developer.freshdesk.com/api/#list_role) * [Agents](https://developers.freshdesk.com/api/#agents) - from 1.1.1 + - [Get](http://developer.freshdesk.com/api/#view_agent) + - [List](http://developer.freshdesk.com/api/#list_all_agents) + - [Update](http://developer.freshdesk.com/api/#update_agent) + - [Delete](http://developer.freshdesk.com/api/#delete_agent) ## Installation @@ -69,8 +85,7 @@ without changing these. To find your API key, follow Freshdesk's step-by-step solution article [How to find your API key](https://support.freshdesk.com/support/solutions/articles/215517). -By default, API v1 is used for backwards compatibility. To specify v1 or v2 -explicitly: +By default, API v2 is used after the deprication of Freshdesk V1 API ```python >>> a = API('company.freshdesk.com', 'q8dnkjaS554Aol21dmnas9d92', version=2) @@ -129,6 +144,18 @@ ticket = a.tickets.create_ticket('This is a sample ticket', tags=['example']) ``` +To Create a ticket with attachments, pass a list of fully quilified file paths with key name 'attachments' + +```python +ticket = a.tickets.create_ticket('This is a sample ticket', + email='example@example.com', + description='This is the description of the ticket', + tags=['example'], + attachments=[ + '/path/to/file1', + '/path/to/file2'] + ) +``` The only positional argument is the subject, which is always required. All other values are optional named arguments, which you can find in the @@ -138,6 +165,15 @@ While all but subject are optional, you will need to specify at least one of: `requester_id`, `email`, `facebook_id`, `phone` or `twitter_id` as the requester of the ticket, or the request will fail. +You can get the list of tickets by using + +```python +ticket = a.tickets.list_tickets(filter_name=None, page=1, per_page=10) +``` + +By defauly `new_and_my_open` filter is used. If you want to list all the tickets without any filter, pass `filter_name=None`. +Pagination is supported. If `page` argument is not passed, all pages are fetched, else specified page is returned. + Updating a ticket is similar to creating a ticket. The only differences are that the ticket ID becomes the first positional argument, and subject becomes an optional named argument. @@ -160,6 +196,17 @@ To delete a ticket, just pass the ticket ID value to the `delete_ticket` method: a.tickets.delete_ticket(4) ``` +### Ticket Fields (API v2) + +To view ticket fields for your freshdesk, use the `list_ticket_fields` method, from +the ticket_fields module: + +```python +>>> a.ticket_fields.list_ticket_fields(type='default_requester') +[, ] + +``` + ### Comments (API v2) To view comments on a ticket (note or reply), use the `list_comments` method, from @@ -208,28 +255,62 @@ arguments. In both methods, the ticket ID and body must be given as positional arguments. -### Contacts/Users (API v1) +### Contacts (API v2) Freshdesk mixes up the naming of contacts and users, depending on whether they are an agent or not. `python-freshdesk` simply calls them all contacts and are represented as `Contact` instances: ```python ->>> repr(a.contacts.get_contact('1234')) +>>> repr(a.contacts.get_contact(1234)) "" ``` -### Timesheets (API v1) +Get the list of contacts using: +```python +>>> repr(a.contacts.list_contacts(page=1, per_page=10)) +[""] +``` +Pagination is supported. If `page` option is not specified, then all the pages are fetched, else specified page is returned. +Contact can be filtered using name or email by passing the filter as `email=abc@xyz.com` or `mobile=123792182138` or `state=deleted` -You can view all timesheets: +Other supported methods are `create_contact`, `update_contact`, `soft_delete_contact`, `permanently_delete_contact`, `restore_contact` +To convert a contact to an agent, use: ```python ->>> a.timesheets.get_all_timesheets() -[, >> repr(a.contacts.make_agent(1)) +[""] ``` -Or filter by ticket number: +### Agents (API v2) + +To get an agent, use: +```python +>>> repr(a.agents.get_agent(1234)) +"" +``` +Get the list of agent using: ```python ->>> a.timesheets.get_timesheet_by_ticket(4) -[] +>>> repr(a.agents.list_agents(page=1, per_page=10)) +[""] ``` +Pagination is supported. If `page` option is not specified, then all the pages are fetched, else specified page is returned. +Agent can be filtered using name or email by passing the filter as `email=abc@xyz.com` or `mobile=123792182138` + +Other supported methods are `update_agent`, `delete_agent` + +### Groups (API v2) + +To get the list of groups, use: +```python +>>> repr(a.groups.list_groups(page=1, per_page=10)) +[""] +``` +Pagination is supported. If `page` option is not specified, then all the pages are fetched, else specified page is returned. + +To get a group, use: +```python +>>> repr(a.groups.get_group(1)) +[""] +``` + diff --git a/freshdesk/__init__.py b/freshdesk/__init__.py index 923b987..5a5df3b 100644 --- a/freshdesk/__init__.py +++ b/freshdesk/__init__.py @@ -1 +1 @@ -__version__ = '1.2.2' +__version__ = '1.2.3' From 24aaa61fa0870a4dd08cbf46e8e77165742baf8b Mon Sep 17 00:00:00 2001 From: prenit-coverfox Date: Sun, 23 Sep 2018 00:44:53 +0530 Subject: [PATCH 34/46] fixed doc --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8d23976..01679c2 100644 --- a/README.md +++ b/README.md @@ -100,7 +100,7 @@ instance. Tickets are loaded as instances of the `freshdesk.v2.models.Ticket` class, and can be iterated over: ```python ->>> a.tickets.list_open_tickets() +>>> a.tickets.list_tickets() [, , ] >>> a.tickets.list_deleted_tickets() [] @@ -119,7 +119,7 @@ set([u'status', u'source_name', u'ticket_type', u'updated_at', ...]) Attributes are automatically converted to native Python objects where appropriate: ```python ->>> a.tickets.list_open_tickets()[0].created_at +>>> a.tickets.list_tickets()[0].created_at datetime.datetime(2014, 12, 5, 14, 7, 44) ``` From 47cb8322ed8fdd1ff8da4b624ab520d7e5b0acd4 Mon Sep 17 00:00:00 2001 From: Sam Kingston Date: Sun, 23 Sep 2018 07:26:57 +1000 Subject: [PATCH 35/46] Fixed some typos in README --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 01679c2..e6dd5e9 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ A library for the [Freshdesk](http://freshdesk.com/) helpdesk system for Python There is support for a limited subset of features, using either Freshdesk API v1 or v2. -After the deprication of Freshdesk V1 API, library uses V2 API only. +After the deprecation of Freshdesk V1 API, library uses V2 API only. Support for the v2 API includes the following features: * [Tickets](http://developer.freshdesk.com/api/#tickets) @@ -85,7 +85,7 @@ without changing these. To find your API key, follow Freshdesk's step-by-step solution article [How to find your API key](https://support.freshdesk.com/support/solutions/articles/215517). -By default, API v2 is used after the deprication of Freshdesk V1 API +By default, API v2 is used after the deprecation of Freshdesk V1 API ```python >>> a = API('company.freshdesk.com', 'q8dnkjaS554Aol21dmnas9d92', version=2) From 39edca5d86e73de5619b1d082d9d8b5c0ae626c8 Mon Sep 17 00:00:00 2001 From: jackson Date: Fri, 8 Mar 2019 02:07:57 -0700 Subject: [PATCH 36/46] Add support for verify ssl and proxies in V2 of the api, closes #34 (#35) * Add support for verify ssl and proxies in V2 of the api, closes #34 --- README.md | 14 ++++++++++++++ freshdesk/v2/api.py | 6 ++++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e6dd5e9..a0f79d8 100644 --- a/README.md +++ b/README.md @@ -93,6 +93,20 @@ By default, API v2 is used after the deprecation of Freshdesk V1 API The `API` class provides access to all the methods exposed by the Freshdesk API. +Optionally, the API v2 can be given SSL verification and/or proxy settings to obey for all requests: + +```python +>>> a = API('company.freshdesk.com', 'q8dnkjaS554Aol21dmnas9d92', verify=False) +``` + +```python +>>> proxies = { +... 'http': 'http://example.proxy:8000', +... 'https': 'https://example.proxy:8443' +... } +>>> a = API('company.freshdesk.com', 'q8dnkjaS554Aol21dmnas9d92', proxies=proxies) +``` + ### Tickets (API v2) The Ticket API is accessed by using the methods assigned to the `a.tickets` diff --git a/freshdesk/v2/api.py b/freshdesk/v2/api.py index 4bec8a4..7a46b8d 100644 --- a/freshdesk/v2/api.py +++ b/freshdesk/v2/api.py @@ -380,7 +380,7 @@ def currently_authenticated_agent(self): class API(object): - def __init__(self, domain, api_key): + def __init__(self, domain, api_key, verify=True, proxies=None): """Creates a wrapper to perform API actions. Arguments: @@ -394,6 +394,8 @@ def __init__(self, domain, api_key): self._api_prefix = 'https://{}/api/v2/'.format(domain.rstrip('/')) self._session = requests.Session() self._session.auth = (api_key, 'unused_with_api_key') + self._session.verify = verify + self._session.proxies = proxies self._session.headers = {'Content-Type': 'application/json'} self.tickets = TicketAPI(self) @@ -445,7 +447,7 @@ def _get(self, url, params={}): def _post(self, url, data={}, **kwargs): """Wrapper around request.post() to use the API prefix. Returns a JSON response.""" if 'files' in kwargs: - req = requests.post(self._api_prefix + url, auth=self._session.auth, data=data, **kwargs) + req = self._session.post(self._api_prefix + url, auth=self._session.auth, data=data, **kwargs) return self._action(req) req = self._session.post(self._api_prefix + url, data=data, **kwargs) From 88ac09aec383be9c27c719bd93ac547059607d21 Mon Sep 17 00:00:00 2001 From: Sam Kingston Date: Sat, 19 Oct 2019 05:26:32 +1000 Subject: [PATCH 37/46] Fix python versions in CI --- .travis.yml | 3 +-- tox.ini | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index defd39f..5fd82b6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,7 @@ language: python python: - "2.7" - - "3.3" - - "3.4" + - "3.6" sudo: false branches: only: diff --git a/tox.ini b/tox.ini index eb3ae4c..1fbd72a 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = pep8,py27,py35 +envlist = pep8,py27,py36 minversion = 1.6 [testenv] From 7d102a5c175d872657c0cc01f4a52fac950ef4c6 Mon Sep 17 00:00:00 2001 From: Artem Gordinsky Date: Fri, 18 Oct 2019 21:34:20 +0200 Subject: [PATCH 38/46] Support binary ticket attachments (#39) Copied from @haiiiiiyun's [snippet](https://github.com/sjkingo/python-freshdesk/issues/36#issuecomment-485713725) in #36 Tested in my code, works both for binary and non-binary attachments. --- freshdesk/v2/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freshdesk/v2/api.py b/freshdesk/v2/api.py index 7a46b8d..0b7e586 100644 --- a/freshdesk/v2/api.py +++ b/freshdesk/v2/api.py @@ -45,7 +45,7 @@ def _create_ticket_with_attachment(self, url, data): for attachment in attachments: file_name = attachment.split("/")[-1:][0] - multipart_data.append(('attachments[]', (file_name, open(attachment), None))) + multipart_data.append(('attachments[]', (file_name, open(attachment, 'rb'), None))) ticket = self._api._post(url, data=data, files=multipart_data) return ticket From 9afebe9516ef1252ac0787b4a668405c1d2058ef Mon Sep 17 00:00:00 2001 From: Samuel Montoya Garcia Date: Fri, 18 Oct 2019 16:58:26 -0300 Subject: [PATCH 39/46] Update api.py (#38) ADD: get time entries --- freshdesk/v2/api.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/freshdesk/v2/api.py b/freshdesk/v2/api.py index 0b7e586..d8f3bfb 100644 --- a/freshdesk/v2/api.py +++ b/freshdesk/v2/api.py @@ -1,7 +1,7 @@ import requests from requests.exceptions import HTTPError import json -from freshdesk.v2.models import Ticket, Comment, Customer, Contact, Group, Company, Agent, Role, TicketField +from freshdesk.v2.models import Ticket, Comment, Customer, Contact, Group, Company, Agent, Role, TicketField, TimeEntry class TicketAPI(object): @@ -301,6 +301,22 @@ def get_role(self, role_id): url = 'roles/%s' % role_id return Role(**self._api._get(url)) +class TimeEntryAPI(object): + def __init__(self, api): + self._api = api + + def list_time_entries(self, ticket_id=None): + url = 'tickets/time_entries' + if ticket_id is not None: + url = 'tickets/%d/time_entries' % ticket_id + timeEntries = [] + for r in self._api._get(url): + timeEntries.append(TimeEntry(**r)) + return timeEntries + + def get_role(self, role_id): + url = 'roles/%s' % role_id + return Role(**self._api._get(url)) class TicketFieldAPI(object): def __init__(self, api): From 48004bc2119e44c6ed1d44c6739f783b8275c9f3 Mon Sep 17 00:00:00 2001 From: Sam Kingston Date: Sat, 19 Oct 2019 06:29:43 +1000 Subject: [PATCH 40/46] Version bump to 1.2.4 [skip ci] --- CHANGELOG.md | 7 ++++++- freshdesk/__init__.py | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f2c43c..33c7c78 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,13 @@ Changelog ========= -v1.2.3- 2018-09-23 +v1.2.4 - 2019-10-19 + * #38: Add time entry API (@smontoya) + * #39: Support binary ticket attachments (@ArtemGordinsky) + * Drop Python 3.3 from Travis CI and add 3.6 (@sjkingo) + +v1.2.3 - 2018-09-23 * #28: Changes from #28 create ticket with attachments (@prenit-coverfox) * #25: Changes from #25 update_contact, restore_contact (@vdboor) diff --git a/freshdesk/__init__.py b/freshdesk/__init__.py index 5a5df3b..daab838 100644 --- a/freshdesk/__init__.py +++ b/freshdesk/__init__.py @@ -1 +1 @@ -__version__ = '1.2.3' +__version__ = '1.2.4' From bb42cd4c6f8ae7bf4682881f69172e92fcecebdc Mon Sep 17 00:00:00 2001 From: Sam Kingston Date: Sat, 19 Oct 2019 06:43:53 +1000 Subject: [PATCH 41/46] Update README --- CHANGELOG.md | 2 +- README.md | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 33c7c78..64750ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ Changelog v1.2.4 - 2019-10-19 - * #38: Add time entry API (@smontoya) + * #38: Add ticket time entry API (@smontoya) * #39: Support binary ticket attachments (@ArtemGordinsky) * Drop Python 3.3 from Travis CI and add 3.6 (@sjkingo) diff --git a/README.md b/README.md index a0f79d8..56f9a3c 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ Support for the v2 API includes the following features: - [Delete](http://developer.freshdesk.com/api/#delete_a_ticket) - [Create OutBound Email](http://developer.freshdesk.com/api/#create_outbound_email) - [List](http://developer.freshdesk.com/api/#list_all_tickets) + - [List Time Entries](https://developers.freshdesk.com/api/#list_all_ticket_timeentries) (as of 1.2.4) - Custom ticket fields (as of 1.1.1) * [Ticket Fields](http://developer.freshdesk.com/api/#ticket_fields) - [List](http://developer.freshdesk.com/api/#list_all_ticket_fields) From acbb4de43a5cce20b86cbb0ffa3b94f048bbb42a Mon Sep 17 00:00:00 2001 From: Artem Gordinsky Date: Mon, 21 Oct 2019 12:33:38 +0200 Subject: [PATCH 42/46] Use custom error classes to make error-handling easier (#40) Use a custom error class to make error-handling easier --- freshdesk/v2/api.py | 45 +++++++++++++++++++++++++-------------- freshdesk/v2/errors.py | 33 +++++++++++++++++++++++++++++ freshdesk/v2/test.py | 48 ++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 108 insertions(+), 18 deletions(-) create mode 100644 freshdesk/v2/errors.py diff --git a/freshdesk/v2/api.py b/freshdesk/v2/api.py index d8f3bfb..a9ab650 100644 --- a/freshdesk/v2/api.py +++ b/freshdesk/v2/api.py @@ -1,7 +1,13 @@ -import requests -from requests.exceptions import HTTPError import json -from freshdesk.v2.models import Ticket, Comment, Customer, Contact, Group, Company, Agent, Role, TicketField, TimeEntry + +import requests +from requests import HTTPError + +from freshdesk.v2.errors import ( + FreshdeskAccessDenied, FreshdeskBadRequest, FreshdeskError, FreshdeskNotFound, FreshdeskRateLimited, + FreshdeskServerError, FreshdeskUnauthorized, +) +from freshdesk.v2.models import Agent, Comment, Company, Contact, Customer, Group, Role, Ticket, TicketField, TimeEntry class TicketAPI(object): @@ -432,26 +438,33 @@ def __init__(self, domain, api_key, verify=True, proxies=None): def _action(self, req): try: j = req.json() - except: - req.raise_for_status() + except ValueError: j = {} - if 'Retry-After' in req.headers: - raise HTTPError('429 Rate Limit Exceeded: API rate-limit has been reached until {} seconds.' - 'See http://freshdesk.com/api#ratelimit'.format(req.headers['Retry-After'])) - - if 'code' in j and j['code'] == "invalid_credentials": - raise HTTPError('401 Unauthorized: Please login with correct credentials') - + error_message = 'Freshdesk Request Failed' if 'errors' in j: - raise HTTPError('{}: {}'.format(j.get('description'), - j.get('errors'))) + error_message = '{}: {}'.format(j.get('description'), j.get('errors')) + + if req.status_code == 400: + raise FreshdeskBadRequest(error_message) + elif req.status_code == 401: + raise FreshdeskUnauthorized(error_message) + elif req.status_code == 403: + raise FreshdeskAccessDenied(error_message) + elif req.status_code == 404: + raise FreshdeskNotFound(error_message) + elif req.status_code == 429: + raise FreshdeskRateLimited( + '429 Rate Limit Exceeded: API rate-limit has been reached until {} seconds. See ' + 'http://freshdesk.com/api#ratelimit'.format(req.headers.get('Retry-After'))) + elif 500 < req.status_code < 600: + raise FreshdeskServerError('{}: Server Error'.format(req.status_code)) # Catch any other errors try: req.raise_for_status() - except Exception as e: - raise HTTPError("{}: {}".format(e, j)) + except HTTPError as e: + raise FreshdeskError("{}: {}".format(e, j)) return j diff --git a/freshdesk/v2/errors.py b/freshdesk/v2/errors.py new file mode 100644 index 0000000..964241d --- /dev/null +++ b/freshdesk/v2/errors.py @@ -0,0 +1,33 @@ +from requests import HTTPError + + +class FreshdeskError(HTTPError): + """ + Base error class. + + Subclassing HTTPError to avoid breaking existing code that expects only HTTPErrors. + """ + + +class FreshdeskBadRequest(FreshdeskError): + """Most 40X and 501 status codes""" + + +class FreshdeskUnauthorized(FreshdeskError): + """401 Unauthorized""" + + +class FreshdeskAccessDenied(FreshdeskError): + """403 Forbidden""" + + +class FreshdeskNotFound(FreshdeskError): + """404""" + + +class FreshdeskRateLimited(FreshdeskError): + """429 Rate Limit Reached""" + + +class FreshdeskServerError(FreshdeskError): + """50X errors""" diff --git a/freshdesk/v2/test.py b/freshdesk/v2/test.py index 5ab6f7b..6e33581 100644 --- a/freshdesk/v2/test.py +++ b/freshdesk/v2/test.py @@ -124,6 +124,17 @@ def test_api_prefix(self): self.assertEqual(api._api_prefix, 'https://test_domain.freshdesk.com/api/v2/') + @responses.activate + def test_400_error(self): + responses.add(responses.GET, + 'https://{}/api/v2/tickets/1'.format(DOMAIN), + status=400) + + api = API('pythonfreshdesk.freshdesk.com', 'test_key') + from freshdesk.v2.errors import FreshdeskBadRequest + with self.assertRaises(FreshdeskBadRequest): + api.tickets.get_ticket(1) + @responses.activate def test_403_error(self): responses.add(responses.GET, @@ -131,8 +142,41 @@ def test_403_error(self): status=403) api = API('pythonfreshdesk.freshdesk.com', 'invalid_api_key') - from requests.exceptions import HTTPError - with self.assertRaises(HTTPError): + from freshdesk.v2.errors import FreshdeskAccessDenied + with self.assertRaises(FreshdeskAccessDenied): + api.tickets.get_ticket(1) + + @responses.activate + def test_404_error(self): + responses.add(responses.GET, + 'https://{}/api/v2/tickets/1'.format(DOMAIN), + status=404) + + api = API('pythonfreshdesk.freshdesk.com', 'test_key') + from freshdesk.v2.errors import FreshdeskNotFound + with self.assertRaises(FreshdeskNotFound): + api.tickets.get_ticket(1) + + @responses.activate + def test_rate_limited_error(self): + responses.add(responses.GET, + 'https://{}/api/v2/tickets/1'.format(DOMAIN), + status=429) + + api = API('pythonfreshdesk.freshdesk.com', 'test_key') + from freshdesk.v2.errors import FreshdeskRateLimited + with self.assertRaises(FreshdeskRateLimited): + api.tickets.get_ticket(1) + + @responses.activate + def test_50x_error(self): + responses.add(responses.GET, + 'https://{}/api/v2/tickets/1'.format(DOMAIN), + status=502) + + api = API('pythonfreshdesk.freshdesk.com', 'test_key') + from freshdesk.v2.errors import FreshdeskServerError + with self.assertRaises(FreshdeskServerError): api.tickets.get_ticket(1) From c10f6df8c5202f12c9f2c7e421e931f7f958df70 Mon Sep 17 00:00:00 2001 From: Artem Gordinsky Date: Wed, 23 Oct 2019 01:35:53 +0200 Subject: [PATCH 43/46] Switch to Pytest (#41) * Switch to Pytest * Refactor a test to use parametrize * Refactor the rest of the tests * Use the common pytest directory structure and test functions instead of classes --- README.md | 4 +- freshdesk/v1/test.py | 411 ------------- freshdesk/v1/tests/__init__.py | 0 freshdesk/v1/tests/conftest.py | 97 +++ .../{ => tests}/sample_json_data/agent_1.json | 0 .../sample_json_data/agent_1_updated.json | 0 .../{ => tests}/sample_json_data/agents.json | 0 .../sample_json_data/all_tickets.json | 0 .../{ => tests}/sample_json_data/contact.json | 0 .../sample_json_data/contacts.json | 0 .../sample_json_data/customer.json | 0 .../sample_json_data/ticket_1.json | 0 .../timeentries_ticket_1.json | 0 freshdesk/v1/tests/test_agent.py | 77 +++ freshdesk/v1/tests/test_api_class.py | 37 ++ freshdesk/v1/tests/test_comment.py | 22 + freshdesk/v1/tests/test_contact.py | 68 +++ freshdesk/v1/tests/test_customer.py | 40 ++ freshdesk/v1/tests/test_ticket.py | 109 ++++ freshdesk/v1/tests/test_timesheets.py | 32 + freshdesk/v2/test.py | 569 ------------------ freshdesk/v2/tests/__init__.py | 0 freshdesk/v2/tests/conftest.py | 106 ++++ .../{ => tests}/sample_json_data/agent_1.json | 0 .../sample_json_data/agent_1_updated.json | 0 .../{ => tests}/sample_json_data/agents.json | 1 - .../sample_json_data/all_tickets.json | 0 .../sample_json_data/attachment.txt | 0 .../{ => tests}/sample_json_data/contact.json | 0 .../sample_json_data/contact_1_agent.json | 0 .../sample_json_data/contact_updated.json | 0 .../sample_json_data/contacts.json | 0 .../sample_json_data/conversations.json | 0 .../sample_json_data/customer.json | 0 .../{ => tests}/sample_json_data/group_1.json | 0 .../{ => tests}/sample_json_data/groups.json | 0 .../{ => tests}/sample_json_data/note_1.json | 0 .../sample_json_data/outbound_email_1.json | 0 .../{ => tests}/sample_json_data/reply_1.json | 0 .../{ => tests}/sample_json_data/role_1.json | 0 .../{ => tests}/sample_json_data/roles.json | 0 .../sample_json_data/ticket_1.json | 0 .../sample_json_data/ticket_1_updated.json | 0 freshdesk/v2/tests/test_agent.py | 77 +++ freshdesk/v2/tests/test_api_class.py | 38 ++ freshdesk/v2/tests/test_comment.py | 36 ++ freshdesk/v2/tests/test_contact.py | 83 +++ freshdesk/v2/tests/test_customer.py | 40 ++ freshdesk/v2/tests/test_group.py | 36 ++ freshdesk/v2/tests/test_role.py | 32 + freshdesk/v2/tests/test_ticket.py | 171 ++++++ test-requirements.txt | 3 +- test.sh | 2 +- tox.ini | 5 +- 54 files changed, 1109 insertions(+), 987 deletions(-) delete mode 100644 freshdesk/v1/test.py create mode 100644 freshdesk/v1/tests/__init__.py create mode 100644 freshdesk/v1/tests/conftest.py rename freshdesk/v1/{ => tests}/sample_json_data/agent_1.json (100%) rename freshdesk/v1/{ => tests}/sample_json_data/agent_1_updated.json (100%) rename freshdesk/v1/{ => tests}/sample_json_data/agents.json (100%) rename freshdesk/v1/{ => tests}/sample_json_data/all_tickets.json (100%) rename freshdesk/v1/{ => tests}/sample_json_data/contact.json (100%) rename freshdesk/v1/{ => tests}/sample_json_data/contacts.json (100%) rename freshdesk/v1/{ => tests}/sample_json_data/customer.json (100%) rename freshdesk/v1/{ => tests}/sample_json_data/ticket_1.json (100%) rename freshdesk/v1/{ => tests}/sample_json_data/timeentries_ticket_1.json (100%) create mode 100644 freshdesk/v1/tests/test_agent.py create mode 100644 freshdesk/v1/tests/test_api_class.py create mode 100644 freshdesk/v1/tests/test_comment.py create mode 100644 freshdesk/v1/tests/test_contact.py create mode 100644 freshdesk/v1/tests/test_customer.py create mode 100644 freshdesk/v1/tests/test_ticket.py create mode 100644 freshdesk/v1/tests/test_timesheets.py delete mode 100644 freshdesk/v2/test.py create mode 100644 freshdesk/v2/tests/__init__.py create mode 100644 freshdesk/v2/tests/conftest.py rename freshdesk/v2/{ => tests}/sample_json_data/agent_1.json (100%) rename freshdesk/v2/{ => tests}/sample_json_data/agent_1_updated.json (100%) rename freshdesk/v2/{ => tests}/sample_json_data/agents.json (98%) rename freshdesk/v2/{ => tests}/sample_json_data/all_tickets.json (100%) rename freshdesk/v2/{ => tests}/sample_json_data/attachment.txt (100%) rename freshdesk/v2/{ => tests}/sample_json_data/contact.json (100%) rename freshdesk/v2/{ => tests}/sample_json_data/contact_1_agent.json (100%) rename freshdesk/v2/{ => tests}/sample_json_data/contact_updated.json (100%) rename freshdesk/v2/{ => tests}/sample_json_data/contacts.json (100%) rename freshdesk/v2/{ => tests}/sample_json_data/conversations.json (100%) rename freshdesk/v2/{ => tests}/sample_json_data/customer.json (100%) rename freshdesk/v2/{ => tests}/sample_json_data/group_1.json (100%) rename freshdesk/v2/{ => tests}/sample_json_data/groups.json (100%) rename freshdesk/v2/{ => tests}/sample_json_data/note_1.json (100%) rename freshdesk/v2/{ => tests}/sample_json_data/outbound_email_1.json (100%) rename freshdesk/v2/{ => tests}/sample_json_data/reply_1.json (100%) rename freshdesk/v2/{ => tests}/sample_json_data/role_1.json (100%) rename freshdesk/v2/{ => tests}/sample_json_data/roles.json (100%) rename freshdesk/v2/{ => tests}/sample_json_data/ticket_1.json (100%) rename freshdesk/v2/{ => tests}/sample_json_data/ticket_1_updated.json (100%) create mode 100644 freshdesk/v2/tests/test_agent.py create mode 100644 freshdesk/v2/tests/test_api_class.py create mode 100644 freshdesk/v2/tests/test_comment.py create mode 100644 freshdesk/v2/tests/test_contact.py create mode 100644 freshdesk/v2/tests/test_customer.py create mode 100644 freshdesk/v2/tests/test_group.py create mode 100644 freshdesk/v2/tests/test_role.py create mode 100644 freshdesk/v2/tests/test_ticket.py diff --git a/README.md b/README.md index 56f9a3c..c011de6 100644 --- a/README.md +++ b/README.md @@ -69,8 +69,8 @@ The easiest way to install is from [PyPi](https://pypi.python.org/pypi/python-fr 3. Optionally, run the test suite: ``` - $ pip install nose - $ nosetests + $ pip install pytest + $ pytest ``` ## Usage diff --git a/freshdesk/v1/test.py b/freshdesk/v1/test.py deleted file mode 100644 index 953cff1..0000000 --- a/freshdesk/v1/test.py +++ /dev/null @@ -1,411 +0,0 @@ -import datetime -import json -import re -import os.path -import responses -from unittest import TestCase - -from freshdesk.v1.api import API -from freshdesk.v1.models import Ticket, Comment, Contact, Customer, TimeEntry, Agent - -""" -Test suite for python-freshdesk. - -We test against a dummy helpdesk created for these tests only. It is: -http://pythonfreshdesk.freshdesk.com/ -""" - -DOMAIN = 'pythonfreshdesk.freshdesk.com' -API_KEY = 'MX4CEAw4FogInimEdRW2' - - -class MockedAPI(API): - def __init__(self, *args): - self.resolver = { - 'get': { - re.compile(r'helpdesk/tickets/filter/all_tickets\?format=json&page=1'): self.read_test_file( - 'all_tickets.json'), - re.compile(r'helpdesk/tickets/filter/new_my_open\?format=json&page=1'): self.read_test_file( - 'all_tickets.json'), - re.compile(r'helpdesk/tickets/filter/spam\?format=json&page=1'): [], - re.compile(r'helpdesk/tickets/filter/deleted\?format=json&page=1'): [], - re.compile(r'helpdesk/tickets/1/time_sheets.json'): self.read_test_file('timeentries_ticket_1.json'), - re.compile(r'helpdesk/tickets/1.json'): self.read_test_file('ticket_1.json'), - re.compile(r'.*&page=2'): [], - re.compile(r'contacts.json'): self.read_test_file('contacts.json'), - re.compile(r'contacts/1.json'): self.read_test_file('contact.json'), - re.compile(r'contacts/1.json'): self.read_test_file('contact.json'), - re.compile(r'agents.json\?$'): self.read_test_file('agents.json'), - re.compile(r'agents/1.json$'): self.read_test_file('agent_1.json'), - re.compile(r'customers/1.json'): self.read_test_file('customer.json'), - re.compile(r'helpdesk/time_sheets.json'): self.read_test_file('timeentries_ticket_1.json'), - re.compile(r'helpdesk/time_sheets.json\?agent_id='): self.read_test_file('timeentries_ticket_1.json'), - re.compile(r'helpdesk/time_sheets.json'): self.read_test_file('timeentries_ticket_1.json'), - }, - 'post': { - re.compile(r'helpdesk/tickets.json'): self.read_test_file('ticket_1.json'), - re.compile(r'contacts.json'): self.read_test_file('contact.json'), - re.compile(r'agents/1.json$'): self.read_test_file('agent_1.json'), - }, - 'put': { - re.compile(r'contacts/1/make_agent.json'): self.read_test_file('agent_1.json'), - re.compile(r'agents/1.json$'): self.read_test_file('agent_1_updated.json'), - }, - 'delete': { - re.compile(r'helpdesk/tickets/1.json'): None, - re.compile(r'contacts/1.json'): None, - re.compile(r'agents/1.json$'): None, - } - } - - super(MockedAPI, self).__init__(*args) - - def read_test_file(self, filename): - path = os.path.join(os.path.dirname(__file__), 'sample_json_data', filename) - return json.loads(open(path, 'r').read()) - - def _get(self, url, *args, **kwargs): - for pattern, j in self.resolver['get'].items(): - if pattern.match(url): - return j - - # No match found, raise 404 - from requests.exceptions import HTTPError - raise HTTPError('404: mocked_api_get() has no pattern for \'{}\''.format(url)) - - def _post(self, url, *args, **kwargs): - for pattern, data in self.resolver['post'].items(): - if pattern.match(url): - return data - - # No match found, raise 404 - from requests.exceptions import HTTPError - raise HTTPError('404: mocked_api_post() has no pattern for \'{}\''.format(url)) - - def _put(self, url, *args, **kwargs): - for pattern, data in self.resolver['put'].items(): - if pattern.match(url): - return data - - # No match found, raise 404 - from requests.exceptions import HTTPError - raise HTTPError('404: mocked_api_put() has no pattern for \'{}\''.format(url)) - - def _delete(self, url, *args, **kwargs): - for pattern, data in self.resolver['delete'].items(): - if pattern.match(url): - return data - - # No match found, raise 404 - from requests.exceptions import HTTPError - raise HTTPError('404: mocked_api_delete() has no pattern for \'{}\''.format(url)) - - -class TestAPIClass(TestCase): - def test_api_prefix(self): - api = API('test_domain', 'test_key') - self.assertEqual(api._api_prefix, 'https://test_domain/') - api = API('test_domain/', 'test_key') - self.assertEqual(api._api_prefix, 'https://test_domain/') - - @responses.activate - def test_403_error(self): - responses.add(responses.GET, - 'https://{}/helpdesk/tickets/1.json'.format(DOMAIN), - status=403) - - api = API(DOMAIN, 'invalid_api_key') - from requests.exceptions import HTTPError - with self.assertRaises(HTTPError): - api.tickets.get_ticket(1) - - @responses.activate - def test_404_error(self): - DOMAIN_404 = 'google.com' - responses.add(responses.GET, - 'https://{}/helpdesk/tickets/1.json'.format(DOMAIN_404), - status=404) - - api = API(DOMAIN_404, 'invalid_api_key') - from requests.exceptions import HTTPError - with self.assertRaises(HTTPError): - api.tickets.get_ticket(1) - - -class TestTicket(TestCase): - @classmethod - def setUpClass(cls): - cls.api = MockedAPI(DOMAIN, API_KEY) - cls.ticket = cls.api.tickets.get_ticket(1) - cls.ticket_json = json.loads(open(os.path.join(os.path.dirname(__file__), - 'sample_json_data', - 'ticket_1.json')).read()) - - def test_str(self): - self.assertEqual(str(self.ticket), 'This is a sample ticket') - - def test_repr(self): - self.assertEqual(repr(self.ticket), '') - - def test_create_ticket(self): - ticket = self.api.tickets.create_ticket('This is a sample ticket', - description='This is a sample ticket, feel free to delete it.', - email='test@example.com', - priority=1, status=2, - tags=['foo', 'bar'], - cc_emails=['test2@example.com']) - self.assertIsInstance(ticket, Ticket) - self.assertEqual(ticket.subject, 'This is a sample ticket') - self.assertEqual(ticket.description, 'This is a sample ticket, feel free to delete it.') - self.assertEqual(ticket.priority, 'low') - self.assertEqual(ticket.status, 'open') - self.assertEqual(ticket.cc_email['cc_emails'], ['test2@example.com']) - self.assertIn('foo', ticket.tags) - self.assertIn('bar', ticket.tags) - - def test_get_ticket(self): - self.assertIsInstance(self.ticket, Ticket) - self.assertEqual(self.ticket.display_id, 1) - self.assertEqual(self.ticket.subject, 'This is a sample ticket') - self.assertEqual(self.ticket.description, 'This is a sample ticket, feel free to delete it.') - - def test_ticket_priority(self): - self.assertEqual(self.ticket._priority, 1) - self.assertEqual(self.ticket.priority, 'low') - - def test_ticket_status(self): - self.assertEqual(self.ticket._status, 2) - self.assertEqual(self.ticket.status, 'open') - - def test_ticket_source(self): - self.assertEqual(self.ticket._source, 2) - self.assertEqual(self.ticket.source, 'portal') - - def test_ticket_datetime(self): - self.assertIsInstance(self.ticket.created_at, datetime.datetime) - self.assertIsInstance(self.ticket.updated_at, datetime.datetime) - - def test_all_tickets(self): - tickets = self.api.tickets.list_all_tickets() - self.assertIsInstance(tickets, list) - self.assertEqual(len(tickets), 1) - self.assertEqual(tickets[0].display_id, self.ticket.display_id) - - def test_open_tickets(self): - tickets = self.api.tickets.list_open_tickets() - self.assertIsInstance(tickets, list) - self.assertEqual(len(tickets), 1) - self.assertEqual(tickets[0].display_id, self.ticket.display_id) - - def test_deleted_tickets(self): - tickets = self.api.tickets.list_deleted_tickets() - self.assertIsInstance(tickets, list) - self.assertEqual(len(tickets), 0) - - def test_spam_tickets(self): - tickets = self.api.tickets.list_tickets(filter_name='spam') - self.assertIsInstance(tickets, list) - self.assertEqual(len(tickets), 0) - - def test_default_filter_name(self): - tickets = self.api.tickets.list_tickets() - self.assertIsInstance(tickets, list) - self.assertEqual(len(tickets), 1) - self.assertEqual(tickets[0].display_id, self.ticket.display_id) - - def test_none_filter_name(self): - tickets = self.api.tickets.list_tickets(filter_name=None) - self.assertIsInstance(tickets, list) - self.assertEqual(len(tickets), 1) - self.assertEqual(tickets[0].display_id, self.ticket.display_id) - - -class TestComment(TestCase): - @classmethod - def setUpClass(cls): - cls.api = MockedAPI(DOMAIN, API_KEY) - cls.ticket = cls.api.tickets.get_ticket(1) - - def test_comments_list(self): - self.assertIsInstance(self.ticket.comments, list) - self.assertEqual(len(self.ticket.comments), 1) - self.assertIsInstance(self.ticket.comments[0], Comment) - - def test_comment_str(self): - self.assertEqual(str(self.ticket.comments[0]), 'This is a reply.') - - def test_comment_repr(self): - self.assertEqual(repr(self.ticket.comments[0]), '>') - - -class TestContact(TestCase): - @classmethod - def setUpClass(cls): - cls.api = MockedAPI(DOMAIN, API_KEY) - cls.contact = cls.api.contacts.get_contact(1) - - def test_get_contact(self): - self.assertIsInstance(self.contact, Contact) - self.assertEqual(self.contact.name, 'Rachel') - self.assertEqual(self.contact.email, 'rachel@freshdesk.com') - self.assertEqual(self.contact.helpdesk_agent, False) - self.assertEqual(self.contact.customer_id, 1) - - def test_list_contacts(self): - contacts = self.api.contacts.list_contacts() - self.assertIsInstance(contacts, list) - self.assertEquals(len(contacts), 2) - self.assertIsInstance(contacts[0], Contact) - self.assertEquals(contacts[0].id, self.contact.id) - self.assertEquals(contacts[0].email, self.contact.email) - self.assertEquals(contacts[0].name, self.contact.name) - - def test_create_contact(self): - contact_data = { - 'name': 'Rachel', - 'email': 'rachel@freshdesk.com' - } - contact = self.api.contacts.create_contact(contact_data) - self.assertIsInstance(contact, Contact) - self.assertEquals(contact.id, self.contact.id) - self.assertEquals(contact.email, self.contact.email) - self.assertEquals(contact.name, self.contact.name) - - def test_make_agent(self): - agent = self.api.contacts.make_agent(self.contact.id) - self.assertIsInstance(agent, Agent) - self.assertEquals(agent.available, True) - self.assertEquals(agent.occasional, False) - self.assertEquals(agent.id, 1) - self.assertEquals(agent.user_id, self.contact.id) - self.assertEquals(agent.user['email'], self.contact.email) - self.assertEquals(agent.user['name'], self.contact.name) - - def test_delete_contact(self): - self.assertEquals(self.api.contacts.delete_contact(1), None) - - def test_contact_datetime(self): - self.assertIsInstance(self.contact.created_at, datetime.datetime) - self.assertIsInstance(self.contact.updated_at, datetime.datetime) - - def test_contact_str(self): - self.assertEqual(str(self.contact), 'Rachel') - - def test_contact_repr(self): - self.assertEqual(repr(self.contact), '') - - -class TestCustomer(TestCase): - @classmethod - def setUpClass(cls): - cls.api = MockedAPI(DOMAIN, API_KEY) - cls.customer = cls.api.customers.get_customer(1) - cls.contact = cls.api.contacts.get_contact(1) - - def test_customer(self): - self.assertIsInstance(self.customer, Customer) - self.assertEqual(self.customer.name, 'ACME Corp.') - self.assertEqual(self.customer.domains, 'acme.com') - self.assertEqual(self.customer.cf_custom_key, 'custom_value') - - def test_contact_datetime(self): - self.assertIsInstance(self.customer.created_at, datetime.datetime) - self.assertIsInstance(self.customer.updated_at, datetime.datetime) - - def test_contact_str(self): - self.assertEqual(str(self.customer), 'ACME Corp.') - - def test_contact_repr(self): - self.assertEqual(repr(self.customer), '') - - def test_get_customer_from_contact(self): - self.customer = self.api.customers.get_customer_from_contact(self.contact) - self.test_customer() - - -class TestTimesheets(TestCase): - @classmethod - def setUpClass(cls): - cls.api = MockedAPI(DOMAIN, API_KEY) - cls.timesheet = cls.api.timesheets.get_timesheet_by_ticket(1) - - def test_timesheet(self): - self.assertIsInstance(self.timesheet, type([])) - self.assertEqual(len(self.timesheet), 3) - self.assertIsInstance(self.timesheet[1], TimeEntry) - self.assertEqual(self.timesheet[1].id, 6000041896) - self.assertEqual(self.timesheet[1].note, "Foo") - self.assertEqual(self.timesheet[1].timespent, "0.33") - - def test_timesheet_str(self): - self.assertEqual(str(self.timesheet[1]), "6000041896") - - def test_timesheet_repr(self): - self.assertEqual(repr(self.timesheet[1]), '') - - def test_get_all_timesheets(self): - self.timesheet = self.api.timesheets.get_all_timesheets() - self.test_timesheet() - self.timesheet = self.api.timesheets.get_all_timesheets(filter_name="agent_id", filter_value="5004272350") - self.test_timesheet() - - -class TestAgent(TestCase): - - @classmethod - def setUpClass(cls): - cls.api = MockedAPI(DOMAIN, API_KEY) - cls.agent = cls.api.agents.get_agent(1) - cls.agent_json = json.loads(open(os.path.join(os.path.dirname(__file__), - 'sample_json_data', - 'agent_1.json')).read()) - - def test_str(self): - self.assertEqual(str(self.agent), 'Rachel') - - def test_repr(self): - self.assertEqual(repr(self.agent), '') - - def test_list_agents(self): - agents = self.api.agents.list_agents() - self.assertIsInstance(agents, list) - self.assertEqual(len(agents), 2) - self.assertEqual(agents[0].id, self.agent.id) - - def test_get_agent(self): - self.assertIsInstance(self.agent, Agent) - self.assertEqual(self.agent.id, 1) - self.assertEqual(self.agent.user['name'], 'Rachel') - self.assertEqual(self.agent.user['email'], 'rachel@freshdesk.com') - self.assertEqual(self.agent.user['mobile'], 1234) - self.assertEqual(self.agent.user['phone'], 5678) - self.assertEqual(self.agent.occasional, False) - - def test_update_agent(self): - values = { - 'occasional': True, - 'contact': { - 'name': 'Updated Name' - } - } - agent = self.api.agents.update_agent(1, **values) - - self.assertEqual(agent.occasional, True) - self.assertEqual(agent.user['name'], 'Updated Name') - - def test_delete_agent(self): - self.assertEquals(self.api.agents.delete_agent(1), None) - - def test_agent_name(self): - self.assertEqual(self.agent.user['name'], 'Rachel') - - def test_agent_mobile(self): - self.assertEqual(self.agent.user['mobile'], 1234) - - def test_agent_state(self): - self.assertEqual(self.agent.available, True) - self.assertEqual(self.agent.occasional, False) - - def test_agent_datetime(self): - self.assertIsInstance(self.agent.created_at, datetime.datetime) - self.assertIsInstance(self.agent.updated_at, datetime.datetime) diff --git a/freshdesk/v1/tests/__init__.py b/freshdesk/v1/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/freshdesk/v1/tests/conftest.py b/freshdesk/v1/tests/conftest.py new file mode 100644 index 0000000..4b6eec4 --- /dev/null +++ b/freshdesk/v1/tests/conftest.py @@ -0,0 +1,97 @@ +import json +import os.path +import re + +import pytest + +from freshdesk.v1.api import API + +DOMAIN = 'pythonfreshdesk.freshdesk.com' +API_KEY = 'MX4CEAw4FogInimEdRW2' + + +class MockedAPI(API): + def __init__(self, *args): + self.resolver = { + 'get': { + re.compile(r'helpdesk/tickets/filter/all_tickets\?format=json&page=1'): self.read_test_file( + 'all_tickets.json'), + re.compile(r'helpdesk/tickets/filter/new_my_open\?format=json&page=1'): self.read_test_file( + 'all_tickets.json'), + re.compile(r'helpdesk/tickets/filter/spam\?format=json&page=1'): [], + re.compile(r'helpdesk/tickets/filter/deleted\?format=json&page=1'): [], + re.compile(r'helpdesk/tickets/1/time_sheets.json'): self.read_test_file('timeentries_ticket_1.json'), + re.compile(r'helpdesk/tickets/1.json'): self.read_test_file('ticket_1.json'), + re.compile(r'.*&page=2'): [], + re.compile(r'contacts.json'): self.read_test_file('contacts.json'), + re.compile(r'contacts/1.json'): self.read_test_file('contact.json'), + re.compile(r'contacts/1.json'): self.read_test_file('contact.json'), + re.compile(r'agents.json\?$'): self.read_test_file('agents.json'), + re.compile(r'agents/1.json$'): self.read_test_file('agent_1.json'), + re.compile(r'customers/1.json'): self.read_test_file('customer.json'), + re.compile(r'helpdesk/time_sheets.json'): self.read_test_file('timeentries_ticket_1.json'), + re.compile(r'helpdesk/time_sheets.json\?agent_id='): self.read_test_file('timeentries_ticket_1.json'), + re.compile(r'helpdesk/time_sheets.json'): self.read_test_file('timeentries_ticket_1.json'), + }, + 'post': { + re.compile(r'helpdesk/tickets.json'): self.read_test_file('ticket_1.json'), + re.compile(r'contacts.json'): self.read_test_file('contact.json'), + re.compile(r'agents/1.json$'): self.read_test_file('agent_1.json'), + }, + 'put': { + re.compile(r'contacts/1/make_agent.json'): self.read_test_file('agent_1.json'), + re.compile(r'agents/1.json$'): self.read_test_file('agent_1_updated.json'), + }, + 'delete': { + re.compile(r'helpdesk/tickets/1.json'): None, + re.compile(r'contacts/1.json'): None, + re.compile(r'agents/1.json$'): None, + } + } + + super(MockedAPI, self).__init__(*args) + + def read_test_file(self, filename): + path = os.path.join(os.path.dirname(__file__), 'sample_json_data', filename) + return json.loads(open(path, 'r').read()) + + def _get(self, url, *args, **kwargs): + for pattern, j in self.resolver['get'].items(): + if pattern.match(url): + return j + + # No match found, raise 404 + from requests.exceptions import HTTPError + raise HTTPError('404: mocked_api_get() has no pattern for \'{}\''.format(url)) + + def _post(self, url, *args, **kwargs): + for pattern, data in self.resolver['post'].items(): + if pattern.match(url): + return data + + # No match found, raise 404 + from requests.exceptions import HTTPError + raise HTTPError('404: mocked_api_post() has no pattern for \'{}\''.format(url)) + + def _put(self, url, *args, **kwargs): + for pattern, data in self.resolver['put'].items(): + if pattern.match(url): + return data + + # No match found, raise 404 + from requests.exceptions import HTTPError + raise HTTPError('404: mocked_api_put() has no pattern for \'{}\''.format(url)) + + def _delete(self, url, *args, **kwargs): + for pattern, data in self.resolver['delete'].items(): + if pattern.match(url): + return data + + # No match found, raise 404 + from requests.exceptions import HTTPError + raise HTTPError('404: mocked_api_delete() has no pattern for \'{}\''.format(url)) + + +@pytest.fixture() +def api(): + return MockedAPI(DOMAIN, API_KEY) diff --git a/freshdesk/v1/sample_json_data/agent_1.json b/freshdesk/v1/tests/sample_json_data/agent_1.json similarity index 100% rename from freshdesk/v1/sample_json_data/agent_1.json rename to freshdesk/v1/tests/sample_json_data/agent_1.json diff --git a/freshdesk/v1/sample_json_data/agent_1_updated.json b/freshdesk/v1/tests/sample_json_data/agent_1_updated.json similarity index 100% rename from freshdesk/v1/sample_json_data/agent_1_updated.json rename to freshdesk/v1/tests/sample_json_data/agent_1_updated.json diff --git a/freshdesk/v1/sample_json_data/agents.json b/freshdesk/v1/tests/sample_json_data/agents.json similarity index 100% rename from freshdesk/v1/sample_json_data/agents.json rename to freshdesk/v1/tests/sample_json_data/agents.json diff --git a/freshdesk/v1/sample_json_data/all_tickets.json b/freshdesk/v1/tests/sample_json_data/all_tickets.json similarity index 100% rename from freshdesk/v1/sample_json_data/all_tickets.json rename to freshdesk/v1/tests/sample_json_data/all_tickets.json diff --git a/freshdesk/v1/sample_json_data/contact.json b/freshdesk/v1/tests/sample_json_data/contact.json similarity index 100% rename from freshdesk/v1/sample_json_data/contact.json rename to freshdesk/v1/tests/sample_json_data/contact.json diff --git a/freshdesk/v1/sample_json_data/contacts.json b/freshdesk/v1/tests/sample_json_data/contacts.json similarity index 100% rename from freshdesk/v1/sample_json_data/contacts.json rename to freshdesk/v1/tests/sample_json_data/contacts.json diff --git a/freshdesk/v1/sample_json_data/customer.json b/freshdesk/v1/tests/sample_json_data/customer.json similarity index 100% rename from freshdesk/v1/sample_json_data/customer.json rename to freshdesk/v1/tests/sample_json_data/customer.json diff --git a/freshdesk/v1/sample_json_data/ticket_1.json b/freshdesk/v1/tests/sample_json_data/ticket_1.json similarity index 100% rename from freshdesk/v1/sample_json_data/ticket_1.json rename to freshdesk/v1/tests/sample_json_data/ticket_1.json diff --git a/freshdesk/v1/sample_json_data/timeentries_ticket_1.json b/freshdesk/v1/tests/sample_json_data/timeentries_ticket_1.json similarity index 100% rename from freshdesk/v1/sample_json_data/timeentries_ticket_1.json rename to freshdesk/v1/tests/sample_json_data/timeentries_ticket_1.json diff --git a/freshdesk/v1/tests/test_agent.py b/freshdesk/v1/tests/test_agent.py new file mode 100644 index 0000000..4667fa2 --- /dev/null +++ b/freshdesk/v1/tests/test_agent.py @@ -0,0 +1,77 @@ +import datetime +import json +import os.path + +import pytest + +from freshdesk.v1.models import Agent + + +@pytest.fixture +def agent(api): + return api.agents.get_agent(1) + + +@pytest.fixture +def agent_json(): + return json.loads(open(os.path.join(os.path.dirname(__file__), 'sample_json_data', 'agent_1.json')).read()) + + +def test_str(agent): + assert str(agent) == 'Rachel' + + +def test_repr(agent): + assert repr(agent) == '' + + +def test_list_agents(api, agent): + agents = api.agents.list_agents() + assert isinstance(agents, list) + assert len(agents) == 2 + assert agents[0].id == agent.id + + +def test_get_agent(agent): + assert isinstance(agent, Agent) + assert agent.id == 1 + assert agent.user['name'] == 'Rachel' + assert agent.user['email'] == 'rachel@freshdesk.com' + assert agent.user['mobile'] == 1234 + assert agent.user['phone'] == 5678 + assert agent.occasional is False + + +def test_update_agent(api): + values = { + 'occasional': True, + 'contact': { + 'name': 'Updated Name' + } + } + agent = api.agents.update_agent(1, **values) + + assert agent.occasional is True + assert agent.user['name'] == 'Updated Name' + + +def test_delete_agent(api): + assert api.agents.delete_agent(1) is None + + +def test_agent_name(agent): + assert agent.user['name'] == 'Rachel' + + +def test_agent_mobile(agent): + assert agent.user['mobile'] == 1234 + + +def test_agent_state(agent): + assert agent.available is True + assert agent.occasional is False + + +def test_agent_datetime(agent): + assert isinstance(agent.created_at, datetime.datetime) + assert isinstance(agent.updated_at, datetime.datetime) diff --git a/freshdesk/v1/tests/test_api_class.py b/freshdesk/v1/tests/test_api_class.py new file mode 100644 index 0000000..936edee --- /dev/null +++ b/freshdesk/v1/tests/test_api_class.py @@ -0,0 +1,37 @@ +import pytest +import responses + +from freshdesk.v1.api import API +from freshdesk.v1.tests.conftest import DOMAIN + + +def test_api_prefix(): + api = API('test_domain', 'test_key') + assert api._api_prefix == 'https://test_domain/' + api = API('test_domain/', 'test_key') + assert api._api_prefix == 'https://test_domain/' + + +@responses.activate +def test_403_error(): + responses.add(responses.GET, + 'https://{}/helpdesk/tickets/1.json'.format(DOMAIN), + status=403) + + api = API(DOMAIN, 'invalid_api_key') + from requests.exceptions import HTTPError + with pytest.raises(HTTPError): + api.tickets.get_ticket(1) + + +@responses.activate +def test_404_error(): + DOMAIN_404 = 'google.com' + responses.add(responses.GET, + 'https://{}/helpdesk/tickets/1.json'.format(DOMAIN_404), + status=404) + + api = API(DOMAIN_404, 'invalid_api_key') + from requests.exceptions import HTTPError + with pytest.raises(HTTPError): + api.tickets.get_ticket(1) diff --git a/freshdesk/v1/tests/test_comment.py b/freshdesk/v1/tests/test_comment.py new file mode 100644 index 0000000..b323b09 --- /dev/null +++ b/freshdesk/v1/tests/test_comment.py @@ -0,0 +1,22 @@ +import pytest + +from freshdesk.v1.models import Comment + + +@pytest.fixture +def ticket(api): + return api.tickets.get_ticket(1) + + +def test_comments_list(ticket): + assert isinstance(ticket.comments, list) + assert len(ticket.comments) == 1 + assert isinstance(ticket.comments[0], Comment) + + +def test_comment_str(ticket): + assert str(ticket.comments[0]) == 'This is a reply.' + + +def test_comment_repr(ticket): + assert repr(ticket.comments[0]) == '>' diff --git a/freshdesk/v1/tests/test_contact.py b/freshdesk/v1/tests/test_contact.py new file mode 100644 index 0000000..7718744 --- /dev/null +++ b/freshdesk/v1/tests/test_contact.py @@ -0,0 +1,68 @@ +import datetime + +import pytest + +from freshdesk.v1.models import Contact, Agent + + +@pytest.fixture +def contact(api): + return api.contacts.get_contact(1) + + +def test_get_contact(contact): + assert isinstance(contact, Contact) + assert contact.name == 'Rachel' + assert contact.email == 'rachel@freshdesk.com' + assert contact.helpdesk_agent is False + assert contact.customer_id == 1 + + +def test_list_contacts(api, contact): + contacts = api.contacts.list_contacts() + assert isinstance(contacts, list) + assert len(contacts) == 2 + assert isinstance(contacts[0], Contact) + assert contacts[0].id == contact.id + assert contacts[0].email == contact.email + assert contacts[0].name == contact.name + + +def test_create_contact(api): + contact_data = { + 'name': 'Rachel', + 'email': 'rachel@freshdesk.com' + } + contact = api.contacts.create_contact(contact_data) + assert isinstance(contact, Contact) + assert contact.id == contact.id + assert contact.email == contact.email + assert contact.name == contact.name + + +def test_make_agent(api, contact): + agent = api.contacts.make_agent(contact.id) + assert isinstance(agent, Agent) + assert agent.available is True + assert agent.occasional is False + assert agent.id == 1 + assert agent.user_id == contact.id + assert agent.user['email'] == contact.email + assert agent.user['name'] == contact.name + + +def test_delete_contact(api): + assert api.contacts.delete_contact(1) is None + + +def test_contact_datetime(contact): + assert isinstance(contact.created_at, datetime.datetime) + assert isinstance(contact.updated_at, datetime.datetime) + + +def test_contact_str(contact): + assert str(contact) == 'Rachel' + + +def test_contact_repr(contact): + assert repr(contact) == '' diff --git a/freshdesk/v1/tests/test_customer.py b/freshdesk/v1/tests/test_customer.py new file mode 100644 index 0000000..1744a60 --- /dev/null +++ b/freshdesk/v1/tests/test_customer.py @@ -0,0 +1,40 @@ +import datetime + +import pytest + +from freshdesk.v1.models import Customer + + +@pytest.fixture +def customer(api): + return api.customers.get_customer(1) + + +@pytest.fixture +def contact(api): + return api.contacts.get_contact(1) + + +def test_customer(customer): + assert isinstance(customer, Customer) + assert customer.name == 'ACME Corp.' + assert customer.domains == 'acme.com' + assert customer.cf_custom_key == 'custom_value' + + +def test_contact_datetime(customer): + assert isinstance(customer.created_at, datetime.datetime) + assert isinstance(customer.updated_at, datetime.datetime) + + +def test_contact_str(customer): + assert str(customer) == 'ACME Corp.' + + +def test_contact_repr(customer): + assert repr(customer) == '' + + +def test_get_customer_from_contact(api, contact): + customer = api.customers.get_customer_from_contact(contact) + test_customer(customer) diff --git a/freshdesk/v1/tests/test_ticket.py b/freshdesk/v1/tests/test_ticket.py new file mode 100644 index 0000000..88dbd71 --- /dev/null +++ b/freshdesk/v1/tests/test_ticket.py @@ -0,0 +1,109 @@ +import datetime +import json +import os.path + +import pytest + +from freshdesk.v1.models import Ticket + + +@pytest.fixture +def ticket(api): + return api.tickets.get_ticket(1) + + +@pytest.fixture +def ticket_json(): + return json.loads(open(os.path.join(os.path.dirname(__file__), 'sample_json_data', 'ticket_1.json')).read()) + + +def test_str(ticket): + assert str(ticket) == 'This is a sample ticket' + + +def test_repr(ticket): + assert repr(ticket) == '' + + +def test_create_ticket(api): + ticket = api.tickets.create_ticket('This is a sample ticket', + description='This is a sample ticket, feel free to delete it.', + email='test@example.com', + priority=1, status=2, + tags=['foo', 'bar'], + cc_emails=['test2@example.com']) + assert isinstance(ticket, Ticket) + assert ticket.subject == 'This is a sample ticket' + assert ticket.description, 'This is a sample ticket == feel free to delete it.' + assert ticket.priority == 'low' + assert ticket.status == 'open' + assert ticket.cc_email['cc_emails'] == ['test2@example.com'] + assert 'foo' in ticket.tags + assert 'bar' in ticket.tags + + +def test_get_ticket(ticket): + assert isinstance(ticket, Ticket) + assert ticket.display_id == 1 + assert ticket.subject == 'This is a sample ticket' + assert ticket.description, 'This is a sample ticket == feel free to delete it.' + + +def test_ticket_priority(ticket): + assert ticket._priority == 1 + assert ticket.priority == 'low' + + +def test_ticket_status(ticket): + assert ticket._status == 2 + assert ticket.status == 'open' + + +def test_ticket_source(ticket): + assert ticket._source == 2 + assert ticket.source == 'portal' + + +def test_ticket_datetime(ticket): + assert isinstance(ticket.created_at, datetime.datetime) + assert isinstance(ticket.updated_at, datetime.datetime) + + +def test_all_tickets(api, ticket): + tickets = api.tickets.list_all_tickets() + assert isinstance(tickets, list) + assert len(tickets) == 1 + assert tickets[0].display_id == ticket.display_id + + +def test_open_tickets(api, ticket): + tickets = api.tickets.list_open_tickets() + assert isinstance(tickets, list) + assert len(tickets) == 1 + assert tickets[0].display_id == ticket.display_id + + +def test_deleted_tickets(api): + tickets = api.tickets.list_deleted_tickets() + assert isinstance(tickets, list) + assert len(tickets) == 0 + + +def test_spam_tickets(api): + tickets = api.tickets.list_tickets(filter_name='spam') + assert isinstance(tickets, list) + assert len(tickets) == 0 + + +def test_default_filter_name(api, ticket): + tickets = api.tickets.list_tickets() + assert isinstance(tickets, list) + assert len(tickets) == 1 + assert tickets[0].display_id == ticket.display_id + + +def test_none_filter_name(api, ticket): + tickets = api.tickets.list_tickets(filter_name=None) + assert isinstance(tickets, list) + assert len(tickets) == 1 + assert tickets[0].display_id == ticket.display_id diff --git a/freshdesk/v1/tests/test_timesheets.py b/freshdesk/v1/tests/test_timesheets.py new file mode 100644 index 0000000..c9ff246 --- /dev/null +++ b/freshdesk/v1/tests/test_timesheets.py @@ -0,0 +1,32 @@ +import pytest + +from freshdesk.v1.models import TimeEntry + + +@pytest.fixture +def timesheet(api): + return api.timesheets.get_timesheet_by_ticket(1) + + +def test_timesheet(timesheet): + assert isinstance(timesheet, type([])) + assert len(timesheet) == 3 + assert isinstance(timesheet[1], TimeEntry) + assert timesheet[1].id == 6000041896 + assert timesheet[1].note == "Foo" + assert timesheet[1].timespent == "0.33" + + +def test_timesheet_str(timesheet): + assert str(timesheet[1]) == "6000041896" + + +def test_timesheet_repr(timesheet): + assert repr(timesheet[1]) == '' + + +def test_get_all_timesheets(api): + timesheet = api.timesheets.get_all_timesheets() + test_timesheet(timesheet) + timesheet = api.timesheets.get_all_timesheets(filter_name="agent_id", filter_value="5004272350") + test_timesheet(timesheet) diff --git a/freshdesk/v2/test.py b/freshdesk/v2/test.py deleted file mode 100644 index 6e33581..0000000 --- a/freshdesk/v2/test.py +++ /dev/null @@ -1,569 +0,0 @@ -import datetime -import json -import re -import os.path - -import responses -from unittest import TestCase - -from freshdesk.v2.api import API -from freshdesk.v2.models import Ticket, Comment, Contact, Customer, Group, Agent, Role - -""" -Test suite for python-freshdesk. - -We test against a dummy helpdesk created for these tests only. It is: -https://pythonfreshdesk.freshdesk.com/ -""" - -DOMAIN = 'pythonfreshdesk.freshdesk.com' -API_KEY = 'MX4CEAw4FogInimEdRW2' - - -class MockedAPI(API): - def __init__(self, *args): - self.resolver = { - 'get': { - re.compile(r'tickets\?filter=new_and_my_open&page=1&per_page=100'): self.read_test_file('all_tickets.json'), - re.compile(r'tickets\?filter=deleted&page=1&per_page=100'): self.read_test_file('all_tickets.json'), - re.compile(r'tickets\?filter=spam&page=1&per_page=100'): self.read_test_file('all_tickets.json'), - re.compile(r'tickets\?filter=watching&page=1&per_page=100'): self.read_test_file('all_tickets.json'), - re.compile(r'tickets\?page=1&per_page=100'): self.read_test_file('all_tickets.json'), - re.compile(r'tickets/1$'): self.read_test_file('ticket_1.json'), - re.compile(r'tickets/1/conversations'): self.read_test_file('conversations.json'), - re.compile(r'contacts\?page=1&per_page=100$'): self.read_test_file('contacts.json'), - re.compile(r'contacts/1$'): self.read_test_file('contact.json'), - re.compile(r'customers/1$'): self.read_test_file('customer.json'), - re.compile(r'groups\?page=1&per_page=100$'): self.read_test_file('groups.json'), - re.compile(r'groups/1$'): self.read_test_file('group_1.json'), - re.compile(r'roles$'): self.read_test_file('roles.json'), - re.compile(r'roles/1$'): self.read_test_file('role_1.json'), - re.compile(r'agents\?email=abc@xyz.com&page=1&per_page=100'): self.read_test_file('agent_1.json'), - re.compile(r'agents\?mobile=1234&page=1&per_page=100'): self.read_test_file('agent_1.json'), - re.compile(r'agents\?phone=5678&page=1&per_page=100'): self.read_test_file('agent_1.json'), - re.compile(r'agents\?state=fulltime&page=1&per_page=100'): self.read_test_file('agent_1.json'), - re.compile(r'agents\?page=1&per_page=100'): self.read_test_file('agents.json'), - re.compile(r'agents/1$'): self.read_test_file('agent_1.json'), - }, - 'post': { - re.compile(r'tickets$'): self.read_test_file('ticket_1.json'), - re.compile(r'tickets/outbound_email$'): self.read_test_file('outbound_email_1.json'), - re.compile(r'tickets/1/notes$'): self.read_test_file('note_1.json'), - re.compile(r'tickets/1/reply$'): self.read_test_file('reply_1.json'), - re.compile(r'contacts$'): self.read_test_file('contact.json'), - }, - 'put': { - re.compile(r'tickets/1$'): self.read_test_file('ticket_1_updated.json'), - re.compile(r'contacts/1$'): self.read_test_file('contact_updated.json'), - re.compile(r'contacts/1/restore$'): self.read_test_file('contact.json'), - re.compile(r'contacts/1/make_agent$'): self.read_test_file('contact_1_agent.json'), - re.compile(r'agents/1$'): self.read_test_file('agent_1_updated.json'), - }, - 'delete': { - re.compile(r'tickets/1$'): None, - re.compile(r'agents/1$'): None, - re.compile(r'contacts/1$'): None, - re.compile(r'contacts/1/hard_delete\?force=True$'): None, - } - } - - super(MockedAPI, self).__init__(*args) - - def read_test_file(self, filename): - path = os.path.join(os.path.dirname(__file__), 'sample_json_data', filename) - return json.loads(open(path, 'r').read()) - - def _get(self, url, *args, **kwargs): - for pattern, data in self.resolver['get'].items(): - if pattern.match(url): - return data - - # No match found, raise 404 - from requests.exceptions import HTTPError - raise HTTPError('404: mocked_api_get() has no pattern for \'{}\''.format(url)) - - def _post(self, url, *args, **kwargs): - for pattern, data in self.resolver['post'].items(): - if pattern.match(url): - return data - - # No match found, raise 404 - from requests.exceptions import HTTPError - raise HTTPError('404: mocked_api_post() has no pattern for \'{}\''.format(url)) - - def _put(self, url, *args, **kwargs): - for pattern, data in self.resolver['put'].items(): - if pattern.match(url): - return data - - # No match found, raise 404 - from requests.exceptions import HTTPError - raise HTTPError('404: mocked_api_put() has no pattern for \'{}\''.format(url)) - - def _delete(self, url, *args, **kwargs): - for pattern, data in self.resolver['delete'].items(): - if pattern.match(url): - return data - - # No match found, raise 404 - from requests.exceptions import HTTPError - raise HTTPError('404: mocked_api_delete() has no pattern for \'{}\''.format(url)) - - -class TestAPIClass(TestCase): - - def test_custom_cname(self): - with self.assertRaises(AttributeError): - API('custom_cname_domain', 'invalid_api_key') - - def test_api_prefix(self): - api = API('test_domain.freshdesk.com', 'test_key') - self.assertEqual(api._api_prefix, - 'https://test_domain.freshdesk.com/api/v2/') - api = API('test_domain.freshdesk.com/', 'test_key') - self.assertEqual(api._api_prefix, - 'https://test_domain.freshdesk.com/api/v2/') - - @responses.activate - def test_400_error(self): - responses.add(responses.GET, - 'https://{}/api/v2/tickets/1'.format(DOMAIN), - status=400) - - api = API('pythonfreshdesk.freshdesk.com', 'test_key') - from freshdesk.v2.errors import FreshdeskBadRequest - with self.assertRaises(FreshdeskBadRequest): - api.tickets.get_ticket(1) - - @responses.activate - def test_403_error(self): - responses.add(responses.GET, - 'https://{}/api/v2/tickets/1'.format(DOMAIN), - status=403) - - api = API('pythonfreshdesk.freshdesk.com', 'invalid_api_key') - from freshdesk.v2.errors import FreshdeskAccessDenied - with self.assertRaises(FreshdeskAccessDenied): - api.tickets.get_ticket(1) - - @responses.activate - def test_404_error(self): - responses.add(responses.GET, - 'https://{}/api/v2/tickets/1'.format(DOMAIN), - status=404) - - api = API('pythonfreshdesk.freshdesk.com', 'test_key') - from freshdesk.v2.errors import FreshdeskNotFound - with self.assertRaises(FreshdeskNotFound): - api.tickets.get_ticket(1) - - @responses.activate - def test_rate_limited_error(self): - responses.add(responses.GET, - 'https://{}/api/v2/tickets/1'.format(DOMAIN), - status=429) - - api = API('pythonfreshdesk.freshdesk.com', 'test_key') - from freshdesk.v2.errors import FreshdeskRateLimited - with self.assertRaises(FreshdeskRateLimited): - api.tickets.get_ticket(1) - - @responses.activate - def test_50x_error(self): - responses.add(responses.GET, - 'https://{}/api/v2/tickets/1'.format(DOMAIN), - status=502) - - api = API('pythonfreshdesk.freshdesk.com', 'test_key') - from freshdesk.v2.errors import FreshdeskServerError - with self.assertRaises(FreshdeskServerError): - api.tickets.get_ticket(1) - - -class TestTicket(TestCase): - - @classmethod - def setUpClass(cls): - cls.api = MockedAPI(DOMAIN, API_KEY) - cls.ticket = cls.api.tickets.get_ticket(1) - cls.ticket_json = json.loads(open(os.path.join(os.path.dirname(__file__), - 'sample_json_data', - 'ticket_1.json')).read()) - cls.outbound_email_json = json.loads(open(os.path.join(os.path.dirname(__file__), - 'sample_json_data', - 'outbound_email_1.json')).read()) - - def test_str(self): - self.assertEqual(str(self.ticket), 'This is a sample ticket') - - def test_repr(self): - self.assertEqual(repr(self.ticket), '') - - def test_get_ticket(self): - self.assertIsInstance(self.ticket, Ticket) - self.assertEqual(self.ticket.id, 1) - self.assertEqual(self.ticket.subject, 'This is a sample ticket') - self.assertEqual(self.ticket.description_text, 'This is a sample ticket, feel free to delete it.') - self.assertEqual(self.ticket.cc_emails, ['test2@example.com']) - self.assertIn('foo', self.ticket.tags) - self.assertIn('bar', self.ticket.tags) - - def test_create_ticket(self): - attachment_path = os.path.join(os.path.dirname(__file__), 'sample_json_data', 'attachment.txt') - ticket = self.api.tickets.create_ticket('This is a sample ticket', - description='This is a sample ticket, feel free to delete it.', - email='test@example.com', - priority=1, status=2, - tags=['foo', 'bar'], - cc_emails=['test2@example.com'], - attachments=(attachment_path,)) - self.assertIsInstance(ticket, Ticket) - self.assertEqual(ticket.subject, 'This is a sample ticket') - self.assertEqual(ticket.description_text, 'This is a sample ticket, feel free to delete it.') - self.assertEqual(ticket.priority, 'low') - self.assertEqual(ticket.status, 'open') - self.assertEqual(ticket.cc_emails, ['test2@example.com']) - self.assertIn('foo', ticket.tags) - self.assertIn('bar', ticket.tags) - - def test_create_outbound_email(self): - j = self.outbound_email_json.copy() - email = 'test@example.com' - subject = 'This is a sample outbound email' - description = 'This is a sample outbound email, feel free to delete it.' - email_config_id = 5000054536 - values = { - 'status': 5, - 'priority': 1, - 'tags': ['foo', 'bar'], - 'cc_emails': ['test2@example.com'] - } - - email = self.api.tickets.create_outbound_email( - subject, - description, - email, - email_config_id, - **values - ) - - self.assertEqual(email.description_text, j['description_text']) - self.assertEqual(email._priority, j['priority']) - self.assertEqual(email._status, j['status']) - self.assertEqual(email.cc_emails, j['cc_emails']) - self.assertIn('foo', email.tags) - self.assertIn('bar', email.tags) - - def test_update_ticket(self): - j = self.ticket_json.copy() - values = { - 'subject': 'Test subject update', - 'priority': 3, - 'status': 4, - 'tags': ['hello', 'world'] - } - j.update(values) - - ticket = self.api.tickets.update_ticket(j['id'], **values) - self.assertEqual(ticket.subject, 'Test subject update') - self.assertEqual(ticket.status, 'resolved') - self.assertEqual(ticket.priority, 'high') - self.assertIn('hello', ticket.tags) - self.assertIn('world', ticket.tags) - - def test_delete_ticket(self): - self.assertEquals(self.api.tickets.delete_ticket(1), None) - - def test_ticket_priority(self): - self.assertEqual(self.ticket._priority, 1) - self.assertEqual(self.ticket.priority, 'low') - - def test_ticket_status(self): - self.assertEqual(self.ticket._status, 2) - self.assertEqual(self.ticket.status, 'open') - - def test_ticket_source(self): - self.assertEqual(self.ticket._source, 2) - self.assertEqual(self.ticket.source, 'portal') - - def test_ticket_datetime(self): - self.assertIsInstance(self.ticket.created_at, datetime.datetime) - self.assertIsInstance(self.ticket.updated_at, datetime.datetime) - - def test_new_and_my_open_tickets(self): - tickets = self.api.tickets.list_new_and_my_open_tickets() - self.assertIsInstance(tickets, list) - self.assertEqual(len(tickets), 1) - self.assertEqual(tickets[0].id, self.ticket.id) - - def test_deleted_tickets(self): - tickets = self.api.tickets.list_deleted_tickets() - self.assertIsInstance(tickets, list) - self.assertEqual(len(tickets), 1) - - def test_watched_tickets(self): - tickets = self.api.tickets.list_watched_tickets() - self.assertIsInstance(tickets, list) - self.assertEqual(len(tickets), 1) - self.assertEqual(tickets[0].id, self.ticket.id) - - def test_spam_tickets(self): - tickets = self.api.tickets.list_tickets(filter_name='spam') - self.assertIsInstance(tickets, list) - self.assertEqual(len(tickets), 1) - - def test_default_filter_name(self): - tickets = self.api.tickets.list_tickets() - self.assertIsInstance(tickets, list) - self.assertEqual(len(tickets), 1) - self.assertEqual(tickets[0].id, self.ticket.id) - - def test_none_filter_name(self): - tickets = self.api.tickets.list_tickets(filter_name=None) - self.assertIsInstance(tickets, list) - self.assertEqual(len(tickets), 1) - self.assertEqual(tickets[0].id, self.ticket.id) - - -class TestComment(TestCase): - @classmethod - def setUpClass(cls): - cls.api = MockedAPI(DOMAIN, API_KEY) - cls.comments = cls.api.comments.list_comments(1) - cls.comments_json = json.loads(open(os.path.join( - os.path.dirname(__file__), - 'sample_json_data', - 'conversations.json')).read()) - - def test_comments_list(self): - self.assertIsInstance(self.comments, list) - self.assertEqual(len(self.comments), 2) - self.assertIsInstance(self.comments[0], Comment) - - def test_comment_str(self): - self.assertEqual(str(self.comments[0]), 'This is a private note') - - def test_comment_repr(self): - self.assertEqual(repr(self.comments[0]), '') - - def test_create_note(self): - comment = self.api.comments.create_note(1, 'This is a private note') - self.assertIsInstance(comment, Comment) - self.assertEqual(comment.body_text, 'This is a private note') - self.assertEqual(comment.source, 'note') - - def test_create_reply(self): - comment = self.api.comments.create_reply(1, 'This is a reply') - self.assertIsInstance(comment, Comment) - self.assertEqual(comment.body_text, 'This is a reply') - self.assertEqual(comment.source, 'reply') - - -class TestContact(TestCase): - @classmethod - def setUpClass(cls): - cls.api = MockedAPI(DOMAIN, API_KEY) - cls.contact = cls.api.contacts.get_contact(1) - - def test_get_contact(self): - self.assertIsInstance(self.contact, Contact) - self.assertEqual(self.contact.name, 'Rachel') - self.assertEqual(self.contact.email, 'rachel@freshdesk.com') - self.assertEqual(self.contact.helpdesk_agent, False) - self.assertEqual(self.contact.customer_id, 1) - - def test_list_contact(self): - contacts = self.api.contacts.list_contacts() - self.assertIsInstance(contacts, list) - self.assertIsInstance(contacts[0], Contact) - self.assertEquals(len(contacts), 2) - self.assertEquals(contacts[0].__dict__, self.contact.__dict__) - - def test_create_contact(self): - contact_data = { - 'name': 'Rachel', - 'email': 'rachel@freshdesk.com' - } - contact = self.api.contacts.create_contact(contact_data) - self.assertIsInstance(contact, Contact) - self.assertEquals(contact.email, self.contact.email) - self.assertEquals(contact.name, self.contact.name) - - def test_update_contact(self): - contact_data = { - 'name': 'New Name' - } - contact = self.api.contacts.update_contact(1, **contact_data) - self.assertIsInstance(contact, Contact) - self.assertEquals(contact.name, 'New Name') - - def test_soft_delete_contact(self): - self.assertEquals(self.api.contacts.soft_delete_contact(1), None) - - def test_permanently_delete_contact(self): - self.assertEquals(self.api.contacts.permanently_delete_contact(1), None) - - def test_restore_contact(self): - self.api.contacts.restore_contact(1) - contact = self.api.contacts.get_contact(1) - self.assertIsInstance(contact, Contact) - self.assertEquals(contact.deleted, False) - - def test_make_agent(self): - agent = self.api.contacts.make_agent(self.contact.id) - self.assertIsInstance(agent, Agent) - self.assertEquals(agent.available, True) - self.assertEquals(agent.occasional, False) - self.assertEquals(agent.contact['email'], self.contact.email) - self.assertEquals(agent.contact['name'], self.contact.name) - - def test_contact_datetime(self): - self.assertIsInstance(self.contact.created_at, datetime.datetime) - self.assertIsInstance(self.contact.updated_at, datetime.datetime) - - def test_contact_str(self): - self.assertEqual(str(self.contact), 'Rachel') - - def test_contact_repr(self): - self.assertEqual(repr(self.contact), '') - - -class TestCustomer(TestCase): - @classmethod - def setUpClass(cls): - cls.api = MockedAPI(DOMAIN, API_KEY) - cls.customer = cls.api.customers.get_customer('1') - cls.contact = cls.api.contacts.get_contact(1) - - def test_customer(self): - self.assertIsInstance(self.customer, Customer) - self.assertEqual(self.customer.name, 'ACME Corp.') - self.assertEqual(self.customer.domains, 'acme.com') - self.assertEqual(self.customer.cf_custom_key, 'custom_value') - - def test_customer_datetime(self): - self.assertIsInstance(self.customer.created_at, datetime.datetime) - self.assertIsInstance(self.customer.updated_at, datetime.datetime) - - def test_customer_str(self): - self.assertEqual(str(self.customer), 'ACME Corp.') - - def test_customer_repr(self): - self.assertEqual(repr(self.customer), '') - - def test_get_customer_from_contact(self): - self.customer = self.api.customers.get_customer_from_contact(self.contact) - self.test_customer() - - -class TestGroup(TestCase): - @classmethod - def setUpClass(cls): - cls.api = MockedAPI(DOMAIN, API_KEY) - cls.group = cls.api.groups.get_group(1) - - def test_list_groups(self): - groups = self.api.groups.list_groups() - self.assertIsInstance(groups, list) - self.assertEqual(len(groups), 2) - self.assertEqual(groups[0].id, self.group.id) - - def test_group(self): - self.assertIsInstance(self.group, Group) - self.assertEqual(self.group.name, 'Entertainers') - self.assertEqual(self.group.description, 'Singers dancers and stand up comedians') - - def test_group_datetime(self): - self.assertIsInstance(self.group.created_at, datetime.datetime) - self.assertIsInstance(self.group.updated_at, datetime.datetime) - - def test_group_str(self): - self.assertEqual(str(self.group), 'Entertainers') - - def test_group_repr(self): - self.assertEqual(repr(self.group), '') - -class TestRole(TestCase): - @classmethod - def setUpClass(cls): - cls.api = MockedAPI(DOMAIN, API_KEY) - cls.role = cls.api.roles.get_role(1) - - def test_list_roles(self): - roles = self.api.roles.list_roles() - self.assertIsInstance(roles, list) - self.assertEqual(len(roles), 2) - self.assertEqual(roles[0].id, self.role.id) - - def test_role(self): - self.assertIsInstance(self.role, Role) - self.assertEqual(self.role.name, 'Agent') - self.assertEqual(self.role.description, 'Can log, view, reply, update and resolve tickets and manage contacts.') - - def test_group_datetime(self): - self.assertIsInstance(self.role.created_at, datetime.datetime) - self.assertIsInstance(self.role.updated_at, datetime.datetime) - - def test_group_repr(self): - self.assertEqual(repr(self.role), '') - - -class TestAgent(TestCase): - - @classmethod - def setUpClass(cls): - cls.api = MockedAPI(DOMAIN, API_KEY) - cls.agent = cls.api.agents.get_agent(1) - cls.agent_json = json.loads(open(os.path.join(os.path.dirname(__file__), - 'sample_json_data', - 'agent_1.json')).read()) - - def test_str(self): - self.assertEqual(str(self.agent), 'Rachel') - - def test_repr(self): - self.assertEqual(repr(self.agent), '') - - def test_get_agent(self): - self.assertIsInstance(self.agent, Agent) - self.assertEqual(self.agent.id, 1) - self.assertEqual(self.agent.contact['name'], 'Rachel') - self.assertEqual(self.agent.contact['email'], 'rachel@freshdesk.com') - self.assertEqual(self.agent.contact['mobile'], 1234) - self.assertEqual(self.agent.contact['phone'], 5678) - self.assertEqual(self.agent.occasional, False) - - def test_update_agent(self): - values = { - 'occasional': True, - 'contact': { - 'name': 'Updated Name' - } - } - agent = self.api.agents.update_agent(1, **values) - - self.assertEqual(agent.occasional, True) - self.assertEqual(agent.contact['name'], 'Updated Name') - - def test_delete_agent(self): - self.assertEquals(self.api.agents.delete_agent(1), None) - - def test_agent_name(self): - self.assertEqual(self.agent.contact['name'], 'Rachel') - - def test_agent_mobile(self): - self.assertEqual(self.agent.contact['mobile'], 1234) - - def test_agent_state(self): - self.assertEqual(self.agent.available, True) - self.assertEqual(self.agent.occasional, False) - - def test_agent_datetime(self): - self.assertIsInstance(self.agent.created_at, datetime.datetime) - self.assertIsInstance(self.agent.updated_at, datetime.datetime) - - def test_none_filter_name(self): - agents = self.api.agents.list_agents() - self.assertIsInstance(agents, list) - self.assertEqual(len(agents), 2) - self.assertEqual(agents[0].id, self.agent.id) diff --git a/freshdesk/v2/tests/__init__.py b/freshdesk/v2/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/freshdesk/v2/tests/conftest.py b/freshdesk/v2/tests/conftest.py new file mode 100644 index 0000000..f44b12c --- /dev/null +++ b/freshdesk/v2/tests/conftest.py @@ -0,0 +1,106 @@ +import json +import os.path +import re + +import pytest + +from freshdesk.v2.api import API + +DOMAIN = 'pythonfreshdesk.freshdesk.com' +API_KEY = 'MX4CEAw4FogInimEdRW2' + + +class MockedAPI(API): + def __init__(self, *args): + self.resolver = { + 'get': { + re.compile(r'tickets\?filter=new_and_my_open&page=1&per_page=100'): self.read_test_file( + 'all_tickets.json'), + re.compile(r'tickets\?filter=deleted&page=1&per_page=100'): self.read_test_file('all_tickets.json'), + re.compile(r'tickets\?filter=spam&page=1&per_page=100'): self.read_test_file('all_tickets.json'), + re.compile(r'tickets\?filter=watching&page=1&per_page=100'): self.read_test_file('all_tickets.json'), + re.compile(r'tickets\?page=1&per_page=100'): self.read_test_file('all_tickets.json'), + re.compile(r'tickets/1$'): self.read_test_file('ticket_1.json'), + re.compile(r'tickets/1/conversations'): self.read_test_file('conversations.json'), + re.compile(r'contacts\?page=1&per_page=100$'): self.read_test_file('contacts.json'), + re.compile(r'contacts/1$'): self.read_test_file('contact.json'), + re.compile(r'customers/1$'): self.read_test_file('customer.json'), + re.compile(r'groups\?page=1&per_page=100$'): self.read_test_file('groups.json'), + re.compile(r'groups/1$'): self.read_test_file('group_1.json'), + re.compile(r'roles$'): self.read_test_file('roles.json'), + re.compile(r'roles/1$'): self.read_test_file('role_1.json'), + re.compile(r'agents\?email=abc@xyz.com&page=1&per_page=100'): self.read_test_file('agent_1.json'), + re.compile(r'agents\?mobile=1234&page=1&per_page=100'): self.read_test_file('agent_1.json'), + re.compile(r'agents\?phone=5678&page=1&per_page=100'): self.read_test_file('agent_1.json'), + re.compile(r'agents\?state=fulltime&page=1&per_page=100'): self.read_test_file('agent_1.json'), + re.compile(r'agents\?page=1&per_page=100'): self.read_test_file('agents.json'), + re.compile(r'agents/1$'): self.read_test_file('agent_1.json'), + }, + 'post': { + re.compile(r'tickets$'): self.read_test_file('ticket_1.json'), + re.compile(r'tickets/outbound_email$'): self.read_test_file('outbound_email_1.json'), + re.compile(r'tickets/1/notes$'): self.read_test_file('note_1.json'), + re.compile(r'tickets/1/reply$'): self.read_test_file('reply_1.json'), + re.compile(r'contacts$'): self.read_test_file('contact.json'), + }, + 'put': { + re.compile(r'tickets/1$'): self.read_test_file('ticket_1_updated.json'), + re.compile(r'contacts/1$'): self.read_test_file('contact_updated.json'), + re.compile(r'contacts/1/restore$'): self.read_test_file('contact.json'), + re.compile(r'contacts/1/make_agent$'): self.read_test_file('contact_1_agent.json'), + re.compile(r'agents/1$'): self.read_test_file('agent_1_updated.json'), + }, + 'delete': { + re.compile(r'tickets/1$'): None, + re.compile(r'agents/1$'): None, + re.compile(r'contacts/1$'): None, + re.compile(r'contacts/1/hard_delete\?force=True$'): None, + } + } + + super(MockedAPI, self).__init__(*args) + + def read_test_file(self, filename): + path = os.path.join(os.path.dirname(__file__), 'sample_json_data', filename) + return json.loads(open(path, 'r').read()) + + def _get(self, url, *args, **kwargs): + for pattern, data in self.resolver['get'].items(): + if pattern.match(url): + return data + + # No match found, raise 404 + from requests.exceptions import HTTPError + raise HTTPError('404: mocked_api_get() has no pattern for \'{}\''.format(url)) + + def _post(self, url, *args, **kwargs): + for pattern, data in self.resolver['post'].items(): + if pattern.match(url): + return data + + # No match found, raise 404 + from requests.exceptions import HTTPError + raise HTTPError('404: mocked_api_post() has no pattern for \'{}\''.format(url)) + + def _put(self, url, *args, **kwargs): + for pattern, data in self.resolver['put'].items(): + if pattern.match(url): + return data + + # No match found, raise 404 + from requests.exceptions import HTTPError + raise HTTPError('404: mocked_api_put() has no pattern for \'{}\''.format(url)) + + def _delete(self, url, *args, **kwargs): + for pattern, data in self.resolver['delete'].items(): + if pattern.match(url): + return data + + # No match found, raise 404 + from requests.exceptions import HTTPError + raise HTTPError('404: mocked_api_delete() has no pattern for \'{}\''.format(url)) + + +@pytest.fixture() +def api(): + return MockedAPI(DOMAIN, API_KEY) diff --git a/freshdesk/v2/sample_json_data/agent_1.json b/freshdesk/v2/tests/sample_json_data/agent_1.json similarity index 100% rename from freshdesk/v2/sample_json_data/agent_1.json rename to freshdesk/v2/tests/sample_json_data/agent_1.json diff --git a/freshdesk/v2/sample_json_data/agent_1_updated.json b/freshdesk/v2/tests/sample_json_data/agent_1_updated.json similarity index 100% rename from freshdesk/v2/sample_json_data/agent_1_updated.json rename to freshdesk/v2/tests/sample_json_data/agent_1_updated.json diff --git a/freshdesk/v2/sample_json_data/agents.json b/freshdesk/v2/tests/sample_json_data/agents.json similarity index 98% rename from freshdesk/v2/sample_json_data/agents.json rename to freshdesk/v2/tests/sample_json_data/agents.json index e679a7b..d7bcc22 100644 --- a/freshdesk/v2/sample_json_data/agents.json +++ b/freshdesk/v2/tests/sample_json_data/agents.json @@ -26,7 +26,6 @@ "available":true, "occasional":false, "signature":null, - "signature":null, "id":432, "ticket_scope":1, "created_at":"2015-08-28T11:47:58Z", diff --git a/freshdesk/v2/sample_json_data/all_tickets.json b/freshdesk/v2/tests/sample_json_data/all_tickets.json similarity index 100% rename from freshdesk/v2/sample_json_data/all_tickets.json rename to freshdesk/v2/tests/sample_json_data/all_tickets.json diff --git a/freshdesk/v2/sample_json_data/attachment.txt b/freshdesk/v2/tests/sample_json_data/attachment.txt similarity index 100% rename from freshdesk/v2/sample_json_data/attachment.txt rename to freshdesk/v2/tests/sample_json_data/attachment.txt diff --git a/freshdesk/v2/sample_json_data/contact.json b/freshdesk/v2/tests/sample_json_data/contact.json similarity index 100% rename from freshdesk/v2/sample_json_data/contact.json rename to freshdesk/v2/tests/sample_json_data/contact.json diff --git a/freshdesk/v2/sample_json_data/contact_1_agent.json b/freshdesk/v2/tests/sample_json_data/contact_1_agent.json similarity index 100% rename from freshdesk/v2/sample_json_data/contact_1_agent.json rename to freshdesk/v2/tests/sample_json_data/contact_1_agent.json diff --git a/freshdesk/v2/sample_json_data/contact_updated.json b/freshdesk/v2/tests/sample_json_data/contact_updated.json similarity index 100% rename from freshdesk/v2/sample_json_data/contact_updated.json rename to freshdesk/v2/tests/sample_json_data/contact_updated.json diff --git a/freshdesk/v2/sample_json_data/contacts.json b/freshdesk/v2/tests/sample_json_data/contacts.json similarity index 100% rename from freshdesk/v2/sample_json_data/contacts.json rename to freshdesk/v2/tests/sample_json_data/contacts.json diff --git a/freshdesk/v2/sample_json_data/conversations.json b/freshdesk/v2/tests/sample_json_data/conversations.json similarity index 100% rename from freshdesk/v2/sample_json_data/conversations.json rename to freshdesk/v2/tests/sample_json_data/conversations.json diff --git a/freshdesk/v2/sample_json_data/customer.json b/freshdesk/v2/tests/sample_json_data/customer.json similarity index 100% rename from freshdesk/v2/sample_json_data/customer.json rename to freshdesk/v2/tests/sample_json_data/customer.json diff --git a/freshdesk/v2/sample_json_data/group_1.json b/freshdesk/v2/tests/sample_json_data/group_1.json similarity index 100% rename from freshdesk/v2/sample_json_data/group_1.json rename to freshdesk/v2/tests/sample_json_data/group_1.json diff --git a/freshdesk/v2/sample_json_data/groups.json b/freshdesk/v2/tests/sample_json_data/groups.json similarity index 100% rename from freshdesk/v2/sample_json_data/groups.json rename to freshdesk/v2/tests/sample_json_data/groups.json diff --git a/freshdesk/v2/sample_json_data/note_1.json b/freshdesk/v2/tests/sample_json_data/note_1.json similarity index 100% rename from freshdesk/v2/sample_json_data/note_1.json rename to freshdesk/v2/tests/sample_json_data/note_1.json diff --git a/freshdesk/v2/sample_json_data/outbound_email_1.json b/freshdesk/v2/tests/sample_json_data/outbound_email_1.json similarity index 100% rename from freshdesk/v2/sample_json_data/outbound_email_1.json rename to freshdesk/v2/tests/sample_json_data/outbound_email_1.json diff --git a/freshdesk/v2/sample_json_data/reply_1.json b/freshdesk/v2/tests/sample_json_data/reply_1.json similarity index 100% rename from freshdesk/v2/sample_json_data/reply_1.json rename to freshdesk/v2/tests/sample_json_data/reply_1.json diff --git a/freshdesk/v2/sample_json_data/role_1.json b/freshdesk/v2/tests/sample_json_data/role_1.json similarity index 100% rename from freshdesk/v2/sample_json_data/role_1.json rename to freshdesk/v2/tests/sample_json_data/role_1.json diff --git a/freshdesk/v2/sample_json_data/roles.json b/freshdesk/v2/tests/sample_json_data/roles.json similarity index 100% rename from freshdesk/v2/sample_json_data/roles.json rename to freshdesk/v2/tests/sample_json_data/roles.json diff --git a/freshdesk/v2/sample_json_data/ticket_1.json b/freshdesk/v2/tests/sample_json_data/ticket_1.json similarity index 100% rename from freshdesk/v2/sample_json_data/ticket_1.json rename to freshdesk/v2/tests/sample_json_data/ticket_1.json diff --git a/freshdesk/v2/sample_json_data/ticket_1_updated.json b/freshdesk/v2/tests/sample_json_data/ticket_1_updated.json similarity index 100% rename from freshdesk/v2/sample_json_data/ticket_1_updated.json rename to freshdesk/v2/tests/sample_json_data/ticket_1_updated.json diff --git a/freshdesk/v2/tests/test_agent.py b/freshdesk/v2/tests/test_agent.py new file mode 100644 index 0000000..d635910 --- /dev/null +++ b/freshdesk/v2/tests/test_agent.py @@ -0,0 +1,77 @@ +import datetime +import json +import os.path + +import pytest + +from freshdesk.v2.models import Agent + + +@pytest.fixture +def agent(api): + return api.agents.get_agent(1) + + +@pytest.fixture +def agent_json(): + return json.loads(open(os.path.join(os.path.dirname(__file__), 'sample_json_data', 'agent_1.json')).read()) + + +def test_str(agent): + assert str(agent) == 'Rachel' + + +def test_repr(agent): + assert repr(agent) == '' + + +def test_get_agent(agent): + assert isinstance(agent, Agent) + assert agent.id == 1 + assert agent.contact['name'] == 'Rachel' + assert agent.contact['email'] == 'rachel@freshdesk.com' + assert agent.contact['mobile'] == 1234 + assert agent.contact['phone'] == 5678 + assert agent.occasional is False + + +def test_update_agent(api): + values = { + 'occasional': True, + 'contact': { + 'name': 'Updated Name' + } + } + agent = api.agents.update_agent(1, **values) + + assert agent.occasional is True + assert agent.contact['name'] == 'Updated Name' + + +def test_delete_agent(api): + assert api.agents.delete_agent(1) is None + + +def test_agent_name(agent): + assert agent.contact['name'] == 'Rachel' + + +def test_agent_mobile(agent): + assert agent.contact['mobile'] == 1234 + + +def test_agent_state(agent): + assert agent.available is True + assert agent.occasional is False + + +def test_agent_datetime(agent): + assert isinstance(agent.created_at, datetime.datetime) + assert isinstance(agent.updated_at, datetime.datetime) + + +def test_none_filter_name(api, agent): + agents = api.agents.list_agents() + assert isinstance(agents, list) + assert len(agents) == 2 + assert agents[0].id == agent.id diff --git a/freshdesk/v2/tests/test_api_class.py b/freshdesk/v2/tests/test_api_class.py new file mode 100644 index 0000000..e128d9e --- /dev/null +++ b/freshdesk/v2/tests/test_api_class.py @@ -0,0 +1,38 @@ +import pytest +import responses + +from freshdesk.v2.api import API +from freshdesk.v2.errors import ( + FreshdeskBadRequest, FreshdeskAccessDenied, FreshdeskNotFound, FreshdeskError, + FreshdeskRateLimited, + FreshdeskServerError, +) +from freshdesk.v2.tests.conftest import DOMAIN + + +def test_custom_cname(): + with pytest.raises(AttributeError): + API('custom_cname_domain', 'invalid_api_key') + + +def test_api_prefix(): + api = API('test_domain.freshdesk.com', 'test_key') + assert api._api_prefix == 'https://test_domain.freshdesk.com/api/v2/' + api = API('test_domain.freshdesk.com/', 'test_key') + assert api._api_prefix == 'https://test_domain.freshdesk.com/api/v2/' + + +@responses.activate +@pytest.mark.parametrize( + ('status_code', 'exception'), + [(400, FreshdeskBadRequest), (403, FreshdeskAccessDenied), (404, FreshdeskNotFound), + (418, FreshdeskError), (429, FreshdeskRateLimited), (502, FreshdeskServerError)] +) +def test_errors(status_code, exception): + responses.add(responses.GET, + 'https://{}/api/v2/tickets/1'.format(DOMAIN), + status=status_code) + + api = API('pythonfreshdesk.freshdesk.com', 'test_key') + with pytest.raises(exception): + api.tickets.get_ticket(1) diff --git a/freshdesk/v2/tests/test_comment.py b/freshdesk/v2/tests/test_comment.py new file mode 100644 index 0000000..b841941 --- /dev/null +++ b/freshdesk/v2/tests/test_comment.py @@ -0,0 +1,36 @@ +import pytest + +from freshdesk.v2.models import Comment + + +@pytest.fixture +def comments(api): + return api.comments.list_comments(1) + + +def test_comments_list(comments): + assert isinstance(comments, list) + assert len(comments) == 2 + assert isinstance(comments[0], Comment) + + +def test_comment_str(comments): + assert str(comments[0]) == 'This is a private note' + + +def test_comment_repr(comments): + assert repr(comments[0]) == '' + + +def test_create_note(api): + comment = api.comments.create_note(1, 'This is a private note') + assert isinstance(comment, Comment) + assert comment.body_text == 'This is a private note' + assert comment.source == 'note' + + +def test_create_reply(api): + comment = api.comments.create_reply(1, 'This is a reply') + assert isinstance(comment, Comment) + assert comment.body_text == 'This is a reply' + assert comment.source == 'reply' diff --git a/freshdesk/v2/tests/test_contact.py b/freshdesk/v2/tests/test_contact.py new file mode 100644 index 0000000..73d6980 --- /dev/null +++ b/freshdesk/v2/tests/test_contact.py @@ -0,0 +1,83 @@ +import datetime + +import pytest + +from freshdesk.v2.models import Contact, Agent + + +@pytest.fixture +def contact(api): + return api.contacts.get_contact(1) + + +def test_get_contact(contact): + assert isinstance(contact, Contact) + assert contact.name == 'Rachel' + assert contact.email == 'rachel@freshdesk.com' + assert contact.helpdesk_agent is False + assert contact.customer_id == 1 + + +def test_list_contact(api, contact): + contacts = api.contacts.list_contacts() + assert isinstance(contacts, list) + assert isinstance(contacts[0], Contact) + assert len(contacts) == 2 + assert contacts[0].__dict__ == contact.__dict__ + + +def test_create_contact(api): + contact_data = { + 'name': 'Rachel', + 'email': 'rachel@freshdesk.com' + } + contact = api.contacts.create_contact(contact_data) + assert isinstance(contact, Contact) + assert contact.email == contact.email + assert contact.name == contact.name + + +def test_update_contact(api): + contact_data = { + 'name': 'New Name' + } + contact = api.contacts.update_contact(1, **contact_data) + assert isinstance(contact, Contact) + assert contact.name == 'New Name' + + +def test_soft_delete_contact(api): + assert api.contacts.soft_delete_contact(1) is None + + +def test_permanently_delete_contact(api): + assert api.contacts.permanently_delete_contact(1) is None + + +def test_restore_contact(api): + api.contacts.restore_contact(1) + contact = api.contacts.get_contact(1) + assert isinstance(contact, Contact) + assert contact.deleted is False + + +def test_make_agent(api, contact): + agent = api.contacts.make_agent(contact.id) + assert isinstance(agent, Agent) + assert agent.available is True + assert agent.occasional is False + assert agent.contact['email'] == contact.email + assert agent.contact['name'] == contact.name + + +def test_contact_datetime(contact): + assert isinstance(contact.created_at, datetime.datetime) + assert isinstance(contact.updated_at, datetime.datetime) + + +def test_contact_str(contact): + assert str(contact) == 'Rachel' + + +def test_contact_repr(contact): + assert repr(contact) == '' diff --git a/freshdesk/v2/tests/test_customer.py b/freshdesk/v2/tests/test_customer.py new file mode 100644 index 0000000..1964d69 --- /dev/null +++ b/freshdesk/v2/tests/test_customer.py @@ -0,0 +1,40 @@ +import datetime + +import pytest + +from freshdesk.v2.models import Customer + + +@pytest.fixture +def customer(api): + return api.customers.get_customer('1') + + +@pytest.fixture +def contact(api): + return api.contacts.get_contact(1) + + +def test_customer(customer): + assert isinstance(customer, Customer) + assert customer.name == 'ACME Corp.' + assert customer.domains == 'acme.com' + assert customer.cf_custom_key == 'custom_value' + + +def test_customer_datetime(customer): + assert isinstance(customer.created_at, datetime.datetime) + assert isinstance(customer.updated_at, datetime.datetime) + + +def test_customer_str(customer): + assert str(customer) == 'ACME Corp.' + + +def test_customer_repr(customer): + assert repr(customer) == '' + + +def test_get_customer_from_contact(api, contact): + customer = api.customers.get_customer_from_contact(contact) + test_customer(customer) diff --git a/freshdesk/v2/tests/test_group.py b/freshdesk/v2/tests/test_group.py new file mode 100644 index 0000000..931d3b2 --- /dev/null +++ b/freshdesk/v2/tests/test_group.py @@ -0,0 +1,36 @@ +import datetime + +import pytest + +from freshdesk.v2.models import Group + + +@pytest.fixture +def group(api): + return api.groups.get_group(1) + + +def test_list_groups(api, group): + groups = api.groups.list_groups() + assert isinstance(groups, list) + assert len(groups) == 2 + assert groups[0].id == group.id + + +def test_group(group): + assert isinstance(group, Group) + assert group.name == 'Entertainers' + assert group.description == 'Singers dancers and stand up comedians' + + +def test_group_datetime(group): + assert isinstance(group.created_at, datetime.datetime) + assert isinstance(group.updated_at, datetime.datetime) + + +def test_group_str(group): + assert str(group) == 'Entertainers' + + +def test_group_repr(group): + assert repr(group) == '' diff --git a/freshdesk/v2/tests/test_role.py b/freshdesk/v2/tests/test_role.py new file mode 100644 index 0000000..32879ac --- /dev/null +++ b/freshdesk/v2/tests/test_role.py @@ -0,0 +1,32 @@ +import datetime + +import pytest + +from freshdesk.v2.models import Role + + +@pytest.fixture +def role(api): + return api.roles.get_role(1) + + +def test_list_roles(api, role): + roles = api.roles.list_roles() + assert isinstance(roles, list) + assert len(roles) == 2 + assert roles[0].id == role.id + + +def test_role(role): + assert isinstance(role, Role) + assert role.name == 'Agent' + assert role.description, 'Can log, view, reply == update and resolve tickets and manage contacts.' + + +def test_group_datetime(role): + assert isinstance(role.created_at, datetime.datetime) + assert isinstance(role.updated_at, datetime.datetime) + + +def test_group_repr(role): + assert repr(role) == '' diff --git a/freshdesk/v2/tests/test_ticket.py b/freshdesk/v2/tests/test_ticket.py new file mode 100644 index 0000000..52920b3 --- /dev/null +++ b/freshdesk/v2/tests/test_ticket.py @@ -0,0 +1,171 @@ +import datetime +import json +import os.path + +import pytest + +from freshdesk.v2.models import Ticket + + +@pytest.fixture +def ticket(api): + return api.tickets.get_ticket(1) + + +@pytest.fixture +def ticket_json(): + return json.loads(open(os.path.join(os.path.dirname(__file__), 'sample_json_data', 'ticket_1.json')).read()) + + +@pytest.fixture +def outbound_email_json(api): + return json.loads( + open(os.path.join(os.path.dirname(__file__), 'sample_json_data', 'outbound_email_1.json')).read()) + + +def test_str(ticket): + assert str(ticket) == 'This is a sample ticket' + + +def test_repr(ticket): + assert repr(ticket) == '' + + +def test_get_ticket(ticket): + assert isinstance(ticket, Ticket) + assert ticket.id == 1 + assert ticket.subject == 'This is a sample ticket' + assert ticket.description_text, 'This is a sample ticket == feel free to delete it.' + assert ticket.cc_emails == ['test2@example.com'] + assert 'foo' in ticket.tags + assert 'bar' in ticket.tags + + +def test_create_ticket(api): + attachment_path = os.path.join(os.path.dirname(__file__), 'sample_json_data', 'attachment.txt') + ticket = api.tickets.create_ticket('This is a sample ticket', + description='This is a sample ticket, feel free to delete it.', + email='test@example.com', + priority=1, status=2, + tags=['foo', 'bar'], + cc_emails=['test2@example.com'], + attachments=(attachment_path,)) + assert isinstance(ticket, Ticket) + assert ticket.subject == 'This is a sample ticket' + assert ticket.description_text, 'This is a sample ticket == feel free to delete it.' + assert ticket.priority == 'low' + assert ticket.status == 'open' + assert ticket.cc_emails == ['test2@example.com'] + assert 'foo' in ticket.tags + assert 'bar' in ticket.tags + + +def test_create_outbound_email(api, outbound_email_json): + j = outbound_email_json.copy() + email = 'test@example.com' + subject = 'This is a sample outbound email' + description = 'This is a sample outbound email, feel free to delete it.' + email_config_id = 5000054536 + values = { + 'status': 5, + 'priority': 1, + 'tags': ['foo', 'bar'], + 'cc_emails': ['test2@example.com'] + } + + email = api.tickets.create_outbound_email( + subject, + description, + email, + email_config_id, + **values + ) + + assert email.description_text == j['description_text'] + assert email._priority == j['priority'] + assert email._status == j['status'] + assert email.cc_emails == j['cc_emails'] + assert 'foo' in email.tags + assert 'bar' in email.tags + + +def test_update_ticket(api, ticket_json): + j = ticket_json.copy() + values = { + 'subject': 'Test subject update', + 'priority': 3, + 'status': 4, + 'tags': ['hello', 'world'] + } + j.update(values) + + ticket = api.tickets.update_ticket(j['id'], **values) + assert ticket.subject == 'Test subject update' + assert ticket.status == 'resolved' + assert ticket.priority == 'high' + assert 'hello' in ticket.tags + assert 'world' in ticket.tags + + +def test_delete_ticket(api): + assert api.tickets.delete_ticket(1) is None + + +def test_ticket_priority(ticket): + assert ticket._priority == 1 + assert ticket.priority == 'low' + + +def test_ticket_status(ticket): + assert ticket._status == 2 + assert ticket.status == 'open' + + +def test_ticket_source(ticket): + assert ticket._source == 2 + assert ticket.source == 'portal' + + +def test_ticket_datetime(ticket): + assert isinstance(ticket.created_at, datetime.datetime) + assert isinstance(ticket.updated_at, datetime.datetime) + + +def test_new_and_my_open_tickets(api, ticket): + tickets = api.tickets.list_new_and_my_open_tickets() + assert isinstance(tickets, list) + assert len(tickets) == 1 + assert tickets[0].id == ticket.id + + +def test_deleted_tickets(api): + tickets = api.tickets.list_deleted_tickets() + assert isinstance(tickets, list) + assert len(tickets) == 1 + + +def test_watched_tickets(api, ticket): + tickets = api.tickets.list_watched_tickets() + assert isinstance(tickets, list) + assert len(tickets) == 1 + assert tickets[0].id == ticket.id + + +def test_spam_tickets(api): + tickets = api.tickets.list_tickets(filter_name='spam') + assert isinstance(tickets, list) + assert len(tickets) == 1 + + +def test_default_filter_name(api, ticket): + tickets = api.tickets.list_tickets() + assert isinstance(tickets, list) + assert len(tickets) == 1 + assert tickets[0].id == ticket.id + + +def test_none_filter_name(api, ticket): + tickets = api.tickets.list_tickets(filter_name=None) + assert isinstance(tickets, list) + assert len(tickets) == 1 + assert tickets[0].id == ticket.id diff --git a/test-requirements.txt b/test-requirements.txt index 8474bc2..2b13f10 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,5 +1,6 @@ coveralls -nose +pytest +pytest-cov python-dateutil requests responses diff --git a/test.sh b/test.sh index e46d1ff..6768c51 100755 --- a/test.sh +++ b/test.sh @@ -1,2 +1,2 @@ #!/bin/sh -nosetests --with-coverage --cover-package=freshdesk +pytest --cov=freshdesk diff --git a/tox.ini b/tox.ini index 1fbd72a..7374ad1 100644 --- a/tox.ini +++ b/tox.ini @@ -12,8 +12,9 @@ deps = coverage responses mock - nose -commands = nosetests -v --with-coverage --cover-package=freshdesk + pytest + pytest-cov +commands = pytest -v --cov=freshdesk [testenv:venv] commands = {posargs} From f949ed2a2a99dba091cc996a878f4590426aace5 Mon Sep 17 00:00:00 2001 From: Sam Kingston Date: Wed, 23 Oct 2019 15:54:50 +1000 Subject: [PATCH 44/46] Fix typo in Company repr --- freshdesk/v2/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freshdesk/v2/models.py b/freshdesk/v2/models.py index 9b9ea9b..c5a1ecd 100644 --- a/freshdesk/v2/models.py +++ b/freshdesk/v2/models.py @@ -108,7 +108,7 @@ def __str__(self): return self.name def __repr__(self): - return ''.format(self.name) + return ''.format(self.name) class Agent(FreshdeskModel): def __str__(self): From 53b320430a4b1a22c9e6d3ede7153dbeae7ada63 Mon Sep 17 00:00:00 2001 From: Sam Kingston Date: Wed, 23 Oct 2019 15:55:51 +1000 Subject: [PATCH 45/46] Version bump --- CHANGELOG.md | 5 +++++ freshdesk/__init__.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 64750ae..6c22a0e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,11 @@ Changelog ========= +v1.2.5 - 2019-10-23 + + * #41: Refactor tests and switch to pytest (@ArtemGordinsky) + * Fix typo in Company repr (@sjkingo) + v1.2.4 - 2019-10-19 * #38: Add ticket time entry API (@smontoya) diff --git a/freshdesk/__init__.py b/freshdesk/__init__.py index daab838..09964d6 100644 --- a/freshdesk/__init__.py +++ b/freshdesk/__init__.py @@ -1 +1 @@ -__version__ = '1.2.4' +__version__ = '1.2.5' From 6024e27a5ffcdb18c41ee3a563b5f1f9739d5502 Mon Sep 17 00:00:00 2001 From: Andy Botting Date: Wed, 29 Jan 2020 09:31:11 +1100 Subject: [PATCH 46/46] Add support for ticket filter (search) To do more complex ticket filtering, the FreshDesk API support the /search/tickets? endpoint with a required query parameter, using a specific query syntax as documented at: https://developer.freshdesk.com/api/#filter_tickets The API returns a fixed number of results per page (30) with 10 the maximum number of pages. --- README.md | 1 + freshdesk/v2/api.py | 23 +++++ freshdesk/v2/tests/conftest.py | 1 + .../tests/sample_json_data/all_tickets.json | 1 + .../sample_json_data/search_tickets.json | 90 +++++++++++++++++++ freshdesk/v2/tests/test_ticket.py | 7 ++ 6 files changed, 123 insertions(+) create mode 100644 freshdesk/v2/tests/sample_json_data/search_tickets.json diff --git a/README.md b/README.md index c011de6..e3faeaf 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ Support for the v2 API includes the following features: - [Delete](http://developer.freshdesk.com/api/#delete_a_ticket) - [Create OutBound Email](http://developer.freshdesk.com/api/#create_outbound_email) - [List](http://developer.freshdesk.com/api/#list_all_tickets) + - [Filter](https://developer.freshdesk.com/api/#filter_tickets) - [List Time Entries](https://developers.freshdesk.com/api/#list_all_ticket_timeentries) (as of 1.2.4) - Custom ticket fields (as of 1.1.1) * [Ticket Fields](http://developer.freshdesk.com/api/#ticket_fields) diff --git a/freshdesk/v2/api.py b/freshdesk/v2/api.py index a9ab650..e14a540 100644 --- a/freshdesk/v2/api.py +++ b/freshdesk/v2/api.py @@ -134,6 +134,29 @@ def list_deleted_tickets(self): """Lists all deleted tickets.""" return self.list_tickets(filter_name='deleted') + def filter_tickets(self, query, **kwargs): + """Filter tickets by a given query string. The query string must be in + the format specified in the API documentation at: + https://developer.freshdesk.com/api/#filter_tickets + + query = "(ticket_field:integer OR ticket_field:'string') AND ticket_field:boolean" + """ + url = 'search/tickets?' + page = 1 if not 'page' in kwargs else kwargs['page'] + per_page = 30 + + tickets = [] + while True: + this_page = self._api._get(url + 'page=%d&query=%s' + % (page, repr(query)), kwargs) + this_page = this_page['results'] + tickets += this_page + if len(this_page) < per_page or page == 10 or 'page' in kwargs: + break + page += 1 + + return [Ticket(**t) for t in tickets] + class CommentAPI(object): def __init__(self, api): diff --git a/freshdesk/v2/tests/conftest.py b/freshdesk/v2/tests/conftest.py index f44b12c..cbbe82b 100644 --- a/freshdesk/v2/tests/conftest.py +++ b/freshdesk/v2/tests/conftest.py @@ -35,6 +35,7 @@ def __init__(self, *args): re.compile(r'agents\?state=fulltime&page=1&per_page=100'): self.read_test_file('agent_1.json'), re.compile(r'agents\?page=1&per_page=100'): self.read_test_file('agents.json'), re.compile(r'agents/1$'): self.read_test_file('agent_1.json'), + re.compile(r'search/tickets\?page=1&query="tag:\'mytag\'"'): self.read_test_file('search_tickets.json'), }, 'post': { re.compile(r'tickets$'): self.read_test_file('ticket_1.json'), diff --git a/freshdesk/v2/tests/sample_json_data/all_tickets.json b/freshdesk/v2/tests/sample_json_data/all_tickets.json index 1caab2d..2ab7178 100644 --- a/freshdesk/v2/tests/sample_json_data/all_tickets.json +++ b/freshdesk/v2/tests/sample_json_data/all_tickets.json @@ -38,6 +38,7 @@ "responder_name": "Sam Kingston", "to_emails": null, "product_id": null, + "tags": ["mytag"], "custom_field": {} } ] diff --git a/freshdesk/v2/tests/sample_json_data/search_tickets.json b/freshdesk/v2/tests/sample_json_data/search_tickets.json new file mode 100644 index 0000000..dbb5f2d --- /dev/null +++ b/freshdesk/v2/tests/sample_json_data/search_tickets.json @@ -0,0 +1,90 @@ +{ + "total": 2, + "results": [ + { + "cc_email": { + "cc_emails": [], + "fwd_emails": [], + "reply_cc": [] + }, + "created_at": "2014-12-31T12:27:09+10:00", + "deleted": false, + "delta": true, + "description": "This is a sample ticket, feel free to delete it.", + "description_html": "
This is a sample ticket, feel free to delete it.
", + "id": 1, + "due_by": "2015-01-05T12:27:09+10:00", + "email_config_id": null, + "frDueBy": "2015-01-01T12:27:09+10:00", + "fr_escalated": false, + "group_id": null, + "isescalated": false, + "owner_id": null, + "priority": 1, + "requester_id": 5004272351, + "responder_id": 5004272350, + "source": 2, + "spam": false, + "status": 2, + "subject": "This is a sample ticket", + "ticket_type": "Question", + "to_email": null, + "trained": false, + "updated_at": "2015-01-01T10:58:39+10:00", + "urgent": false, + "status_name": "Open", + "requester_status_name": "Being Processed", + "priority_name": "Low", + "source_name": "Portal", + "requester_name": "Rachel", + "responder_name": "Sam Kingston", + "to_emails": null, + "product_id": null, + "tags": ["mytag"], + "custom_field": {} + }, + { + "cc_email": { + "cc_emails": [], + "fwd_emails": [], + "reply_cc": [] + }, + "created_at": "2020-01-01T16:21:45+10:00", + "deleted": false, + "delta": true, + "description": "This is another ticket.", + "description_html": "
This is a another ticket.
", + "id": 1, + "due_by": "2020-01-02T16:21:45+10:00", + "email_config_id": null, + "frDueBy": "2020-01-02T16:21:45+10:00", + "fr_escalated": false, + "group_id": null, + "isescalated": false, + "owner_id": null, + "priority": 1, + "requester_id": 5004272351, + "responder_id": 5004272350, + "source": 2, + "spam": false, + "status": 2, + "subject": "This is another ticket", + "ticket_type": "Question", + "to_email": null, + "trained": false, + "updated_at": "2020-01-01T17:02:12+10:00", + "urgent": false, + "status_name": "Open", + "requester_status_name": "Being Processed", + "priority_name": "Low", + "source_name": "Portal", + "requester_name": "Rachel", + "responder_name": "Sam Kingston", + "to_emails": null, + "product_id": null, + "tags": ["mytag", "myothertag"], + "custom_field": {} + } + + ] +} diff --git a/freshdesk/v2/tests/test_ticket.py b/freshdesk/v2/tests/test_ticket.py index 52920b3..2df331e 100644 --- a/freshdesk/v2/tests/test_ticket.py +++ b/freshdesk/v2/tests/test_ticket.py @@ -169,3 +169,10 @@ def test_none_filter_name(api, ticket): assert isinstance(tickets, list) assert len(tickets) == 1 assert tickets[0].id == ticket.id + + +def test_filter_query(api, ticket): + tickets = api.tickets.filter_tickets(query="tag:'mytag'") + assert isinstance(tickets, list) + assert len(tickets) == 2 + assert 'mytag' in tickets[0].tags