Spaces:
Sleeping
Sleeping
Update app.py
Browse files
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 |
-
|
| 447 |
-
|
| 448 |
-
|
| 449 |
-
|
| 450 |
-
|
| 451 |
-
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 477 |
"""
|
| 478 |
-
|
| 479 |
-
|
| 480 |
-
|
| 481 |
-
|
| 482 |
"""
|
| 483 |
-
|
| 484 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 485 |
|
| 486 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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
|
| 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
|
| 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
|
| 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 |
-
|
| 618 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|