MichaelWelsch commited on
Commit
ce1b2e3
·
verified ·
1 Parent(s): 0f792c2

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +102 -23
app.py CHANGED
@@ -443,12 +443,14 @@ def email_generate_async(token_id: str, variables: dict, items: List[dict]) -> d
443
 
444
 
445
  def wholix_login(email: str, password: str) -> str:
446
- res = req(f"{WHOLIX_BASE_URL}/api/v1/auth/login", method="POST",
447
- json_body={"email": email, "password": password}, timeout=(5.0, 15.0))
448
- token = (res or {}).get("token")
449
- if not token:
450
- raise RuntimeError("Wholix-Login fehlgeschlagen.")
451
- return token
 
 
452
 
453
  # ===================== Helfer für Platzhalter-E-Mail =======================
454
 
@@ -473,23 +475,90 @@ def _make_placeholder_email(record: dict) -> str:
473
 
474
  # ===================== Wholix Store + Fallbacks ============================
475
 
476
- def _store_with_fallbacks(token: str, payload: dict, module: str) -> dict:
 
 
 
 
 
 
 
477
  """
478
- Versucht 3 Stufen, um 422 zu vermeiden:
479
- 1) Original-Payload
480
- 2) Mit reparierten/entfernten URLs
481
- 3) Minimal-Record (email, firstname, lastname, company_name, exclude_hash, tags)
482
  """
483
- url = f"{WHOLIX_BASE_URL}/api/v1/table-object-data/store-objects"
484
- headers = {"Authorization": f"Bearer {token}"}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
485
 
486
- def _post(body):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
487
  return req(url, method="POST", headers=headers, json_body=body, timeout=(5.0, 30.0))
488
 
489
  # 1: Original
490
  body1 = {"module": module, "action": "store", "data": [payload]}
491
  try:
492
- return _post(body1)
493
  except HTTPError as e:
494
  if e.status != 422:
495
  raise
@@ -504,7 +573,7 @@ def _store_with_fallbacks(token: str, payload: dict, module: str) -> dict:
504
  else:
505
  p2.pop(k, None)
506
  try:
507
- return _post({"module": module, "action": "store", "data": [p2]})
508
  except HTTPError as e:
509
  if e.status != 422:
510
  raise
@@ -518,25 +587,25 @@ def _store_with_fallbacks(token: str, payload: dict, module: str) -> dict:
518
  "exclude_hash": p2.get("exclude_hash") or _slug(uuid.uuid4().hex[:8]),
519
  "tags": p2.get("tags") or {"keys": ["no-email"], "values": ["no-email"]},
520
  }
521
- return _post({"module": module, "action": "store", "data": [minimal]})
522
 
523
 
524
  def wholix_store_contact(token: str, record: dict, module: str = "Contacts", allow_placeholder: bool = True) -> dict:
525
  """
526
  Sendet NUR erlaubte Felder an Wholix und saniert problematische Werte.
527
  IMMER speicherbar: erzeugt bei Bedarf Placeholder-Mail (example.com) + Tag.
528
- Nutzt _store_with_fallbacks gegen 422.
529
  """
530
  if not isinstance(record, dict):
531
  raise ValueError("Wholix: record muss ein dict sein.")
532
 
533
- # E-Mail prüfen / ggf. generieren
534
  def _clean_str(v):
535
  if v is None:
536
  return None
537
  s = str(v).strip()
538
  return s if s else None
539
 
 
540
  email = _clean_str(record.get("email"))
541
  if not email or not EMAIL_RE.match(email):
542
  if allow_placeholder:
@@ -603,7 +672,7 @@ def wholix_store_contact(token: str, record: dict, module: str = "Contacts", all
603
  # Pflichtfeld sicher (jetzt inkl. Platzhalter möglich)
604
  out["email"] = _clean_str(email)
605
 
606
- # POST mit Fallback-Logik
607
  return _store_with_fallbacks(token, out, module)
608
 
609
 
@@ -614,8 +683,12 @@ def wholix_fetch_excludes(token: str,
614
  per_page: int = 500,
615
  max_pages: int = 100,
616
  dedupe: bool = True) -> List[Dict[str, str]]:
617
- path = f"{WHOLIX_BASE_URL}/api/v1/table-object-data/fetch-paginated-results"
618
- headers = {"Authorization": f"Bearer {token}"}
 
 
 
 
619
  out: List[Dict[str, str]] = []
620
  seen: Set[str] = set()
621
 
@@ -624,8 +697,14 @@ def wholix_fetch_excludes(token: str,
624
  if page > last_page:
625
  break
626
  payload = {"module": module_name, "action": "search", "page": page, "per_page": per_page}
 
627
  try:
628
- res = req(path, method="POST", headers=headers, json_body=payload, timeout=(5.0, 30.0))
 
 
 
 
 
629
  except Exception as e:
630
  APP_LOG.warning(f"Wholix-Excludes Page {page} Fehler: {e}")
631
  break
 
443
 
444
 
445
  def wholix_login(email: str, password: str) -> str:
446
+ """
447
+ Kompatibel zur alten Signatur (liefert weiterhin ein Token-String),
448
+ richtet aber zusätzlich den globalen Auto-ReLogin-Manager _WHOLIX_SES ein.
449
+ """
450
+ global _WHOLIX_SES
451
+ _WHOLIX_SES = WholixSession(email, password, base_url=WHOLIX_BASE_URL, session=GLOBAL_SES)
452
+ return _WHOLIX_SES.get_token()
453
+
454
 
455
  # ===================== Helfer für Platzhalter-E-Mail =======================
456
 
 
475
 
476
  # ===================== Wholix Store + Fallbacks ============================
477
 
478
+ # ============================= Wholix Auth ================================
479
+
480
+ # ============================= Wholix Auth (NEU) =============================
481
+
482
+ # Globaler Session-Manager (wird in wholix_login gesetzt)
483
+ _WHOLIX_SES = None # type: Optional["WholixSession"]
484
+
485
+ class WholixSession:
486
  """
487
+ Kapselt Token-Handling:
488
+ - Login mit email/password
489
+ - Autorisierte Requests mit Auto-ReLogin bei 401/403/419 (ein Retry)
490
+ - Thread-sicher
491
  """
492
+ def __init__(self, email: str, password: str, base_url: str = WHOLIX_BASE_URL, session: Optional[requests.Session] = None):
493
+ self.email = email
494
+ self.password = password
495
+ self.base_url = base_url.rstrip("/")
496
+ self._token = None
497
+ self._lock = threading.RLock()
498
+ self._ses = session or GLOBAL_SES
499
+
500
+ def _login(self) -> str:
501
+ res = req(f"{self.base_url}/api/v1/auth/login", method="POST",
502
+ json_body={"email": self.email, "password": self.password},
503
+ timeout=(5.0, 15.0), session=self._ses)
504
+ token = (res or {}).get("token")
505
+ if not token:
506
+ raise RuntimeError("Wholix-Login fehlgeschlagen.")
507
+ return token
508
+
509
+ def get_token(self) -> str:
510
+ if self._token:
511
+ return self._token
512
+ with self._lock:
513
+ if not self._token:
514
+ self._token = self._login()
515
+ return self._token
516
+
517
+ def auth_headers(self) -> Dict[str, str]:
518
+ return {"Authorization": f"Bearer {self.get_token()}"}
519
+
520
+ def req_authed(self, path: str, *, method: str = "GET", headers: Optional[Dict[str, str]] = None,
521
+ json_body: Any = None, data: Any = None, timeout: Tuple[float, float] = (5.0, 30.0)):
522
+ url = path if path.startswith("http") else f"{self.base_url}{path}"
523
+ hdrs = {}
524
+ hdrs.update(headers or {})
525
+ hdrs.update(self.auth_headers())
526
+ try:
527
+ return req(url, method=method, headers=hdrs, json_body=json_body, data=data, timeout=timeout, session=self._ses)
528
+ except HTTPError as e:
529
+ if e.status in (401, 403, 419):
530
+ with self._lock:
531
+ self._token = None
532
+ self._token = self._login()
533
+ hdrs = {}
534
+ hdrs.update(headers or {})
535
+ hdrs.update(self.auth_headers())
536
+ return req(url, method=method, headers=hdrs, json_body=json_body, data=data, timeout=timeout, session=self._ses)
537
+ raise
538
+
539
 
540
+
541
+ def _store_with_fallbacks(token: str, payload: dict, module: str) -> dict:
542
+ """
543
+ Versucht 3 Stufen, um 422 zu vermeiden.
544
+ NEU: nutzt, wenn vorhanden, den globalen WholixSession-Manager (_WHOLIX_SES)
545
+ mit Auto-ReLogin. Fällt sonst auf das übergebene Token zurück.
546
+ """
547
+ url_path = "/api/v1/table-object-data/store-objects"
548
+
549
+ def _post_with_session(body):
550
+ # bevorzugt Auto-ReLogin-Session
551
+ if _WHOLIX_SES is not None:
552
+ return _WHOLIX_SES.req_authed(url_path, method="POST", json_body=body, timeout=(5.0, 30.0))
553
+ # Fallback: manuelles Token (legacy)
554
+ url = f"{WHOLIX_BASE_URL}{url_path}"
555
+ headers = {"Authorization": f"Bearer {token}"}
556
  return req(url, method="POST", headers=headers, json_body=body, timeout=(5.0, 30.0))
557
 
558
  # 1: Original
559
  body1 = {"module": module, "action": "store", "data": [payload]}
560
  try:
561
+ return _post_with_session(body1)
562
  except HTTPError as e:
563
  if e.status != 422:
564
  raise
 
573
  else:
574
  p2.pop(k, None)
575
  try:
576
+ return _post_with_session({"module": module, "action": "store", "data": [p2]})
577
  except HTTPError as e:
578
  if e.status != 422:
579
  raise
 
587
  "exclude_hash": p2.get("exclude_hash") or _slug(uuid.uuid4().hex[:8]),
588
  "tags": p2.get("tags") or {"keys": ["no-email"], "values": ["no-email"]},
589
  }
590
+ return _post_with_session({"module": module, "action": "store", "data": [minimal]})
591
 
592
 
593
  def wholix_store_contact(token: str, record: dict, module: str = "Contacts", allow_placeholder: bool = True) -> dict:
594
  """
595
  Sendet NUR erlaubte Felder an Wholix und saniert problematische Werte.
596
  IMMER speicherbar: erzeugt bei Bedarf Placeholder-Mail (example.com) + Tag.
597
+ Nutzt _store_with_fallbacks (mit Auto-ReLogin via _WHOLIX_SES, falls vorhanden).
598
  """
599
  if not isinstance(record, dict):
600
  raise ValueError("Wholix: record muss ein dict sein.")
601
 
 
602
  def _clean_str(v):
603
  if v is None:
604
  return None
605
  s = str(v).strip()
606
  return s if s else None
607
 
608
+ # E-Mail prüfen / ggf. generieren
609
  email = _clean_str(record.get("email"))
610
  if not email or not EMAIL_RE.match(email):
611
  if allow_placeholder:
 
672
  # Pflichtfeld sicher (jetzt inkl. Platzhalter möglich)
673
  out["email"] = _clean_str(email)
674
 
675
+ # POST mit Fallback-Logik (nutzt Auto-ReLogin falls _WHOLIX_SES vorhanden)
676
  return _store_with_fallbacks(token, out, module)
677
 
678
 
 
683
  per_page: int = 500,
684
  max_pages: int = 100,
685
  dedupe: bool = True) -> List[Dict[str, str]]:
686
+ """
687
+ Paginiert Excludes laden.
688
+ NEU: nutzt, wenn vorhanden, den globalen WholixSession-Manager (_WHOLIX_SES)
689
+ mit Auto-ReLogin. Fällt sonst auf das übergebene Token zurück.
690
+ """
691
+ path = "/api/v1/table-object-data/fetch-paginated-results"
692
  out: List[Dict[str, str]] = []
693
  seen: Set[str] = set()
694
 
 
697
  if page > last_page:
698
  break
699
  payload = {"module": module_name, "action": "search", "page": page, "per_page": per_page}
700
+
701
  try:
702
+ if _WHOLIX_SES is not None:
703
+ res = _WHOLIX_SES.req_authed(path, method="POST", json_body=payload, timeout=(5.0, 30.0))
704
+ else:
705
+ url = f"{WHOLIX_BASE_URL}{path}"
706
+ headers = {"Authorization": f"Bearer {token}"}
707
+ res = req(url, method="POST", headers=headers, json_body=payload, timeout=(5.0, 30.0))
708
  except Exception as e:
709
  APP_LOG.warning(f"Wholix-Excludes Page {page} Fehler: {e}")
710
  break