amochka 0.1.3__tar.gz → 0.1.4__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.
- {amochka-0.1.3 → amochka-0.1.4}/PKG-INFO +1 -1
- {amochka-0.1.3 → amochka-0.1.4}/amochka/client.py +128 -31
- {amochka-0.1.3 → amochka-0.1.4}/amochka.egg-info/PKG-INFO +1 -1
- {amochka-0.1.3 → amochka-0.1.4}/setup.py +1 -1
- {amochka-0.1.3 → amochka-0.1.4}/README.md +0 -0
- {amochka-0.1.3 → amochka-0.1.4}/amochka/__init__.py +0 -0
- {amochka-0.1.3 → amochka-0.1.4}/amochka.egg-info/SOURCES.txt +0 -0
- {amochka-0.1.3 → amochka-0.1.4}/amochka.egg-info/dependency_links.txt +0 -0
- {amochka-0.1.3 → amochka-0.1.4}/amochka.egg-info/requires.txt +0 -0
- {amochka-0.1.3 → amochka-0.1.4}/amochka.egg-info/top_level.txt +0 -0
- {amochka-0.1.3 → amochka-0.1.4}/setup.cfg +0 -0
|
@@ -19,7 +19,15 @@ RATE_LIMIT = 7 # Максимум 7 запросов в секунду
|
|
|
19
19
|
class Deal(dict):
|
|
20
20
|
"""
|
|
21
21
|
Объект сделки расширяет стандартный словарь данными из custom_fields_values.
|
|
22
|
-
|
|
22
|
+
|
|
23
|
+
Обеспечивает два способа доступа к кастомным полям:
|
|
24
|
+
1. get(key): при обращении по названию (строкой) или по ID поля (integer)
|
|
25
|
+
возвращает текстовое значение поля (например, «Дурина Юлия»).
|
|
26
|
+
2. get_id(key): возвращает идентификатор выбранного варианта (enum_id) для полей типа select.
|
|
27
|
+
Если в данных enum_id отсутствует, производится поиск в переданной конфигурации полей,
|
|
28
|
+
сравнение выполняется без учёта регистра и лишних пробелов.
|
|
29
|
+
|
|
30
|
+
Параметр custom_fields_config – словарь, где ключи – ID полей, а значения – модели полей.
|
|
23
31
|
"""
|
|
24
32
|
def __init__(self, data, custom_fields_config=None):
|
|
25
33
|
super().__init__(data)
|
|
@@ -33,9 +41,11 @@ class Deal(dict):
|
|
|
33
41
|
values = field.get("values")
|
|
34
42
|
if field_name and values and isinstance(values, list) and len(values) > 0:
|
|
35
43
|
key_name = field_name.lower().strip()
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
44
|
+
stored_value = values[0].get("value")
|
|
45
|
+
stored_enum_id = values[0].get("enum_id") # может быть None для некоторых полей
|
|
46
|
+
# Сохраняем полную информацию (и для get() и для get_id())
|
|
47
|
+
self._custom[key_name] = {"value": stored_value, "enum_id": stored_enum_id}
|
|
48
|
+
logger.debug(f"Set custom field '{key_name}' = {{'value': {stored_value}, 'enum_id': {stored_enum_id}}}")
|
|
39
49
|
field_id = field.get("field_id")
|
|
40
50
|
if field_id is not None and values and isinstance(values, list) and len(values) > 0:
|
|
41
51
|
stored_value = values[0].get("value")
|
|
@@ -72,7 +82,12 @@ class Deal(dict):
|
|
|
72
82
|
def get_id(self, key, default=None):
|
|
73
83
|
"""
|
|
74
84
|
Возвращает идентификатор выбранного варианта (enum_id) для кастомного поля.
|
|
75
|
-
|
|
85
|
+
Если значение enum_id отсутствует в данных, производится поиск в конфигурации кастомных полей,
|
|
86
|
+
сравнение значения выполняется без учёта регистра и пробелов.
|
|
87
|
+
|
|
88
|
+
:param key: Название поля (строка) или ID поля (integer).
|
|
89
|
+
:param default: Значение по умолчанию, если enum_id не найден.
|
|
90
|
+
:return: Идентификатор выбранного варианта (целое число) или default.
|
|
76
91
|
"""
|
|
77
92
|
stored = None
|
|
78
93
|
if isinstance(key, str):
|
|
@@ -105,22 +120,57 @@ class Deal(dict):
|
|
|
105
120
|
class AmoCRMClient:
|
|
106
121
|
"""
|
|
107
122
|
Клиент для работы с API amoCRM.
|
|
108
|
-
|
|
123
|
+
|
|
124
|
+
Основные функции:
|
|
125
|
+
- load_token: Загружает и проверяет токен авторизации.
|
|
126
|
+
- _make_request: Выполняет HTTP-запрос с учетом ограничения по скорости.
|
|
127
|
+
- get_deal_by_id: Получает данные сделки по ID и возвращает объект Deal.
|
|
128
|
+
- get_custom_fields_mapping: Загружает и кэширует список кастомных полей.
|
|
129
|
+
- find_custom_field_id: Ищет кастомное поле по его названию.
|
|
130
|
+
- update_lead: Обновляет сделку, включая стандартные и кастомные поля.
|
|
131
|
+
|
|
132
|
+
Дополнительно можно задать уровень логирования через параметр log_level,
|
|
133
|
+
либо полностью отключить логирование, установив disable_logging=True.
|
|
109
134
|
"""
|
|
110
|
-
def __init__(
|
|
135
|
+
def __init__(
|
|
136
|
+
self,
|
|
137
|
+
base_url,
|
|
138
|
+
token_file=None,
|
|
139
|
+
cache_file=None,
|
|
140
|
+
log_level=logging.INFO,
|
|
141
|
+
disable_logging=False,
|
|
142
|
+
cache_enabled=True,
|
|
143
|
+
cache_storage='file', # 'file' или 'memory'
|
|
144
|
+
cache_hours=24 # время жизни кэша в часах, или None для бесконечного кэша
|
|
145
|
+
):
|
|
111
146
|
"""
|
|
112
|
-
Инициализирует клиента, задавая базовый URL, токен авторизации и
|
|
147
|
+
Инициализирует клиента, задавая базовый URL, токен авторизации и настройки кэша для кастомных полей.
|
|
113
148
|
|
|
114
|
-
:param
|
|
149
|
+
:param base_url: Базовый URL API amoCRM.
|
|
150
|
+
:param token_file: Файл, содержащий токен авторизации.
|
|
151
|
+
:param cache_file: Файл для кэширования данных кастомных полей.
|
|
152
|
+
:param log_level: Уровень логирования (например, logging.DEBUG, logging.INFO).
|
|
153
|
+
:param disable_logging: Если True, логирование будет отключено.
|
|
154
|
+
:param cache_enabled: Если False, кэширование отключается (остальные параметры игнорируются).
|
|
155
|
+
:param cache_storage: 'file' для файлового кэша, 'memory' для кэша только в оперативной памяти.
|
|
156
|
+
:param cache_hours: Время жизни кэша в часах. Если None – кэш считается бесконечным.
|
|
115
157
|
"""
|
|
116
158
|
self.base_url = base_url.rstrip('/')
|
|
117
159
|
domain = self.base_url.split("//")[-1].split(".")[0]
|
|
118
160
|
self.domain = domain
|
|
119
161
|
self.token_file = token_file or os.path.join(os.path.expanduser('~'), '.amocrm_token.json')
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
162
|
+
|
|
163
|
+
# Если выбран файловый кэш, определяем имя файла, иначе он не используется
|
|
164
|
+
if cache_storage.lower() == 'file':
|
|
165
|
+
if not cache_file:
|
|
166
|
+
cache_file = f"custom_fields_cache_{self.domain}.json"
|
|
167
|
+
self.cache_file = cache_file
|
|
168
|
+
else:
|
|
169
|
+
self.cache_file = None
|
|
170
|
+
self.cache_enabled = cache_enabled
|
|
171
|
+
self.cache_storage = cache_storage.lower() # 'file' или 'memory'
|
|
172
|
+
self.cache_hours = cache_hours
|
|
173
|
+
|
|
124
174
|
self.token = self.load_token()
|
|
125
175
|
self._custom_fields_mapping = None
|
|
126
176
|
|
|
@@ -132,7 +182,12 @@ class AmoCRMClient:
|
|
|
132
182
|
logger.debug(f"AmoCRMClient initialized for domain {self.domain}")
|
|
133
183
|
|
|
134
184
|
def load_token(self):
|
|
135
|
-
|
|
185
|
+
"""
|
|
186
|
+
Загружает токен авторизации из файла или строки, проверяет его срок действия.
|
|
187
|
+
|
|
188
|
+
:return: Действительный access_token.
|
|
189
|
+
:raises Exception: Если токен не найден или истёк.
|
|
190
|
+
"""
|
|
136
191
|
data = None
|
|
137
192
|
if os.path.exists(self.token_file):
|
|
138
193
|
with open(self.token_file, 'r') as f:
|
|
@@ -160,7 +215,16 @@ class AmoCRMClient:
|
|
|
160
215
|
@sleep_and_retry
|
|
161
216
|
@limits(calls=RATE_LIMIT, period=1)
|
|
162
217
|
def _make_request(self, method, endpoint, params=None, data=None):
|
|
163
|
-
|
|
218
|
+
"""
|
|
219
|
+
Выполняет HTTP-запрос к API amoCRM с учетом ограничения по скорости (rate limit).
|
|
220
|
+
|
|
221
|
+
:param method: HTTP-метод (GET, PATCH, POST, DELETE и т.д.).
|
|
222
|
+
:param endpoint: Конечная точка API (начинается с /api/v4/).
|
|
223
|
+
:param params: GET-параметры запроса.
|
|
224
|
+
:param data: Данные, отправляемые в JSON-формате.
|
|
225
|
+
:return: Ответ в формате JSON или None (если статус 204).
|
|
226
|
+
:raises Exception: При получении кода ошибки, отличного от 200/204.
|
|
227
|
+
"""
|
|
164
228
|
url = f"{self.base_url}{endpoint}"
|
|
165
229
|
headers = {
|
|
166
230
|
"Authorization": f"Bearer {self.token}",
|
|
@@ -176,7 +240,12 @@ class AmoCRMClient:
|
|
|
176
240
|
return response.json()
|
|
177
241
|
|
|
178
242
|
def get_deal_by_id(self, deal_id):
|
|
179
|
-
|
|
243
|
+
"""
|
|
244
|
+
Получает данные сделки по её ID и возвращает объект Deal.
|
|
245
|
+
|
|
246
|
+
:param deal_id: ID сделки.
|
|
247
|
+
:return: Объект Deal, включающий данные стандартных и кастомных полей.
|
|
248
|
+
"""
|
|
180
249
|
endpoint = f"/api/v4/leads/{deal_id}"
|
|
181
250
|
params = {'with': 'contacts,companies,catalog_elements,loss_reason,tags'}
|
|
182
251
|
data = self._make_request("GET", endpoint, params=params)
|
|
@@ -187,10 +256,13 @@ class AmoCRMClient:
|
|
|
187
256
|
def _save_custom_fields_cache(self, mapping):
|
|
188
257
|
"""
|
|
189
258
|
Сохраняет кэш кастомных полей в файл, если используется файловый кэш.
|
|
190
|
-
Если
|
|
259
|
+
Если кэширование отключено или выбран кэш в памяти, операция пропускается.
|
|
191
260
|
"""
|
|
192
|
-
if not self.
|
|
193
|
-
logger.debug("
|
|
261
|
+
if not self.cache_enabled:
|
|
262
|
+
logger.debug("Caching disabled; cache not saved.")
|
|
263
|
+
return
|
|
264
|
+
if self.cache_storage != 'file':
|
|
265
|
+
logger.debug("Using memory caching; no file cache saved.")
|
|
194
266
|
return
|
|
195
267
|
cache_data = {"last_updated": time.time(), "mapping": mapping}
|
|
196
268
|
with open(self.cache_file, "w") as f:
|
|
@@ -200,10 +272,13 @@ class AmoCRMClient:
|
|
|
200
272
|
def _load_custom_fields_cache(self):
|
|
201
273
|
"""
|
|
202
274
|
Загружает кэш кастомных полей из файла, если используется файловый кэш.
|
|
203
|
-
Если
|
|
275
|
+
Если кэширование отключено или выбран кэш в памяти, возвращает None.
|
|
204
276
|
"""
|
|
205
|
-
if not self.
|
|
206
|
-
logger.debug("
|
|
277
|
+
if not self.cache_enabled:
|
|
278
|
+
logger.debug("Caching disabled; no cache loaded.")
|
|
279
|
+
return None
|
|
280
|
+
if self.cache_storage != 'file':
|
|
281
|
+
logger.debug("Using memory caching; cache will be kept in memory only.")
|
|
207
282
|
return None
|
|
208
283
|
if os.path.exists(self.cache_file):
|
|
209
284
|
with open(self.cache_file, "r") as f:
|
|
@@ -216,21 +291,27 @@ class AmoCRMClient:
|
|
|
216
291
|
return None
|
|
217
292
|
return None
|
|
218
293
|
|
|
219
|
-
def get_custom_fields_mapping(self, force_update=False
|
|
294
|
+
def get_custom_fields_mapping(self, force_update=False):
|
|
220
295
|
"""
|
|
221
296
|
Возвращает словарь отображения кастомных полей для сделок.
|
|
222
297
|
Если данные кэшированы и не устарели, возвращает кэш; иначе выполняет запросы для получения данных.
|
|
223
298
|
"""
|
|
224
|
-
if not force_update:
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
299
|
+
if not force_update and self._custom_fields_mapping is not None:
|
|
300
|
+
return self._custom_fields_mapping
|
|
301
|
+
|
|
302
|
+
cache_data = self._load_custom_fields_cache() if self.cache_enabled else None
|
|
303
|
+
if cache_data:
|
|
304
|
+
last_updated = cache_data.get("last_updated", 0)
|
|
305
|
+
if self.cache_hours is not None:
|
|
306
|
+
if time.time() - last_updated < self.cache_hours * 3600:
|
|
231
307
|
self._custom_fields_mapping = cache_data.get("mapping")
|
|
232
308
|
logger.debug("Using cached custom fields mapping.")
|
|
233
309
|
return self._custom_fields_mapping
|
|
310
|
+
else:
|
|
311
|
+
# Бесконечный кэш – не проверяем срок
|
|
312
|
+
self._custom_fields_mapping = cache_data.get("mapping")
|
|
313
|
+
logger.debug("Using cached custom fields mapping (infinite cache).")
|
|
314
|
+
return self._custom_fields_mapping
|
|
234
315
|
|
|
235
316
|
mapping = {}
|
|
236
317
|
page = 1
|
|
@@ -249,12 +330,16 @@ class AmoCRMClient:
|
|
|
249
330
|
|
|
250
331
|
logger.debug("Custom fields mapping fetched (содержимое маппинга не выводится полностью).")
|
|
251
332
|
self._custom_fields_mapping = mapping
|
|
252
|
-
self.
|
|
333
|
+
if self.cache_enabled:
|
|
334
|
+
self._save_custom_fields_cache(mapping)
|
|
253
335
|
return mapping
|
|
254
336
|
|
|
255
337
|
def find_custom_field_id(self, search_term):
|
|
256
338
|
"""
|
|
257
339
|
Ищет кастомное поле по заданному названию (или части названия).
|
|
340
|
+
|
|
341
|
+
:param search_term: Строка для поиска по имени поля.
|
|
342
|
+
:return: Кортеж (field_id, field_obj) если найдено, иначе (None, None).
|
|
258
343
|
"""
|
|
259
344
|
mapping = self.get_custom_fields_mapping()
|
|
260
345
|
search_term_lower = search_term.lower().strip()
|
|
@@ -272,6 +357,18 @@ class AmoCRMClient:
|
|
|
272
357
|
def update_lead(self, lead_id, update_fields: dict, tags_to_add: list = None, tags_to_delete: list = None):
|
|
273
358
|
"""
|
|
274
359
|
Обновляет сделку, задавая новые значения для стандартных и кастомных полей.
|
|
360
|
+
|
|
361
|
+
Для кастомных полей:
|
|
362
|
+
- Если значение передается как целое число, оно интерпретируется как идентификатор варианта (enum_id)
|
|
363
|
+
для полей типа select.
|
|
364
|
+
- Если значение передается как строка, используется ключ "value".
|
|
365
|
+
|
|
366
|
+
:param lead_id: ID сделки, которую нужно обновить.
|
|
367
|
+
:param update_fields: Словарь с полями для обновления. Ключи могут быть стандартными или названием кастомного поля.
|
|
368
|
+
:param tags_to_add: Список тегов для добавления к сделке.
|
|
369
|
+
:param tags_to_delete: Список тегов для удаления из сделки.
|
|
370
|
+
:return: Ответ API в формате JSON.
|
|
371
|
+
:raises Exception: Если одно из кастомных полей не найдено.
|
|
275
372
|
"""
|
|
276
373
|
payload = {}
|
|
277
374
|
standard_fields = {
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|