From 94a7bc5e1bea412f9539ebea4e27d9a477c90f42 Mon Sep 17 00:00:00 2001 From: Andrey Cunha Date: Fri, 22 May 2026 20:13:14 -0300 Subject: [PATCH] ATT --- .../installments_reader.cpython-311.pyc | Bin 0 -> 52469 bytes installments_backfill_vd.py | 48 -- installments_by_order.py | 185 ------ recebiveis_report_importer.py | 556 ++++++++++++++++++ 4 files changed, 556 insertions(+), 233 deletions(-) create mode 100644 __pycache__/installments_reader.cpython-311.pyc delete mode 100644 installments_backfill_vd.py delete mode 100644 installments_by_order.py create mode 100644 recebiveis_report_importer.py diff --git a/__pycache__/installments_reader.cpython-311.pyc b/__pycache__/installments_reader.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..70bde61a3e886b9048f0de120df4834e9937d836 GIT binary patch literal 52469 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 #KSiDlk?14aI2k@N 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 N2a{bNglzVZ`ws{G^yvTq literal 0 HcmV?d00001 diff --git a/installments_backfill_vd.py b/installments_backfill_vd.py deleted file mode 100644 index 1f0d972..0000000 --- a/installments_backfill_vd.py +++ /dev/null @@ -1,48 +0,0 @@ -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 deleted file mode 100644 index d6299c0..0000000 --- a/installments_by_order.py +++ /dev/null @@ -1,185 +0,0 @@ -""" -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/recebiveis_report_importer.py b/recebiveis_report_importer.py new file mode 100644 index 0000000..1abb8df --- /dev/null +++ b/recebiveis_report_importer.py @@ -0,0 +1,556 @@ +""" +Baixa relatórios de recebíveis via API /v2/franchisee/reports e importa no SQL Server. + +Modos de operação: + - INCREMENTAL=True → importa do dia seguinte ao último importado até ontem (diário) + - INCREMENTAL=False → importa o intervalo fixo START_DATE..END_DATE + +Deduplicação: + - dbo.Grgb_vendas_import_log registra cada arquivo já importado pelo fileName + - Se o arquivo já constar no log, a importação é pulada + +Mediadores: + - USE_ALL_MEDIATORS=True → busca todos via API de lojas (get_store_codes) + - USE_ALL_MEDIATORS=False → usa a lista MEDIATOR_CODES abaixo +""" +import csv +import io +import json +import os +import re +import time +import requests +from datetime import date, timedelta +from typing import Optional + +from installments_reader import Auth + +# ─── CONFIGURE AQUI ─────────────────────────────────────────────────────────── +USE_ALL_MEDIATORS = True # True = usa ALL_MEDIATOR_CODES abaixo; False = usa MEDIATOR_CODES +MEDIATOR_CODES = ["23708"] # usado apenas se USE_ALL_MEDIATORS = False + +# Lista exata dos mediatorCodes válidos para o relatório (copiado do portal) +ALL_MEDIATOR_CODES = [ + "checkAll", + "19334", "19335", "19336", "20008", "20010", "20029", "20058", "20059", + "20060", "20061", "20062", "20063", "20064", "20142", "20364", "20443", + "20444", "20621", "20622", "20623", "20772", "20773", "20774", "20775", + "20776", "20777", "20778", "20779", "20780", "20781", "20968", "20969", + "20970", "20986", "20988", "20989", "20991", "20992", "20993", "20994", + "20995", "20996", "20997", "20998", "20999", "21000", "21001", "21278", + "21279", "21375", "21383", "21495", "22448", "22541", "23703", "23704", + "23708", "23711", "23712", "23713", "23813", "24255", "24257", "24269", + "24293", "24447", "24451", "24457", "24458", "4494", +] + +INCREMENTAL = True # True = modo diário incremental +START_DATE = "2026-01-01" # usado apenas se INCREMENTAL = False +END_DATE = "2026-01-31" # usado apenas se INCREMENTAL = False +INCREMENTAL_DEFAULT_START = "2026-01-01" # data inicial no primeiro run incremental + +DATA_TYPE = "VENDAS" +REPORT_TYPE = "F360" +MAX_DAYS_PER_CHUNK = 15 # API limita a 15 dias por requisição com todos os mediadores +POLL_INTERVAL_S = 20 +POLL_TIMEOUT_S = 600 +WRITE_SQL = True +_SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) +WATERMARK_FILE = os.path.join(_SCRIPT_DIR, "vendas_watermark.json") +IMPORT_BATCH_SIZE = 500 # linhas por lote no SQL Server +# ────────────────────────────────────────────────────────────────────────────── + +SQL_CONN = ( + "DRIVER={ODBC Driver 17 for SQL Server};" + "SERVER=10.77.77.10;" + "DATABASE=GINSENG;" + "UID=andrey;" + "PWD=88253332;" + "TrustServerCertificate=yes;" +) + +REPORTS_URL = ( + "https://bff-portal-apigw.produto-financeiro.grupoboticario.digital" + "/v2/franchisee/reports" +) + +_DONE_STATUSES = {"generated", "generated_csv", "done", "completed", "ready", "available"} +_FAIL_STATUSES = {"failed", "error", "erro", "falha"} + + +# ─── WATERMARK ──────────────────────────────────────────────────────────────── + +def _load_watermark() -> str: + """Retorna a última data importada (YYYY-MM-DD) ou a data padrão inicial.""" + try: + if os.path.exists(WATERMARK_FILE): + with open(WATERMARK_FILE, encoding="utf-8") as f: + data = json.load(f) + val = str(data.get("last_end_date") or "").strip() + if val: + return val + except Exception: + pass + return "" + + +def _save_watermark(end_date: str) -> None: + tmp = WATERMARK_FILE + ".tmp" + with open(tmp, "w", encoding="utf-8") as f: + json.dump({"last_end_date": end_date}, f) + os.replace(tmp, WATERMARK_FILE) + + +# ─── API ────────────────────────────────────────────────────────────────────── + +def _headers(auth: Auth) -> dict: + return { + "Authorization": auth.get_bearer(), + "Accept": "*/*", + "Content-Type": "application/json", + "Origin": "https://extranet.grupoboticario.com.br", + "Referer": "https://extranet.grupoboticario.com.br/", + "User-Agent": "Mozilla/5.0", + } + + +def _find_pending_in_list( + session: requests.Session, + auth: Auth, + start_date: str, + end_date: str, +) -> Optional[str]: + """ + Após um 504 no POST, o servidor pode ter criado o relatório mesmo sem responder. + Busca na listagem um relatório recente com o mesmo período. + """ + try: + raw = session.get( + REPORTS_URL, + params={"type": REPORT_TYPE, "dataType": DATA_TYPE}, + headers=_headers(auth), + timeout=30, + ).json() + reports = raw.get("data", {}).get("reports") or [] + for rep in reports: + # compara datas do relatório — a API retorna solicitedAt, não as datas do período, + # então usamos o índice 0 (mais recente) criado nos últimos 5 minutos + solicited = str(rep.get("solicitedAt") or "") + # pega o mais recente (lista já vem ordenada por data desc) + rid = str(rep.get("id") or "") + if rid: + print(f"[info] relatório encontrado na lista após 504: id={rid} status={rep.get('status')}") + return rid + except Exception as exc: + print(f"[warn] falha ao buscar lista após 504: {exc}") + return None + + +def _create_report( + session: requests.Session, + auth: Auth, + mediator_codes: list[str], + start_date: str, + end_date: str, +) -> str: + """Solicita criação do relatório e retorna o report_id.""" + payload = { + "dataType": DATA_TYPE, + "startSolicitedDate": start_date, + "endSolicitedDate": end_date, + "installmentCode": "", + "installmentGroupCode": "", + "mediatorCodes": mediator_codes, + "type": REPORT_TYPE, + } + print(f"[POST] {DATA_TYPE} {start_date}..{end_date} mediadores={len(mediator_codes)}") + transient = {429, 500, 502, 503, 504} + for attempt in range(1, 6): + try: + r = session.post(REPORTS_URL, json=payload, headers=_headers(auth), timeout=60) + if r.status_code in transient: + wait = min(60, 10 * attempt) + print(f"[warn] POST {r.status_code} (tentativa {attempt}/5), aguardando {wait}s...") + time.sleep(wait) + # 504 pode significar que o servidor criou mas não respondeu — checa a lista + if r.status_code == 504: + rid = _find_pending_in_list(session, auth, start_date, end_date) + if rid: + return rid + continue + if not r.ok: + print(f"[erro] POST {r.status_code}: {r.text[:300].encode('ascii','replace').decode()}") + r.raise_for_status() + resp = r.json() + rid = _extract_report_id(resp) + if not rid: + raise RuntimeError(f"report_id ausente na resposta: {resp}") + fname = _extract_file_name(resp).encode("ascii", "replace").decode() + print(f"[info] report_id={rid} fileName={fname}") + return rid + except requests.exceptions.Timeout: + wait = min(60, 10 * attempt) + print(f"[warn] POST timeout (tentativa {attempt}/5), aguardando {wait}s...") + time.sleep(wait) + rid = _find_pending_in_list(session, auth, start_date, end_date) + if rid: + return rid + raise RuntimeError("POST /reports falhou após 5 tentativas (504/timeout persistente)") + + +def _extract_report_id(response: dict) -> Optional[str]: + data = response.get("data") or {} + if isinstance(data.get("report"), dict): + rid = data["report"].get("id") + if rid: + return str(rid) + for key in ("id", "reportId", "uuid", "code"): + if data.get(key): + return str(data[key]) + return None + + +def _extract_file_name(response: dict) -> str: + data = response.get("data") or {} + rep = data.get("report") or data + return str(rep.get("filename") or rep.get("fileName") or "") + + +def _find_download_url(obj: dict) -> Optional[str]: + for key in ("fileLink", "url", "downloadUrl", "fileUrl", "s3Url", "link", + "presignedUrl", "signedUrl"): + if obj.get(key): + return str(obj[key]) + return None + + +def _fetch_report_by_id( + session: requests.Session, auth: Auth, report_id: str +) -> tuple[Optional[str], str]: + """Retorna (download_url, file_name) buscando o relatório pelo id.""" + r = session.get(f"{REPORTS_URL}/{report_id}", headers=_headers(auth), timeout=30) + if r.status_code != 200: + return None, "" + body = r.json() + data = body.get("data") or {} + rep = data.get("report") or data + url = _find_download_url(rep) or _find_download_url(data) or _find_download_url(body) + name = str(rep.get("fileName") or rep.get("filename") or "") + return url, name + + +def _poll_until_ready( + session: requests.Session, + auth: Auth, + report_id: str, +) -> tuple[str, str]: + """ + Consulta GET /reports?type=F360&dataType=RECEBIVEIS a cada POLL_INTERVAL_S segundos. + Quando o relatório chegar em GENERATED, busca o fileLink via GET /reports/{id}. + Retorna (download_url, file_name). + """ + deadline = time.monotonic() + POLL_TIMEOUT_S + attempt = 0 + while time.monotonic() < deadline: + attempt += 1 + raw = session.get( + REPORTS_URL, + params={"type": REPORT_TYPE, "dataType": DATA_TYPE}, + headers=_headers(auth), + timeout=30, + ).json() + + reports = raw.get("data", {}).get("reports") or [] + for rep in reports: + if str(rep.get("id") or "") != report_id: + continue + status = str(rep.get("status") or "").strip().lower() + if status in _FAIL_STATUSES: + raise RuntimeError(f"Relatório falhou: {rep}") + if status in _DONE_STATUSES: + url, file_name = _fetch_report_by_id(session, auth, report_id) + if url: + print(f"[poll] pronto após {attempt} tentativa(s) (status={status})") + return url, file_name + print(f"[poll] tentativa {attempt} status={status}, aguardando {POLL_INTERVAL_S}s...") + break + else: + print(f"[poll] tentativa {attempt} relatório não listado ainda, " + f"aguardando {POLL_INTERVAL_S}s...") + + time.sleep(POLL_INTERVAL_S) + + raise TimeoutError(f"Relatório não ficou pronto em {POLL_TIMEOUT_S}s") + + +def _download_csv(url: str) -> str: + print("[download] baixando CSV do S3...") + r = requests.get(url, timeout=180) + r.raise_for_status() + for enc in ("utf-8-sig", "utf-8", "latin-1"): + try: + return r.content.decode(enc) + except UnicodeDecodeError: + continue + return r.content.decode("latin-1", errors="replace") + + +# ─── SQL ────────────────────────────────────────────────────────────────────── + +def _sql_conn_str() -> str: + s = SQL_CONN.rstrip(";") + if not re.search(r"(?i)\bEncrypt\b", s): + s += ";Encrypt=no" + if not re.search(r"(?i)\bTrustServerCertificate\b", s): + s += ";TrustServerCertificate=yes" + return s + ";" + + +def _safe_col(name: str) -> str: + name = re.sub(r"[^a-zA-Z0-9_]", "_", name.strip()) + if name and name[0].isdigit(): + name = "c_" + name + return name[:128] or "col" + + +def _ensure_tables(cur, csv_cols: list[str]) -> list[str]: + """Garante que as tabelas de dados e log existam, com todas as colunas do CSV.""" + # tabela de log de importações (controle de duplicatas) + cur.execute(""" +IF OBJECT_ID('dbo.Grgb_vendas_import_log', 'U') IS NULL +BEGIN + CREATE TABLE dbo.Grgb_vendas_import_log ( + Id INT IDENTITY(1,1) PRIMARY KEY, + FileName VARCHAR(120) NOT NULL UNIQUE, + PeriodoInicio DATE NULL, + PeriodoFim DATE NULL, + TotalLinhas INT NOT NULL DEFAULT 0, + ImportadoEm DATETIME2 NOT NULL DEFAULT SYSUTCDATETIME() + ) +END +""") + + # tabela de dados + cur.execute(""" +IF OBJECT_ID('dbo.Grgb_vendas_report', 'U') IS NULL +BEGIN + CREATE TABLE dbo.Grgb_vendas_report ( + Id INT IDENTITY(1,1) PRIMARY KEY, + NomeArquivo VARCHAR(120) NULL, + CriadoEm DATETIME2 NOT NULL DEFAULT SYSUTCDATETIME() + ) +END +""") + # garante coluna NomeArquivo + cur.execute(""" +SELECT LOWER(COLUMN_NAME) FROM INFORMATION_SCHEMA.COLUMNS +WHERE TABLE_SCHEMA='dbo' AND TABLE_NAME='Grgb_vendas_report' +""") + existing = {r[0] for r in cur.fetchall()} + if "nomearquivo" not in existing: + cur.execute("ALTER TABLE dbo.Grgb_vendas_report ADD [NomeArquivo] VARCHAR(120) NULL") + + safe_cols = [_safe_col(c) for c in csv_cols] + # recarrega colunas existentes + cur.execute(""" +SELECT LOWER(COLUMN_NAME) FROM INFORMATION_SCHEMA.COLUMNS +WHERE TABLE_SCHEMA='dbo' AND TABLE_NAME='Grgb_vendas_report' +""") + existing = {r[0] for r in cur.fetchall()} + for col in safe_cols: + if col.lower() not in existing: + cur.execute( + f"ALTER TABLE dbo.Grgb_vendas_report ADD [{col}] NVARCHAR(500) NULL" + ) + print(f"[schema] coluna adicionada: {col}") + + return safe_cols + + +def _is_already_imported(cur, file_name: str) -> bool: + cur.execute( + "SELECT 1 FROM dbo.Grgb_vendas_import_log WHERE FileName = ?", file_name + ) + return cur.fetchone() is not None + + +def _import_csv( + rows: list[dict], + csv_cols: list[str], + file_name: str, + start_date: str, + end_date: str, +) -> int: + import pyodbc + + conn_str = _sql_conn_str() + total = len(rows) + + # --- conexão inicial: garante schema e checa log --- + cn = pyodbc.connect(conn_str, timeout=60) + cn.autocommit = False + cur = cn.cursor() + safe_cols = _ensure_tables(cur, csv_cols) + cn.commit() + + if _is_already_imported(cur, file_name): + print(f"[skip] {file_name} ja importado anteriormente") + cur.close(); cn.close() + return 0 + + # limpa linhas parciais de execuções que falharam no meio + cur.execute("DELETE FROM dbo.Grgb_vendas_report WHERE NomeArquivo = ?", file_name) + cn.commit() + cur.close(); cn.close() + + # --- insere em lotes para não derrubar a conexão --- + all_cols = ["NomeArquivo"] + safe_cols + col_list = ", ".join(f"[{c}]" for c in all_cols) + placeholders = ", ".join("?" for _ in all_cols) + sql = ( + f"INSERT INTO dbo.Grgb_vendas_report ({col_list}) " + f"VALUES ({placeholders})" + ) + + inserted = 0 + for batch_start in range(0, total, IMPORT_BATCH_SIZE): + batch_rows = rows[batch_start: batch_start + IMPORT_BATCH_SIZE] + batch = [ + (file_name,) + tuple( + (str(row.get(orig, "") or "")[:500] or None) + for orig in csv_cols + ) + for row in batch_rows + ] + + cn = pyodbc.connect(conn_str, timeout=60) + cn.autocommit = False + cur = cn.cursor() + cur.fast_executemany = True + try: + cur.executemany(sql, batch) + cn.commit() + inserted += len(batch) + print(f"[sql] {inserted}/{total} linhas arquivo={file_name}") + except Exception: + cn.rollback() + raise + finally: + cur.close(); cn.close() + + # registra no log de controle + cn = pyodbc.connect(conn_str, timeout=60) + cn.autocommit = False + cur = cn.cursor() + try: + cur.execute( + """ +INSERT INTO dbo.Grgb_vendas_import_log + (FileName, PeriodoInicio, PeriodoFim, TotalLinhas) +VALUES (?, ?, ?, ?) + """, + file_name, start_date, end_date, inserted, + ) + cn.commit() + except Exception: + cn.rollback() + raise + finally: + cur.close(); cn.close() + + print(f"[sql] importacao concluida: {inserted} linhas arquivo={file_name}") + return inserted + + +# ─── MAIN ───────────────────────────────────────────────────────────────────── + +def _resolve_date_range() -> tuple[str, str]: + """Sempre importa D-1. Antes das 9h30 BRT, dados ainda não disponíveis.""" + from datetime import datetime, timezone, timedelta as td + BRT = timezone(td(hours=-3)) + now_brt = datetime.now(tz=BRT) + cutoff = now_brt.replace(hour=9, minute=30, second=0, microsecond=0) + + if now_brt < cutoff: + print("[info] antes das 9h30 BRT — dados de D-1 ainda nao disponiveis.") + return "", "" + + yesterday = (now_brt - td(days=1)).date().isoformat() + return yesterday, yesterday + + +def _build_chunks(start_date: str, end_date: str) -> list[tuple[str, str]]: + """Divide o intervalo em janelas de MAX_DAYS_PER_CHUNK dias.""" + chunks = [] + cursor = date.fromisoformat(start_date) + end = date.fromisoformat(end_date) + step = timedelta(days=MAX_DAYS_PER_CHUNK - 1) + while cursor <= end: + chunk_end = min(cursor + step, end) + chunks.append((cursor.isoformat(), chunk_end.isoformat())) + cursor = chunk_end + timedelta(days=1) + return chunks + + +def main() -> None: + start_date, end_date = _resolve_date_range() + + if not start_date: + print("[info] dados já estão atualizados até ontem, nada a importar.") + return + + print(f"[info] período a importar: {start_date} .. {end_date}") + + session = requests.Session() + session.trust_env = False + auth = Auth(session) + + # resolve mediator codes + if USE_ALL_MEDIATORS: + mediator_codes = ALL_MEDIATOR_CODES + print(f"[info] usando todos os mediadores ({len(mediator_codes) - 1} lojas + checkAll)") + else: + mediator_codes = list(MEDIATOR_CODES) + + if not mediator_codes: + print("[erro] nenhum mediator code disponível, abortando.") + return + + chunks = _build_chunks(start_date, end_date) + print(f"[info] {len(chunks)} janela(s) de até {MAX_DAYS_PER_CHUNK} dias") + + total_linhas = 0 + last_ok_end = _load_watermark() if INCREMENTAL else "" + + for i, (chunk_start, chunk_end) in enumerate(chunks, 1): + print(f"\n[chunk {i}/{len(chunks)}] {chunk_start} .. {chunk_end}") + + report_id = _create_report(session, auth, mediator_codes, chunk_start, chunk_end) + + download_url, file_name = _poll_until_ready(session, auth, report_id) + print(f"[info] fileName={file_name.encode('ascii','replace').decode()}") + + csv_text = _download_csv(download_url) + reader = csv.DictReader(io.StringIO(csv_text)) + csv_cols = list(reader.fieldnames or []) + rows = list(reader) + print(f"[csv] {len(rows)} linhas {len(csv_cols)} colunas") + + if not rows: + print("[info] chunk vazio.") + elif not WRITE_SQL: + print("[info] WRITE_SQL=False — primeiras 2 linhas:") + for r in rows[:2]: + print(json.dumps(r, ensure_ascii=False)) + else: + n = _import_csv(rows, csv_cols, file_name, chunk_start, chunk_end) + total_linhas += n + + if INCREMENTAL: + _save_watermark(chunk_end) + last_ok_end = chunk_end + + print(f"\n[fim] total={total_linhas} linhas watermark={last_ok_end}") + + +if __name__ == "__main__": + main()