salure-helpers-planday 0.0.3__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 @@
1
+ from salure_helpers.planday.planday import Planday
@@ -0,0 +1,380 @@
1
+ import pandas as pd
2
+ import requests
3
+ import json
4
+ import datetime
5
+ import time
6
+
7
+
8
+ class Planday:
9
+
10
+ def __init__(self, refresh_token: str, client_id: str, planday_base_url: str = 'https://openapi.planday.com/'):
11
+ self.base_url = planday_base_url
12
+ self.refresh_token = refresh_token
13
+ self.client_id = client_id
14
+
15
+ def __get_access_token(self):
16
+ """
17
+ In this function, the access_token for planday is retrieved.
18
+ :return: returns the retrieved access_token that can be used to generate child tokens per portal
19
+ """
20
+ url = 'https://id.planday.com/connect/token'
21
+ headers = {'Content-Type': 'application/x-www-form-urlencoded'}
22
+ body = {
23
+ "client_id": self.client_id,
24
+ "grant_type": "refresh_token",
25
+ "refresh_token": self.refresh_token
26
+ }
27
+ response = requests.post(url=url, headers=headers, data=body).json()
28
+ access_token = response['access_token']
29
+
30
+ return access_token
31
+
32
+ def __get_child_token(self, portal_id: str):
33
+ """
34
+ In this function, the access_token for a specified portal is retrieved.
35
+ :return: returns the retrieved access_token that can be used to access data in a portal
36
+ """
37
+ access_token = self.__get_access_token()
38
+ url = 'https://id.planday.com/connect/token'
39
+ headers = {'Content-Type': 'application/x-www-form-urlencoded'}
40
+ body = {
41
+ "client_id": self.client_id,
42
+ "grant_type": "token_exchange",
43
+ "subject_token_type": "openapi:access_token",
44
+ "subject_token": access_token,
45
+ "resource": portal_id
46
+ }
47
+ response = requests.post(url=url, headers=headers, data=body).json()
48
+ child_token = response['access_token']
49
+
50
+ return child_token
51
+
52
+ def __get_headers(self, access_token: str):
53
+ """
54
+ Returns headers for a planday request
55
+ :param access_token: child token for a portal request
56
+ :return: headers
57
+ """
58
+ headers = {
59
+ 'X-ClientId': self.client_id,
60
+ 'Authorization': f'Bearer {access_token}',
61
+ 'Content-Type': "application/json",
62
+ 'Connection': 'keep-alive'
63
+ }
64
+
65
+ return headers
66
+
67
+ def get_portals(self) -> pd.DataFrame:
68
+ """
69
+ This function returns all possible portals (environments) for the access token
70
+ :return: dataframe with results
71
+ """
72
+ url = f'{self.base_url}portal/v1.0/info'
73
+ access_token = self.__get_access_token()
74
+ headers = self.__get_headers(access_token=access_token)
75
+
76
+ try:
77
+ response = requests.get(url=url, headers=headers)
78
+ except requests.exceptions.ConnectionError:
79
+ print("Connection error, retrying...")
80
+ time.sleep(5)
81
+ response = requests.get(url=url, headers=headers)
82
+
83
+ if 200 <= response.status_code < 300:
84
+ df = pd.DataFrame(response.json()['data']['portals'])
85
+ return df
86
+ else:
87
+ raise ConnectionError(f"Planday returned an error while retrieving portals: {response.status_code, response.text}")
88
+
89
+ def get_departments(self, portal_id: str) -> pd.DataFrame:
90
+ """
91
+ This function returns all departments in the specified portal.
92
+ :param portal_id: portal ID from planday. See get_portals
93
+ :return: dataframe with results
94
+ """
95
+ url = f'{self.base_url}hr/v1/departments'
96
+ access_token = self.__get_child_token(portal_id=portal_id)
97
+ headers = self.__get_headers(access_token=access_token)
98
+
99
+ try:
100
+ response = requests.get(url=url, headers=headers)
101
+ except requests.exceptions.ConnectionError:
102
+ print("Connection error, retrying...")
103
+ time.sleep(5)
104
+ response = requests.get(url=url, headers=headers)
105
+
106
+ if 200 <= response.status_code < 300:
107
+ df = pd.DataFrame(response.json()['data'])
108
+ return df
109
+ else:
110
+ raise ConnectionError(f"Planday returned an error while retrieving departments: {response.status_code, response.text}")
111
+
112
+ def get_contract_rules(self, portal_id: str) -> pd.DataFrame:
113
+ """
114
+ This function returns all contract rules in the specified portal.
115
+ :param portal_id: portal ID from planday. See get_portals
116
+ :return: dataframe with results
117
+ """
118
+ url = f'{self.base_url}contractrules/v1.0/contractrules'
119
+ access_token = self.__get_child_token(portal_id=portal_id)
120
+ headers = self.__get_headers(access_token=access_token)
121
+
122
+ try:
123
+ response = requests.get(url=url, headers=headers)
124
+ except requests.exceptions.ConnectionError:
125
+ print("Connection error, retrying...")
126
+ time.sleep(5)
127
+ response = requests.get(url=url, headers=headers)
128
+
129
+ if 200 <= response.status_code < 300:
130
+ df = pd.DataFrame(response.json()['data'])
131
+ return df
132
+ else:
133
+ raise ConnectionError(f"Planday returned an error while retrieving contract rules: {response.status_code, response.text}")
134
+
135
+ def get_custom_fields(self, portal_id: str) -> pd.DataFrame:
136
+ """
137
+ This function returns all custom fields in the specified portal.
138
+ :param portal_id: portal ID from planday. See get_portals
139
+ :return: dataframe with results
140
+ """
141
+ url = f'{self.base_url}hr/v1.0/employees/fielddefinitions'
142
+ access_token = self.__get_child_token(portal_id=portal_id)
143
+ headers = self.__get_headers(access_token=access_token)
144
+
145
+ try:
146
+ response = requests.get(url=url, headers=headers)
147
+ except requests.exceptions.ConnectionError:
148
+ print("Connection error, retrying...")
149
+ time.sleep(5)
150
+ response = requests.get(url=url, headers=headers)
151
+
152
+ if 200 <= response.status_code < 300:
153
+ df = pd.DataFrame(response.json()['data'])
154
+ return df
155
+ else:
156
+ raise ConnectionError(f"Planday returned an error while retrieving custom fields: {response.status_code, response.text}")
157
+
158
+ def get_shifts(self, portal_id: str, from_date: datetime, to_date: datetime) -> pd.DataFrame:
159
+ """
160
+ This function returns all shifts in the specified portal for the specified period.
161
+ :param portal_id: portal ID from planday. See get_portals
162
+ :param from_date: startdate for shift entries to get
163
+ :param to_date: enddate for shift entries to get
164
+ :return: dataframe with results
165
+ """
166
+ url = f"{self.base_url}scheduling/v1.0/shifts"
167
+ access_token = self.__get_child_token(portal_id=portal_id)
168
+ headers = self.__get_headers(access_token=access_token)
169
+ total_response = []
170
+ got_all_results = False
171
+ no_of_loops = 0
172
+ retry = 0
173
+
174
+ while not got_all_results:
175
+ params = {"limit": "50", "offset": f"{50 * no_of_loops}", "to": to_date, "from": from_date}
176
+ error = False
177
+ response = ''
178
+
179
+ try:
180
+ response = requests.get(url=url, headers=headers, params=params)
181
+ except requests.exceptions.ConnectionError:
182
+ print("Connection error, retrying...")
183
+ time.sleep(5)
184
+ error = True
185
+
186
+ if error is False and response.status_code == 200:
187
+ response_json = response.json()
188
+ no_of_loops += 1
189
+ got_all_results = False if len(response_json['data']) == 50 else True
190
+ total_response += response_json['data']
191
+ retry = 0
192
+ else:
193
+ if retry < 5:
194
+ retry += 1
195
+ time.sleep(5)
196
+ else:
197
+ raise ConnectionError(f"Planday returned an error while retrieving shifts: {response.status_code, response.text}")
198
+
199
+ print(f"Received {len(total_response)} shifts from Planday")
200
+
201
+ df = pd.DataFrame(total_response)
202
+
203
+ return df
204
+
205
+ def get_shift_types(self, portal_id: str) -> pd.DataFrame:
206
+ """
207
+ This function returns all shifttypes in the specified portal.
208
+ :param portal_id: portal ID from planday. See get_portals
209
+ :return: dataframe with results
210
+ """
211
+ url = f"{self.base_url}scheduling/v1.0/shifttypes"
212
+ access_token = self.__get_child_token(portal_id=portal_id)
213
+ headers = self.__get_headers(access_token=access_token)
214
+ total_response = []
215
+ got_all_results = False
216
+ no_of_loops = 0
217
+ retry = 0
218
+
219
+ while not got_all_results:
220
+ params = {"limit": "50", "offset": f"{50 * no_of_loops}"}
221
+ error = False
222
+ response = ''
223
+
224
+ try:
225
+ response = requests.get(url=url, headers=headers, params=params)
226
+ except requests.exceptions.ConnectionError:
227
+ print("Connection error, retrying...")
228
+ time.sleep(5)
229
+ error = True
230
+
231
+ if error is False and response.status_code == 200:
232
+ response_json = response.json()
233
+ no_of_loops += 1
234
+ got_all_results = False if len(response_json['data']) == 50 else True
235
+ total_response += response_json['data']
236
+
237
+ else:
238
+ if retry < 5:
239
+ retry += 1
240
+ time.sleep(5)
241
+ else:
242
+ raise ConnectionError(f"Planday returned an error while retrieving shifttypes: {response.status_code, response.text}")
243
+
244
+ print(f"Received {len(total_response)} shifttypes from Planday")
245
+
246
+ df = pd.DataFrame(total_response)
247
+
248
+ return df
249
+
250
+ def get_payroll_report(self, portal_id: str, from_date: datetime, to_date: datetime, department_id: str) -> pd.DataFrame:
251
+ """
252
+ This function returns the payroll report for the specified portal with the give dates and give department.
253
+ :param portal_id: portal ID from planday. See get_portals
254
+ :param from_date: startdate for payroll report
255
+ :param to_date: enddate for payroll report
256
+ :param department_id: department for payroll report. See get_departments
257
+ :return: dataframe with results
258
+ """
259
+ url = f'{self.base_url}/payroll/v1/payroll'
260
+ access_token = self.__get_child_token(portal_id=portal_id)
261
+ headers = self.__get_headers(access_token=access_token)
262
+ response = requests.get(url=url, headers=headers, params={"departmentIds": department_id, "from": from_date, "to": to_date})
263
+ if 200 <= response.status_code < 300:
264
+ df = pd.DataFrame(response.json()['shiftsPayroll'])
265
+ return df
266
+ else:
267
+ raise ConnectionError(f"Planday returned an error while retrieving payroll report: {response.status_code, response.text}")
268
+
269
+ def get_employees(self, portal_id: str) -> pd.DataFrame:
270
+ """
271
+ This function returns all employees in the specified portal.
272
+ :param portal_id: portal ID from planday. See get_portals
273
+ :return: dataframe with results
274
+ """
275
+ url = f'{self.base_url}hr/v1.0/employees'
276
+ access_token = self.__get_child_token(portal_id=portal_id)
277
+ headers = self.__get_headers(access_token=access_token)
278
+ total_response = []
279
+ got_all_results = False
280
+ no_of_loops = 0
281
+ retry = 0
282
+
283
+ while not got_all_results:
284
+ params = {"limit": "50", "offset": f"{50 * no_of_loops}"}
285
+ error = False
286
+ response = ''
287
+
288
+ try:
289
+ response = requests.get(url=url, headers=headers, params=params)
290
+ except requests.exceptions.ConnectionError:
291
+ print("Connection error, retrying...")
292
+ time.sleep(5)
293
+ error = True
294
+
295
+ if error is False and response.status_code == 200:
296
+ response_json = response.json()
297
+ no_of_loops += 1
298
+ got_all_results = False if len(response_json['data']) == 50 else True
299
+ total_response += response_json['data']
300
+
301
+ else:
302
+ if retry < 5:
303
+ retry += 1
304
+ time.sleep(5)
305
+ else:
306
+ raise ConnectionError(f"Planday returned an error while retrieving employees: {response.status_code, response.text}")
307
+
308
+ print(f"Received {len(total_response)} employees from Planday")
309
+
310
+ df = pd.DataFrame(total_response)
311
+
312
+ return df
313
+
314
+ def get_employee_types(self, portal_id: str) -> pd.DataFrame:
315
+ """
316
+ This function returns all employee types in the specified portal.
317
+ :param portal_id: portal ID from planday. See get_portals
318
+ :return: dataframe with results
319
+ """
320
+ url = f'{self.base_url}hr/v1.0/employeetypes'
321
+ access_token = self.__get_child_token(portal_id=portal_id)
322
+ headers = self.__get_headers(access_token=access_token)
323
+ total_response = []
324
+ got_all_results = False
325
+ no_of_loops = 0
326
+ retry = 0
327
+
328
+ while not got_all_results:
329
+ params = {"limit": "50", "offset": f"{50 * no_of_loops}"}
330
+ error = False
331
+ response = ''
332
+
333
+ try:
334
+ response = requests.get(url=url, headers=headers, params=params)
335
+ except requests.exceptions.ConnectionError:
336
+ print("Connection error, retrying...")
337
+ time.sleep(5)
338
+ error = True
339
+
340
+ if error is False and response.status_code == 200:
341
+ response_json = response.json()
342
+ no_of_loops += 1
343
+ got_all_results = False if len(response_json['data']) == 50 else True
344
+ total_response += response_json['data']
345
+
346
+ else:
347
+ if retry < 5:
348
+ retry += 1
349
+ time.sleep(5)
350
+ else:
351
+ raise ConnectionError(f"Planday returned an error while retrieving employee types: {response.status_code, response.text}")
352
+
353
+ print(f"Received {len(total_response)} employee types from Planday")
354
+
355
+ df = pd.DataFrame(total_response)
356
+
357
+ return df
358
+
359
+ def upload_data(self, method: str, endpoint: str, portal_id: str, data: dict):
360
+ """
361
+ Generic function to upload data to Zenegy via POST, PUT or DELETE. Should be made more specific to ensure easy operation
362
+ :param endpoint: the url endpoint
363
+ :param portal_id: portal ID from planday. See get_portals
364
+ :param data: the payload which should be sent as body to Planday. Only with PUT or POST
365
+ :param method: choose between POST, PUT or DELETE
366
+ :return:
367
+ """
368
+ if method != 'PUT' and method != 'POST' and method != 'DELETE':
369
+ raise ValueError('Parameter method should be PUT, POST or DELETE (in uppercase)')
370
+
371
+ url = f'{self.base_url}{endpoint}'
372
+ access_token = self.__get_child_token(portal_id=portal_id)
373
+ headers = self.__get_headers(access_token=access_token)
374
+
375
+ if method == 'POST' or method == 'PUT':
376
+ response = requests.request(method, url, headers=headers, data=json.dumps(data))
377
+ else:
378
+ response = requests.request(method, url, headers=headers)
379
+
380
+ return response
@@ -0,0 +1,18 @@
1
+ Metadata-Version: 2.4
2
+ Name: salure_helpers_planday
3
+ Version: 0.0.3
4
+ Summary: Planday wrapper from Salure
5
+ Author: D&A Salure
6
+ Author-email: support@salureconnnect.com
7
+ License: Salure License
8
+ Requires-Dist: salure-helpers-salureconnect>=1
9
+ Requires-Dist: pandas<=1.35,>=1
10
+ Requires-Dist: requests<=3,>=2
11
+ Dynamic: author
12
+ Dynamic: author-email
13
+ Dynamic: description
14
+ Dynamic: license
15
+ Dynamic: requires-dist
16
+ Dynamic: summary
17
+
18
+ Planday wrapper from Salure
@@ -0,0 +1,6 @@
1
+ salure_helpers/planday/__init__.py,sha256=4JpUucJXy-jBAANL5N2fCvFF7Lb9-opiKqtfgoXNABs,50
2
+ salure_helpers/planday/planday.py,sha256=GyjJ0m7dlGTjZg0MqnGuRb529bZHc-qXtrsgEdCdQ5Y,15381
3
+ salure_helpers_planday-0.0.3.dist-info/METADATA,sha256=aTfLktlG5XVVh5gAzeplVNd4NfCi8s_Rte7BFhbk5_4,442
4
+ salure_helpers_planday-0.0.3.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
5
+ salure_helpers_planday-0.0.3.dist-info/top_level.txt,sha256=N7hffwCZW8hULnj7XDFLMXOL-1WrbdfP5QnBK3touFM,15
6
+ salure_helpers_planday-0.0.3.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
+ salure_helpers