diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index f31d7431..eaa785e2 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -43,11 +43,15 @@ jobs: - name: zizmor run: uvx zizmor@1.30.1 --offline .github/workflows + # A formatação de example/ não é conferida: o ruff format reescreveria + # quase todas as células dos notebooks e desfaria o alinhamento dos + # comentários que explicam cada argumento e cada linha de dado. - name: Ruff format run: uv run --python 3.13 $EXTRAS ruff format --check src tests + # example/ruff.toml dá aos exemplos só as regras de correção. - name: Ruff check - run: uv run --python 3.13 $EXTRAS ruff check src tests + run: uv run --python 3.13 $EXTRAS ruff check src tests example - name: ty run: uv run --python 3.13 $EXTRAS ty check diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4c284c4c..06b22218 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,25 +1,29 @@ # Os hooks rodam as mesmas versões do extra dev do pyproject.toml, pelo uv, e # o job quality do CI (tests.yml) roda os mesmos comandos. # Instalação: uv run --extra dev pre-commit install -# Os hooks disparam com mudanças em src/ e tests/. Os workflows de +# Os hooks disparam com mudanças em src/, tests/ e example/. Os workflows de # .github/workflows/ não têm hook: o passo zizmor do job quality os confere, e # o comando dele roda também na máquina local. -files: ^(src|tests)/ +files: ^(src|tests|example)/ repos: - repo: local hooks: + # Como no CI, a formatação de example/ não é conferida; o motivo está no + # passo Ruff format de tests.yml. - id: ruff-format name: ruff format entry: uv run --extra dev ruff format --force-exclude language: system types_or: [python, pyi] + exclude: ^example/ require_serial: true - id: ruff-check name: ruff check entry: uv run --extra dev ruff check --force-exclude language: system - types_or: [python, pyi] + # jupyter inclui os notebooks de example/, que o ruff confere. + types_or: [python, pyi, jupyter] require_serial: true # ty resolve os imports dos extras opcionais, por isso eles vão junto. diff --git a/CHANGELOG.md b/CHANGELOG.md index 9372032b..87794e96 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ e este projeto adere ao [Versionamento Semântico](https://semver.org/lang/pt-BR ## [Unreleased] +### Alterado + +- O `ruff check` do CI e do pre-commit confere também `example/`, com as regras de correção listadas em `example/ruff.toml` e sem as de estilo de biblioteca. Na célula que configura a chave, os notebooks de exemplo capturam só o `ImportError` de fora do Colab e as exceções que `userdata.get` levanta, e mostram a mensagem; antes, um `except:` com `pass` engolia também o secret ausente e o Ctrl+C (#211). + ## [0.11.1] - 2026-10-29 ### Corrigido diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f80cfdda..7b1b2c3f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -13,7 +13,7 @@ cd dataframeit uv run --extra dev pytest # suíte de testes uv run --extra dev --extra all --extra search-all --extra codex pytest --cov # cobertura de ramos, exige 100% uv run --extra dev --extra all --extra search-all --extra polars --extra excel --extra codex pytest --count=2 --repeat-scope=session # suíte duas vezes no mesmo processo, sem pular teste -uv run --extra dev ruff check src tests # lint +uv run --extra dev ruff check src tests example # lint uv run --extra docs mkdocs serve # documentação em http://127.0.0.1:8000 ``` @@ -40,4 +40,4 @@ A documentação tem duas árvores com o mesmo conteúdo: `docs/` em português, ## Exemplos -Os notebooks de `example/` são versionados sem saídas. O teste `tests/test_exemplos.py` confere que eles usam só parâmetros que existem em `dataframeit()`. +Os notebooks de `example/` são versionados sem saídas. O teste `tests/test_exemplos.py` confere que eles usam só parâmetros que existem em `dataframeit()`. O `ruff check` confere os exemplos só com as regras de correção, listadas em `example/ruff.toml`, sem as de estilo de biblioteca, como docstring e anotação de tipo. diff --git a/example/01_basic.ipynb b/example/01_basic.ipynb index 78e806c4..959efbf9 100644 --- a/example/01_basic.ipynb +++ b/example/01_basic.ipynb @@ -56,10 +56,18 @@ "# Opção 1: Usando Google Colab Secrets (recomendado)\n", "try:\n", " from google.colab import userdata\n", - " os.environ['OPENAI_API_KEY'] = userdata.get('OPENAI_API_KEY')\n", - " print(\"API Key carregada dos Secrets do Colab\")\n", - "except:\n", - " pass\n", + "except ImportError:\n", + " print(\"Fora do Colab: a chave vem da variável de ambiente\")\n", + "else:\n", + " try:\n", + " os.environ['OPENAI_API_KEY'] = userdata.get('OPENAI_API_KEY')\n", + " print(\"API Key carregada dos Secrets do Colab\")\n", + " except (\n", + " userdata.SecretNotFoundError,\n", + " userdata.NotebookAccessError,\n", + " userdata.TimeoutException,\n", + " ) as erro:\n", + " print(f\"Secret do Colab não lido: {erro}\")\n", "\n", "# Opção 2: Inserir manualmente (descomente e preencha)\n", "# os.environ['OPENAI_API_KEY'] = 'sua-chave-aqui'\n", @@ -84,9 +92,11 @@ "metadata": {}, "outputs": [], "source": [ - "from pydantic import BaseModel, Field\n", "from typing import Literal\n", + "\n", "import pandas as pd\n", + "from pydantic import BaseModel, Field\n", + "\n", "from dataframeit import dataframeit" ] }, diff --git a/example/02_error_handling.ipynb b/example/02_error_handling.ipynb index 7c701e36..24212689 100644 --- a/example/02_error_handling.ipynb +++ b/example/02_error_handling.ipynb @@ -53,10 +53,18 @@ "# Opção 1: Usando Google Colab Secrets (recomendado)\n", "try:\n", " from google.colab import userdata\n", - " os.environ['OPENAI_API_KEY'] = userdata.get('OPENAI_API_KEY')\n", - " print(\"API Key carregada dos Secrets do Colab\")\n", - "except:\n", - " pass\n", + "except ImportError:\n", + " print(\"Fora do Colab: a chave vem da variável de ambiente\")\n", + "else:\n", + " try:\n", + " os.environ['OPENAI_API_KEY'] = userdata.get('OPENAI_API_KEY')\n", + " print(\"API Key carregada dos Secrets do Colab\")\n", + " except (\n", + " userdata.SecretNotFoundError,\n", + " userdata.NotebookAccessError,\n", + " userdata.TimeoutException,\n", + " ) as erro:\n", + " print(f\"Secret do Colab não lido: {erro}\")\n", "\n", "# Opção 2: Inserir manualmente (descomente e preencha)\n", "# os.environ['OPENAI_API_KEY'] = 'sua-chave-aqui'\n", @@ -80,9 +88,11 @@ "metadata": {}, "outputs": [], "source": [ - "from pydantic import BaseModel, Field\n", "from typing import Literal\n", + "\n", "import pandas as pd\n", + "from pydantic import BaseModel, Field\n", + "\n", "from dataframeit import dataframeit" ] }, @@ -260,7 +270,7 @@ "if len(linhas_com_erro) > 0:\n", " print(f\"{len(linhas_com_erro)} linha(s) com erro:\\n\")\n", "\n", - " for idx, row in linhas_com_erro.iterrows():\n", + " for _, row in linhas_com_erro.iterrows():\n", " print(f\"Linha {row['id']}:\")\n", " print(f\" Texto: '{row['texto']}'\")\n", " print(f\" Erro: {row['_error_details']}\")\n", diff --git a/example/03_resume.ipynb b/example/03_resume.ipynb index b9dd3536..60e17f2d 100644 --- a/example/03_resume.ipynb +++ b/example/03_resume.ipynb @@ -50,10 +50,18 @@ "\n", "try:\n", " from google.colab import userdata\n", - " os.environ['OPENAI_API_KEY'] = userdata.get('OPENAI_API_KEY')\n", - " print(\"API Key carregada dos Secrets do Colab\")\n", - "except:\n", - " pass\n", + "except ImportError:\n", + " print(\"Fora do Colab: a chave vem da variável de ambiente\")\n", + "else:\n", + " try:\n", + " os.environ['OPENAI_API_KEY'] = userdata.get('OPENAI_API_KEY')\n", + " print(\"API Key carregada dos Secrets do Colab\")\n", + " except (\n", + " userdata.SecretNotFoundError,\n", + " userdata.NotebookAccessError,\n", + " userdata.TimeoutException,\n", + " ) as erro:\n", + " print(f\"Secret do Colab não lido: {erro}\")\n", "\n", "# os.environ['OPENAI_API_KEY'] = 'sua-chave-aqui'\n", "\n", @@ -76,11 +84,13 @@ "metadata": {}, "outputs": [], "source": [ - "from pydantic import BaseModel, Field\n", + "import os\n", "from typing import Literal\n", + "\n", "import pandas as pd\n", - "from dataframeit import dataframeit, read_df\n", - "import os" + "from pydantic import BaseModel, Field\n", + "\n", + "from dataframeit import dataframeit, read_df" ] }, { diff --git a/example/04_custom_placeholder.ipynb b/example/04_custom_placeholder.ipynb index c32a09f1..565c84f5 100644 --- a/example/04_custom_placeholder.ipynb +++ b/example/04_custom_placeholder.ipynb @@ -50,10 +50,18 @@ "\n", "try:\n", " from google.colab import userdata\n", - " os.environ['OPENAI_API_KEY'] = userdata.get('OPENAI_API_KEY')\n", - " print(\"API Key carregada dos Secrets do Colab\")\n", - "except:\n", - " pass\n", + "except ImportError:\n", + " print(\"Fora do Colab: a chave vem da variável de ambiente\")\n", + "else:\n", + " try:\n", + " os.environ['OPENAI_API_KEY'] = userdata.get('OPENAI_API_KEY')\n", + " print(\"API Key carregada dos Secrets do Colab\")\n", + " except (\n", + " userdata.SecretNotFoundError,\n", + " userdata.NotebookAccessError,\n", + " userdata.TimeoutException,\n", + " ) as erro:\n", + " print(f\"Secret do Colab não lido: {erro}\")\n", "\n", "# os.environ['OPENAI_API_KEY'] = 'sua-chave-aqui'\n", "\n", @@ -76,9 +84,11 @@ "metadata": {}, "outputs": [], "source": [ - "from pydantic import BaseModel, Field\n", "from typing import Literal\n", + "\n", "import pandas as pd\n", + "from pydantic import BaseModel, Field\n", + "\n", "from dataframeit import dataframeit" ] }, diff --git a/example/05_advanced_legal.ipynb b/example/05_advanced_legal.ipynb index 817cd3f8..f61d0f58 100644 --- a/example/05_advanced_legal.ipynb +++ b/example/05_advanced_legal.ipynb @@ -52,10 +52,18 @@ "\n", "try:\n", " from google.colab import userdata\n", - " os.environ['OPENAI_API_KEY'] = userdata.get('OPENAI_API_KEY')\n", - " print(\"API Key carregada dos Secrets do Colab\")\n", - "except:\n", - " pass\n", + "except ImportError:\n", + " print(\"Fora do Colab: a chave vem da variável de ambiente\")\n", + "else:\n", + " try:\n", + " os.environ['OPENAI_API_KEY'] = userdata.get('OPENAI_API_KEY')\n", + " print(\"API Key carregada dos Secrets do Colab\")\n", + " except (\n", + " userdata.SecretNotFoundError,\n", + " userdata.NotebookAccessError,\n", + " userdata.TimeoutException,\n", + " ) as erro:\n", + " print(f\"Secret do Colab não lido: {erro}\")\n", "\n", "# os.environ['OPENAI_API_KEY'] = 'sua-chave-aqui'\n", "\n", @@ -78,9 +86,11 @@ "metadata": {}, "outputs": [], "source": [ - "from pydantic import BaseModel, Field\n", "from typing import Literal\n", + "\n", "import pandas as pd\n", + "from pydantic import BaseModel, Field\n", + "\n", "from dataframeit import dataframeit" ] }, diff --git a/example/06_polars.ipynb b/example/06_polars.ipynb index 1ea9aada..7981c421 100644 --- a/example/06_polars.ipynb +++ b/example/06_polars.ipynb @@ -50,10 +50,18 @@ "\n", "try:\n", " from google.colab import userdata\n", - " os.environ['OPENAI_API_KEY'] = userdata.get('OPENAI_API_KEY')\n", - " print(\"API Key carregada dos Secrets do Colab\")\n", - "except:\n", - " pass\n", + "except ImportError:\n", + " print(\"Fora do Colab: a chave vem da variável de ambiente\")\n", + "else:\n", + " try:\n", + " os.environ['OPENAI_API_KEY'] = userdata.get('OPENAI_API_KEY')\n", + " print(\"API Key carregada dos Secrets do Colab\")\n", + " except (\n", + " userdata.SecretNotFoundError,\n", + " userdata.NotebookAccessError,\n", + " userdata.TimeoutException,\n", + " ) as erro:\n", + " print(f\"Secret do Colab não lido: {erro}\")\n", "\n", "# os.environ['OPENAI_API_KEY'] = 'sua-chave-aqui'\n", "\n", @@ -76,12 +84,14 @@ "metadata": {}, "outputs": [], "source": [ - "from pydantic import BaseModel, Field\n", + "import time\n", "from typing import Literal\n", - "import polars as pl\n", + "\n", "import pandas as pd\n", - "from dataframeit import dataframeit\n", - "import time" + "import polars as pl\n", + "from pydantic import BaseModel, Field\n", + "\n", + "from dataframeit import dataframeit" ] }, { @@ -161,7 +171,7 @@ "# CRIAR POLARS DATAFRAME\n", "df_polars = pl.DataFrame(dados)\n", "\n", - "print(f\"DataFrame Polars criado:\")\n", + "print(\"DataFrame Polars criado:\")\n", "print(f\" Tipo: {type(df_polars)}\")\n", "print(f\" Linhas: {len(df_polars)}\")\n", "print(f\" Colunas: {df_polars.columns}\")\n", diff --git a/example/07_multiple_data_types.ipynb b/example/07_multiple_data_types.ipynb index ff0118b7..982a841d 100644 --- a/example/07_multiple_data_types.ipynb +++ b/example/07_multiple_data_types.ipynb @@ -50,10 +50,18 @@ "\n", "try:\n", " from google.colab import userdata\n", - " os.environ['OPENAI_API_KEY'] = userdata.get('OPENAI_API_KEY')\n", - " print(\"API Key carregada dos Secrets do Colab\")\n", - "except:\n", - " pass\n", + "except ImportError:\n", + " print(\"Fora do Colab: a chave vem da variável de ambiente\")\n", + "else:\n", + " try:\n", + " os.environ['OPENAI_API_KEY'] = userdata.get('OPENAI_API_KEY')\n", + " print(\"API Key carregada dos Secrets do Colab\")\n", + " except (\n", + " userdata.SecretNotFoundError,\n", + " userdata.NotebookAccessError,\n", + " userdata.TimeoutException,\n", + " ) as erro:\n", + " print(f\"Secret do Colab não lido: {erro}\")\n", "\n", "# os.environ['OPENAI_API_KEY'] = 'sua-chave-aqui'\n", "\n", @@ -76,9 +84,11 @@ "metadata": {}, "outputs": [], "source": [ - "from pydantic import BaseModel, Field\n", "from typing import Literal\n", + "\n", "import pandas as pd\n", + "from pydantic import BaseModel, Field\n", + "\n", "from dataframeit import dataframeit" ] }, diff --git a/example/08_rate_limiting.ipynb b/example/08_rate_limiting.ipynb index 9b67ab28..e1814656 100644 --- a/example/08_rate_limiting.ipynb +++ b/example/08_rate_limiting.ipynb @@ -52,11 +52,19 @@ "\n", "try:\n", " from google.colab import userdata\n", - " os.environ['OPENAI_API_KEY'] = userdata.get('OPENAI_API_KEY')\n", - " os.environ['GOOGLE_API_KEY'] = userdata.get('GOOGLE_API_KEY')\n", - " print(\"API Key carregada dos Secrets do Colab\")\n", - "except:\n", - " pass\n", + "except ImportError:\n", + " print(\"Fora do Colab: a chave vem da variável de ambiente\")\n", + "else:\n", + " try:\n", + " os.environ['OPENAI_API_KEY'] = userdata.get('OPENAI_API_KEY')\n", + " os.environ['GOOGLE_API_KEY'] = userdata.get('GOOGLE_API_KEY')\n", + " print(\"API Key carregada dos Secrets do Colab\")\n", + " except (\n", + " userdata.SecretNotFoundError,\n", + " userdata.NotebookAccessError,\n", + " userdata.TimeoutException,\n", + " ) as erro:\n", + " print(f\"Secret do Colab não lido: {erro}\")\n", "\n", "# os.environ['OPENAI_API_KEY'] = 'sua-chave-aqui'\n", "# os.environ['GOOGLE_API_KEY'] = 'sua-chave-aqui'\n", @@ -80,9 +88,11 @@ "metadata": {}, "outputs": [], "source": [ - "from pydantic import BaseModel, Field\n", "from typing import Literal\n", + "\n", "import pandas as pd\n", + "from pydantic import BaseModel, Field\n", + "\n", "from dataframeit import dataframeit" ] }, @@ -337,7 +347,7 @@ " max_delay=30.0\n", ")\n", "\n", - "print(f\"\\nProcessamento concluído com dupla proteção!\")\n", + "print(\"\\nProcessamento concluído com dupla proteção!\")\n", "\n", "# Verificar se houve erros\n", "if '_dataframeit_status' in resultado_protegido.columns:\n", diff --git a/example/example_09_web_search.py b/example/example_09_web_search.py index 0bb4dc44..c301416b 100644 --- a/example/example_09_web_search.py +++ b/example/example_09_web_search.py @@ -28,9 +28,11 @@ Os créditos gastos ficam na coluna _search_credits e no resumo ao fim da execução. """ -from pydantic import BaseModel, Field from typing import Literal + import pandas as pd +from pydantic import BaseModel, Field + from dataframeit import dataframeit # ============================================================================ diff --git a/example/ruff.toml b/example/ruff.toml new file mode 100644 index 00000000..c6f68b90 --- /dev/null +++ b/example/ruff.toml @@ -0,0 +1,31 @@ +# Regras do ruff para os notebooks e scripts de example/. O ruff usa a +# configuração mais próxima de cada arquivo conferido, então esta vale no lugar +# da do pyproject.toml para todo arquivo desta pasta, e o `extend` traz de lá a +# versão do Python e o comprimento de linha. Por isso uma única invocação, como +# o `ruff check src tests example` do CI ou o hook do pre-commit, que recebe os +# arquivos alterados, confere src/ e tests/ com o pyproject.toml e example/ com +# este arquivo, sem conflito. +# +# Os exemplos ficam só com as regras de correção, porque é deles que se copia +# código: pycodestyle e pyflakes (E, F), com nome indefinido, import sem uso e +# except sem tipo (E722); armadilhas do bugbear (B); ordem dos imports (I); +# sintaxe moderna (UP); e except que engole o erro (S110, BLE). As regras de +# estilo de biblioteca, como docstring, anotação de tipo, aspas e proibição de +# print, não cabem em notebook didático. +# +# O select daqui substitui o ["ALL"] do pyproject.toml. Per-file-ignores só +# tiram regras do ALL: chegar a este conjunto por eles exigiria listar cada +# família do ALL que fica de fora, e toda família nova do ruff passaria a valer +# nos exemplos sem ninguém escolher. O select substitui também o ignore herdado, +# e por isso o E501 volta a ser ignorado aqui: o que passa de 100 colunas nos +# exemplos são as descrições dos campos e os textos de entrada. +extend = "../pyproject.toml" + +[lint] +select = ["E", "F", "B", "I", "UP", "S110", "BLE"] +ignore = ["E501"] + +[lint.isort] +# Na raiz, o ruff acha o pacote em src/ e separa o import dele dos de terceiros. +# Nesta pasta ele procuraria em example/, por isso o nome vai explícito. +known-first-party = ["dataframeit"]