diff --git a/CHANGELOG.md b/CHANGELOG.md index 63a6411c..c82974d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,9 +13,7 @@ e este projeto adere ao [Versionamento Semântico](https://semver.org/lang/pt-BR ### Corrigido -- Nos providers `claude_code` e `codex`, o status HTTP 408 ou 409 dá `ProviderTransientError`, e a linha é repetida com backoff, como no erro de SDK que chega pelo LangChain; no 408, o aviso sai com a caixa de timeout. Antes, os dois status davam `ProviderError` definitivo, e a linha ficava com erro na primeira falha (#274). -- A caixa genérica de erro (`ERRO NO PROCESSAMENTO`) mostra a mensagem em até três linhas, terminada em reticências quando passa disso, e a moldura fica alinhada nas linhas do tipo e dos detalhes. Antes, a mensagem era cortada em 66 caracteres sem aviso, e no `claude_code` o corte caía dentro do prefixo `Claude Code SDK retornou erro (subtipo, status N): `, sem mostrar nada do texto do SDK (#275). -- No provider `claude_code`, o `api_error_status` que o SDK entrega como texto de dígitos ou float inteiro, como `"401"` ou `401.0`, é classificado como o status inteiro: `"401"` dá a caixa de autenticação sem nova tentativa, e `"429"` reduz o paralelismo. `True`, `False` e os demais valores contam como status ausente; antes, `True` passava como status e virava `ProviderError` definitivo com `status_code=True` (#276). + ## [0.11.1] - 2026-10-29 diff --git a/src/dataframeit/core.py b/src/dataframeit/core.py index 029e5ff3..12a11082 100644 --- a/src/dataframeit/core.py +++ b/src/dataframeit/core.py @@ -1841,9 +1841,18 @@ def dataframeit( # noqa: C901, PLR0912, PLR0913, PLR0915, PLR0917 (API pública "(marcadas em _error_details); repetir a chamada com reprocess_columns " "refaz todas as linhas, inclusive as já reprocessadas" ) + # resume=True sobre a saída só retoma quando a saída traz a coluna de + # texto, e só a saída de entrada DataFrame a traz; para list, dict e + # Series a via de retomada é o checkpoint. + output_has_text = conversion_info.original_type in ( + ORIGINAL_TYPE_PANDAS_DF, + ORIGINAL_TYPE_POLARS_DF, + ) if checkpoint is not None: - how = "rode de novo com o mesmo checkpoint_path, ou com resume=True sobre esta saída" - elif conversion_info.original_type in (ORIGINAL_TYPE_PANDAS_DF, ORIGINAL_TYPE_POLARS_DF): + how = "rode de novo com o mesmo checkpoint_path" + if output_has_text: + how += ", ou com resume=True sobre esta saída" + elif output_has_text: how = "rode de novo com resume=True sobre esta saída" else: how = ( diff --git a/tests/test_interrupcao.py b/tests/test_interrupcao.py index 507c9362..a8970009 100644 --- a/tests/test_interrupcao.py +++ b/tests/test_interrupcao.py @@ -1,5 +1,6 @@ """Falha que impede as linhas seguintes interrompe a execução sem gastá-las.""" +import re import signal import sys import threading @@ -185,6 +186,48 @@ def test_aviso_diz_como_retomar_conforme_a_entrada(entrada, trecho): _roda(entrada(), llm) +_SO_CHECKPOINT = "rode de novo com o mesmo checkpoint_path." +_CHECKPOINT_OU_RESUME = ( + "rode de novo com o mesmo checkpoint_path, ou com resume=True sobre esta saída." +) +_SO_RESUME = "rode de novo com resume=True sobre esta saída." +_SEM_VIA = ( + "a entrada não é um DataFrame e a saída não traz o texto, então só um " + "checkpoint_path permite retomar." +) + + +@pytest.mark.parametrize( + ("entrada", "com_checkpoint", "via"), + [ + (lambda: ["a", "b", "c"], True, _SO_CHECKPOINT), + (lambda: {"k1": "a", "k2": "b", "k3": "c"}, True, _SO_CHECKPOINT), + (lambda: pd.DataFrame({"texto": ["a", "b", "c"]}), True, _CHECKPOINT_OU_RESUME), + (lambda: ["a", "b", "c"], False, _SEM_VIA), + (lambda: {"k1": "a", "k2": "b", "k3": "c"}, False, _SEM_VIA), + (lambda: pd.DataFrame({"texto": ["a", "b", "c"]}), False, _SO_RESUME), + ], + ids=[ + "lista-com-checkpoint", + "dict-com-checkpoint", + "dataframe-com-checkpoint", + "lista-sem-checkpoint", + "dict-sem-checkpoint", + "dataframe-sem-checkpoint", + ], +) +def test_aviso_so_oferece_resume_sobre_a_saida_quando_ela_traz_o_texto( + entrada, com_checkpoint, via, tmp_path +): + # O texto inteiro da via de retomada é fixado, ancorado no fim da mensagem, + # para que nenhuma via a mais apareça. + _, llm = _llm_que_esgota(1, ProviderUsageLimitError("usage limit")) + extra = {"checkpoint_path": tmp_path / "c.csv", "batch_size": 1} if com_checkpoint else {} + + with pytest.warns(UserWarning, match=re.escape(f"Para retomar, {via}") + "$"): + _roda(entrada(), llm, **extra) + + def test_aviso_com_checkpoint_manda_usar_o_mesmo_arquivo(tmp_path): _, llm = _llm_que_esgota(1, ProviderUsageLimitError("usage limit"))