amochka 0.4.6__tar.gz → 0.4.8__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {amochka-0.4.6 → amochka-0.4.8}/PKG-INFO +1 -1
- {amochka-0.4.6 → amochka-0.4.8}/amochka/__init__.py +1 -1
- {amochka-0.4.6 → amochka-0.4.8}/amochka.egg-info/PKG-INFO +1 -1
- {amochka-0.4.6 → amochka-0.4.8}/etl/config.py +11 -0
- {amochka-0.4.6 → amochka-0.4.8}/etl/loaders.py +15 -1
- {amochka-0.4.6 → amochka-0.4.8}/etl/run_etl.py +18 -5
- {amochka-0.4.6 → amochka-0.4.8}/etl/transformers.py +12 -4
- {amochka-0.4.6 → amochka-0.4.8}/pyproject.toml +1 -1
- {amochka-0.4.6 → amochka-0.4.8}/README.md +0 -0
- {amochka-0.4.6 → amochka-0.4.8}/amochka/client.py +0 -0
- {amochka-0.4.6 → amochka-0.4.8}/amochka/errors.py +0 -0
- {amochka-0.4.6 → amochka-0.4.8}/amochka/etl.py +0 -0
- {amochka-0.4.6 → amochka-0.4.8}/amochka.egg-info/SOURCES.txt +0 -0
- {amochka-0.4.6 → amochka-0.4.8}/amochka.egg-info/dependency_links.txt +0 -0
- {amochka-0.4.6 → amochka-0.4.8}/amochka.egg-info/requires.txt +0 -0
- {amochka-0.4.6 → amochka-0.4.8}/amochka.egg-info/top_level.txt +0 -0
- {amochka-0.4.6 → amochka-0.4.8}/etl/__init__.py +0 -0
- {amochka-0.4.6 → amochka-0.4.8}/etl/extractors.py +0 -0
- {amochka-0.4.6 → amochka-0.4.8}/etl/migrations/001_create_tables.sql +0 -0
- {amochka-0.4.6 → amochka-0.4.8}/setup.cfg +0 -0
- {amochka-0.4.6 → amochka-0.4.8}/tests/test_cache.py +0 -0
- {amochka-0.4.6 → amochka-0.4.8}/tests/test_client.py +0 -0
- {amochka-0.4.6 → amochka-0.4.8}/tests/test_etl.py +0 -0
- {amochka-0.4.6 → amochka-0.4.8}/tests/test_http.py +0 -0
- {amochka-0.4.6 → amochka-0.4.8}/tests/test_notes_events.py +0 -0
- {amochka-0.4.6 → amochka-0.4.8}/tests/test_security.py +0 -0
- {amochka-0.4.6 → amochka-0.4.8}/tests/test_utils.py +0 -0
|
@@ -32,6 +32,12 @@ def _load_env_file(path: Path) -> Dict[str, str]:
|
|
|
32
32
|
return env
|
|
33
33
|
|
|
34
34
|
|
|
35
|
+
def _get_bool(value: Optional[str], default: bool = False) -> bool:
|
|
36
|
+
if value is None:
|
|
37
|
+
return default
|
|
38
|
+
return value.strip().lower() in {"1", "true", "yes", "y", "on"}
|
|
39
|
+
|
|
40
|
+
|
|
35
41
|
@dataclass
|
|
36
42
|
class DatabaseConfig:
|
|
37
43
|
"""Конфигурация подключения к PostgreSQL."""
|
|
@@ -139,6 +145,7 @@ class ETLConfig:
|
|
|
139
145
|
batch_size: int = 100
|
|
140
146
|
window_minutes: int = 120 # Окно выгрузки по умолчанию (2 часа)
|
|
141
147
|
log_level: str = "INFO"
|
|
148
|
+
include_companies: bool = False
|
|
142
149
|
|
|
143
150
|
@classmethod
|
|
144
151
|
def from_env(cls, env_path: Optional[Path] = None) -> "ETLConfig":
|
|
@@ -180,12 +187,15 @@ class ETLConfig:
|
|
|
180
187
|
)
|
|
181
188
|
)
|
|
182
189
|
|
|
190
|
+
include_companies = _get_bool(os.environ.get("ETL_INCLUDE_COMPANIES"), False)
|
|
191
|
+
|
|
183
192
|
return cls(
|
|
184
193
|
database=db_config,
|
|
185
194
|
accounts=accounts,
|
|
186
195
|
batch_size=int(os.environ.get("ETL_BATCH_SIZE", "100") or "100"),
|
|
187
196
|
window_minutes=int(os.environ.get("ETL_WINDOW_MINUTES", "120") or "120"),
|
|
188
197
|
log_level=os.environ.get("ETL_LOG_LEVEL", "INFO") or "INFO",
|
|
198
|
+
include_companies=include_companies,
|
|
189
199
|
)
|
|
190
200
|
|
|
191
201
|
|
|
@@ -213,6 +223,7 @@ DEFAULT_CONFIG = ETLConfig(
|
|
|
213
223
|
],
|
|
214
224
|
batch_size=100,
|
|
215
225
|
window_minutes=120,
|
|
226
|
+
include_companies=False,
|
|
216
227
|
)
|
|
217
228
|
|
|
218
229
|
|
|
@@ -415,12 +415,26 @@ class PostgresLoader:
|
|
|
415
415
|
result = cursor.fetchone()
|
|
416
416
|
return result[0] if result else None
|
|
417
417
|
|
|
418
|
-
def load_transformed_contact(
|
|
418
|
+
def load_transformed_contact(
|
|
419
|
+
self,
|
|
420
|
+
cursor,
|
|
421
|
+
transformed: TransformedContact,
|
|
422
|
+
user_id_map: Optional[Dict[int, int]] = None,
|
|
423
|
+
) -> int:
|
|
419
424
|
"""Загружает полностью трансформированный контакт."""
|
|
420
425
|
contacts_id = self.upsert_contact(cursor, transformed.contact)
|
|
421
426
|
|
|
422
427
|
# Факты по контакту
|
|
423
428
|
transformed.contact_facts["contacts_id"] = contacts_id
|
|
429
|
+
|
|
430
|
+
# Преобразуем users_id из amoCRM ID во внутренний ID (если найден).
|
|
431
|
+
# Если пользователя нет в маппинге — оставляем внешний ID как есть.
|
|
432
|
+
if user_id_map:
|
|
433
|
+
amo_user_id = transformed.contact_facts.get("users_id")
|
|
434
|
+
if amo_user_id is not None:
|
|
435
|
+
internal_user_id = user_id_map.get(amo_user_id)
|
|
436
|
+
if internal_user_id is not None:
|
|
437
|
+
transformed.contact_facts["users_id"] = internal_user_id
|
|
424
438
|
registered_id = self._get_or_create_date_id(cursor, transformed.contact_facts.get("created_date"))
|
|
425
439
|
|
|
426
440
|
cursor.execute(
|
|
@@ -88,6 +88,7 @@ def sync_leads_with_contacts(
|
|
|
88
88
|
updated_from: Optional[datetime] = None,
|
|
89
89
|
updated_to: Optional[datetime] = None,
|
|
90
90
|
pipeline_ids: Optional[List[int]] = None,
|
|
91
|
+
include_companies: bool = False,
|
|
91
92
|
batch_size: int = 100,
|
|
92
93
|
) -> dict:
|
|
93
94
|
"""
|
|
@@ -101,11 +102,16 @@ def sync_leads_with_contacts(
|
|
|
101
102
|
Returns:
|
|
102
103
|
dict с ключами: leads_count, contacts_count
|
|
103
104
|
"""
|
|
104
|
-
contact_transformer = ContactTransformer(mybi_account_id)
|
|
105
|
+
contact_transformer = ContactTransformer(mybi_account_id, include_companies=include_companies)
|
|
105
106
|
|
|
106
107
|
# Загружаем справочники для денормализации
|
|
107
108
|
pipelines_map, statuses_map = extractor.load_pipelines_and_statuses()
|
|
108
|
-
lead_transformer = LeadTransformer(
|
|
109
|
+
lead_transformer = LeadTransformer(
|
|
110
|
+
mybi_account_id,
|
|
111
|
+
pipelines_map,
|
|
112
|
+
statuses_map,
|
|
113
|
+
include_companies=include_companies,
|
|
114
|
+
)
|
|
109
115
|
|
|
110
116
|
# 1. Собираем сделки и ID контактов из API
|
|
111
117
|
leads_iter = extractor.iter_leads(
|
|
@@ -134,6 +140,7 @@ def sync_leads_with_contacts(
|
|
|
134
140
|
with conn.cursor() as cursor:
|
|
135
141
|
# Проверяем какие контакты уже есть в БД
|
|
136
142
|
existing_contacts = loader.build_contact_id_map(cursor, mybi_account_id)
|
|
143
|
+
user_id_map = loader.build_user_id_map(cursor, mybi_account_id)
|
|
137
144
|
missing_contact_ids = contact_ids - set(existing_contacts.keys())
|
|
138
145
|
|
|
139
146
|
if missing_contact_ids:
|
|
@@ -141,7 +148,7 @@ def sync_leads_with_contacts(
|
|
|
141
148
|
|
|
142
149
|
for contact in extractor.iter_contacts(contact_ids=list(missing_contact_ids)):
|
|
143
150
|
transformed = contact_transformer.transform(contact)
|
|
144
|
-
loader.load_transformed_contact(cursor, transformed)
|
|
151
|
+
loader.load_transformed_contact(cursor, transformed, user_id_map)
|
|
145
152
|
contacts_loaded += 1
|
|
146
153
|
|
|
147
154
|
if contacts_loaded % batch_size == 0:
|
|
@@ -260,6 +267,7 @@ def sync_contacts(
|
|
|
260
267
|
contact_ids: Optional[Set[int]] = None,
|
|
261
268
|
updated_from: Optional[datetime] = None,
|
|
262
269
|
updated_to: Optional[datetime] = None,
|
|
270
|
+
include_companies: bool = False,
|
|
263
271
|
batch_size: int = 100,
|
|
264
272
|
) -> int:
|
|
265
273
|
"""
|
|
@@ -278,7 +286,7 @@ def sync_contacts(
|
|
|
278
286
|
Returns:
|
|
279
287
|
Количество загруженных контактов
|
|
280
288
|
"""
|
|
281
|
-
contact_transformer = ContactTransformer(mybi_account_id)
|
|
289
|
+
contact_transformer = ContactTransformer(mybi_account_id, include_companies=include_companies)
|
|
282
290
|
|
|
283
291
|
# Загружаем все обновлённые контакты за период
|
|
284
292
|
contacts_iter = extractor.iter_contacts(
|
|
@@ -292,6 +300,7 @@ def sync_contacts(
|
|
|
292
300
|
|
|
293
301
|
with loader.connection() as conn:
|
|
294
302
|
with conn.cursor() as cursor:
|
|
303
|
+
user_id_map = loader.build_user_id_map(cursor, mybi_account_id)
|
|
295
304
|
for i, contact in enumerate(contacts_iter):
|
|
296
305
|
contact_id = contact.get("id")
|
|
297
306
|
|
|
@@ -301,7 +310,7 @@ def sync_contacts(
|
|
|
301
310
|
continue
|
|
302
311
|
|
|
303
312
|
transformed = contact_transformer.transform(contact)
|
|
304
|
-
loader.load_transformed_contact(cursor, transformed)
|
|
313
|
+
loader.load_transformed_contact(cursor, transformed, user_id_map)
|
|
305
314
|
loaded_count += 1
|
|
306
315
|
|
|
307
316
|
# Отслеживаем максимальный updated_at
|
|
@@ -482,6 +491,7 @@ def run_etl_for_account(
|
|
|
482
491
|
entities: List[str],
|
|
483
492
|
window_minutes: int,
|
|
484
493
|
full_sync: bool = False,
|
|
494
|
+
include_companies: bool = False,
|
|
485
495
|
batch_size: int = 100,
|
|
486
496
|
) -> dict:
|
|
487
497
|
"""
|
|
@@ -535,6 +545,7 @@ def run_etl_for_account(
|
|
|
535
545
|
updated_from=leads_updated_from,
|
|
536
546
|
updated_to=updated_to,
|
|
537
547
|
pipeline_ids=account.pipeline_ids,
|
|
548
|
+
include_companies=include_companies,
|
|
538
549
|
batch_size=batch_size,
|
|
539
550
|
)
|
|
540
551
|
stats["leads"] = result["leads_count"]
|
|
@@ -576,6 +587,7 @@ def run_etl_for_account(
|
|
|
576
587
|
contact_ids=contact_ids_in_pipelines, # Только контакты из наших воронок
|
|
577
588
|
updated_from=contacts_updated_from,
|
|
578
589
|
updated_to=updated_to,
|
|
590
|
+
include_companies=include_companies,
|
|
579
591
|
batch_size=batch_size,
|
|
580
592
|
)
|
|
581
593
|
else:
|
|
@@ -675,6 +687,7 @@ def main():
|
|
|
675
687
|
entities,
|
|
676
688
|
config.window_minutes,
|
|
677
689
|
full_sync=args.full,
|
|
690
|
+
include_companies=config.include_companies,
|
|
678
691
|
batch_size=config.batch_size,
|
|
679
692
|
)
|
|
680
693
|
all_stats[account.name] = stats
|
|
@@ -78,7 +78,13 @@ class TransformedEvent:
|
|
|
78
78
|
class LeadTransformer:
|
|
79
79
|
"""Трансформер для сделок (leads)."""
|
|
80
80
|
|
|
81
|
-
def __init__(
|
|
81
|
+
def __init__(
|
|
82
|
+
self,
|
|
83
|
+
account_id: int,
|
|
84
|
+
pipelines_map: Optional[Dict[int, str]] = None,
|
|
85
|
+
statuses_map: Optional[Dict[int, Dict]] = None,
|
|
86
|
+
include_companies: bool = False,
|
|
87
|
+
):
|
|
82
88
|
"""
|
|
83
89
|
Инициализирует трансформер.
|
|
84
90
|
|
|
@@ -90,6 +96,7 @@ class LeadTransformer:
|
|
|
90
96
|
self.account_id = account_id
|
|
91
97
|
self.pipelines_map = pipelines_map or {}
|
|
92
98
|
self.statuses_map = statuses_map or {}
|
|
99
|
+
self.include_companies = include_companies
|
|
93
100
|
|
|
94
101
|
def transform(self, lead: Dict[str, Any]) -> TransformedLead:
|
|
95
102
|
"""
|
|
@@ -145,7 +152,7 @@ class LeadTransformer:
|
|
|
145
152
|
main_contact_id = contacts[0].get("id")
|
|
146
153
|
|
|
147
154
|
# Компания из _embedded.companies
|
|
148
|
-
companies = embedded.get("companies", [])
|
|
155
|
+
companies = embedded.get("companies", []) if self.include_companies else []
|
|
149
156
|
company_id = companies[0].get("id") if companies else None
|
|
150
157
|
|
|
151
158
|
lead_facts_record = {
|
|
@@ -221,8 +228,9 @@ class LeadTransformer:
|
|
|
221
228
|
class ContactTransformer:
|
|
222
229
|
"""Трансформер для контактов."""
|
|
223
230
|
|
|
224
|
-
def __init__(self, account_id: int):
|
|
231
|
+
def __init__(self, account_id: int, include_companies: bool = False):
|
|
225
232
|
self.account_id = account_id
|
|
233
|
+
self.include_companies = include_companies
|
|
226
234
|
|
|
227
235
|
def transform(self, contact: Dict[str, Any]) -> TransformedContact:
|
|
228
236
|
"""Преобразует контакт из amoCRM в структуру mybi."""
|
|
@@ -249,7 +257,7 @@ class ContactTransformer:
|
|
|
249
257
|
|
|
250
258
|
# Компания из _embedded
|
|
251
259
|
embedded = contact.get("_embedded", {})
|
|
252
|
-
companies = embedded.get("companies", [])
|
|
260
|
+
companies = embedded.get("companies", []) if self.include_companies else []
|
|
253
261
|
if companies:
|
|
254
262
|
contact_record["company"] = companies[0].get("name")
|
|
255
263
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|