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 @@
|
|
|
1
|
+
salure_helpers
|