amochka 0.1.7__py3-none-any.whl → 0.1.8__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.
- amochka/__init__.py +1 -1
- amochka/client.py +100 -12
- {amochka-0.1.7.dist-info → amochka-0.1.8.dist-info}/METADATA +1 -1
- amochka-0.1.8.dist-info/RECORD +7 -0
- amochka-0.1.7.dist-info/RECORD +0 -7
- {amochka-0.1.7.dist-info → amochka-0.1.8.dist-info}/WHEEL +0 -0
- {amochka-0.1.7.dist-info → amochka-0.1.8.dist-info}/top_level.txt +0 -0
amochka/__init__.py
CHANGED
amochka/client.py
CHANGED
|
@@ -223,7 +223,11 @@ class AmoCRMClient:
|
|
|
223
223
|
token_file=None,
|
|
224
224
|
cache_config=None,
|
|
225
225
|
log_level=logging.INFO,
|
|
226
|
-
disable_logging=False
|
|
226
|
+
disable_logging=False,
|
|
227
|
+
*,
|
|
228
|
+
client_id: Optional[str] = None,
|
|
229
|
+
client_secret: Optional[str] = None,
|
|
230
|
+
redirect_uri: Optional[str] = None,
|
|
227
231
|
):
|
|
228
232
|
"""
|
|
229
233
|
Инициализирует клиента, задавая базовый URL, токен авторизации и настройки кэша для кастомных полей.
|
|
@@ -238,6 +242,11 @@ class AmoCRMClient:
|
|
|
238
242
|
domain = self.base_url.split("//")[-1].split(".")[0]
|
|
239
243
|
self.domain = domain
|
|
240
244
|
self.token_file = token_file or os.path.join(os.path.expanduser('~'), '.amocrm_token.json')
|
|
245
|
+
|
|
246
|
+
# OAuth2 credentials (используются для авто‑refresh токена)
|
|
247
|
+
self.client_id = client_id
|
|
248
|
+
self.client_secret = client_secret
|
|
249
|
+
self.redirect_uri = redirect_uri
|
|
241
250
|
|
|
242
251
|
# Создаем логгер для конкретного экземпляра клиента
|
|
243
252
|
self.logger = logging.getLogger(f"{__name__}.{self.domain}")
|
|
@@ -266,15 +275,19 @@ class AmoCRMClient:
|
|
|
266
275
|
|
|
267
276
|
self.logger.debug(f"AmoCRMClient initialized for domain {self.domain}")
|
|
268
277
|
|
|
269
|
-
self.token =
|
|
278
|
+
self.token = None
|
|
279
|
+
self.refresh_token = None
|
|
280
|
+
self.expires_at = None
|
|
281
|
+
self.load_token()
|
|
270
282
|
self._custom_fields_mapping = None
|
|
271
283
|
|
|
272
284
|
def load_token(self):
|
|
273
285
|
"""
|
|
274
286
|
Загружает токен авторизации из файла или строки, проверяет его срок действия.
|
|
287
|
+
При наличии refresh_token и учётных данных пробует обновить токен.
|
|
275
288
|
|
|
276
289
|
:return: Действительный access_token.
|
|
277
|
-
:raises Exception: Если токен не найден или
|
|
290
|
+
:raises Exception: Если токен не найден или истёк и нет возможности обновить.
|
|
278
291
|
"""
|
|
279
292
|
data = None
|
|
280
293
|
if os.path.exists(self.token_file):
|
|
@@ -288,17 +301,34 @@ class AmoCRMClient:
|
|
|
288
301
|
except Exception as e:
|
|
289
302
|
raise Exception("Токен не найден и не удалось распарсить переданное содержимое.") from e
|
|
290
303
|
|
|
304
|
+
self.refresh_token = data.get('refresh_token', self.refresh_token)
|
|
305
|
+
self.client_id = data.get('client_id', self.client_id)
|
|
306
|
+
self.client_secret = data.get('client_secret', self.client_secret)
|
|
307
|
+
self.redirect_uri = data.get('redirect_uri', self.redirect_uri)
|
|
308
|
+
|
|
291
309
|
expires_at_str = data.get('expires_at')
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
310
|
+
expires_at = None
|
|
311
|
+
if expires_at_str:
|
|
312
|
+
try:
|
|
313
|
+
expires_at = datetime.fromisoformat(expires_at_str).timestamp()
|
|
314
|
+
except Exception:
|
|
315
|
+
try:
|
|
316
|
+
expires_at = float(expires_at_str)
|
|
317
|
+
except Exception:
|
|
318
|
+
expires_at = None
|
|
319
|
+
self.expires_at = expires_at
|
|
320
|
+
|
|
321
|
+
access_token = data.get('access_token')
|
|
322
|
+
if access_token and expires_at and time.time() < expires_at:
|
|
298
323
|
self.logger.debug("Token is valid.")
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
324
|
+
self.token = access_token
|
|
325
|
+
return access_token
|
|
326
|
+
|
|
327
|
+
if self.refresh_token and self.client_id and self.client_secret and self.redirect_uri:
|
|
328
|
+
self.logger.info("Access token истёк, пробую обновить через refresh_token…")
|
|
329
|
+
return self._refresh_access_token()
|
|
330
|
+
|
|
331
|
+
raise Exception("Токен истёк или некорректен, и нет данных для refresh_token. Обновите токен.")
|
|
302
332
|
|
|
303
333
|
@sleep_and_retry
|
|
304
334
|
@limits(calls=RATE_LIMIT, period=1)
|
|
@@ -321,6 +351,13 @@ class AmoCRMClient:
|
|
|
321
351
|
}
|
|
322
352
|
self.logger.debug(f"Making {method} request to {url} with params {params} and data {data}")
|
|
323
353
|
response = requests.request(method, url, headers=headers, params=params, json=data, timeout=timeout)
|
|
354
|
+
|
|
355
|
+
if response.status_code == 401 and self.refresh_token:
|
|
356
|
+
self.logger.info("Получен 401, пробую обновить токен и повторить запрос…")
|
|
357
|
+
self._refresh_access_token()
|
|
358
|
+
headers["Authorization"] = f"Bearer {self.token}"
|
|
359
|
+
response = requests.request(method, url, headers=headers, params=params, json=data, timeout=timeout)
|
|
360
|
+
|
|
324
361
|
if response.status_code not in (200, 204):
|
|
325
362
|
self.logger.error(f"Request error {response.status_code}: {response.text}")
|
|
326
363
|
raise Exception(f"Ошибка запроса: {response.status_code}, {response.text}")
|
|
@@ -328,6 +365,57 @@ class AmoCRMClient:
|
|
|
328
365
|
return None
|
|
329
366
|
return response.json()
|
|
330
367
|
|
|
368
|
+
def _refresh_access_token(self):
|
|
369
|
+
"""Обновляет access_token по refresh_token и сохраняет его в token_file."""
|
|
370
|
+
if not all([self.refresh_token, self.client_id, self.client_secret, self.redirect_uri]):
|
|
371
|
+
raise Exception("Нельзя обновить токен: отсутствует refresh_token или client_id/client_secret/redirect_uri")
|
|
372
|
+
|
|
373
|
+
payload = {
|
|
374
|
+
"client_id": self.client_id,
|
|
375
|
+
"client_secret": self.client_secret,
|
|
376
|
+
"grant_type": "refresh_token",
|
|
377
|
+
"refresh_token": self.refresh_token,
|
|
378
|
+
"redirect_uri": self.redirect_uri,
|
|
379
|
+
}
|
|
380
|
+
token_url = f"{self.base_url}/oauth2/access_token"
|
|
381
|
+
self.logger.debug(f"Refreshing token via {token_url}")
|
|
382
|
+
resp = requests.post(token_url, json=payload, timeout=10)
|
|
383
|
+
if resp.status_code != 200:
|
|
384
|
+
self.logger.error(f"Не удалось обновить токен: {resp.status_code} {resp.text}")
|
|
385
|
+
raise Exception(f"Не удалось обновить токен: {resp.status_code}")
|
|
386
|
+
|
|
387
|
+
data = resp.json() or {}
|
|
388
|
+
access_token = data.get("access_token")
|
|
389
|
+
refresh_token = data.get("refresh_token", self.refresh_token)
|
|
390
|
+
expires_in = data.get("expires_in")
|
|
391
|
+
if not access_token:
|
|
392
|
+
raise Exception("Ответ на refresh не содержит access_token")
|
|
393
|
+
|
|
394
|
+
expires_at = None
|
|
395
|
+
if expires_in:
|
|
396
|
+
expires_at = time.time() + int(expires_in)
|
|
397
|
+
|
|
398
|
+
self.token = access_token
|
|
399
|
+
self.refresh_token = refresh_token
|
|
400
|
+
self.expires_at = expires_at
|
|
401
|
+
|
|
402
|
+
if self.token_file:
|
|
403
|
+
try:
|
|
404
|
+
with open(self.token_file, "w") as f:
|
|
405
|
+
json.dump({
|
|
406
|
+
"access_token": access_token,
|
|
407
|
+
"refresh_token": refresh_token,
|
|
408
|
+
"expires_at": datetime.fromtimestamp(expires_at).isoformat() if expires_at else None,
|
|
409
|
+
"client_id": self.client_id,
|
|
410
|
+
"client_secret": self.client_secret,
|
|
411
|
+
"redirect_uri": self.redirect_uri,
|
|
412
|
+
}, f)
|
|
413
|
+
self.logger.debug(f"Новый токен сохранён в {self.token_file}")
|
|
414
|
+
except Exception as exc:
|
|
415
|
+
self.logger.error(f"Не удалось сохранить обновлённый токен: {exc}")
|
|
416
|
+
|
|
417
|
+
return access_token
|
|
418
|
+
|
|
331
419
|
def _to_timestamp(self, value: Optional[Union[int, float, str, datetime]]) -> Optional[int]:
|
|
332
420
|
"""
|
|
333
421
|
Преобразует значение даты/времени в Unix timestamp.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
amochka/__init__.py,sha256=LTjIPz4-miZ7A90vIUnbBKIpc_CyEPqRgnoqzEZngtw,620
|
|
2
|
+
amochka/client.py,sha256=zs72v79nplCiRvI-ccVauctCQRILYrWfvbol8G8w2L0,54392
|
|
3
|
+
amochka/etl.py,sha256=N8rXNFbtmlKfsYpgr7HDcP4enoj63XQPWuTDxGuMhw4,8901
|
|
4
|
+
amochka-0.1.8.dist-info/METADATA,sha256=BQXYM8C4pmHBUvl-siS3luV-YKMOuFvuNg05t9Hh7Iw,2218
|
|
5
|
+
amochka-0.1.8.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
6
|
+
amochka-0.1.8.dist-info/top_level.txt,sha256=y5qXFXJUECmDwO6hyupsuYcTpZKZyByeE9e-1sa2U24,8
|
|
7
|
+
amochka-0.1.8.dist-info/RECORD,,
|
amochka-0.1.7.dist-info/RECORD
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
amochka/__init__.py,sha256=RdQyNTzygG3l4X52op5afzvHjEjvYJB_yZz-jVd8R54,620
|
|
2
|
-
amochka/client.py,sha256=hRO7e0kGmvyw1RZR9hMXBQnwrH7n-7m-izCqjag_QS4,50047
|
|
3
|
-
amochka/etl.py,sha256=N8rXNFbtmlKfsYpgr7HDcP4enoj63XQPWuTDxGuMhw4,8901
|
|
4
|
-
amochka-0.1.7.dist-info/METADATA,sha256=RovMukJ-TfsjV4UHCb3_P098JkbyeHs_ETfZdwDlFi0,2218
|
|
5
|
-
amochka-0.1.7.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
6
|
-
amochka-0.1.7.dist-info/top_level.txt,sha256=y5qXFXJUECmDwO6hyupsuYcTpZKZyByeE9e-1sa2U24,8
|
|
7
|
-
amochka-0.1.7.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|