amochka 0.1.8__tar.gz → 0.3.0__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.3.0/PKG-INFO +126 -0
- amochka-0.3.0/README.md +94 -0
- {amochka-0.1.8/amochka → amochka-0.3.0}/amochka/__init__.py +1 -1
- {amochka-0.1.8/amochka → amochka-0.3.0}/amochka/client.py +305 -31
- amochka-0.3.0/amochka.egg-info/PKG-INFO +126 -0
- amochka-0.3.0/amochka.egg-info/SOURCES.txt +18 -0
- amochka-0.3.0/amochka.egg-info/requires.txt +4 -0
- {amochka-0.1.8/amochka → amochka-0.3.0}/amochka.egg-info/top_level.txt +1 -0
- amochka-0.3.0/etl/__init__.py +7 -0
- amochka-0.3.0/etl/config.py +236 -0
- amochka-0.3.0/etl/extractors.py +354 -0
- amochka-0.3.0/etl/loaders.py +813 -0
- amochka-0.3.0/etl/migrations/001_create_tables.sql +346 -0
- amochka-0.3.0/etl/run_etl.py +684 -0
- amochka-0.3.0/etl/transformers.py +470 -0
- amochka-0.3.0/pyproject.toml +50 -0
- amochka-0.3.0/tests/test_client.py +259 -0
- amochka-0.1.8/MANIFEST.in +0 -12
- amochka-0.1.8/PKG-INFO +0 -40
- amochka-0.1.8/README.md +0 -17
- amochka-0.1.8/amochka/amochka.egg-info/PKG-INFO +0 -40
- amochka-0.1.8/amochka/amochka.egg-info/SOURCES.txt +0 -11
- amochka-0.1.8/amochka/amochka.egg-info/requires.txt +0 -2
- amochka-0.1.8/setup.py +0 -27
- {amochka-0.1.8/amochka → amochka-0.3.0}/amochka/etl.py +0 -0
- {amochka-0.1.8/amochka → amochka-0.3.0}/amochka.egg-info/dependency_links.txt +0 -0
- {amochka-0.1.8 → amochka-0.3.0}/setup.cfg +0 -0
amochka-0.3.0/PKG-INFO
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: amochka
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Python library for working with amoCRM API with ETL capabilities
|
|
5
|
+
Author-email: Timur <timurdt@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/yourusername/amochka
|
|
8
|
+
Project-URL: Documentation, https://github.com/yourusername/amochka
|
|
9
|
+
Project-URL: Repository, https://github.com/yourusername/amochka
|
|
10
|
+
Project-URL: Bug Tracker, https://github.com/yourusername/amochka/issues
|
|
11
|
+
Keywords: amocrm,crm,api,client,automation,etl
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.6
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.7
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
24
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
25
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
26
|
+
Requires-Python: >=3.6
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
Requires-Dist: requests>=2.25.0
|
|
29
|
+
Requires-Dist: ratelimit>=2.2.0
|
|
30
|
+
Requires-Dist: psycopg2-binary>=2.9.0
|
|
31
|
+
Requires-Dist: python-dotenv>=1.0.0
|
|
32
|
+
|
|
33
|
+
# amochka
|
|
34
|
+
|
|
35
|
+
Официальная документация API amocrm - https://www.amocrm.ru/developers/content/crm_platform/api-reference
|
|
36
|
+
|
|
37
|
+
**amochka** — библиотека для работы с API amoCRM на Python. Она поддерживает:
|
|
38
|
+
- Получение данных сделок с вложенными сущностями (контакты, компании, теги, и т.д.)
|
|
39
|
+
- Редактирование сделок, включая обновление стандартных и кастомных полей
|
|
40
|
+
- Поддержку нескольких amoCRM-аккаунтов с персистентным кэшированием кастомных полей для каждого аккаунта отдельно
|
|
41
|
+
- Ограничение запросов (7 запросов в секунду) с использованием декораторов из библиотеки `ratelimit`
|
|
42
|
+
|
|
43
|
+
### Основные функции
|
|
44
|
+
|
|
45
|
+
- `get_deal_by_id(deal_id)` — получение детальной информации по сделке
|
|
46
|
+
- `get_pipelines()` — список воронок и статусов
|
|
47
|
+
- `fetch_updated_leads_raw(pipeline_id, updated_from, ...)` — выгрузка необработанных сделок за период
|
|
48
|
+
|
|
49
|
+
## Требования к окружению
|
|
50
|
+
|
|
51
|
+
Python 3.8 или новее. Потребуются пакеты `requests` и `ratelimit`.
|
|
52
|
+
|
|
53
|
+
## Установка
|
|
54
|
+
|
|
55
|
+
Установите зависимости командой:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
pip install requests ratelimit
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Затем скопируйте репозиторий или установите пакет из PyPI (после публикации):
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
pip install amochka
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Кэширование кастомных полей
|
|
68
|
+
|
|
69
|
+
Для уменьшения количества запросов к API кастомные поля кэшируются персистентно. Если параметр cache_file не указан, имя файла кэша генерируется автоматически на основе домена amoCRM-аккаунта. Вы можете обновлять кэш принудительно, передавая параметр force_update=True в метод get_custom_fields_mapping() или настроить время жизни кэша (по умолчанию — 24 часа).
|
|
70
|
+
|
|
71
|
+
## Выгрузка обновленных сделок
|
|
72
|
+
|
|
73
|
+
Метод `fetch_updated_leads_raw()` позволяет получить все сделки из указанной воронки, которые были изменены в заданный промежуток времени. Результат можно сохранить в JSON-файл без какой‑либо обработки:
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
from datetime import datetime, timedelta
|
|
77
|
+
from amochka import AmoCRMClient, CacheConfig
|
|
78
|
+
|
|
79
|
+
client = AmoCRMClient(
|
|
80
|
+
base_url="https://bneginskogo.amocrm.ru",
|
|
81
|
+
token_file="/path/to/token.json",
|
|
82
|
+
cache_config=CacheConfig.disabled(),
|
|
83
|
+
disable_logging=True
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
three_hours_ago = datetime.utcnow() - timedelta(hours=3)
|
|
87
|
+
client.fetch_updated_leads_raw(6241334, updated_from=three_hours_ago, save_to_file="leads.json")
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Пример получаемого JSON (укороченный):
|
|
91
|
+
|
|
92
|
+
```json
|
|
93
|
+
[
|
|
94
|
+
{
|
|
95
|
+
"id": 26282337,
|
|
96
|
+
"name": "Автосделка: Заявка от (Максим Брокер Дубай Бюро Негинского)",
|
|
97
|
+
"custom_fields_values": [
|
|
98
|
+
{
|
|
99
|
+
"field_name": "roistat",
|
|
100
|
+
"values": [{"value": "2026"}]
|
|
101
|
+
}
|
|
102
|
+
],
|
|
103
|
+
"_embedded": {
|
|
104
|
+
"tags": [
|
|
105
|
+
{"id": 179813, "name": "WZ (Федор 971568113315)"}
|
|
106
|
+
]
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
]
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Для подключения к реальному аккаунту сохраните JSON с OAuth‑токеном и укажите его путь в параметре `token_file` при создании клиента. Базовый URL можно взять из переменной окружения `AMO_BASE_URL`.
|
|
113
|
+
|
|
114
|
+
## Тесты
|
|
115
|
+
|
|
116
|
+
Файл `tests/test_client.py` содержит небольшой набор автоматических тестов, написанных на [pytest](https://docs.pytest.org/). Они запускают методы клиента на подставном классе `DummyClient` и проверяют, что функции работают так, как ожидается. Запустить тесты можно командой:
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
pytest -q
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Эти тесты помогают убедиться, что изменения в коде не ломают основную функциональность.
|
|
123
|
+
|
|
124
|
+
## Пример использования `fetch_updated_leads_raw`
|
|
125
|
+
|
|
126
|
+
Кроме примера в разделе выше, код из `example_fetch.py` демонстрирует полный процесс получения сделок и сохранения их в файл.
|
amochka-0.3.0/README.md
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# amochka
|
|
2
|
+
|
|
3
|
+
Официальная документация API amocrm - https://www.amocrm.ru/developers/content/crm_platform/api-reference
|
|
4
|
+
|
|
5
|
+
**amochka** — библиотека для работы с API amoCRM на Python. Она поддерживает:
|
|
6
|
+
- Получение данных сделок с вложенными сущностями (контакты, компании, теги, и т.д.)
|
|
7
|
+
- Редактирование сделок, включая обновление стандартных и кастомных полей
|
|
8
|
+
- Поддержку нескольких amoCRM-аккаунтов с персистентным кэшированием кастомных полей для каждого аккаунта отдельно
|
|
9
|
+
- Ограничение запросов (7 запросов в секунду) с использованием декораторов из библиотеки `ratelimit`
|
|
10
|
+
|
|
11
|
+
### Основные функции
|
|
12
|
+
|
|
13
|
+
- `get_deal_by_id(deal_id)` — получение детальной информации по сделке
|
|
14
|
+
- `get_pipelines()` — список воронок и статусов
|
|
15
|
+
- `fetch_updated_leads_raw(pipeline_id, updated_from, ...)` — выгрузка необработанных сделок за период
|
|
16
|
+
|
|
17
|
+
## Требования к окружению
|
|
18
|
+
|
|
19
|
+
Python 3.8 или новее. Потребуются пакеты `requests` и `ratelimit`.
|
|
20
|
+
|
|
21
|
+
## Установка
|
|
22
|
+
|
|
23
|
+
Установите зависимости командой:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pip install requests ratelimit
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Затем скопируйте репозиторий или установите пакет из PyPI (после публикации):
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install amochka
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Кэширование кастомных полей
|
|
36
|
+
|
|
37
|
+
Для уменьшения количества запросов к API кастомные поля кэшируются персистентно. Если параметр cache_file не указан, имя файла кэша генерируется автоматически на основе домена amoCRM-аккаунта. Вы можете обновлять кэш принудительно, передавая параметр force_update=True в метод get_custom_fields_mapping() или настроить время жизни кэша (по умолчанию — 24 часа).
|
|
38
|
+
|
|
39
|
+
## Выгрузка обновленных сделок
|
|
40
|
+
|
|
41
|
+
Метод `fetch_updated_leads_raw()` позволяет получить все сделки из указанной воронки, которые были изменены в заданный промежуток времени. Результат можно сохранить в JSON-файл без какой‑либо обработки:
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from datetime import datetime, timedelta
|
|
45
|
+
from amochka import AmoCRMClient, CacheConfig
|
|
46
|
+
|
|
47
|
+
client = AmoCRMClient(
|
|
48
|
+
base_url="https://bneginskogo.amocrm.ru",
|
|
49
|
+
token_file="/path/to/token.json",
|
|
50
|
+
cache_config=CacheConfig.disabled(),
|
|
51
|
+
disable_logging=True
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
three_hours_ago = datetime.utcnow() - timedelta(hours=3)
|
|
55
|
+
client.fetch_updated_leads_raw(6241334, updated_from=three_hours_ago, save_to_file="leads.json")
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Пример получаемого JSON (укороченный):
|
|
59
|
+
|
|
60
|
+
```json
|
|
61
|
+
[
|
|
62
|
+
{
|
|
63
|
+
"id": 26282337,
|
|
64
|
+
"name": "Автосделка: Заявка от (Максим Брокер Дубай Бюро Негинского)",
|
|
65
|
+
"custom_fields_values": [
|
|
66
|
+
{
|
|
67
|
+
"field_name": "roistat",
|
|
68
|
+
"values": [{"value": "2026"}]
|
|
69
|
+
}
|
|
70
|
+
],
|
|
71
|
+
"_embedded": {
|
|
72
|
+
"tags": [
|
|
73
|
+
{"id": 179813, "name": "WZ (Федор 971568113315)"}
|
|
74
|
+
]
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
]
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Для подключения к реальному аккаунту сохраните JSON с OAuth‑токеном и укажите его путь в параметре `token_file` при создании клиента. Базовый URL можно взять из переменной окружения `AMO_BASE_URL`.
|
|
81
|
+
|
|
82
|
+
## Тесты
|
|
83
|
+
|
|
84
|
+
Файл `tests/test_client.py` содержит небольшой набор автоматических тестов, написанных на [pytest](https://docs.pytest.org/). Они запускают методы клиента на подставном классе `DummyClient` и проверяют, что функции работают так, как ожидается. Запустить тесты можно командой:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
pytest -q
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Эти тесты помогают убедиться, что изменения в коде не ломают основную функциональность.
|
|
91
|
+
|
|
92
|
+
## Пример использования `fetch_updated_leads_raw`
|
|
93
|
+
|
|
94
|
+
Кроме примера в разделе выше, код из `example_fetch.py` демонстрирует полный процесс получения сделок и сохранения их в файл.
|
|
@@ -174,33 +174,77 @@ class Deal(dict):
|
|
|
174
174
|
class CacheConfig:
|
|
175
175
|
"""
|
|
176
176
|
Конфигурация кэширования для AmoCRMClient.
|
|
177
|
-
|
|
177
|
+
|
|
178
178
|
Параметры:
|
|
179
179
|
enabled (bool): Включено ли кэширование
|
|
180
180
|
storage (str): Тип хранилища ('file' или 'memory')
|
|
181
|
-
|
|
182
|
-
|
|
181
|
+
base_dir (str): Базовая директория для кэша (по умолчанию ~/.amocrm/cache/)
|
|
182
|
+
file (str): Путь к файлу кэша (устаревший, для обратной совместимости)
|
|
183
|
+
lifetime_hours (int|dict|None): Время жизни кэша в часах
|
|
184
|
+
- int: одинаковое время для всех типов данных
|
|
185
|
+
- dict: разное время для каждого типа (например, {'pipelines': 168, 'users': 24})
|
|
186
|
+
- None: бесконечный кэш
|
|
183
187
|
"""
|
|
184
|
-
|
|
188
|
+
DEFAULT_LIFETIMES = {
|
|
189
|
+
'custom_fields': 24,
|
|
190
|
+
'pipelines': 168, # 7 дней
|
|
191
|
+
'users': 24,
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
def __init__(self, enabled=True, storage='file', base_dir=None, file=None, lifetime_hours='default'):
|
|
185
195
|
self.enabled = enabled
|
|
186
196
|
self.storage = storage.lower()
|
|
187
|
-
self.
|
|
188
|
-
self.
|
|
189
|
-
|
|
197
|
+
self.base_dir = base_dir or os.path.join(os.path.expanduser('~'), '.amocrm', 'cache')
|
|
198
|
+
self.file = file # Для обратной совместимости с custom fields
|
|
199
|
+
|
|
200
|
+
# Обработка lifetime_hours: может быть int, dict, None, или 'default'
|
|
201
|
+
if lifetime_hours == 'default':
|
|
202
|
+
# Используем дефолтные значения
|
|
203
|
+
self.lifetime_hours = self.DEFAULT_LIFETIMES.copy()
|
|
204
|
+
elif isinstance(lifetime_hours, dict):
|
|
205
|
+
# Объединяем дефолтные значения с пользовательскими
|
|
206
|
+
self.lifetime_hours = {**self.DEFAULT_LIFETIMES, **lifetime_hours}
|
|
207
|
+
elif lifetime_hours is None:
|
|
208
|
+
# Бесконечный кэш для всех типов
|
|
209
|
+
self.lifetime_hours = None
|
|
210
|
+
elif isinstance(lifetime_hours, (int, float)):
|
|
211
|
+
# Одинаковое время для всех типов
|
|
212
|
+
self.lifetime_hours = {
|
|
213
|
+
'custom_fields': lifetime_hours,
|
|
214
|
+
'pipelines': lifetime_hours,
|
|
215
|
+
'users': lifetime_hours,
|
|
216
|
+
}
|
|
217
|
+
else:
|
|
218
|
+
# Fallback на дефолтные значения
|
|
219
|
+
self.lifetime_hours = self.DEFAULT_LIFETIMES.copy()
|
|
220
|
+
|
|
221
|
+
def get_lifetime(self, data_type):
|
|
222
|
+
"""
|
|
223
|
+
Получает время жизни кэша для указанного типа данных.
|
|
224
|
+
|
|
225
|
+
:param data_type: Тип данных ('custom_fields', 'pipelines', 'users')
|
|
226
|
+
:return: Время жизни в часах или None для бесконечного кэша
|
|
227
|
+
"""
|
|
228
|
+
if self.lifetime_hours is None:
|
|
229
|
+
return None
|
|
230
|
+
if isinstance(self.lifetime_hours, dict):
|
|
231
|
+
return self.lifetime_hours.get(data_type, 24)
|
|
232
|
+
return self.lifetime_hours
|
|
233
|
+
|
|
190
234
|
@classmethod
|
|
191
235
|
def disabled(cls):
|
|
192
236
|
"""Создает конфигурацию с отключенным кэшированием"""
|
|
193
237
|
return cls(enabled=False)
|
|
194
|
-
|
|
238
|
+
|
|
195
239
|
@classmethod
|
|
196
240
|
def memory_only(cls, lifetime_hours=24):
|
|
197
241
|
"""Создает конфигурацию с кэшированием только в памяти"""
|
|
198
242
|
return cls(enabled=True, storage='memory', lifetime_hours=lifetime_hours)
|
|
199
|
-
|
|
243
|
+
|
|
200
244
|
@classmethod
|
|
201
|
-
def file_cache(cls, file=None, lifetime_hours=
|
|
245
|
+
def file_cache(cls, file=None, base_dir=None, lifetime_hours='default'):
|
|
202
246
|
"""Создает конфигурацию с файловым кэшированием"""
|
|
203
|
-
return cls(enabled=True, storage='file', file=file, lifetime_hours=lifetime_hours)
|
|
247
|
+
return cls(enabled=True, storage='file', base_dir=base_dir, file=file, lifetime_hours=lifetime_hours)
|
|
204
248
|
|
|
205
249
|
class AmoCRMClient:
|
|
206
250
|
"""
|
|
@@ -274,12 +318,16 @@ class AmoCRMClient:
|
|
|
274
318
|
self.cache_config.file = f"custom_fields_cache_{self.domain}.json"
|
|
275
319
|
|
|
276
320
|
self.logger.debug(f"AmoCRMClient initialized for domain {self.domain}")
|
|
277
|
-
|
|
321
|
+
|
|
278
322
|
self.token = None
|
|
279
323
|
self.refresh_token = None
|
|
280
324
|
self.expires_at = None
|
|
281
325
|
self.load_token()
|
|
326
|
+
|
|
327
|
+
# Memory caches для разных типов данных
|
|
282
328
|
self._custom_fields_mapping = None
|
|
329
|
+
self._pipelines_cache = None
|
|
330
|
+
self._users_cache = None
|
|
283
331
|
|
|
284
332
|
def load_token(self):
|
|
285
333
|
"""
|
|
@@ -416,6 +464,133 @@ class AmoCRMClient:
|
|
|
416
464
|
|
|
417
465
|
return access_token
|
|
418
466
|
|
|
467
|
+
def _extract_account_name(self):
|
|
468
|
+
"""
|
|
469
|
+
Извлекает имя аккаунта из пути к файлу токена.
|
|
470
|
+
|
|
471
|
+
Примеры:
|
|
472
|
+
~/.amocrm/accounts/bneginskogo.json -> default
|
|
473
|
+
~/.amocrm/accounts/bneginskogo_eng.json -> eng
|
|
474
|
+
~/.amocrm/accounts/bneginskogo_thai.json -> thai
|
|
475
|
+
|
|
476
|
+
:return: Имя аккаунта или 'default'
|
|
477
|
+
"""
|
|
478
|
+
if not self.token_file:
|
|
479
|
+
return 'default'
|
|
480
|
+
|
|
481
|
+
# Получаем имя файла без расширения
|
|
482
|
+
filename = os.path.splitext(os.path.basename(self.token_file))[0]
|
|
483
|
+
|
|
484
|
+
# Проверяем паттерн: base_name или base_name_account
|
|
485
|
+
parts = filename.split('_')
|
|
486
|
+
if len(parts) > 1:
|
|
487
|
+
# Последняя часть - это имя аккаунта (eng, thai и т.д.)
|
|
488
|
+
return parts[-1]
|
|
489
|
+
|
|
490
|
+
return 'default'
|
|
491
|
+
|
|
492
|
+
def _get_cache_file_path(self, data_type):
|
|
493
|
+
"""
|
|
494
|
+
Получает путь к файлу кэша для указанного типа данных.
|
|
495
|
+
|
|
496
|
+
:param data_type: Тип данных ('custom_fields', 'pipelines', 'users')
|
|
497
|
+
:return: Путь к файлу кэша
|
|
498
|
+
"""
|
|
499
|
+
# Для custom_fields используем старый путь, если указан (обратная совместимость)
|
|
500
|
+
if data_type == 'custom_fields' and self.cache_config.file:
|
|
501
|
+
return self.cache_config.file
|
|
502
|
+
|
|
503
|
+
# Создаем директорию кэша, если не существует
|
|
504
|
+
os.makedirs(self.cache_config.base_dir, exist_ok=True)
|
|
505
|
+
|
|
506
|
+
# Формируем имя файла: {account}_{data_type}.json
|
|
507
|
+
account_name = self._extract_account_name()
|
|
508
|
+
cache_filename = f"{account_name}_{data_type}.json"
|
|
509
|
+
return os.path.join(self.cache_config.base_dir, cache_filename)
|
|
510
|
+
|
|
511
|
+
def _is_cache_valid(self, data_type, last_updated):
|
|
512
|
+
"""
|
|
513
|
+
Проверяет, валиден ли кэш на основе времени последнего обновления.
|
|
514
|
+
|
|
515
|
+
:param data_type: Тип данных ('custom_fields', 'pipelines', 'users')
|
|
516
|
+
:param last_updated: Время последнего обновления (timestamp)
|
|
517
|
+
:return: True если кэш валиден, False если устарел
|
|
518
|
+
"""
|
|
519
|
+
lifetime = self.cache_config.get_lifetime(data_type)
|
|
520
|
+
|
|
521
|
+
if lifetime is None:
|
|
522
|
+
# Бесконечный кэш
|
|
523
|
+
return True
|
|
524
|
+
|
|
525
|
+
# Проверяем срок жизни
|
|
526
|
+
return time.time() - last_updated < lifetime * 3600
|
|
527
|
+
|
|
528
|
+
def _save_cache(self, data_type, data):
|
|
529
|
+
"""
|
|
530
|
+
Сохраняет данные в кэш.
|
|
531
|
+
|
|
532
|
+
:param data_type: Тип данных ('custom_fields', 'pipelines', 'users')
|
|
533
|
+
:param data: Данные для сохранения
|
|
534
|
+
"""
|
|
535
|
+
if not self.cache_config.enabled:
|
|
536
|
+
self.logger.debug(f"Caching disabled; {data_type} cache not saved.")
|
|
537
|
+
return
|
|
538
|
+
|
|
539
|
+
if self.cache_config.storage != 'file':
|
|
540
|
+
self.logger.debug(f"Using memory caching; {data_type} cache not saved to file.")
|
|
541
|
+
return
|
|
542
|
+
|
|
543
|
+
cache_file = self._get_cache_file_path(data_type)
|
|
544
|
+
cache_data = {
|
|
545
|
+
"last_updated": time.time(),
|
|
546
|
+
"data": data
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
try:
|
|
550
|
+
with open(cache_file, "w", encoding="utf-8") as f:
|
|
551
|
+
json.dump(cache_data, f, ensure_ascii=False, indent=2)
|
|
552
|
+
self.logger.debug(f"{data_type} cache saved to {cache_file}")
|
|
553
|
+
except Exception as e:
|
|
554
|
+
self.logger.error(f"Failed to save {data_type} cache: {e}")
|
|
555
|
+
|
|
556
|
+
def _load_cache(self, data_type):
|
|
557
|
+
"""
|
|
558
|
+
Загружает данные из кэша.
|
|
559
|
+
|
|
560
|
+
:param data_type: Тип данных ('custom_fields', 'pipelines', 'users')
|
|
561
|
+
:return: Кэшированные данные или None
|
|
562
|
+
"""
|
|
563
|
+
if not self.cache_config.enabled:
|
|
564
|
+
self.logger.debug(f"Caching disabled; no {data_type} cache loaded.")
|
|
565
|
+
return None
|
|
566
|
+
|
|
567
|
+
if self.cache_config.storage != 'file':
|
|
568
|
+
self.logger.debug(f"Using memory caching; {data_type} cache kept in memory only.")
|
|
569
|
+
return None
|
|
570
|
+
|
|
571
|
+
cache_file = self._get_cache_file_path(data_type)
|
|
572
|
+
|
|
573
|
+
if not os.path.exists(cache_file):
|
|
574
|
+
self.logger.debug(f"{data_type} cache file not found: {cache_file}")
|
|
575
|
+
return None
|
|
576
|
+
|
|
577
|
+
try:
|
|
578
|
+
with open(cache_file, "r", encoding="utf-8") as f:
|
|
579
|
+
cache_data = json.load(f)
|
|
580
|
+
|
|
581
|
+
last_updated = cache_data.get("last_updated", 0)
|
|
582
|
+
|
|
583
|
+
if not self._is_cache_valid(data_type, last_updated):
|
|
584
|
+
self.logger.debug(f"{data_type} cache expired.")
|
|
585
|
+
return None
|
|
586
|
+
|
|
587
|
+
self.logger.debug(f"{data_type} cache loaded from {cache_file}")
|
|
588
|
+
return cache_data.get("data")
|
|
589
|
+
|
|
590
|
+
except Exception as e:
|
|
591
|
+
self.logger.error(f"Error loading {data_type} cache: {e}")
|
|
592
|
+
return None
|
|
593
|
+
|
|
419
594
|
def _to_timestamp(self, value: Optional[Union[int, float, str, datetime]]) -> Optional[int]:
|
|
420
595
|
"""
|
|
421
596
|
Преобразует значение даты/времени в Unix timestamp.
|
|
@@ -701,6 +876,45 @@ class AmoCRMClient:
|
|
|
701
876
|
"""
|
|
702
877
|
return list(self.iter_users(*args, **kwargs))
|
|
703
878
|
|
|
879
|
+
def get_users_cached(self, force_update=False):
|
|
880
|
+
"""
|
|
881
|
+
Возвращает список пользователей с кэшированием (по умолчанию 24 часа).
|
|
882
|
+
|
|
883
|
+
Использует трехуровневое кэширование:
|
|
884
|
+
1. Memory cache (самый быстрый)
|
|
885
|
+
2. File cache (персистентный)
|
|
886
|
+
3. API request (если кэш устарел или отсутствует)
|
|
887
|
+
|
|
888
|
+
:param force_update: Если True, игнорирует кэш и загружает данные из API
|
|
889
|
+
:return: Список пользователей
|
|
890
|
+
"""
|
|
891
|
+
# 1. Проверяем memory cache
|
|
892
|
+
if not force_update and self._users_cache is not None:
|
|
893
|
+
self.logger.debug("Using memory-cached users.")
|
|
894
|
+
return self._users_cache
|
|
895
|
+
|
|
896
|
+
# 2. Проверяем file cache
|
|
897
|
+
if not force_update and self.cache_config.enabled:
|
|
898
|
+
cached_data = self._load_cache('users')
|
|
899
|
+
if cached_data is not None:
|
|
900
|
+
self._users_cache = cached_data
|
|
901
|
+
self.logger.debug("Users loaded from file cache.")
|
|
902
|
+
return cached_data
|
|
903
|
+
|
|
904
|
+
# 3. Загружаем из API
|
|
905
|
+
self.logger.debug("Fetching users from API...")
|
|
906
|
+
users = self.fetch_users()
|
|
907
|
+
|
|
908
|
+
# Сохраняем в memory cache
|
|
909
|
+
self._users_cache = users
|
|
910
|
+
|
|
911
|
+
# Сохраняем в file cache
|
|
912
|
+
if self.cache_config.enabled:
|
|
913
|
+
self._save_cache('users', users)
|
|
914
|
+
|
|
915
|
+
self.logger.debug(f"Fetched {len(users)} users from API.")
|
|
916
|
+
return users
|
|
917
|
+
|
|
704
918
|
def iter_pipelines(
|
|
705
919
|
self,
|
|
706
920
|
limit: int = 250,
|
|
@@ -785,29 +999,46 @@ class AmoCRMClient:
|
|
|
785
999
|
|
|
786
1000
|
def get_custom_fields_mapping(self, force_update=False):
|
|
787
1001
|
"""
|
|
788
|
-
Возвращает словарь отображения кастомных полей для
|
|
789
|
-
|
|
1002
|
+
Возвращает словарь отображения кастомных полей для сделок с кэшированием (по умолчанию 24 часа).
|
|
1003
|
+
|
|
1004
|
+
Использует трехуровневое кэширование:
|
|
1005
|
+
1. Memory cache (самый быстрый)
|
|
1006
|
+
2. File cache (персистентный)
|
|
1007
|
+
3. API request (если кэш устарел или отсутствует)
|
|
1008
|
+
|
|
1009
|
+
:param force_update: Если True, игнорирует кэш и загружает данные из API
|
|
1010
|
+
:return: Словарь с кастомными полями (ключ - field_id, значение - объект поля)
|
|
790
1011
|
"""
|
|
1012
|
+
# 1. Проверяем memory cache
|
|
791
1013
|
if not force_update and self._custom_fields_mapping is not None:
|
|
1014
|
+
self.logger.debug("Using memory-cached custom fields mapping.")
|
|
792
1015
|
return self._custom_fields_mapping
|
|
793
1016
|
|
|
794
|
-
|
|
795
|
-
if
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
1017
|
+
# 2. Проверяем file cache (с поддержкой старого формата)
|
|
1018
|
+
if not force_update and self.cache_config.enabled:
|
|
1019
|
+
# Пробуем новый формат
|
|
1020
|
+
cached_data = self._load_cache('custom_fields')
|
|
1021
|
+
if cached_data is not None:
|
|
1022
|
+
self._custom_fields_mapping = cached_data
|
|
1023
|
+
self.logger.debug("Custom fields loaded from file cache (new format).")
|
|
1024
|
+
return cached_data
|
|
1025
|
+
|
|
1026
|
+
# Пробуем старый формат для обратной совместимости
|
|
1027
|
+
legacy_cache = self._load_custom_fields_cache()
|
|
1028
|
+
if legacy_cache:
|
|
1029
|
+
mapping = legacy_cache.get("mapping")
|
|
1030
|
+
if mapping:
|
|
1031
|
+
self._custom_fields_mapping = mapping
|
|
1032
|
+
self.logger.debug("Custom fields loaded from legacy cache format.")
|
|
1033
|
+
# Мигрируем в новый формат
|
|
1034
|
+
self._save_cache('custom_fields', mapping)
|
|
1035
|
+
return mapping
|
|
1036
|
+
|
|
1037
|
+
# 3. Загружаем из API
|
|
1038
|
+
self.logger.debug("Fetching custom fields from API...")
|
|
808
1039
|
mapping = {}
|
|
809
1040
|
page = 1
|
|
810
|
-
total_pages = 1
|
|
1041
|
+
total_pages = 1
|
|
811
1042
|
while page <= total_pages:
|
|
812
1043
|
endpoint = f"/api/v4/leads/custom_fields?limit=250&page={page}"
|
|
813
1044
|
response = self._make_request("GET", endpoint)
|
|
@@ -820,10 +1051,14 @@ class AmoCRMClient:
|
|
|
820
1051
|
else:
|
|
821
1052
|
break
|
|
822
1053
|
|
|
823
|
-
|
|
1054
|
+
# Сохраняем в memory cache
|
|
824
1055
|
self._custom_fields_mapping = mapping
|
|
1056
|
+
|
|
1057
|
+
# Сохраняем в file cache (новый формат)
|
|
825
1058
|
if self.cache_config.enabled:
|
|
826
|
-
self.
|
|
1059
|
+
self._save_cache('custom_fields', mapping)
|
|
1060
|
+
|
|
1061
|
+
self.logger.debug(f"Fetched {len(mapping)} custom fields from API.")
|
|
827
1062
|
return mapping
|
|
828
1063
|
|
|
829
1064
|
def find_custom_field_id(self, search_term):
|
|
@@ -1099,3 +1334,42 @@ class AmoCRMClient:
|
|
|
1099
1334
|
return pipelines
|
|
1100
1335
|
self.logger.error("Не удалось получить воронки из amoCRM")
|
|
1101
1336
|
raise Exception("Ошибка получения воронок из amoCRM")
|
|
1337
|
+
|
|
1338
|
+
def get_pipelines_cached(self, force_update=False):
|
|
1339
|
+
"""
|
|
1340
|
+
Возвращает список воронок с кэшированием (по умолчанию 7 дней).
|
|
1341
|
+
|
|
1342
|
+
Использует трехуровневое кэширование:
|
|
1343
|
+
1. Memory cache (самый быстрый)
|
|
1344
|
+
2. File cache (персистентный)
|
|
1345
|
+
3. API request (если кэш устарел или отсутствует)
|
|
1346
|
+
|
|
1347
|
+
:param force_update: Если True, игнорирует кэш и загружает данные из API
|
|
1348
|
+
:return: Список воронок со статусами
|
|
1349
|
+
"""
|
|
1350
|
+
# 1. Проверяем memory cache
|
|
1351
|
+
if not force_update and self._pipelines_cache is not None:
|
|
1352
|
+
self.logger.debug("Using memory-cached pipelines.")
|
|
1353
|
+
return self._pipelines_cache
|
|
1354
|
+
|
|
1355
|
+
# 2. Проверяем file cache
|
|
1356
|
+
if not force_update and self.cache_config.enabled:
|
|
1357
|
+
cached_data = self._load_cache('pipelines')
|
|
1358
|
+
if cached_data is not None:
|
|
1359
|
+
self._pipelines_cache = cached_data
|
|
1360
|
+
self.logger.debug("Pipelines loaded from file cache.")
|
|
1361
|
+
return cached_data
|
|
1362
|
+
|
|
1363
|
+
# 3. Загружаем из API
|
|
1364
|
+
self.logger.debug("Fetching pipelines from API...")
|
|
1365
|
+
pipelines = self.fetch_pipelines()
|
|
1366
|
+
|
|
1367
|
+
# Сохраняем в memory cache
|
|
1368
|
+
self._pipelines_cache = pipelines
|
|
1369
|
+
|
|
1370
|
+
# Сохраняем в file cache
|
|
1371
|
+
if self.cache_config.enabled:
|
|
1372
|
+
self._save_cache('pipelines', pipelines)
|
|
1373
|
+
|
|
1374
|
+
self.logger.debug(f"Fetched {len(pipelines)} pipelines from API.")
|
|
1375
|
+
return pipelines
|