amochka 0.1.7__tar.gz → 0.1.8__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: amochka
3
- Version: 0.1.7
3
+ Version: 0.1.8
4
4
  Summary: Библиотека для работы с API amoCRM
5
5
  Home-page:
6
6
  Author: Timurka
@@ -2,7 +2,7 @@
2
2
  amochka: Библиотека для работы с API amoCRM.
3
3
  """
4
4
 
5
- __version__ = "0.1.7"
5
+ __version__ = "0.1.8"
6
6
 
7
7
  from .client import AmoCRMClient, CacheConfig
8
8
  from .etl import (
@@ -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 = self.load_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
- try:
293
- expires_at = datetime.fromisoformat(expires_at_str).timestamp()
294
- except Exception:
295
- expires_at = float(expires_at_str)
296
-
297
- if expires_at and time.time() < expires_at:
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
- return data.get('access_token')
300
- else:
301
- raise Exception("Токен найден, но он истёк. Обновите токен.")
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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: amochka
3
- Version: 0.1.7
3
+ Version: 0.1.8
4
4
  Summary: Библиотека для работы с API amoCRM
5
5
  Home-page:
6
6
  Author: Timurka
@@ -5,7 +5,7 @@ README = (Path(__file__).parent / "README.md").read_text(encoding="utf-8")
5
5
 
6
6
  setup(
7
7
  name='amochka',
8
- version='0.1.7',
8
+ version='0.1.8',
9
9
  package_dir={"": "amochka"},
10
10
  packages=find_packages(where="amochka"),
11
11
  install_requires=[
File without changes
File without changes
File without changes
File without changes