amochka 0.1.8__py3-none-any.whl → 0.3.0__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 CHANGED
@@ -2,7 +2,7 @@
2
2
  amochka: Библиотека для работы с API amoCRM.
3
3
  """
4
4
 
5
- __version__ = "0.1.8"
5
+ __version__ = "0.2.0"
6
6
 
7
7
  from .client import AmoCRMClient, CacheConfig
8
8
  from .etl import (
amochka/client.py CHANGED
@@ -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
- file (str): Путь к файлу кэша (используется только при storage='file')
182
- lifetime_hours (int|None): Время жизни кэша в часах (None для бесконечного)
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
- def __init__(self, enabled=True, storage='file', file=None, lifetime_hours=24):
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.file = file
188
- self.lifetime_hours = lifetime_hours
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=24):
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
- cache_data = self._load_custom_fields_cache() if self.cache_config.enabled else None
795
- if cache_data:
796
- last_updated = cache_data.get("last_updated", 0)
797
- if self.cache_config.lifetime_hours is not None:
798
- if time.time() - last_updated < self.cache_config.lifetime_hours * 3600:
799
- self._custom_fields_mapping = cache_data.get("mapping")
800
- self.logger.debug("Using cached custom fields mapping.")
801
- return self._custom_fields_mapping
802
- else:
803
- # Бесконечный кэш не проверяем срок
804
- self._custom_fields_mapping = cache_data.get("mapping")
805
- self.logger.debug("Using cached custom fields mapping (infinite cache).")
806
- return self._custom_fields_mapping
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
- self.logger.debug("Custom fields mapping fetched (содержимое маппинга не выводится полностью).")
1054
+ # Сохраняем в memory cache
824
1055
  self._custom_fields_mapping = mapping
1056
+
1057
+ # Сохраняем в file cache (новый формат)
825
1058
  if self.cache_config.enabled:
826
- self._save_custom_fields_cache(mapping)
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
@@ -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` демонстрирует полный процесс получения сделок и сохранения их в файл.
@@ -0,0 +1,14 @@
1
+ amochka/__init__.py,sha256=NFAgMbhBnrx3nF--MeY9Chpu5gtZ5kVn-QYnmO3Nhpk,620
2
+ amochka/client.py,sha256=mGoDEE0XOt-c9f2FmqFVzDMrXQel_zComtyAFXdCHjg,66155
3
+ amochka/etl.py,sha256=N8rXNFbtmlKfsYpgr7HDcP4enoj63XQPWuTDxGuMhw4,8901
4
+ etl/__init__.py,sha256=bp9fPqbKlOc7xzs27diHEvysy1FgBrwlpX6GnR6GL9U,255
5
+ etl/config.py,sha256=YY6M7pib_XD7bjPW8J_iWzt2hoXzDpx6yIvZHmlUlrE,8873
6
+ etl/extractors.py,sha256=-QCBZ6PoJ51j0drNQaH5bLfvjPqAQmfVgaT1D_ZSwjI,12909
7
+ etl/loaders.py,sha256=nQx6TDwnuHVWzjmuKY9v-23hmmk4Ex6ZoGtb3PApO1k,31974
8
+ etl/run_etl.py,sha256=p_2NxJwXMiACMETvRsjrozMgz66U9ezDNSWZXUieNMs,26262
9
+ etl/transformers.py,sha256=OwYJ_9l3oqvy2Y3-umXjAGweOIqlfRI0iSiCFPrcQ8E,17867
10
+ etl/migrations/001_create_tables.sql,sha256=YrSaZjpofC1smjYx0bM4eHQumboruIBY3fwRDlJLLSo,15749
11
+ amochka-0.3.0.dist-info/METADATA,sha256=5dZFPOs2wupzc0VsYVXyk1pJ-F4Kwm997N9EA_Z7htE,6371
12
+ amochka-0.3.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
13
+ amochka-0.3.0.dist-info/top_level.txt,sha256=grRX8aLFG-yYKPsAqCD6sUBmdLSQeOMHsc9Dl6S7Lzo,12
14
+ amochka-0.3.0.dist-info/RECORD,,
etl/__init__.py ADDED
@@ -0,0 +1,7 @@
1
+ """
2
+ ETL-коннектор для выгрузки данных из amoCRM в PostgreSQL.
3
+
4
+ Структура таблиц совместима с mybi.ru для работы с существующими dbt-моделями.
5
+ """
6
+
7
+ __version__ = "0.1.0"