brynq-sdk-zoho 3.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2 @@
1
+ from .upload_zoho_desk import UploadZohoDesk
2
+ from .extract_zoho_desk import ExtractZohoDesk
@@ -0,0 +1,257 @@
1
+ import os
2
+ import sys
3
+ import pandas as pd
4
+ from typing import Union, List, Optional, Literal
5
+ import requests
6
+ import json
7
+ from brynq_sdk_brynq import BrynQ
8
+
9
+ basedir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
10
+ sys.path.append(basedir)
11
+
12
+
13
+ class ExtractZohoDesk(BrynQ):
14
+
15
+ def __init__(self, system_type: Optional[Literal['source', 'target']] = None, debug: bool = False):
16
+ """
17
+ For the full documentation, see: https://avisi-apps.gitbook.io/tracket/api/
18
+ """
19
+ super().__init__()
20
+ self.headers = self._get_authentication(system_type)
21
+ self.base_url = "https://desk.zoho.com/api/v1/"
22
+ self.payload = {}
23
+ self.timeout = 3600
24
+
25
+ def _get_authentication(self, system_type):
26
+ """
27
+ Get the credentials for the Tracket API from BrynQ, with those credentials, get the access_token for Tracket.
28
+ Return the headers with the access_token.
29
+ """
30
+ # Get credentials from BrynQ
31
+ credentials = self.interfaces.credentials.get(system="zoho-desk", system_type=system_type)
32
+ credentials = credentials.get('data')
33
+
34
+ # With those credentials, get the access_token from Tracket
35
+ headers = {
36
+ 'Authorization': f'Zoho-oauthtoken {credentials.get("access_token")}'
37
+ }
38
+ return headers
39
+
40
+ def get_zoho_accounts(self, query_params=""):
41
+ """
42
+ This function gets all the accounts from zoho and saves them as df_zoho_accounts
43
+ :return: df_zoho_accounts
44
+ """
45
+ base_url = f"{self.base_url}accounts"
46
+ return self._multiple_calls(base_url, query_params)
47
+
48
+ def get_zoho_agents(self, query_params=""):
49
+ """
50
+ This function gets the user data from zoho and saves the data to df_zoho_users
51
+ :return:
52
+ """
53
+ base_url = f"{self.base_url}agents"
54
+ return self._multiple_calls(base_url, query_params)
55
+
56
+ def get_zoho_agent(self, agent_id, query_params=""):
57
+ """
58
+ This function gets the user data from zoho and saves the data to df_zoho_users
59
+ :return:
60
+ """
61
+ url = f"{self.base_url}agents/{agent_id}?{query_params}"
62
+ return self._single_call(url)
63
+
64
+ def get_zoho_contacts(self, query_params=""):
65
+ """
66
+ This function gets the zoho contact information from zoho desk and saves the data to df_zoho_contacts
67
+ :return:
68
+ """
69
+ base_url = f"{self.base_url}contacts"
70
+ return self._multiple_calls(base_url, query_params)
71
+
72
+ def get_recent_zoho_tickets(self, query_params=""):
73
+ """
74
+ This function gets the newest 100 tickets form Zoho-Desk
75
+ :return:
76
+ """
77
+ url = f"{self.base_url}tickets?limit=100&from=0&{query_params}"
78
+ return self._single_call(url)
79
+
80
+ def get_all_zoho_tickets(self, query_params: str = "") -> pd.DataFrame:
81
+ """
82
+ This function gets the zoho contact information from zoho desk and saves the data to df_zoho_contacts
83
+ :return:
84
+ """
85
+ base_url = f"{self.base_url}tickets"
86
+ return self._multiple_calls(base_url, query_params)
87
+
88
+ def get_archived_zoho_tickets(self, query_params: str = "") -> pd.DataFrame:
89
+ """
90
+ This function gets the zoho contact information from zoho desk and saves the data to df_zoho_contacts
91
+ :return:
92
+ """
93
+ base_url = f"{self.base_url}tickets/archivedTickets"
94
+ return self._multiple_calls(base_url, query_params)
95
+
96
+ def get_active_ticket_timers(self, tickets: pd.DataFrame, query_params: str = "") -> pd.DataFrame:
97
+ """=
98
+ This function gets all the active ticket timers from the tickets given in the tickets dataframe.
99
+ :param tickets: dataframe with the ticket_id's
100
+ :param query_params: query parameters for the API call
101
+ :return: pd.DataFrame with the ticket timers
102
+ """
103
+ df = pd.DataFrame()
104
+ count = 0
105
+ for index, ticket in tickets.iterrows():
106
+ count = count + 1
107
+ print(f"Checking for ticket number {ticket.ticket_id}. ticket {count} / " + str(
108
+ len(tickets.index)))
109
+ url = f"{self.base_url}tickets/{ticket.ticket_id}/activeTimer?{query_params}"
110
+ df_temp = self._single_call(url)
111
+ df_temp['ticket_id'] = ticket.ticket_id
112
+ df_temp['link'] = ticket.link
113
+ df_temp['ticket_number'] = ticket.ticket_number
114
+ df = pd.concat([df, df_temp])
115
+ df = df.reset_index(drop=True)
116
+ return df
117
+
118
+ def get_zoho_ticket_timers(self, tickets, query_params=""):
119
+ """
120
+ This function gets all the ticket timers from the recent tickets if there already exists a database. Otherwise,
121
+ it will get all the ticket timers. the ticket timers are saved to df_zoho_ticket_timers
122
+ :return:
123
+ """
124
+ df = pd.DataFrame()
125
+ count = 0
126
+ for ticket_id in tickets["ticket_id"]:
127
+ count = count + 1
128
+ print(f"Checking for ticket number {ticket_id}. ticket {count} / " + str(
129
+ len(tickets.index)))
130
+ url = f"{self.base_url}tickets/{ticket_id}/timeEntry?{query_params}"
131
+ df_temp = self._single_call(url)
132
+ df = pd.concat([df, df_temp])
133
+ df = df.reset_index(drop=True)
134
+ return df
135
+
136
+ def _multiple_calls(self, base_url, query_params) -> pd.DataFrame:
137
+ """
138
+ This function helps the API calls to do multiple calls in one function
139
+ :return:
140
+ """
141
+ df = pd.DataFrame()
142
+ end_of_loop = False
143
+ offset = 1
144
+ while not end_of_loop:
145
+ url = f"{base_url}?from={offset}&limit=90&{query_params}"
146
+ df_temp = self._single_call(url)
147
+ df = pd.concat([df_temp, df])
148
+ if len(df_temp) != 90:
149
+ end_of_loop = True
150
+ else:
151
+ offset += 90
152
+ return df
153
+
154
+ def _single_call(self, url: str) -> pd.DataFrame:
155
+ """
156
+ This function helps the API calls to do a single call in one function
157
+ :return:
158
+ """
159
+ response = requests.request("GET", url, headers=self.headers, data=self.payload, timeout=self.timeout)
160
+ if response.status_code == 401:
161
+ response = requests.request("GET", url, headers=self.headers, data=self.payload, timeout=self.timeout)
162
+ if response.status_code == 200:
163
+ df = response.json()
164
+ if 'data' in df:
165
+ df = pd.json_normalize(df['data'])
166
+ else:
167
+ df = pd.json_normalize(df)
168
+ return df
169
+ elif response.status_code == 204:
170
+ return pd.DataFrame()
171
+ else:
172
+ raise Exception(response.text)
173
+
174
+ def get_zoho_articles(self, exclude_category_ids: list = []) -> list:
175
+ """
176
+ Retrieves all Zoho articles, excluding articles with specified category ID
177
+ Category ID's can be easily found in your KB-Admin in the link of any article.
178
+ """
179
+ limit = 50
180
+ offset = 1
181
+ all_articles = []
182
+
183
+ while True:
184
+ url = f'{self.base_url}articles'
185
+ params = {"status": "Draft,Published", "from": offset, "limit": limit}
186
+ response = requests.get(url, headers=self.headers, params=params, timeout=120)
187
+ if response.status_code == 200:
188
+ articles = response.json().get('data', [])
189
+ if not articles:
190
+ break
191
+
192
+ # Filter out articles with the excluded category ID
193
+ filtered_articles = []
194
+ for article in articles:
195
+ article_category_id = article.get('categoryId')
196
+ locale = article.get('locale', "")
197
+ if article_category_id not in exclude_category_ids and locale == "nl":
198
+ filtered_articles.append(article)
199
+
200
+ all_articles.extend(filtered_articles)
201
+
202
+ if len(articles) < limit:
203
+ break
204
+ else:
205
+ error_text = response.text
206
+ all_articles.insert("Failed to retrieve Zoho articles, status: {response.status_code}, body: {error_text}")
207
+ offset += limit
208
+ return all_articles
209
+
210
+ def get_base_article_by_id(self, article_id: str) -> dict:
211
+ """
212
+ Retrieves the base (non-versioned) Zoho article by ID.
213
+ """
214
+ try:
215
+ detail_url = f"{self.base_url}articles/{article_id}"
216
+ response = requests.get(detail_url, headers=self.headers, timeout=60)
217
+ if response.status_code == 200:
218
+ return response.json()
219
+ message = f"Failed to retrieve base article for article ID: {article_id}, status: {response.status_code}"
220
+ return message
221
+ except Exception as e:
222
+ message = f"Error getting base article by id: {e}"
223
+ return message
224
+
225
+ def get_zoho_article_latest(self, zoho_article: dict) -> dict:
226
+ """
227
+ Retrieves detailed article data from Zoho Desk using the base article object.
228
+ Uses the article's id and, if available, its latestVersion to fetch the right version.
229
+ """
230
+ try:
231
+ article_id = zoho_article.get("id")
232
+ if not article_id:
233
+ message = "Error: No article id provided in base article object."
234
+ return message
235
+
236
+ latest_version = zoho_article.get("latestVersion")
237
+
238
+ if latest_version:
239
+ versioned_article_url = f"{self.base_url}articles/{article_id}?version={latest_version}"
240
+ version_response = requests.get(versioned_article_url, headers=self.headers, timeout=60)
241
+ if version_response.status_code == 200:
242
+ return version_response.json()
243
+ message = f"Failed to retrieve versioned article data, status: {version_response.status_code}"
244
+ return message
245
+
246
+ detail_url = f"{self.base_url}articles/{article_id}"
247
+ response = requests.get(detail_url, headers=self.headers, timeout=60)
248
+ if response.status_code == 200:
249
+ article_data = response.json()
250
+ print(f"Retrieved detailed article data for article ID: {article_id}")
251
+ return article_data
252
+ else:
253
+ message = f"Failed to retrieve versioned article data, status: {version_response.status_code}"
254
+ return message
255
+ except Exception as e:
256
+ message = "Retrieving latest Zoho Article Failed."
257
+ return message
@@ -0,0 +1,121 @@
1
+ import os
2
+ import sys
3
+ import pandas as pd
4
+ from typing import Union, List, Optional, Literal
5
+ import requests
6
+ import json
7
+ import re
8
+ from brynq_sdk_brynq import BrynQ
9
+ basedir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
10
+ sys.path.append(basedir)
11
+
12
+
13
+ class UploadZohoDesk(BrynQ):
14
+
15
+ def __init__(self, system_type: Optional[Literal['source', 'target']] = None, debug: bool = False):
16
+ """
17
+ For the full documentation, see: https://avisi-apps.gitbook.io/tracket/api/
18
+ """
19
+ super().__init__()
20
+ self.headers = self._get_authentication(system_type)
21
+ self.base_url = "https://desk.zoho.com/api/v1/"
22
+ self.timeout = 3600
23
+
24
+ def _get_authentication(self, system_type):
25
+ """
26
+ Get the credentials for the Traket API from BrynQ, with those credentials, get the access_token for Tracket.
27
+ Return the headers with the access_token.
28
+ """
29
+ # Get credentials from BrynQ
30
+ credentials = self.interfaces.credentials.get(system="zoho-desk", system_type=system_type)
31
+ credentials = credentials.get('data')
32
+
33
+ headers = {
34
+ 'Authorization': f'Zoho-oauthtoken {credentials.get("access_token")}',
35
+ 'Content-Type': 'application/json'
36
+ }
37
+ return headers
38
+
39
+ def update_ticket_time_entry(self, ticket_id, time_entry_id, payload):
40
+ """
41
+ This function updates the time entry of a ticket in zoho desk
42
+ :param ticket_id: str
43
+ :param time_entry_id: str
44
+ :param payload: dict
45
+ """
46
+ url = f"{self.base_url}tickets/{ticket_id}/timeEntry/{time_entry_id}"
47
+ response = requests.request("PATCH", url, headers=self.headers, data=json.dumps(payload), timeout=self.timeout)
48
+ return response
49
+
50
+ def update_article(self, article_id: str, updated_content: str, category_id: int, status="Draft") -> str:
51
+ """
52
+ Updates the Zoho article with the updated content as a draft.
53
+ Returns the web URL where the draft can be reviewed, or an empty string if the update fails.
54
+
55
+ Status can be:
56
+ - Draft
57
+ - Published
58
+ - Review
59
+ - Unpublished
60
+ """
61
+ try:
62
+
63
+ payload = {
64
+ "answer": updated_content,
65
+ "status": status
66
+ }
67
+ if category_id is not None:
68
+ payload["categoryId"] = category_id
69
+ update_url = f"{self.base_url}articles/{article_id}"
70
+ update_headers = self.headers
71
+
72
+ update_response = requests.patch(update_url, headers=update_headers, data=json.dumps(payload), timeout=60)
73
+ if update_response.status_code == 200:
74
+ update_data = update_response.json()
75
+ web_url = update_data.get("webUrl", "")
76
+ return web_url
77
+ else:
78
+ message = f"Uploading Draft failed. Response: {update_response.status_code}"
79
+ return message
80
+ except Exception as e:
81
+ message = "Uploading Draft failed."
82
+ return message
83
+
84
+ def upload_translation(self, translated, article_data, locale = "en-us"):
85
+ try:
86
+ article_id = article_data.get("id")
87
+ title = article_data.get("title")
88
+
89
+ url = f"{self.base_url}articles/{article_id}/translations"
90
+
91
+ # Get authorId from the article data or use a default
92
+ author_id = article_data.get("authorId") or article_data.get("createdBy", {}).get("id")
93
+ payload = {
94
+ "title" : title,
95
+ "answer" : translated,
96
+ "status" : "Draft",
97
+ "locale" : locale,
98
+ "authorId" : author_id
99
+ }
100
+ response = requests.post(url, headers=self.headers, data=json.dumps(payload), timeout=60)
101
+ if response.status_code == 200:
102
+ update_data = response.json()
103
+ web_url = update_data.get("webUrl", "")
104
+ return web_url
105
+ else:
106
+ url = f"{self.base_url}articles/{article_id}/translations/{locale}"
107
+ payload = {
108
+ "answer" : translated,
109
+ "status" : "Draft"
110
+ }
111
+ response = requests.patch(url, headers=self.headers, data=json.dumps(payload), timeout=60)
112
+ if response.status_code == 200:
113
+ update_data = response.json()
114
+ web_url = update_data.get("webUrl", "")
115
+ return web_url
116
+ else:
117
+ message = "Uploading Translation failed."
118
+ return message
119
+ except Exception as e:
120
+ message = "Uploading Translation failed."
121
+ return message
@@ -0,0 +1,16 @@
1
+ Metadata-Version: 2.4
2
+ Name: brynq_sdk_zoho
3
+ Version: 3.1.0
4
+ Summary: ZOHO wrapper from BrynQ
5
+ Author: BrynQ
6
+ Author-email: support@brynq.com
7
+ License: BrynQ License
8
+ Requires-Dist: brynq-sdk-brynq>=2
9
+ Dynamic: author
10
+ Dynamic: author-email
11
+ Dynamic: description
12
+ Dynamic: license
13
+ Dynamic: requires-dist
14
+ Dynamic: summary
15
+
16
+ ZOHO wrapper from BrynQ
@@ -0,0 +1,7 @@
1
+ brynq_sdk_zoho/__init__.py,sha256=M96skogQw8AFzkaIV6sQVJ5kcFCjQdpvAm7pOb6wlwQ,92
2
+ brynq_sdk_zoho/extract_zoho_desk.py,sha256=jH2Xz6xCZ13HGTL3pMUaiBz0TrFm5a8CVjHnD8zuD98,10559
3
+ brynq_sdk_zoho/upload_zoho_desk.py,sha256=6QPHbqoITEbgF7xiizuCSC_KVWGlGBvUUb6UhvBUxl0,4737
4
+ brynq_sdk_zoho-3.1.0.dist-info/METADATA,sha256=dG3HGOx90cGzkcskb659iCZXp-35mxBYSRObHhVuyr0,335
5
+ brynq_sdk_zoho-3.1.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
6
+ brynq_sdk_zoho-3.1.0.dist-info/top_level.txt,sha256=fZz4ke-rlYeMe1pK9axdAKdbyGlRMUTBxlJU-a0Kn6Q,15
7
+ brynq_sdk_zoho-3.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.10.2)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ brynq_sdk_zoho