amochka 0.1.7__tar.gz → 0.1.9__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.9/PKG-INFO +124 -0
- amochka-0.1.9/README.md +94 -0
- amochka-0.1.9/amochka/amochka.egg-info/PKG-INFO +124 -0
- {amochka-0.1.7 → amochka-0.1.9}/amochka/amochka.egg-info/SOURCES.txt +3 -6
- amochka-0.1.9/amochka/amochka.egg-info/requires.txt +2 -0
- amochka-0.1.9/amochka/amochka.egg-info/top_level.txt +1 -0
- amochka-0.1.9/pyproject.toml +47 -0
- amochka-0.1.9/tests/test_client.py +259 -0
- amochka-0.1.7/MANIFEST.in +0 -12
- amochka-0.1.7/PKG-INFO +0 -40
- amochka-0.1.7/README.md +0 -17
- amochka-0.1.7/amochka/amochka/__init__.py +0 -28
- amochka-0.1.7/amochka/amochka/client.py +0 -1013
- amochka-0.1.7/amochka/amochka/etl.py +0 -302
- amochka-0.1.7/amochka/amochka.egg-info/PKG-INFO +0 -40
- amochka-0.1.7/amochka/amochka.egg-info/requires.txt +0 -2
- amochka-0.1.7/amochka/amochka.egg-info/top_level.txt +0 -1
- amochka-0.1.7/setup.py +0 -27
- {amochka-0.1.7 → amochka-0.1.9}/amochka/amochka.egg-info/dependency_links.txt +0 -0
- {amochka-0.1.7 → amochka-0.1.9}/setup.cfg +0 -0
amochka-0.1.9/PKG-INFO
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: amochka
|
|
3
|
+
Version: 0.1.9
|
|
4
|
+
Summary: Python library for working with amoCRM API
|
|
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
|
|
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
|
+
|
|
31
|
+
# amochka
|
|
32
|
+
|
|
33
|
+
Официальная документация API amocrm - https://www.amocrm.ru/developers/content/crm_platform/api-reference
|
|
34
|
+
|
|
35
|
+
**amochka** — библиотека для работы с API amoCRM на Python. Она поддерживает:
|
|
36
|
+
- Получение данных сделок с вложенными сущностями (контакты, компании, теги, и т.д.)
|
|
37
|
+
- Редактирование сделок, включая обновление стандартных и кастомных полей
|
|
38
|
+
- Поддержку нескольких amoCRM-аккаунтов с персистентным кэшированием кастомных полей для каждого аккаунта отдельно
|
|
39
|
+
- Ограничение запросов (7 запросов в секунду) с использованием декораторов из библиотеки `ratelimit`
|
|
40
|
+
|
|
41
|
+
### Основные функции
|
|
42
|
+
|
|
43
|
+
- `get_deal_by_id(deal_id)` — получение детальной информации по сделке
|
|
44
|
+
- `get_pipelines()` — список воронок и статусов
|
|
45
|
+
- `fetch_updated_leads_raw(pipeline_id, updated_from, ...)` — выгрузка необработанных сделок за период
|
|
46
|
+
|
|
47
|
+
## Требования к окружению
|
|
48
|
+
|
|
49
|
+
Python 3.8 или новее. Потребуются пакеты `requests` и `ratelimit`.
|
|
50
|
+
|
|
51
|
+
## Установка
|
|
52
|
+
|
|
53
|
+
Установите зависимости командой:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
pip install requests ratelimit
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Затем скопируйте репозиторий или установите пакет из PyPI (после публикации):
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
pip install amochka
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Кэширование кастомных полей
|
|
66
|
+
|
|
67
|
+
Для уменьшения количества запросов к API кастомные поля кэшируются персистентно. Если параметр cache_file не указан, имя файла кэша генерируется автоматически на основе домена amoCRM-аккаунта. Вы можете обновлять кэш принудительно, передавая параметр force_update=True в метод get_custom_fields_mapping() или настроить время жизни кэша (по умолчанию — 24 часа).
|
|
68
|
+
|
|
69
|
+
## Выгрузка обновленных сделок
|
|
70
|
+
|
|
71
|
+
Метод `fetch_updated_leads_raw()` позволяет получить все сделки из указанной воронки, которые были изменены в заданный промежуток времени. Результат можно сохранить в JSON-файл без какой‑либо обработки:
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
from datetime import datetime, timedelta
|
|
75
|
+
from amochka import AmoCRMClient, CacheConfig
|
|
76
|
+
|
|
77
|
+
client = AmoCRMClient(
|
|
78
|
+
base_url="https://bneginskogo.amocrm.ru",
|
|
79
|
+
token_file="/path/to/token.json",
|
|
80
|
+
cache_config=CacheConfig.disabled(),
|
|
81
|
+
disable_logging=True
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
three_hours_ago = datetime.utcnow() - timedelta(hours=3)
|
|
85
|
+
client.fetch_updated_leads_raw(6241334, updated_from=three_hours_ago, save_to_file="leads.json")
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Пример получаемого JSON (укороченный):
|
|
89
|
+
|
|
90
|
+
```json
|
|
91
|
+
[
|
|
92
|
+
{
|
|
93
|
+
"id": 26282337,
|
|
94
|
+
"name": "Автосделка: Заявка от (Максим Брокер Дубай Бюро Негинского)",
|
|
95
|
+
"custom_fields_values": [
|
|
96
|
+
{
|
|
97
|
+
"field_name": "roistat",
|
|
98
|
+
"values": [{"value": "2026"}]
|
|
99
|
+
}
|
|
100
|
+
],
|
|
101
|
+
"_embedded": {
|
|
102
|
+
"tags": [
|
|
103
|
+
{"id": 179813, "name": "WZ (Федор 971568113315)"}
|
|
104
|
+
]
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
]
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Для подключения к реальному аккаунту сохраните JSON с OAuth‑токеном и укажите его путь в параметре `token_file` при создании клиента. Базовый URL можно взять из переменной окружения `AMO_BASE_URL`.
|
|
111
|
+
|
|
112
|
+
## Тесты
|
|
113
|
+
|
|
114
|
+
Файл `tests/test_client.py` содержит небольшой набор автоматических тестов, написанных на [pytest](https://docs.pytest.org/). Они запускают методы клиента на подставном классе `DummyClient` и проверяют, что функции работают так, как ожидается. Запустить тесты можно командой:
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
pytest -q
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Эти тесты помогают убедиться, что изменения в коде не ломают основную функциональность.
|
|
121
|
+
|
|
122
|
+
## Пример использования `fetch_updated_leads_raw`
|
|
123
|
+
|
|
124
|
+
Кроме примера в разделе выше, код из `example_fetch.py` демонстрирует полный процесс получения сделок и сохранения их в файл.
|
amochka-0.1.9/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` демонстрирует полный процесс получения сделок и сохранения их в файл.
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: amochka
|
|
3
|
+
Version: 0.1.9
|
|
4
|
+
Summary: Python library for working with amoCRM API
|
|
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
|
|
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
|
+
|
|
31
|
+
# amochka
|
|
32
|
+
|
|
33
|
+
Официальная документация API amocrm - https://www.amocrm.ru/developers/content/crm_platform/api-reference
|
|
34
|
+
|
|
35
|
+
**amochka** — библиотека для работы с API amoCRM на Python. Она поддерживает:
|
|
36
|
+
- Получение данных сделок с вложенными сущностями (контакты, компании, теги, и т.д.)
|
|
37
|
+
- Редактирование сделок, включая обновление стандартных и кастомных полей
|
|
38
|
+
- Поддержку нескольких amoCRM-аккаунтов с персистентным кэшированием кастомных полей для каждого аккаунта отдельно
|
|
39
|
+
- Ограничение запросов (7 запросов в секунду) с использованием декораторов из библиотеки `ratelimit`
|
|
40
|
+
|
|
41
|
+
### Основные функции
|
|
42
|
+
|
|
43
|
+
- `get_deal_by_id(deal_id)` — получение детальной информации по сделке
|
|
44
|
+
- `get_pipelines()` — список воронок и статусов
|
|
45
|
+
- `fetch_updated_leads_raw(pipeline_id, updated_from, ...)` — выгрузка необработанных сделок за период
|
|
46
|
+
|
|
47
|
+
## Требования к окружению
|
|
48
|
+
|
|
49
|
+
Python 3.8 или новее. Потребуются пакеты `requests` и `ratelimit`.
|
|
50
|
+
|
|
51
|
+
## Установка
|
|
52
|
+
|
|
53
|
+
Установите зависимости командой:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
pip install requests ratelimit
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Затем скопируйте репозиторий или установите пакет из PyPI (после публикации):
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
pip install amochka
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Кэширование кастомных полей
|
|
66
|
+
|
|
67
|
+
Для уменьшения количества запросов к API кастомные поля кэшируются персистентно. Если параметр cache_file не указан, имя файла кэша генерируется автоматически на основе домена amoCRM-аккаунта. Вы можете обновлять кэш принудительно, передавая параметр force_update=True в метод get_custom_fields_mapping() или настроить время жизни кэша (по умолчанию — 24 часа).
|
|
68
|
+
|
|
69
|
+
## Выгрузка обновленных сделок
|
|
70
|
+
|
|
71
|
+
Метод `fetch_updated_leads_raw()` позволяет получить все сделки из указанной воронки, которые были изменены в заданный промежуток времени. Результат можно сохранить в JSON-файл без какой‑либо обработки:
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
from datetime import datetime, timedelta
|
|
75
|
+
from amochka import AmoCRMClient, CacheConfig
|
|
76
|
+
|
|
77
|
+
client = AmoCRMClient(
|
|
78
|
+
base_url="https://bneginskogo.amocrm.ru",
|
|
79
|
+
token_file="/path/to/token.json",
|
|
80
|
+
cache_config=CacheConfig.disabled(),
|
|
81
|
+
disable_logging=True
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
three_hours_ago = datetime.utcnow() - timedelta(hours=3)
|
|
85
|
+
client.fetch_updated_leads_raw(6241334, updated_from=three_hours_ago, save_to_file="leads.json")
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Пример получаемого JSON (укороченный):
|
|
89
|
+
|
|
90
|
+
```json
|
|
91
|
+
[
|
|
92
|
+
{
|
|
93
|
+
"id": 26282337,
|
|
94
|
+
"name": "Автосделка: Заявка от (Максим Брокер Дубай Бюро Негинского)",
|
|
95
|
+
"custom_fields_values": [
|
|
96
|
+
{
|
|
97
|
+
"field_name": "roistat",
|
|
98
|
+
"values": [{"value": "2026"}]
|
|
99
|
+
}
|
|
100
|
+
],
|
|
101
|
+
"_embedded": {
|
|
102
|
+
"tags": [
|
|
103
|
+
{"id": 179813, "name": "WZ (Федор 971568113315)"}
|
|
104
|
+
]
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
]
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Для подключения к реальному аккаунту сохраните JSON с OAuth‑токеном и укажите его путь в параметре `token_file` при создании клиента. Базовый URL можно взять из переменной окружения `AMO_BASE_URL`.
|
|
111
|
+
|
|
112
|
+
## Тесты
|
|
113
|
+
|
|
114
|
+
Файл `tests/test_client.py` содержит небольшой набор автоматических тестов, написанных на [pytest](https://docs.pytest.org/). Они запускают методы клиента на подставном классе `DummyClient` и проверяют, что функции работают так, как ожидается. Запустить тесты можно командой:
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
pytest -q
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Эти тесты помогают убедиться, что изменения в коде не ломают основную функциональность.
|
|
121
|
+
|
|
122
|
+
## Пример использования `fetch_updated_leads_raw`
|
|
123
|
+
|
|
124
|
+
Кроме примера в разделе выше, код из `example_fetch.py` демонстрирует полный процесс получения сделок и сохранения их в файл.
|
|
@@ -1,11 +1,8 @@
|
|
|
1
|
-
MANIFEST.in
|
|
2
1
|
README.md
|
|
3
|
-
|
|
4
|
-
amochka/amochka/__init__.py
|
|
5
|
-
amochka/amochka/client.py
|
|
6
|
-
amochka/amochka/etl.py
|
|
2
|
+
pyproject.toml
|
|
7
3
|
amochka/amochka.egg-info/PKG-INFO
|
|
8
4
|
amochka/amochka.egg-info/SOURCES.txt
|
|
9
5
|
amochka/amochka.egg-info/dependency_links.txt
|
|
10
6
|
amochka/amochka.egg-info/requires.txt
|
|
11
|
-
amochka/amochka.egg-info/top_level.txt
|
|
7
|
+
amochka/amochka.egg-info/top_level.txt
|
|
8
|
+
tests/test_client.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "amochka"
|
|
7
|
+
version = "0.1.9"
|
|
8
|
+
description = "Python library for working with amoCRM API"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
authors = [
|
|
11
|
+
{name = "Timur", email = "timurdt@gmail.com"}
|
|
12
|
+
]
|
|
13
|
+
license = {text = "MIT"}
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Operating System :: OS Independent",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3.6",
|
|
21
|
+
"Programming Language :: Python :: 3.7",
|
|
22
|
+
"Programming Language :: Python :: 3.8",
|
|
23
|
+
"Programming Language :: Python :: 3.9",
|
|
24
|
+
"Programming Language :: Python :: 3.10",
|
|
25
|
+
"Programming Language :: Python :: 3.11",
|
|
26
|
+
"Programming Language :: Python :: 3.12",
|
|
27
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
28
|
+
"Topic :: Internet :: WWW/HTTP",
|
|
29
|
+
]
|
|
30
|
+
keywords = ["amocrm", "crm", "api", "client", "automation"]
|
|
31
|
+
requires-python = ">=3.6"
|
|
32
|
+
dependencies = [
|
|
33
|
+
"requests>=2.25.0",
|
|
34
|
+
"ratelimit>=2.2.0"
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
[project.urls]
|
|
38
|
+
Homepage = "https://github.com/yourusername/amochka"
|
|
39
|
+
Documentation = "https://github.com/yourusername/amochka"
|
|
40
|
+
Repository = "https://github.com/yourusername/amochka"
|
|
41
|
+
"Bug Tracker" = "https://github.com/yourusername/amochka/issues"
|
|
42
|
+
|
|
43
|
+
[tool.setuptools.packages.find]
|
|
44
|
+
where = ["amochka"]
|
|
45
|
+
|
|
46
|
+
[tool.setuptools.package-dir]
|
|
47
|
+
"" = "amochka"
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
from datetime import datetime, timedelta
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")))
|
|
10
|
+
|
|
11
|
+
from amochka import (
|
|
12
|
+
AmoCRMClient,
|
|
13
|
+
CacheConfig,
|
|
14
|
+
export_contacts_to_ndjson,
|
|
15
|
+
export_events_to_ndjson,
|
|
16
|
+
export_leads_to_ndjson,
|
|
17
|
+
export_notes_to_ndjson,
|
|
18
|
+
export_pipelines_to_ndjson,
|
|
19
|
+
export_users_to_ndjson,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class DummyClient(AmoCRMClient):
|
|
24
|
+
def __init__(self):
|
|
25
|
+
token_data = {
|
|
26
|
+
"access_token": "x",
|
|
27
|
+
"expires_at": str(int(datetime.utcnow().timestamp()) + 3600)
|
|
28
|
+
}
|
|
29
|
+
super().__init__(
|
|
30
|
+
base_url="https://example.com",
|
|
31
|
+
token_file=json.dumps(token_data),
|
|
32
|
+
cache_config=CacheConfig.disabled(),
|
|
33
|
+
disable_logging=True
|
|
34
|
+
)
|
|
35
|
+
self.calls = []
|
|
36
|
+
contact_11 = {
|
|
37
|
+
"id": 11,
|
|
38
|
+
"updated_at": 110,
|
|
39
|
+
}
|
|
40
|
+
contact_12 = {
|
|
41
|
+
"id": 12,
|
|
42
|
+
"updated_at": 111,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
self._data = {
|
|
46
|
+
"/api/v4/leads": [
|
|
47
|
+
{
|
|
48
|
+
"_embedded": {
|
|
49
|
+
"leads": [
|
|
50
|
+
{"id": 1, "updated_at": 100, "_embedded": {"contacts": [{"id": 11}, {"id": 12}]}}
|
|
51
|
+
]
|
|
52
|
+
},
|
|
53
|
+
"_page_count": 2,
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
"_embedded": {
|
|
57
|
+
"leads": [
|
|
58
|
+
{"id": 2, "updated_at": 101, "_embedded": {"contacts": [{"id": 11}]}}
|
|
59
|
+
]
|
|
60
|
+
},
|
|
61
|
+
"_page_count": 2,
|
|
62
|
+
},
|
|
63
|
+
],
|
|
64
|
+
"/api/v4/contacts": [
|
|
65
|
+
{
|
|
66
|
+
"_embedded": {
|
|
67
|
+
"contacts": [
|
|
68
|
+
contact_11,
|
|
69
|
+
]
|
|
70
|
+
},
|
|
71
|
+
"_page_count": 1,
|
|
72
|
+
}
|
|
73
|
+
],
|
|
74
|
+
"/api/v4/leads/notes": [
|
|
75
|
+
{
|
|
76
|
+
"_embedded": {
|
|
77
|
+
"notes": [
|
|
78
|
+
{"id": 21, "updated_at": 120, "entity_id": 1}
|
|
79
|
+
]
|
|
80
|
+
},
|
|
81
|
+
"_page_count": 1,
|
|
82
|
+
}
|
|
83
|
+
],
|
|
84
|
+
"/api/v4/events": [
|
|
85
|
+
{
|
|
86
|
+
"_embedded": {
|
|
87
|
+
"events": [
|
|
88
|
+
{"id": 31, "created_at": 130, "entity_id": 1}
|
|
89
|
+
]
|
|
90
|
+
},
|
|
91
|
+
"_page_count": 1,
|
|
92
|
+
}
|
|
93
|
+
],
|
|
94
|
+
"/api/v4/users": [
|
|
95
|
+
{
|
|
96
|
+
"_embedded": {"users": [{"id": 41, "updated_at": 140}]},
|
|
97
|
+
"_page_count": 1,
|
|
98
|
+
}
|
|
99
|
+
],
|
|
100
|
+
"/api/v4/leads/pipelines": [
|
|
101
|
+
{
|
|
102
|
+
"_embedded": {"pipelines": [{"id": 51, "updated_at": 150}]},
|
|
103
|
+
"_page_count": 1,
|
|
104
|
+
}
|
|
105
|
+
],
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
self._single_contacts = {
|
|
109
|
+
11: contact_11,
|
|
110
|
+
12: contact_12,
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
def _make_request(self, method, endpoint, params=None, data=None, timeout=10):
|
|
114
|
+
self.calls.append((method, endpoint, params, data, timeout))
|
|
115
|
+
params = params or {}
|
|
116
|
+
page = params.get("page", 1)
|
|
117
|
+
payloads = self._data.get(endpoint, [])
|
|
118
|
+
index = max(page - 1, 0)
|
|
119
|
+
if isinstance(payloads, list) and index < len(payloads):
|
|
120
|
+
return payloads[index]
|
|
121
|
+
if endpoint.startswith("/api/v4/contacts/"):
|
|
122
|
+
contact_id = int(endpoint.rsplit("/", 1)[-1])
|
|
123
|
+
return self._single_contacts.get(contact_id, {})
|
|
124
|
+
return {"_embedded": {}}
|
|
125
|
+
|
|
126
|
+
def test_fetch_updated_leads_raw(tmp_path):
|
|
127
|
+
client = DummyClient()
|
|
128
|
+
file_path = tmp_path / "leads.json"
|
|
129
|
+
leads = client.fetch_updated_leads_raw(
|
|
130
|
+
1,
|
|
131
|
+
updated_from=datetime.utcnow(),
|
|
132
|
+
save_to_file=str(file_path),
|
|
133
|
+
include_contacts=True,
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
assert [lead["id"] for lead in leads] == [1, 2]
|
|
137
|
+
method, endpoint, params, data, timeout = client.calls[0]
|
|
138
|
+
assert endpoint == "/api/v4/leads"
|
|
139
|
+
assert params.get("with") == "contacts"
|
|
140
|
+
assert params.get("filter[pipeline_id]") == "1"
|
|
141
|
+
assert timeout == 10
|
|
142
|
+
assert file_path.exists()
|
|
143
|
+
data = json.loads(file_path.read_text())
|
|
144
|
+
assert len(data) == 2
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def test_export_helpers_write_expected_files(tmp_path):
|
|
148
|
+
client = DummyClient()
|
|
149
|
+
export_dir = tmp_path / "exports"
|
|
150
|
+
export_dir.mkdir()
|
|
151
|
+
|
|
152
|
+
window_end = datetime.utcnow()
|
|
153
|
+
window_start = window_end - timedelta(minutes=15)
|
|
154
|
+
|
|
155
|
+
collected_contacts = set()
|
|
156
|
+
|
|
157
|
+
def _collect(lead):
|
|
158
|
+
embedded = lead.get("_embedded") or {}
|
|
159
|
+
for contact in embedded.get("contacts", []):
|
|
160
|
+
contact_id = contact.get("id")
|
|
161
|
+
if contact_id is not None:
|
|
162
|
+
collected_contacts.add(int(contact_id))
|
|
163
|
+
|
|
164
|
+
leads_file = export_dir / "leads.ndjson"
|
|
165
|
+
leads_count = export_leads_to_ndjson(
|
|
166
|
+
client,
|
|
167
|
+
leads_file,
|
|
168
|
+
account_id=123,
|
|
169
|
+
start=window_start,
|
|
170
|
+
end=window_end,
|
|
171
|
+
include_contacts=True,
|
|
172
|
+
on_record=_collect,
|
|
173
|
+
)
|
|
174
|
+
assert leads_count == 2
|
|
175
|
+
assert collected_contacts == {11, 12}
|
|
176
|
+
|
|
177
|
+
contacts_file = export_dir / "contacts.ndjson"
|
|
178
|
+
contacts_count = export_contacts_to_ndjson(
|
|
179
|
+
client,
|
|
180
|
+
contacts_file,
|
|
181
|
+
account_id=123,
|
|
182
|
+
contact_ids=sorted(collected_contacts),
|
|
183
|
+
)
|
|
184
|
+
assert contacts_count == 2
|
|
185
|
+
|
|
186
|
+
notes_file = export_dir / "lead_notes.ndjson"
|
|
187
|
+
notes_count = export_notes_to_ndjson(
|
|
188
|
+
client,
|
|
189
|
+
notes_file,
|
|
190
|
+
account_id=123,
|
|
191
|
+
entity="lead",
|
|
192
|
+
start=window_start,
|
|
193
|
+
end=window_end,
|
|
194
|
+
)
|
|
195
|
+
assert notes_count == 1
|
|
196
|
+
|
|
197
|
+
events_file = export_dir / "lead_events.ndjson"
|
|
198
|
+
events_count = export_events_to_ndjson(
|
|
199
|
+
client,
|
|
200
|
+
events_file,
|
|
201
|
+
account_id=123,
|
|
202
|
+
entity="lead",
|
|
203
|
+
start=window_start,
|
|
204
|
+
end=window_end,
|
|
205
|
+
)
|
|
206
|
+
assert events_count == 1
|
|
207
|
+
|
|
208
|
+
users_file = export_dir / "users.ndjson"
|
|
209
|
+
users_count = export_users_to_ndjson(
|
|
210
|
+
client,
|
|
211
|
+
users_file,
|
|
212
|
+
account_id=123,
|
|
213
|
+
)
|
|
214
|
+
assert users_count == 1
|
|
215
|
+
|
|
216
|
+
pipelines_file = export_dir / "pipelines.ndjson"
|
|
217
|
+
pipelines_count = export_pipelines_to_ndjson(
|
|
218
|
+
client,
|
|
219
|
+
pipelines_file,
|
|
220
|
+
account_id=123,
|
|
221
|
+
)
|
|
222
|
+
assert pipelines_count == 1
|
|
223
|
+
|
|
224
|
+
def _read_lines(path: Path):
|
|
225
|
+
return [json.loads(line) for line in path.read_text().splitlines() if line]
|
|
226
|
+
|
|
227
|
+
leads_lines = _read_lines(leads_file)
|
|
228
|
+
assert leads_lines[0]["entity"] == "lead"
|
|
229
|
+
assert leads_lines[0]["account_id"] == 123
|
|
230
|
+
assert leads_lines[0]["updated_at"] == 100
|
|
231
|
+
assert leads_lines[0]["payload"]["id"] == 1
|
|
232
|
+
|
|
233
|
+
contacts_lines = _read_lines(contacts_file)
|
|
234
|
+
assert contacts_lines[0]["entity"] == "contact"
|
|
235
|
+
assert contacts_lines[0]["payload"]["id"] == 11
|
|
236
|
+
|
|
237
|
+
notes_lines = _read_lines(notes_file)
|
|
238
|
+
assert notes_lines[0]["entity"] == "lead_note"
|
|
239
|
+
assert notes_lines[0]["updated_at"] == 120
|
|
240
|
+
|
|
241
|
+
events_lines = _read_lines(events_file)
|
|
242
|
+
assert events_lines[0]["entity"] == "lead_event"
|
|
243
|
+
assert events_lines[0]["updated_at"] == 130
|
|
244
|
+
|
|
245
|
+
users_lines = _read_lines(users_file)
|
|
246
|
+
assert users_lines[0]["entity"] == "user"
|
|
247
|
+
|
|
248
|
+
pipelines_lines = _read_lines(pipelines_file)
|
|
249
|
+
assert pipelines_lines[0]["entity"] == "pipeline"
|
|
250
|
+
|
|
251
|
+
contacts_call = next(item for item in client.calls if item[1] == "/api/v4/contacts")
|
|
252
|
+
assert contacts_call[2]["filter[id]"] == "11,12"
|
|
253
|
+
assert "filter[updated_at][from]" not in contacts_call[2]
|
|
254
|
+
|
|
255
|
+
events_call = next(item for item in client.calls if item[1] == "/api/v4/events")
|
|
256
|
+
assert events_call[2]["filter[entity]"] == "lead"
|
|
257
|
+
|
|
258
|
+
notes_call = next(item for item in client.calls if item[1] == "/api/v4/leads/notes")
|
|
259
|
+
assert notes_call[2]["filter[updated_at][from]"] is None or notes_call[2]["filter[updated_at][from]"] >= 0
|