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.
Files changed (27) hide show
  1. {amochka-0.4.6 → amochka-0.4.8}/PKG-INFO +1 -1
  2. {amochka-0.4.6 → amochka-0.4.8}/amochka/__init__.py +1 -1
  3. {amochka-0.4.6 → amochka-0.4.8}/amochka.egg-info/PKG-INFO +1 -1
  4. {amochka-0.4.6 → amochka-0.4.8}/etl/config.py +11 -0
  5. {amochka-0.4.6 → amochka-0.4.8}/etl/loaders.py +15 -1
  6. {amochka-0.4.6 → amochka-0.4.8}/etl/run_etl.py +18 -5
  7. {amochka-0.4.6 → amochka-0.4.8}/etl/transformers.py +12 -4
  8. {amochka-0.4.6 → amochka-0.4.8}/pyproject.toml +1 -1
  9. {amochka-0.4.6 → amochka-0.4.8}/README.md +0 -0
  10. {amochka-0.4.6 → amochka-0.4.8}/amochka/client.py +0 -0
  11. {amochka-0.4.6 → amochka-0.4.8}/amochka/errors.py +0 -0
  12. {amochka-0.4.6 → amochka-0.4.8}/amochka/etl.py +0 -0
  13. {amochka-0.4.6 → amochka-0.4.8}/amochka.egg-info/SOURCES.txt +0 -0
  14. {amochka-0.4.6 → amochka-0.4.8}/amochka.egg-info/dependency_links.txt +0 -0
  15. {amochka-0.4.6 → amochka-0.4.8}/amochka.egg-info/requires.txt +0 -0
  16. {amochka-0.4.6 → amochka-0.4.8}/amochka.egg-info/top_level.txt +0 -0
  17. {amochka-0.4.6 → amochka-0.4.8}/etl/__init__.py +0 -0
  18. {amochka-0.4.6 → amochka-0.4.8}/etl/extractors.py +0 -0
  19. {amochka-0.4.6 → amochka-0.4.8}/etl/migrations/001_create_tables.sql +0 -0
  20. {amochka-0.4.6 → amochka-0.4.8}/setup.cfg +0 -0
  21. {amochka-0.4.6 → amochka-0.4.8}/tests/test_cache.py +0 -0
  22. {amochka-0.4.6 → amochka-0.4.8}/tests/test_client.py +0 -0
  23. {amochka-0.4.6 → amochka-0.4.8}/tests/test_etl.py +0 -0
  24. {amochka-0.4.6 → amochka-0.4.8}/tests/test_http.py +0 -0
  25. {amochka-0.4.6 → amochka-0.4.8}/tests/test_notes_events.py +0 -0
  26. {amochka-0.4.6 → amochka-0.4.8}/tests/test_security.py +0 -0
  27. {amochka-0.4.6 → amochka-0.4.8}/tests/test_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: amochka
3
- Version: 0.4.6
3
+ Version: 0.4.8
4
4
  Summary: Python library for working with amoCRM API with ETL capabilities
5
5
  Author-email: Timur <timurdt@gmail.com>
6
6
  License: MIT
@@ -2,7 +2,7 @@
2
2
  amochka: Библиотека для работы с API amoCRM.
3
3
  """
4
4
 
5
- __version__ = "0.4.6"
5
+ __version__ = "0.4.8"
6
6
 
7
7
  from .client import AmoCRMClient, CacheConfig
8
8
  from .errors import (
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: amochka
3
- Version: 0.4.6
3
+ Version: 0.4.8
4
4
  Summary: Python library for working with amoCRM API with ETL capabilities
5
5
  Author-email: Timur <timurdt@gmail.com>
6
6
  License: MIT
@@ -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(self, cursor, transformed: TransformedContact) -> int:
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(mybi_account_id, pipelines_map, statuses_map)
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__(self, account_id: int, pipelines_map: Optional[Dict[int, str]] = None, statuses_map: Optional[Dict[int, Dict]] = None):
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
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "amochka"
7
- version = "0.4.6"
7
+ version = "0.4.8"
8
8
  description = "Python library for working with amoCRM API with ETL capabilities"
9
9
  readme = "README.md"
10
10
  authors = [
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