From bbb9a16986f38f91e88f73054893d9af795c2956 Mon Sep 17 00:00:00 2001 From: "Andrey Cunh@" Date: Fri, 22 May 2026 14:24:01 -0300 Subject: [PATCH] att --- .../installments_backfill_vd.cpython-313.pyc | Bin 0 -> 2044 bytes .../installments_reader.cpython-313.pyc | Bin 38943 -> 45749 bytes __pycache__/trf.cpython-313.pyc | Bin 0 -> 56688 bytes installments_backfill_vd.py | 48 ++ installments_by_order.py | 185 +++++++ installments_reader.py | 473 +++++++++++------- trf_registroerro.py | 246 +++++++++ 7 files changed, 770 insertions(+), 182 deletions(-) create mode 100644 __pycache__/installments_backfill_vd.cpython-313.pyc create mode 100644 __pycache__/trf.cpython-313.pyc create mode 100644 installments_backfill_vd.py create mode 100644 installments_by_order.py create mode 100644 trf_registroerro.py diff --git a/__pycache__/installments_backfill_vd.cpython-313.pyc b/__pycache__/installments_backfill_vd.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..166d2284e5e92422442c4ed78ba09cbeee5b015f GIT binary patch literal 2044 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⁣=Ji zJ4O)6OX&SEc%nA>sKhf5kDgqZPWJ03uo7-m{!{4vnNaqrijXLsTA1hm5)-asNwU2O zhnyLIxD@|YBHx5)h{r)G1)31(T2PeCP9&LHeE7gWhfs)AyNV&)uKe>D`V;iNh#rzR z9ywb61@!)V75Wcj;+HV=B6`TH@@we*DSCuqZG$4nuL@fuYT>7!GF0u|%QKKB%23C` zZ$QBU?!@(Yoo%!A(p4)BBz|_|(C0S^Ii()V4Qj=?crtg5g@OuH;irYRzz-ww;~n=u!kxgJo1VvnU(Hv)y` zK%}ETS~d`62BNuxSGAi6dDr^b`tYp^!Zw_Yvg2WwC+s;D&h*|^ofeKS)k#+F6Rs{@ zR+}P#!Dys(mmt4h z^S&#?KUC_LYF8R=XO(Q`7v4bv4ZpJRO!br1PuGI@QlxpClGrpbMxe0M+Q@seyr-MO z1$EK<`fy$YFaeE@n4>yuuL;+%$l7q*FV2j(7MC8ltu6b0PT7(~^MA+&9_YzZ|0V7)_^K-T^(uI7p>`DR>MQS)1NDkpnj`t<$<-! zw=G@3*uq=CRo7|{wEw!P@h62PD@UF?@$88;Wwf+yqjV&^Yb08DXsK_x@p?ZgT(>4! zonLERFT0ubmCAKnIIr(@)BZaOE0tS**Cwn#dW8P6pcgpfQ4K7UcYiTPQ(61ycYo2R z6#T-G(Cg~mzX_r~>pM#8X{YQvB}L%BYSEmomAzV~0RQzu`9z!a^)mbECdG!-2;mKL zSL^9E*&7)Wh`gaI!*CM`w_sSz$FOK`f$;aUu=3wE5x-3{(W3mZqcR>2cxgMo?j@n4^@VIq%SA$sHt!8^hDFBc7A=puR_Lk|@@ z@e_?GVDQ`$Wi#+{iaZC4A4TL`c?%2vJ$haLfId-`{~AN6@g}4!{wqe^nAVCO1D>eb z1v-I+8Q*UHM^Fy_so>9qM;^D*2H^{jS2{JIs(fPiqr2htSeP=z81Uh@V>0Nb75Nqg zU#lf|@oUQ_gdg7%y4#jTmu^)WY28xu76o6cssIa@XIz^atwfEZ?rCFU)OCZNmNDYbC-e-&Tgqm zR6|({SF)efZDpLJLr_{vnQY75*E_d#Bs-JJ$lTJCpnroFltm0I7BsXpGfC8cC} HWS0IPUj3VS delta 15235 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 literal 0 HcmV?d00001 diff --git a/installments_backfill_vd.py b/installments_backfill_vd.py new file mode 100644 index 0000000..1f0d972 --- /dev/null +++ b/installments_backfill_vd.py @@ -0,0 +1,48 @@ +import os +from datetime import date + +import installments_reader + + +def _setdefault_env(key: str, value: str) -> None: + if not os.getenv(key): + os.environ[key] = value + + +def main() -> None: + today = date.today() + start_of_year = date(today.year, 1, 1) + + defaults = { + "STORE_CHANNEL": "VD", + "START_INSTALLMENT_CHANGE_DATE": start_of_year.isoformat(), + "END_INSTALLMENT_CHANGE_DATE": today.isoformat(), + "CHUNK_DAYS": "30", + "LAST_N_DAYS": "", + "STORE_WORKERS": "1", + "GROUP_WORKERS": "1", + "WRITE_SQL": "1", + "SAVE_JSON": "0", + "INCREMENTAL_MODE": "0", + "LOG_GROUP_CODES": "0", + "INSTALLMENTS_PAGE_SIZE": "50", + "FLUSH_EVERY_PAGES": "50", + "INSTALLMENTS_MIN_INTERVAL_MS": "3500", + "INSTALLMENTS_429_MIN_WAIT_SEC": "45", + "THROTTLE_RECOVERY_PAUSE_SEC": "900", + } + for key, value in defaults.items(): + _setdefault_env(key, value) + + print( + "[backfill] modo=VD " + f"periodo={os.environ['START_INSTALLMENT_CHANGE_DATE']}..{os.environ['END_INSTALLMENT_CHANGE_DATE']} " + f"chunk_days={os.environ['CHUNK_DAYS']} write_sql={os.environ['WRITE_SQL']} " + f"min_interval_ms={os.environ['INSTALLMENTS_MIN_INTERVAL_MS']} " + f"wait429_min_s={os.environ['INSTALLMENTS_429_MIN_WAIT_SEC']}" + ) + installments_reader.main() + + +if __name__ == "__main__": + main() diff --git a/installments_by_order.py b/installments_by_order.py new file mode 100644 index 0000000..d6299c0 --- /dev/null +++ b/installments_by_order.py @@ -0,0 +1,185 @@ +""" +Consulta a API de parcelas por installmentGroupCode (orderNumber) e grava +em DocPedidos + DocPedidosParcelas. + +Fonte dos códigos: SELECT orderNumber FROM Grgb_fiscal_invoices_items +""" +import os +import time +import requests + +import installments_reader +from installments_reader import Auth, get_installments_page, upsert_doc_pedidos_sqlserver + +# ─── CONFIGURE AQUI ─────────────────────────────────────────────────────────── +WRITE_SQL = True # True = grava no banco | False = só lê e exibe +SKIP_EXISTING = True # True = pula códigos já gravados em DocPedidos +BATCH_SIZE = 10 # grava no banco a cada N pedidos processados +MIN_INTERVAL_MS = 3500 # ms mínimo entre chamadas à API +# ────────────────────────────────────────────────────────────────────────────── + +SQL_CONN = ( + "DRIVER={ODBC Driver 17 for SQL Server};" + "SERVER=10.77.77.10;" + "DATABASE=GINSENG;" + "UID=andrey;" + "PWD=88253332;" + "TrustServerCertificate=yes;" +) + + +def _get_order_numbers() -> list[str]: + import pyodbc + cn = pyodbc.connect(SQL_CONN, timeout=30) + cur = cn.cursor() + cur.execute( + "SELECT DISTINCT [orderNumber] " + "FROM [GINSENG].[dbo].[Grgb_fiscal_invoices_items] " + "WHERE [orderNumber] IS NOT NULL AND LTRIM(RTRIM(CAST([orderNumber] AS VARCHAR))) <> ''" + ) + return [str(row[0]).strip() for row in cur.fetchall()] + + +def _get_existing_group_codes() -> set[str]: + import pyodbc + try: + cn = pyodbc.connect(SQL_CONN, timeout=30) + cur = cn.cursor() + cur.execute("SELECT InstallmentGroupCode FROM dbo.DocPedidos") + return {str(row[0]).strip() for row in cur.fetchall()} + except Exception: + return set() + + +def _fetch_all_pages(session: requests.Session, auth: Auth, group_code: str) -> list: + all_items = [] + page = 1 + while True: + body = get_installments_page( + session=session, + auth=auth, + start_date=None, + end_date=None, + installment_change=None, + mediator_code=None, + page=page, + installment_group_code=group_code, + cookie_header=None, + ) + items = ((body.get("data") or {}).get("installments")) or [] + all_items.extend(items) + + pagination = (body.get("data") or {}).get("pagination") or {} + total = int(pagination.get("total") or 0) + limit = int(pagination.get("limit") or len(items) or 10) + total_pages = max(1, (total + limit - 1) // limit) if total else page + + print(f" pagina={page}/{total_pages} itens={len(items)}") + if not items or page >= total_pages: + break + page += 1 + + return all_items + + +def _flush(batch: list, label: str = "") -> dict: + if not batch or not WRITE_SQL: + return {} + stats = upsert_doc_pedidos_sqlserver(batch, SQL_CONN) + print( + f"[sql] {label} " + f"pedidos={stats.get('pedidos', 0)} parcelas={stats.get('parcelas', 0)}" + ) + return stats + + +def main() -> None: + os.environ.setdefault("INSTALLMENTS_MIN_INTERVAL_MS", str(MIN_INTERVAL_MS)) + os.environ.setdefault("INSTALLMENTS_429_MIN_WAIT_SEC", "45") + os.environ.setdefault("THROTTLE_RECOVERY_PAUSE_SEC", "900") + + print("[info] buscando orderNumbers no banco...") + order_numbers = _get_order_numbers() + print(f"[info] total encontrado: {len(order_numbers)}") + + existing: set[str] = set() + if SKIP_EXISTING: + existing = _get_existing_group_codes() + print(f"[info] já em DocPedidos: {len(existing)} — serão pulados") + + pending = [c for c in order_numbers if c not in existing] + print(f"[info] para consultar na API: {len(pending)}\n") + + session = requests.Session() + session.trust_env = False + auth = Auth(session) + + batch: list = [] + batch_first_idx: int = 0 + batch_first_code: str = "" + total_pedidos = 0 + total_parcelas = 0 + erros = 0 + start_ts = time.monotonic() + + def _fmt(s: float) -> str: + s = int(s) + if s < 60: + return f"{s}s" + if s < 3600: + return f"{s // 60}m:{s % 60:02d}s" + return f"{s // 3600}h:{(s % 3600) // 60:02d}m" + + for idx, group_code in enumerate(pending, 1): + elapsed = time.monotonic() - start_ts + avg_s = elapsed / idx + restante = avg_s * (len(pending) - idx) + print(f"[{idx}/{len(pending)}] group_code={group_code} " + f"decorrido={_fmt(elapsed)} restante~{_fmt(restante)}") + try: + items = _fetch_all_pages(session, auth, group_code) + if not items: + print(" sem dados na API") + continue + + if not batch: + batch_first_idx = idx + batch_first_code = group_code + + batch.append({ + "installmentGroupCode": group_code, + "rawResponse": {"data": {"installments": items}}, + "installments": items, + }) + + if len(batch) >= BATCH_SIZE: + label = (f"inserindo consultas {batch_first_idx}..{idx} " + f"(group {batch_first_code} até {group_code})") + stats = _flush(batch, label) + total_pedidos += stats.get("pedidos", 0) + total_parcelas += stats.get("parcelas", 0) + batch = [] + batch_first_idx = 0 + batch_first_code = "" + + except Exception as exc: + print(f" [erro] {exc}") + erros += 1 + + # flush final + if batch: + label = (f"inserindo consultas {batch_first_idx}..{len(pending)} " + f"(group {batch_first_code} até {batch[-1]['installmentGroupCode']})") + else: + label = "flush final" + stats = _flush(batch, label) + total_pedidos += stats.get("pedidos", 0) + total_parcelas += stats.get("parcelas", 0) + + print( + f"\n[fim] pedidos={total_pedidos} parcelas={total_parcelas} erros={erros}" + ) + + +if __name__ == "__main__": + main() diff --git a/installments_reader.py b/installments_reader.py index 9547fe7..717bb97 100644 --- a/installments_reader.py +++ b/installments_reader.py @@ -3,6 +3,7 @@ from concurrent.futures import ThreadPoolExecutor, as_completed import json import os import re +import threading import time from dataclasses import dataclass from datetime import date, timedelta @@ -14,10 +15,27 @@ import requests TOKENS_URL = "https://api.grupoginseng.com.br/api/tokens" STORES_URL = "https://api-extranet.grupoboticario.digital/api/person-logged/stores" INSTALLMENTS_URL = ( - "https://bff-credit-container-portal-apigw.produto-financeiro.grupoboticario.digital/" + "https://bff-credit-container-portal-apigw.prd.produto-financeiro.app.grupoboticario.com.br/" "v1/franchisee/installments" ) +_RATE_LIMIT_LOCK = threading.Lock() +_LAST_INSTALLMENTS_REQUEST_TS = 0.0 + + +def _set_global_backoff(wait_s: float) -> None: + """After a 429, push the shared timestamp forward so ALL threads pause.""" + global _LAST_INSTALLMENTS_REQUEST_TS + min_interval_ms_raw = os.getenv("INSTALLMENTS_MIN_INTERVAL_MS", "0").strip() + try: + min_interval_s = int(min_interval_ms_raw or "0") / 1000.0 + except Exception: + min_interval_s = 0.0 + with _RATE_LIMIT_LOCK: + target = time.monotonic() + wait_s - min_interval_s + if target > _LAST_INSTALLMENTS_REQUEST_TS: + _LAST_INSTALLMENTS_REQUEST_TS = target + def _jwt_payload(jwt_token: str) -> Dict[str, Any]: parts = jwt_token.split(".") @@ -65,12 +83,40 @@ class Auth: self.cache = TokenCache() +def _apply_installments_pacing() -> None: + min_interval_ms_raw = os.getenv("INSTALLMENTS_MIN_INTERVAL_MS", "0").strip() + try: + min_interval_ms = int(min_interval_ms_raw or "0") + except Exception: + min_interval_ms = 0 + if min_interval_ms <= 0: + return + + min_interval_s = float(min_interval_ms) / 1000.0 + global _LAST_INSTALLMENTS_REQUEST_TS + while True: + with _RATE_LIMIT_LOCK: + now = time.monotonic() + wait_s = (_LAST_INSTALLMENTS_REQUEST_TS + min_interval_s) - now + if wait_s <= 0: + _LAST_INSTALLMENTS_REQUEST_TS = now + return + time.sleep(min(wait_s, 1.0)) + + def _headers(auth: Auth, cookie_header: Optional[str]) -> Dict[str, str]: h = { "Authorization": auth.get_bearer(), - "Accept": "application/json, text/plain, */*", + "Accept": "*/*", + "Accept-Language": "pt-BR,pt;q=0.9,en;q=0.8,en-US;q=0.7", + "Cache-Control": "no-cache", + "Pragma": "no-cache", + "Content-Type": "application/json", "Origin": "https://extranet.grupoboticario.com.br", "Referer": "https://extranet.grupoboticario.com.br/", + "Sec-Fetch-Dest": "empty", + "Sec-Fetch-Mode": "cors", + "Sec-Fetch-Site": "cross-site", "User-Agent": "Mozilla/5.0", } if cookie_header: @@ -88,8 +134,11 @@ def get_installments_page( page: int, installment_group_code: Optional[str] = None, cookie_header: Optional[str] = None, + page_size: Optional[int] = None, ) -> Dict[str, Any]: params: Dict[str, Any] = {"page": page} + if page_size and page_size > 0: + params["limit"] = page_size if installment_group_code: params["installmentGroupCode"] = installment_group_code else: @@ -105,6 +154,7 @@ def get_installments_page( max_attempts = 8 transient_status = {429, 500, 502, 503, 504} for attempt in range(max_attempts): + _apply_installments_pacing() r = session.get( INSTALLMENTS_URL, headers=_headers(auth, cookie_header=cookie_header), @@ -124,6 +174,21 @@ def get_installments_page( break if r.status_code in transient_status: wait_s = min(30, 2 ** min(5, attempt)) + if r.status_code == 429: + min_429_wait_raw = os.getenv("INSTALLMENTS_429_MIN_WAIT_SEC", "3").strip() + try: + min_429_wait = float(min_429_wait_raw or "3") + except Exception: + min_429_wait = 3.0 + retry_after = r.headers.get("Retry-After") or r.headers.get("retry-after") + if retry_after: + try: + min_429_wait = max(min_429_wait, float(retry_after)) + print(f"[info] Retry-After da API: {retry_after}s") + except Exception: + pass + wait_s = max(wait_s, min_429_wait) + _set_global_backoff(wait_s) print( f"[warn] erro temporario {r.status_code} no installments " f"(tentativa {attempt + 1}/{max_attempts}), aguardando {wait_s}s..." @@ -142,6 +207,13 @@ def get_installments_page( f"url={INSTALLMENTS_URL}?{urlencode(params)} body={r.text[:500]}" ) if r.status_code in transient_status: + if r.status_code == 429: + recovery_s = float(os.getenv("THROTTLE_RECOVERY_PAUSE_SEC", "900")) + print( + f"[throttle-recovery] 429 persistente apos {max_attempts} tentativas. " + f"Aplicando cooldown global de {int(recovery_s)}s para proxima loja..." + ) + _set_global_backoff(recovery_s) raise RuntimeError( f"Falha temporaria persistente ({r.status_code}) no installments apos {max_attempts} tentativas. " f"url={INSTALLMENTS_URL}?{urlencode(params)} body={r.text[:500]}" @@ -151,14 +223,31 @@ def get_installments_page( f"400 Bad Request em installments. " f"url={INSTALLMENTS_URL}?{urlencode(params)} body={r.text[:500]}" ) + if r.status_code == 412: + raise RuntimeError( + "412 Precondition Failed em installments. " + "A API recusou a pre-condicao da requisicao (normalmente cabecalhos/sessao). " + f"url={INSTALLMENTS_URL}?{urlencode(params)} body={r.text[:500]}" + ) r.raise_for_status() return r.json() -def get_store_codes(session: requests.Session, auth: Auth, cookie_header: Optional[str]) -> list[int]: +def get_store_codes( + session: requests.Session, + auth: Auth, + cookie_header: Optional[str], + allowed_channels: Optional[set[str]] = None, +) -> list[int]: r = session.get(STORES_URL, headers=_headers(auth, cookie_header=cookie_header), timeout=30) r.raise_for_status() data = r.json().get("data") or [] + if allowed_channels: + data = [ + x + for x in data + if str(x.get("channel") or "").strip().casefold() in allowed_channels + ] out = sorted({int(x.get("code")) for x in data if x.get("code") is not None}) return out @@ -453,21 +542,37 @@ WHERE DocPedidoId = ? AND InstallmentCode NOT IN ({placeholders}) def main() -> None: today = date.today() - last_days_env = os.getenv("LAST_N_DAYS", "5").strip() - last_n_days = int(last_days_env) if last_days_env else None - rolling_start = (today - timedelta(days=last_n_days)).isoformat() if last_n_days is not None else None - default_start = date(today.year, 1, 1).isoformat() - default_end = today.isoformat() start_date_env = os.getenv("START_INSTALLMENT_CHANGE_DATE") start_date_fixed = (start_date_env or "").strip() or None + last_days_env = os.getenv("LAST_N_DAYS", "5").strip() + last_n_days = int(last_days_env) if last_days_env else None + rolling_start = None + if (not start_date_fixed) and (last_n_days is not None): + rolling_start = (today - timedelta(days=last_n_days)).isoformat() + chunk_days_env = os.getenv("CHUNK_DAYS", "0").strip() + chunk_days = int(chunk_days_env) if chunk_days_env else 0 + if chunk_days < 0: + chunk_days = 0 + default_start = date(today.year, 1, 1).isoformat() + default_end = today.isoformat() end_date = os.getenv("END_INSTALLMENT_CHANGE_DATE", default_end) installment_change = os.getenv("INSTALLMENT_CHANGE", "CRIACAO").strip() or "CRIACAO" first_page = int(os.getenv("PAGE_START", "1")) max_pages_per_query = int(os.getenv("MAX_PAGES_PER_QUERY", "10000")) + page_size_env = os.getenv("INSTALLMENTS_PAGE_SIZE", "").strip() + page_size = int(page_size_env) if page_size_env else None + flush_every_pages = int(os.getenv("FLUSH_EVERY_PAGES", "50")) # Padrao organizado: uma loja por vez (logs nao ficam intercalados). store_workers = int(os.getenv("STORE_WORKERS", "1")) - group_workers = int(os.getenv("GROUP_WORKERS", "4")) cookie_header = os.getenv("EXTRANET_COOKIE") + store_channel_env = os.getenv("STORE_CHANNEL", "VD").strip() + allowed_channels = { + c.strip().casefold() + for c in re.split(r"[,\s;]+", store_channel_env) + if c.strip() + } + if "all" in allowed_channels or "*" in allowed_channels: + allowed_channels = set() target_mediator_env = os.getenv("TARGET_MEDIATOR_CODE", "").strip() target_mediator = int(target_mediator_env) if target_mediator_env else None resume_from_env = os.getenv("RESUME_FROM_MEDIATOR_CODE", "").strip() @@ -494,7 +599,15 @@ def main() -> None: session.trust_env = False auth = Auth(session) - stores = get_store_codes(session, auth, cookie_header) + stores = get_store_codes( + session, + auth, + cookie_header, + allowed_channels=allowed_channels or None, + ) + if allowed_channels: + channels_txt = ",".join(sorted(c.upper() for c in allowed_channels)) + print(f"[info] filtro de canal ativo: STORE_CHANNEL={channels_txt} (lojas={len(stores)})") if target_mediator is not None: stores = [target_mediator] print(f"[info] consulta focada na loja {target_mediator}") @@ -509,8 +622,10 @@ def main() -> None: print(f"[info] incremental ativo com watermark em {watermark_file}") else: print("[info] incremental desativado") - if last_n_days is not None: + if rolling_start is not None and last_n_days is not None: print(f"[info] janela movel ativa: ultimos {last_n_days} dias ({rolling_start}..{end_date})") + if chunk_days > 0: + print(f"[info] fatiamento de periodo ativo: CHUNK_DAYS={chunk_days}") authorized: list[int] = [] unauthorized: list[int] = [] @@ -531,173 +646,158 @@ def main() -> None: local_session = requests.Session() local_session.trust_env = False local_auth = Auth(local_session) - store_bearer = local_auth.get_bearer() - try: print( f"[consulta] loja={mediator_code} periodo={store_start_date}..{end_date} pagina_inicial={first_page}" ) - page = first_page - all_installments: List[Dict[str, Any]] = [] + store_start_obj = _parse_iso_date(store_start_date) + store_end_obj = _parse_iso_date(end_date) + if not store_start_obj or not store_end_obj: + raise RuntimeError( + f"Periodo invalido para consulta. start={store_start_date} end={end_date}" + ) + if store_start_obj > store_end_obj: + raise RuntimeError( + f"Periodo invalido para consulta. start={store_start_date} end={end_date}" + ) + + windows: List[tuple[str, str]] = [] + if chunk_days > 0: + cursor = store_start_obj + step_days = max(1, chunk_days) + while cursor <= store_end_obj: + win_end = min(store_end_obj, cursor + timedelta(days=step_days - 1)) + windows.append((cursor.isoformat(), win_end.isoformat())) + cursor = win_end + timedelta(days=1) + else: + windows = [(store_start_obj.isoformat(), store_end_obj.isoformat())] + + all_group_codes_seen: set = set() total_api = 0 - total_pages = 1 - while True: - try: - body = get_installments_page( - session=local_session, - auth=local_auth, - start_date=store_start_date, - end_date=end_date, - installment_change=installment_change, - mediator_code=mediator_code, - page=page, - cookie_header=cookie_header, - ) - except Exception as e: - msg = str(e) - if "400 Bad Request" in msg and installment_change: + pages_fetched_total = 0 + total_sql_pedidos = 0 + total_sql_parcelas = 0 + + def _flush(buffer: List[Dict[str, Any]], label: str) -> None: + """Agrupa itens do scan por installmentGroupCode e persiste no banco.""" + nonlocal total_sql_pedidos, total_sql_parcelas + if not buffer: + return + groups: Dict[str, List[Dict[str, Any]]] = {} + for item in _dedupe_installments(buffer): + gc = str(item.get("installmentGroupCode") or "").strip() + if not gc or gc in all_group_codes_seen: + continue + groups.setdefault(gc, []).append(item) + if not groups: + return + all_group_codes_seen.update(groups.keys()) + if log_group_codes: + for gc in groups: + print(f"[grupo] loja={mediator_code} {label} installmentGroupCode={gc}") + print(f"[flush] loja={mediator_code} {label} grupos_novos={len(groups)}") + if write_sql: + sql_rows: List[Dict[str, Any]] = [] + for gc, items in groups.items(): + sql_rows.append({ + "mediatorCode": mediator_code, + "installmentGroupCode": gc, + "rawResponse": {"data": {"installments": items}}, + "installments": items, + }) + if sql_rows: + stats = upsert_doc_pedidos_sqlserver(sql_rows, sql_conn, mediator_code_log=mediator_code) + total_sql_pedidos += stats.get("pedidos", 0) + total_sql_parcelas += stats.get("parcelas", 0) print( - f"[fallback] loja={mediator_code} 400 com installmentChange={installment_change}; " - "tentando sem installmentChange" + f"[sql-flush] loja={mediator_code} {label} " + f"pedidos_upsert={stats.get('pedidos')} parcelas_upsert={stats.get('parcelas')}" ) + + for window_idx, (window_start, window_end) in enumerate(windows, start=1): + print( + f"[consulta-janela] loja={mediator_code} janela={window_idx}/{len(windows)} " + f"periodo={window_start}..{window_end}" + ) + + page = first_page + window_total = 0 + window_total_pages = 1 + window_pages_fetched = 0 + flush_buffer: List[Dict[str, Any]] = [] + while True: + try: body = get_installments_page( session=local_session, auth=local_auth, - start_date=store_start_date, - end_date=end_date, - installment_change=None, + start_date=window_start, + end_date=window_end, + installment_change=installment_change, mediator_code=mediator_code, page=page, cookie_header=cookie_header, + page_size=page_size, ) - else: - raise - installments_page = (((body.get("data") or {}).get("installments")) or []) - all_installments.extend(installments_page) - pagination = ((body.get("data") or {}).get("pagination") or {}) - total_api = int(pagination.get("total") or len(all_installments)) - limit = int(pagination.get("limit") or len(installments_page) or 1) - total_pages = max(1, (total_api + limit - 1) // limit) if total_api else page - print( - f"[pagina-loja] loja={mediator_code} pagina={page}/{total_pages} " - f"itens_pagina={len(installments_page)}" - ) - if not installments_page: - break - if page >= total_pages: - break - if page - first_page + 1 >= max_pages_per_query: + except Exception as e: + msg = str(e) + if "400 Bad Request" in msg and installment_change: + print( + f"[fallback] loja={mediator_code} 400 com installmentChange={installment_change}; " + "tentando sem installmentChange" + ) + body = get_installments_page( + session=local_session, + auth=local_auth, + start_date=window_start, + end_date=window_end, + installment_change=None, + mediator_code=mediator_code, + page=page, + cookie_header=cookie_header, + page_size=page_size, + ) + else: + raise + installments_page = (((body.get("data") or {}).get("installments")) or []) + flush_buffer.extend(installments_page) + pagination = ((body.get("data") or {}).get("pagination") or {}) + window_total = int(pagination.get("total") or 0) + limit = int(pagination.get("limit") or len(installments_page) or 1) + window_total_pages = max(1, (window_total + limit - 1) // limit) if window_total else page + window_pages_fetched += 1 print( - f"[stop] loja={mediator_code} limite MAX_PAGES_PER_QUERY={max_pages_per_query} atingido" + f"[pagina-loja] loja={mediator_code} janela={window_idx}/{len(windows)} " + f"pagina={page}/{window_total_pages} itens_pagina={len(installments_page)}" ) - break - page += 1 + if flush_every_pages > 0 and window_pages_fetched % flush_every_pages == 0: + _flush(flush_buffer, f"janela={window_idx}/{len(windows)} pagina={page}") + flush_buffer = [] + if not installments_page: + break + if page >= window_total_pages: + break + if page - first_page + 1 >= max_pages_per_query: + print( + f"[stop] loja={mediator_code} janela={window_idx}/{len(windows)} " + f"limite MAX_PAGES_PER_QUERY={max_pages_per_query} atingido" + ) + break + page += 1 + + # Flush do restante da janela que não atingiu o threshold + _flush(flush_buffer, f"janela={window_idx}/{len(windows)}") + + total_api += window_total + pages_fetched_total += window_pages_fetched + print( + f"[resultado-janela] loja={mediator_code} janela={window_idx}/{len(windows)} " + f"grupos_acumulados={len(all_group_codes_seen)}" + ) - installments = _dedupe_installments(all_installments) - group_codes = sorted( - { - str(item.get("installmentGroupCode")).strip() - for item in installments - if item.get("installmentGroupCode") is not None - and str(item.get("installmentGroupCode")).strip() - } - ) print( f"[resultado-loja] loja={mediator_code} pedidos_encontrados={total_api} " - f"itens_total_agregados={len(installments)} grupos_unicos={len(group_codes)}" + f"grupos_unicos={len(all_group_codes_seen)}" ) - if log_group_codes: - for gc in group_codes: - print(f"[grupo] loja={mediator_code} installmentGroupCode={gc}") - - pedidos: Dict[str, Any] = {} - - def fetch_group(group_code: str) -> Dict[str, Any]: - try: - group_session = requests.Session() - group_session.trust_env = False - group_auth = Auth(group_session) - # Reutiliza o token da loja para evitar nova ida ao endpoint /api/tokens por grupo. - group_auth.override_bearer = store_bearer - group_page = first_page - all_group_installments: List[Dict[str, Any]] = [] - group_total = 0 - group_total_pages = 1 - while True: - group_body = get_installments_page( - session=group_session, - auth=group_auth, - start_date=None, - end_date=None, - installment_change=None, - mediator_code=None, - page=group_page, - installment_group_code=group_code, - cookie_header=cookie_header, - ) - group_page_items = (((group_body.get("data") or {}).get("installments")) or []) - all_group_installments.extend(group_page_items) - group_pagination = ((group_body.get("data") or {}).get("pagination") or {}) - group_total = int(group_pagination.get("total") or len(all_group_installments)) - group_limit = int(group_pagination.get("limit") or len(group_page_items) or 1) - group_total_pages = ( - max(1, (group_total + group_limit - 1) // group_limit) if group_total else group_page - ) - print( - f"[grupo-pagina] loja={mediator_code} installmentGroupCode={group_code} " - f"pagina={group_page}/{group_total_pages} itens_pagina={len(group_page_items)}" - ) - if not group_page_items: - break - if group_page >= group_total_pages: - break - if group_page - first_page + 1 >= max_pages_per_query: - print( - f"[stop] grupo={group_code} limite MAX_PAGES_PER_QUERY={max_pages_per_query} atingido" - ) - break - group_page += 1 - - group_installments = _dedupe_installments(all_group_installments) - count = len(group_installments) - print(f"[grupo-ok] loja={mediator_code} installmentGroupCode={group_code} itens={count}") - group_body_agg = { - "data": { - "installments": group_installments, - "pagination": {"limit": count, "total": group_total or count}, - }, - "status": 200, - "message": "Success", - } - return { - "pedidoNumero": group_code, - "consulta": { - "installmentGroupCode": group_code, - "pageStart": first_page, - "pagesFetched": group_total_pages, - }, - "ok": True, - "totalItens": count, - "detalhes": group_body_agg, - } - except Exception as e: - print(f"[grupo-erro] loja={mediator_code} installmentGroupCode={group_code} erro={e}") - return { - "pedidoNumero": group_code, - "consulta": { - "installmentGroupCode": group_code, - "pageStart": first_page, - }, - "ok": False, - "error": str(e), - } - - if group_codes: - with ThreadPoolExecutor(max_workers=max(1, group_workers)) as group_pool: - group_futures = {group_pool.submit(fetch_group, gc): gc for gc in group_codes} - for fut in as_completed(group_futures): - gc = group_futures[fut] - pedidos[gc] = fut.result() store_out = { "queryWindow": { @@ -705,35 +805,19 @@ def main() -> None: "endInstallmentChangeDate": end_date, "installmentChange": installment_change, "pageStart": first_page, - "pagesFetched": total_pages, + "pagesFetched": pages_fetched_total, + "windowsFetched": len(windows), + "chunkDays": chunk_days if chunk_days > 0 else None, }, "mediatorCode": mediator_code, - "pedidosEncontradosPagina": len(group_codes), - "pedidos": pedidos, + "pedidosEncontradosPagina": len(all_group_codes_seen), + "grupos_unicos": len(all_group_codes_seen), } if write_sql: - sql_rows: List[Dict[str, Any]] = [] - for gc, pedido_payload in pedidos.items(): - if not pedido_payload.get("ok"): - continue - raw_response = pedido_payload.get("detalhes") or {} - sql_rows.append( - { - "mediatorCode": mediator_code, - "installmentGroupCode": gc, - "rawResponse": raw_response, - "installments": (((raw_response.get("data") or {}).get("installments")) or []), - } - ) - stats = upsert_doc_pedidos_sqlserver( - sql_rows, - sql_conn, - mediator_code_log=mediator_code, - ) - store_out["sqlUpsert"] = stats + store_out["sqlUpsert"] = {"pedidos": total_sql_pedidos, "parcelas": total_sql_parcelas} print( - f"[sql] loja={mediator_code} pedidos_upsert={stats.get('pedidos')} " - f"parcelas_upsert={stats.get('parcelas')}" + f"[sql] loja={mediator_code} pedidos_upsert={total_sql_pedidos} " + f"parcelas_upsert={total_sql_parcelas}" ) if save_json: store_file = os.path.join(output_dir, f"installments_loja_{mediator_code}.json") @@ -781,6 +865,31 @@ def main() -> None: failed[mediator_code] = f"erro no worker: {e}" print(f"[falha] loja={mediator_code} erro no worker: {e}") + retry_wait_sec = int(os.getenv("RETRY_FAILED_WAIT_SEC", "90")) + if failed and retry_wait_sec > 0: + retry_candidates = sorted( + k for k, v in failed.items() if "429" in str(v) or "temporaria" in str(v).lower() + ) + if retry_candidates: + print( + f"[retry] {len(retry_candidates)} loja(s) falharam por throttle. " + f"Aguardando {retry_wait_sec}s antes de retentar sequencialmente..." + ) + time.sleep(retry_wait_sec) + for mc in retry_candidates: + result = process_store(mc) + status = result["status"] + if status == "authorized": + authorized.append(mc) + failed.pop(mc, None) + if incremental_mode: + watermark[str(mc)] = str(result["endDate"]) + save_watermark(watermark_file, watermark) + print(f"[retry-ok] loja={mc}") + else: + failed[mc] = str(result.get("error") or "erro desconhecido") + print(f"[retry-falha] loja={mc} erro={failed[mc]}") + print( "[resumo] " f"lojas_total={len(stores)} autorizadas={len(authorized)} " diff --git a/trf_registroerro.py b/trf_registroerro.py new file mode 100644 index 0000000..086b88c --- /dev/null +++ b/trf_registroerro.py @@ -0,0 +1,246 @@ +import json +from typing import Any, Dict, Optional, Set + +import trf + +FRANCHISES_LIST_URL = "https://sf-relatorios-api.grupoboticario.digital/v1/franchises/list/franchise" + + +def _collect_store_codes(payload: Any) -> list[str]: + keys = ("sapCode", "code", "franchiseId", "franchiseCode", "mediatorCode", "storeCode") + out: list[str] = [] + seen = set() + + def _push(v: Any) -> None: + if v is None: + return + s = str(v).strip() + if not s: + return + if s not in seen: + seen.add(s) + out.append(s) + + def _walk(obj: Any) -> None: + if isinstance(obj, dict): + for k in keys: + if k in obj: + _push(obj.get(k)) + for v in obj.values(): + _walk(v) + return + if isinstance(obj, list): + for item in obj: + _walk(item) + + _walk(payload) + return out + + +class ClientLoja(trf.Client): + def __init__(self, store_code: str): + super().__init__() + self.store_code = str(store_code).strip() + + def get_franchises(self, only_channels: Optional[Set[str]] = None): + r = self.s.get(FRANCHISES_LIST_URL, headers=self._headers_json(), timeout=30) + if r.status_code in (401, 403): + self.auth.invalidate() + r = self.s.get(FRANCHISES_LIST_URL, headers=self._headers_json(), timeout=30) + r.raise_for_status() + + body = r.json() + source = body.get("data") if isinstance(body, dict) and "data" in body else body + lojas = _collect_store_codes(source) + if not lojas: + raise RuntimeError( + "Endpoint de franquias nao retornou codigos de loja validos. " + f"Resposta resumida: {str(body)[:500]}" + ) + + if self.store_code not in lojas: + raise RuntimeError( + f"Loja {self.store_code} nao encontrada/sem permissao no usuario. " + f"Lojas disponiveis: {', '.join(lojas[:30])}{'...' if len(lojas) > 30 else ''}" + ) + return [self.store_code] + + +def sincronizar_paginas_sqlserver_loja( + connection_string: str, + store_code: str, + cp_id: int = 10269, + document_type: str = "EFAT", + limit: int = 100, + start_offset: int = 0, + only_channels: Optional[Set[str]] = None, + commit_cada_paginas: int = 1, +) -> Dict[str, Any]: + cli = ClientLoja(store_code) + offset = start_offset + paginas = 0 + docs_persistidos = 0 + total = None + + with trf.SqlServerSink(connection_string) as sink: + sink.ensure_schema() + while True: + pagina = cli.processar_pagina( + cp_id=cp_id, + document_type=document_type, + offset=offset, + limit=limit, + only_channels=only_channels, + ) + if total is None: + total = int(pagina.get("total") or 0) + + itens = pagina.get("items") or [] + persistidos_pag, novos_pag = sink.persist_items(itens) + docs_persistidos += persistidos_pag + paginas += 1 + + if paginas % commit_cada_paginas == 0: + sink.cn.commit() + + print( + f"[sync][loja={store_code}] offset={offset} count={len(itens)} " + f"novos_pag={novos_pag} persistidos={docs_persistidos} total={total}" + ) + if not pagina.get("hasNext"): + break + offset += limit + + sink.cn.commit() + + return { + "store_code": store_code, + "total": total, + "paginas_processadas": paginas, + "documentos_persistidos": docs_persistidos, + "offset_final": offset, + } + + +def sincronizar_incremental_sqlserver_loja( + connection_string: str, + store_code: str, + cp_id: int = 10269, + document_type: str = "EFAT", + limit: int = 100, + only_channels: Optional[Set[str]] = None, + max_paginas_sem_novidade: int = 3, + max_paginas: int = 20, +) -> Dict[str, Any]: + cli = ClientLoja(store_code) + offset = 0 + paginas = 0 + docs_persistidos = 0 + docs_novos = 0 + sem_novidade = 0 + total = None + + with trf.SqlServerSink(connection_string) as sink: + sink.ensure_schema() + while True: + pagina = cli.processar_pagina( + cp_id=cp_id, + document_type=document_type, + offset=offset, + limit=limit, + only_channels=only_channels, + ) + if total is None: + total = int(pagina.get("total") or 0) + + itens = pagina.get("items") or [] + persistidos_pag, novos_pag = sink.persist_items(itens) + sink.cn.commit() + + docs_persistidos += persistidos_pag + docs_novos += novos_pag + paginas += 1 + sem_novidade = 0 if novos_pag > 0 else (sem_novidade + 1) + + print( + f"[inc][loja={store_code}] offset={offset} count={len(itens)} " + f"novos_pag={novos_pag} sem_novidade={sem_novidade}/{max_paginas_sem_novidade} " + f"total_novos={docs_novos}" + ) + + if sem_novidade >= max_paginas_sem_novidade: + break + if paginas >= max_paginas: + break + if not pagina.get("hasNext"): + break + offset += limit + + return { + "store_code": store_code, + "total": total, + "paginas_processadas": paginas, + "documentos_persistidos": docs_persistidos, + "documentos_novos": docs_novos, + "offset_final": offset, + "parada_por_sem_novidade": sem_novidade >= max_paginas_sem_novidade, + } + + + + +if __name__ == "__main__": + RUN_MODE = "full" # edite: full | incremental | json + if RUN_MODE not in ("full", "incremental", "json"): + raise RuntimeError("RUN_MODE invalido. Use: full, incremental ou json.") + + TARGET_STORE_CODE = "24430" # edite: codigo da loja, ex: 20997 + store_code = str(TARGET_STORE_CODE).strip() + if not store_code: + raise RuntimeError("Preencha TARGET_STORE_CODE no codigo.") + print(f"[info] consulta focada na loja {store_code}") + + if RUN_MODE in ("full", "incremental"): + SQLSERVER_CONN = ( + "DRIVER={ODBC Driver 17 for SQL Server};" + "SERVER=10.77.77.10;" + "DATABASE=GINSENG;" + "UID=andrey;" + "PWD=88253332;" + "TrustServerCertificate=yes;" + ) + + if RUN_MODE == "full": + resultado = sincronizar_paginas_sqlserver_loja( + connection_string=SQLSERVER_CONN, + store_code=store_code, + cp_id=10269, + document_type="EFAT", + limit=100, + start_offset=0, + only_channels=None, + commit_cada_paginas=1, + ) + else: + resultado = sincronizar_incremental_sqlserver_loja( + connection_string=SQLSERVER_CONN, + store_code=store_code, + cp_id=10269, + document_type="EFAT", + limit=100, + only_channels=None, + max_paginas_sem_novidade=5, + max_paginas=15, + ) + else: + c = ClientLoja(store_code) + resultado = c.processar_pagina( + cp_id=10269, + document_type="EFAT", + offset=0, + limit=25, + only_channels=None, + ) + + print(json.dumps(resultado, ensure_ascii=False, indent=2)) +