-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathyclients.py
383 lines (342 loc) · 17.3 KB
/
yclients.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
# coding=utf-8
import datetime
import time
import httpx
import pandas as pd
import ujson
class YClientsAPI:
def __init__(self, token: str, company_id: int, form_id: int, language: str = 'ru-RU'):
self.company_id = company_id
self.form_id = form_id
self.headers = {
"Accept": "application/vnd.yclients.v2+json",
'Accept-Language': language,
'Authorization': "Bearer {}".format(token),
'Cache-Control': "no-cache"
}
# if __show_debugging==True code will show debugging process
self.__show_debugging = False
@staticmethod
def datetime_parser(date_time: str) -> datetime.datetime:
""" datetime in iso8601 format parser """
a, b = date_time.split('+')
dt = datetime.datetime.strptime(a, "%Y-%m-%dT%H:%M:%S")
return dt
def book(self, booking_id: int, fullname: str, phone: str, email: str, staff_id: int,
date_time: str | datetime.datetime,
service_id: int = None, comment: str = None) -> (True, None) or (False, Exception):
""" Make booking """
url = "https://n{}.yclients.com/api/v1/book_record/{}/".format(self.form_id, self.company_id)
payload = {
"phone": phone,
"fullname": fullname,
"email": email,
"comment": comment,
"notify_by_email": 0,
"appointments": [{
"id": booking_id,
"services": [int(service_id)],
"staff_id": int(staff_id or 0),
"datetime": date_time
}]
}
response = httpx.post(url, json=payload, headers=self.headers)
res = ujson.loads(response.text)
if isinstance(res, dict) and res.get('errors'):
return False, res.get('errors', {}).get('message', '')
return True, ''
def get_staff_info(self, staff_id: int) -> dict:
""" Return dict with info about specific staff"""
url = "https://n{}.yclients.com/api/v1/staff/{}/{}".format(self.form_id, self.company_id, staff_id)
response = httpx.get(url, headers=self.headers)
return ujson.loads(response.text)
def get_service_info(self, service_id: int) -> dict:
""" Return dict with info about specific service"""
url = "https://n{}.yclients.com/api/v1/services/{}/{}".format(self.form_id, self.company_id, service_id)
response = httpx.get(url, headers=self.headers)
return ujson.loads(response.text)
def get_staff(self, service_id: int = None, date_time=None) -> dict:
""" Return dict of staff for specific service and date"""
url = "https://n{}.yclients.com/api/v1/book_staff/{}".format(self.form_id, self.company_id)
querystring = {"service_ids[]": int(service_id)} if service_id else {}
querystring.update({"datetime": date_time} if date_time else {})
response = httpx.get(url, headers=self.headers, params=querystring)
return ujson.loads(response.text)
def get_services(self, staff_id: int = None, date_time: int = None) -> dict:
""" Return list of services for specific staff and date"""
url = "https://n{}.yclients.com/api/v1/book_services/{}".format(self.form_id, self.company_id)
querystring = {"staff_id": int(staff_id)} if staff_id else {}
querystring.update({"datetime": date_time} if date_time else {})
response = httpx.get(url, headers=self.headers, params=querystring)
return ujson.loads(response.text)
def get_available_days(self, staff_id: int = None, service_id: int = None) -> dict:
""" Return all available days for specific staff and service"""
url = "https://n{}.yclients.com/api/v1/book_dates/{}".format(self.form_id, self.company_id)
querystring = {"staff_id": int(staff_id)} if staff_id else {}
querystring.update({"service_ids[]": service_id} if service_id else {})
response = httpx.get(url, headers=self.headers, params=querystring)
return ujson.loads(response.text)
def get_available_times(self, staff_id: int, service_id: int = None, day=None) -> dict:
""" Return all available time slots on specific day staff and service"""
url = "https://n{}.yclients.com/api/v1/book_times/{}/{}/{}".format(self.form_id, self.company_id, staff_id, day)
querystring = {}
if service_id:
querystring.update({"service_ids[]": service_id})
response = httpx.request("GET", url, headers=self.headers, params=querystring)
return ujson.loads(response.text)
"""DEBUGGING"""
def show_debugging(self):
print("Debugging prints turned on")
self.__show_debugging = True
def hide_debugging(self):
print("Debugging prints turned off")
self.__show_debugging = False
"""USER AUTHORIZATION"""
def get_user_token(self, login: str, password: str) -> str:
"""
To read clients data you need to obtain user token
:param login: yclients user login
:param password: yclients user login
:return: user token
"""
url = "https://api.yclients.com/api/v1/auth"
querystring = {
"login": login,
"password": password
}
response = httpx.post(url, headers=self.headers, params=querystring)
user_token = ujson.loads(response.text)['data']['user_token']
if self.__show_debugging:
print(f"Obtained user token {user_token}")
return user_token
def update_user_token(self, user_token: str):
"""
After user token was obtained you need to include it in
header of requests that you are sending
:param user_token: user token
:return:
"""
self.headers['Authorization'] = \
self.headers['Authorization'] + f", User {user_token}"
if self.__show_debugging:
print(f"Updated autorisation parameters:"
f" {self.headers['Authorization']}")
def show_user_permissions(self):
"""
:return: json-type data with user permissions
"""
url = f"https://api.yclients.com/api/v1/user/permissions/{self.company_id}"
querystring = {}
response = httpx.request("GET", url, headers=self.headers, params=querystring)
data = ujson.loads(response.text)['data']
print("User permissions:")
print(ujson.dumps(data, indent=4, sort_keys=True))
return data
"""CLIENTS DATA"""
def __get_clients_page(self, page_number: int, session: httpx.Client, clients_per_page: int) -> dict:
"""
Yclients api can't return all clients at once and returns in groups
of maximum size of 200. Those groups are called pages and you can
choose how many clients it will return
:param page_number: number of page
:param session: httpx.Client() object
:param clients_per_page: size of the page
:return: data of clients on the page page_number
"""
st = time.time()
url = "https://api.yclients.com/api/v1/clients/{}".format(self.company_id)
querystring = {}
querystring.update({"count": clients_per_page})
querystring.update({"page": page_number})
response = session.get(url, headers=self.headers, params=querystring)
if self.__show_debugging:
print(f'Clients page {page_number} obtained in {time.time() - st} sec')
return ujson.loads(response.text)
def get_clients_data(self, clients_per_page: int = 200) -> dict:
"""
:param clients_per_page: size of the page
:return: data of all the clients in the system
client's parameters:
'id', 'name', 'phone', 'email', 'card',
'birth_date', 'comment', 'discount', 'visits',
'sex_id', 'sex', 'sms_check', 'sms_bot', 'spent',
'paid', 'balance', 'importance_id', 'importance',
'categories', 'last_change_date', 'custom_fields'
"""
session = httpx.Client(trust_env=False)
# In the first request we obtain total number of clients in system
first_request = self.__get_clients_page(1, session, clients_per_page)
clients_data_list = first_request['data']
# total number of clients
clients_number = first_request['meta']['total_count']
# number of pages that we need to request
pages_number = int(clients_number / clients_per_page) + 1
if self.__show_debugging:
print(f"There are {clients_number} clients in the system")
if pages_number > 1:
print(f"{pages_number} pages will be loaded")
if pages_number == 1:
return clients_data_list
for page in range(2, pages_number + 1):
new_page_request = self.__get_clients_page(page, session, clients_per_page)
clients_data_list.extend(new_page_request['data'])
return clients_data_list
def parse_clients_data(self, clients_data_list: list) -> pd.DataFrame:
"""
:param clients_data_list: list of dictionaries with client data
:return: pd.Dataframe with clients data
"""
columns = clients_data_list[0].keys()
df = pd.DataFrame(columns=columns)
for client_data in clients_data_list:
df = df.append(client_data, ignore_index=True)
if self.__show_debugging:
print("Parsed clients data:")
print(df)
return df
"""VISITS DATA"""
def __get_visits_page(self, cid: int, page_number: int, session: httpx.Client, visits_per_page: int) -> dict:
"""
Yclients api can't return all visits at once and returns in groups
of maximum size of 200. Those groups are called pages and you can
choose how many visits it will return
:param page_number: number of page
:param session: requests.Session() object
:param visits_per_page: size of the page
:return: data of visits on the page page_number
"""
st = time.time()
url = f"https://api.yclients.com/api/v1/records/{self.company_id}"
querystring = {
"client_id": cid,
"count": visits_per_page,
"page": page_number
}
response = session.get(url, headers=self.headers, params=querystring)
if self.__show_debugging:
print(f'Visits page {page_number} obtained in {time.time() - st} sec')
return ujson.loads(response.text)
def get_visits_for_client(self, cid: int, visits_per_page: int = 200, session: httpx.Client = None) -> list:
"""
:param cid: client id
:param visits_per_page: size of the page
:param session: None by default, but if we get visits for multiple clients,
we want to use the same session for all of them
:return: data of all the visits of client cid
visit parameters:
id, company_id, staff_id, services, goods_transactions,
staff, client, comer, clients_count, date, datetime,
create_date, comment, online, visit_attendance, attendance,
confirmed, seance_length, length, sms_before, sms_now,
sms_now_text, email_now, notified, master_request, api_id,
from_url, review_requested, visit_id, created_user_id,
deleted, paid_full, prepaid, prepaid_confirmed,
last_change_date, custom_color, custom_font_color, record_labels,
activity_id, custom_fields, documents, is_sale_bill_printed,
Also some fields include additional information:
- client is a dictionary with parameters:
'id', 'name', 'phone', 'card', 'email',
'success_visits_count', 'fail_visits_count', 'discount'
- documents include list of dictionaries with parameters:
'id', 'type_id', 'storage_id', 'user_id', 'company_id',
'number', 'comment', 'date_created', 'category_id',
'visit_id', 'record_id', 'type_title'
- services include list of dictionaries with parameters:
'id', 'title', 'cost', 'manual_cost', 'cost_per_unit',
'discount', 'first_cost', 'amount'
- staff is a dictionary with parameters:
'id', 'api_id', 'name', 'specialization', 'position',
'avatar', 'avatar_big', 'rating', 'votes_count'
- position is a dictionary with parameters:
'id', 'title'
Parameters description is accesible via:
https://github.com/petroff/api-blueprint/blob/master/apiary.apib
"""
if not session:
session = httpx.Client(trust_env=False)
# In the first request we obtain total number of client visits in system
first_request = self.__get_visits_page(cid=cid,
page_number=1,
session=session,
visits_per_page=visits_per_page)
visits_data_list = first_request['data']
# total number of visits
visits_number = first_request['meta']['total_count']
# number of pages that we need to request
pages_number = int(visits_number / visits_per_page) + 1
if self.__show_debugging:
print(f"There are {visits_number} visits for clients {cid} in the system")
if pages_number > 1:
print(f"{pages_number} pages will be loaded")
if pages_number == 1:
return visits_data_list
for page in range(2, pages_number + 1):
new_page_request = self.__get_visits_page(cid=cid,
page_number=page,
session=session,
visits_per_page=visits_per_page)
visits_data_list.extend(new_page_request['data'])
return visits_data_list
def get_visits_data_for_clients_list(self, cids_list: list, visits_per_page=200) -> dict:
"""
get_visits_for_client funtion wrapper for multiple clients
:param cids_list: list of clients ids
:param visits_per_page: size of the page
:return: dictionary with client id as key and list of visits as value
"""
session = httpx.Client(trust_env=False)
clients_visits_dictionary = {cid: self.get_visits_for_client(cid, visits_per_page, session)
for cid in cids_list}
return clients_visits_dictionary
def get_attended_visits_for_client(self, cid: int, visits_per_page: int = 200,
session: httpx.Client = None) -> list:
"""
Attendance explanation from Yclient API:
2 - The user has confirmed the entry,
1 - The user has arrived, the services are provided,
0 - the user is waiting,
-1 - the user did not come for a visit
:param cid: client id
:param visits_per_page: size of the page
:param session: None by default, but if we get visits for multiple clients,
we want to use the same session for all of them
:return: data of all the visits of client cid where attendance field is equal to 1
"""
all_visits = self.get_visits_for_client(cid=cid,
visits_per_page=visits_per_page,
session=session)
attended_visits = [visit for visit in all_visits if visit['attendance'] == 1]
return attended_visits
def get_attended_visits_dates_information(self, cids_lists: list, visits_per_page: int = 200,
session: httpx.Client = None) -> pd.DataFrame:
"""
:param cids_lists: clients ids list
:param visits_per_page: size of the page
:param session: None by default, but if we get visits for multiple clients,
we want to use the same session for all of them
:return: Dataframe with columns:
- id: client id
- visits_number: number of attended visits
- first_visit: date of client first attended visit
- last_visit: date of client last attended visit
"""
columns = ['id', 'visits_number', 'first_visit', 'last_visit']
df = pd.DataFrame(columns=columns)
for cid in cids_lists:
c_dict = {'id': cid}
attended_visits = self.get_attended_visits_for_client(cid=cid,
visits_per_page=visits_per_page,
session=session
)
visit_dates = [visit['datetime'] for visit in attended_visits]
if not visit_dates:
c_dict['visits_number'] = 0
c_dict['first_visit'] = None
c_dict['last_visit'] = None
else:
visit_dates = list(map(self.datetime_parser, visit_dates))
c_dict['visits_number'] = len(visit_dates)
c_dict['first_visit'] = min(visit_dates).date()
c_dict['last_visit'] = max(visit_dates).date()
df = df.append(c_dict, ignore_index=True)
return df