Adiciona agregador falcao (Jurisprudencia Nacional da JT / CSJT)#325
Adiciona agregador falcao (Jurisprudencia Nacional da JT / CSJT)#325biabrajal wants to merge 2 commits into
Conversation
Novo scraper FalcaoScraper para a busca publica de jurisprudencia da
Justica do Trabalho (jurisprudencia.jt.jus.br, endpoint /no-auth/pesquisa),
cobrindo TST e os 24 TRTs sobre cinco colecoes.
- cjsg / cjsg_download / cjsg_parse com filtros validados por pydantic
(extra="forbid"): colecao, tribunais, relator, orgao_julgador, classe,
fase_processual, prioridade, tem_ementa, somente_ementa, ordenacao e
intervalo data_juntada_*.
- Reverse engineering do backend Angular/SPA: sessionId obrigatorio,
Origin/Referer e User-Agent de navegador exigidos pelo WAF, page 0-based
no wire, tamanho_pagina restrito a {5,10}, teto de 10000 resultados.
- Registrado na factory como "falcao".
- Testes: contrato + granular (offline, via responses) e integracao
(@pytest.mark.integration), com samples por colecao.
- Docs: notebook de exemplo, entradas em _quarto.yml/index.qmd e CHANGELOG.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
bdcdo
left a comment
There was a problem hiding this comment.
Revisão (ultrareview) — 15 achados verificados
Revisão multi-agente em modo recall máximo (11 ângulos independentes → ~40 candidatos → verificação adversarial → sweep final), toda em modo leitura sobre o head 0b5c032. O PR está bem construído — arquitetura consistente com os agregadores irmãos, docstrings no template, CHANGELOG correto e samples sem PII (anonimização da própria API). Os achados abaixo estão ordenados por gravidade.
Bugs de corretude
1. Buscas diferentes se misturam no mesmo diretório de download — client.py:286-290
_preparar_destino cria destino fixo por coleção (falcao_cjsg_{colecao}) com nomes de arquivo fixos, sem limpeza. cjsg_download('dano moral') grava 100 páginas; cjsg_download('assédio', paginas=1) depois sobrescreve só acordaos_0001.json, e cjsg_parse devolve 1 página de "assédio" + 99 páginas obsoletas de "dano moral", sem erro nem aviso. A docstring promete "subdiretório da busca", que não existe. Os irmãos que persistem arquivos isolam por timestamp (courts/_esaj/download.py, courts/tjsp/cjpg_download.py:131-134) — o fix é trivial no padrão da casa. (O fluxo cjsg() escapa por usar TemporaryDirectory fresco.)
2. range com step baixa o dobro de páginas (ou nenhuma) — client.py:301-305
_resolver_paginas reconstrói range(max(1, start), min(stop, total+1)) descartando o step: paginas=range(1, 101, 2) baixa as 100 páginas 1–100 em vez das 50 ímpares (2× requests num backend com rate limit ~50/janela, incluindo páginas que o usuário excluiu); range(10, 0, -1) vira range vazio e baixa 0 páginas em silêncio. paginas=list(range(1, 101, 2)) funciona correto, e PDPJ (client.py:341), TJSP cjpg (cjpg_download.py:142-147) e TJRN honram o step — o mesmo input dá resultados diferentes conforme o scraper. Correção mais profunda possível: normalizar/materializar step≠1 em normalize_paginas, que é a fonte única do contrato de paginas.
3. processo=None em precedentes reais — parse.py:44-48
Documento de precedentes sem a chave numero produz processo=None, violando o shape declarado (OutputCJSGBase.processo: str) — há linhas TRT9 assim no output commitado do próprio notebook (docs/notebooks/falcao.ipynb). O contract test assere df['processo'].notna().all() sobre um sample TST que sempre tem numero, dando falsa segurança.
4. Data inválida com um só limite vaza crua para o backend — client.py:176-183
coerce_brazilian_date faz passthrough do que não parseia e validate_intervalo_datas retorna cedo quando um dos lados é None (utils/params.py:353). cjsg('x', data_juntada_inicio='31/02/2024') (ou 'abc') não levanta o ValueError prometido na docstring e envia dataInicio=31/02/2024 cru na querystring — se o backend ignorar o filtro malformado, a busca devolve resultados sem recorte de data, silenciosamente. Com ambos os limites preenchidos, o mesmo valor levanta o erro amigável.
5. Fallback download_path or "." é ramo morto e a docstring engana — client.py:287
BaseScraper.set_download_path(None) sempre cria tempfile.mkdtemp() (core/base.py:32-33), nunca deixa download_path como None. Quem chamar cjsg_download sem diretorio esperando os JSONs no diretório atual (como diz a docstring) encontra os arquivos em /tmp/tmpXXXX/falcao_cjsg_acordaos — e os perde na limpeza do /tmp se não guardar o path retornado.
6. cjsg(diretorio=...) colide e o rótulo do TypeError aponta o método errado — client.py:139-143 e client.py:281
cjsg repassa **kwargs junto de diretorio=tmp fixo, então diretorio= passado a cjsg dá "got multiple values for keyword argument 'diretorio'" críptico em vez da mensagem canônica. E raise_on_extra_kwargs usa rótulo fixo "FalcaoScraper.cjsg()" mesmo quando o kwarg inválido veio de cjsg_download (a família eSAJ passa method_label por endpoint, _esaj/base.py:182).
Fiscalização e testes
7. O falcao é invisível a toda a rede anti-drift de tests/schemas/ — tests/schemas/test_schema_coverage.py:219
Por ser o primeiro agregador com endpoint cjsg, ele cai num ponto cego: a tupla candidates dos meta-testes de agregadores não contém "cjsg", então nada falha cobrando o registro em EXPECTED_AGGREGATOR_SCHEMAS/EXPECTED_AGGREGATOR_OUTPUT_SCHEMAS (CONTRIBUTING, checklist de schemas item 4) nem em CASES/DOWNLOAD_REFERENCE_CASES do docstring parity (CLAUDE.md, docstrings regra 7). Resultado: extra="forbid", aceitação das 4 formas de paginas, nomes canônicos e paridade docstring↔schema nunca rodam para o falcao — é exatamente o schema "órfão silencioso" que a suíte existe para impedir, e explica o "1778 passed" ser verdadeiro e ainda assim enganoso. O fix precisa das duas pontas: registrar os mappings e adicionar "cjsg" às duas tuplas candidates (senão test_no_stale_entries acusa a entrada nova como órfã).
8. Nenhum teste offline exercita 2+ páginas — test_falcao_contract.py:27
O parâmetro total de _register é código morto; o caminho "página 2 envia page=1, dorme e grava _0002.json" nunca roda em teste — um off-by-one na querystring das páginas 2+ passaria a suíte inteira. Os asserts de querystring também re-parseiam responses.calls com parse_qs inline em vez do query_param_subset_matcher de tests/_helpers.py:173 (CONTRIBUTING, item 3: matcher de payload sempre que possível).
9. Falta o capture script — tests/fixtures/capture/falcao.py
Item 1 do checklist do CONTRIBUTING; todos os agregadores existentes têm o seu (datajud.py, jusbr.py, comunica_cnj.py). Sem ele, os 6 samples não têm procedência reproduzível ("nunca sintetizar samples à mão") e não há como regenerá-los nem detectar drift quando o backend mudar de shape. Nota positiva: os samples estão limpos de PII.
Documentação e tratamento de erro
10. Docstring promete "colunas canônicas garantidas" que o shape real não sustenta — client.py:116-120
Três frentes: (a) busca vazia devolve DataFrame sem coluna alguma — é a convenção de toda a base (verificado em comunica_cnj/datajud/eSAJ), então o defeito é a promessa, única no repo; (b) ementa/data_julgamento não existem nas coleções sem a chave bruta — df['ementa'] em sentencas dá KeyError apesar do "None nas demais" em schemas.py:148; (c) decisoesmonocraticas traz o dado em nomeRedator, que o parser não mapeia — a coluna relator nunca existe nessa coleção e o filtro relator= tende a ser inócuo nela. Sugestão: relativizar a promessa ao shape real (regra do projeto: "Output reflete shape real do parser") ou materializar o núcleo no parser.
11. 403 do WAF vira retry cego + erro opaco — client.py:208
403 está em RETRYABLE_STATUSES (core/http.py:34) e as chamadas não passam on_response: um bloqueio do CloudFront (determinístico — o próprio download.py documenta os gatilhos) paga 3 tentativas com backoff e morre em RetryExhaustedError genérica, enquanto a base tem BotChallengeBlockedError + marker anti_bot exatamente para isso (5 call sites em courts/_trf/download.py). Sem isso, os testes de integração não se beneficiam do xfail ambiental e o recipe -m "integration and not anti_bot" não exclui o falcao. Atenuante: o comunica_cnj também depende do retry genérico.
Reuso e simplificação
12. 3ª cópia da mecânica de paginação — client.py:292-306: _total_paginas/_resolver_paginas repetem comunica_cnj (client.py:146-158) e _esaj/download.py:132-153, e _validar_input é a 2ª cópia do pipeline de validação de agregador com data canônica própria. Pela Regra 1 do #84 já há massa crítica para promover a utils/params.py — as cópias já divergem entre si (eSAJ exclui a página 1, PDPJ honra o step) e compartilham o bug do step.
13. Hooks anunciados e nunca consumidos — client.py:43-44 e schemas.py:85: INPUT_CJSG e COLECOES não são lidos por nada (o código hardcoda InputCJSGFalcao nas linhas 277/283 e a constante de módulo no validator), e BACKEND_DATE_FORMAT nunca é lido — o client hardcoda "%Y-%m-%d" em 2 lugares (183 e 275). Ou consumir via self/cls (como a eSAJ consome self.INPUT_CJSG), ou remover os atributos.
14. field_validator reinventando Literal — schemas.py:106-132: colecao: Literal[...], tamanho_pagina: Literal[5, 10] e ordenacao: Literal[...] | None eliminam ~27 linhas com a mesma validação, no padrão que tjto/tjpa/tjpe/tjsc/_esaj já usam (e que os TODOs #212 do repo apontam como direção).
15. Probe incondicional da página 1 — client.py:208-212: com paginas explícito que não inclui a página 1, paga-se um request extra (payload de até ~1MB, descartado) só para obter um total que a primeira página pedida também traria — o backend é REST stateless e toda resposta carrega quantidadeTotal.
Menores (fora do corte de 15)
_as_csvsem escape de vírgula — confirmado ao vivo que o backend splita o parâmetro como OR, mas nenhum valor legítimo do domínio contém vírgula (formato canônico é "NOME SOBRENOME"), então o gatilho é atípico.model_configredeclarado idêntico ao pai nas duas classes de schema;max(0, pagina-1)defensivo para caso impossível;loggerde módulo morto emdownload.pyeparse.py.- Notebook: célula com linha de exibição morta (
df_filtrado[[...]].head()sobrescrita pelohead()seguinte);json.load(open(...))semwithdentro deTemporaryDirectory(risco dePermissionErrorno Windows). - Integração:
assert len(df) == 10exato acoplado ao corpus vivo do backend; nenhum teste passadatetime.datenas datas apesar de docstring/schema prometerem. relator=''é copiado como string vazia (o guard usais not None, diferente da truthiness das_CLASSE_KEYS), e o teste granular correspondente passa vacuosamente ('' == '').- README sem a linha do falcao na tabela de agregadores (lacuna pré-existente — comunica_cnj também falta).
🤖 Revisão gerada com Claude Code (/code-review max)
O que é
Novo agregador
falcao(FalcaoScraper,jus.scraper("falcao")) para a Jurisprudencia Nacional da Justica do Trabalho — sistema hospedado pelo CSJT emjurisprudencia.jt.jus.br(endpoint publico/no-auth/pesquisa). Uma unica busca cobre o TST e os 24 TRTs sobre cinco colecoes de documentos.API
cjsg(pesquisa, paginas=None, **kwargs)+ parcjsg_download/cjsg_parse, devolvendopd.DataFrame.processo,colecao,tribunal(+relator,classe,ementa,data_julgamento,data_juntadaconforme a colecao). Campos brutos preservados viaextra="allow"no Output.extra="forbid"(kwargs desconhecidos ->TypeError):colecao(acordaosdefault,sentencas,decisoesmonocraticas,precedentes,recursorevista),tribunais,relator,orgao_julgador,classe,fase_processual,prioridade,tem_ementa,somente_ementa,ordenacao(mais_relevante/mais_recente/menos_recente) e intervalodata_juntada_inicio/data_juntada_fim(aceitaDD/MM/AAAA,AAAA-MM-DDoudate; filtradataJuntada).Engenharia reversa do backend (SPA Angular)
Codigo final usa so
requests. Descobertas que viraram comportamento do scraper:sessionIdobrigatorio (sem ele: 403"Tentativa invalida de acesso ao sistema").Origin/Refererdo site e User-Agent de navegador exigidos pelo WAF (CloudFront) — o UA padrao do juscraper toma 403, entaoDEFAULT_HEADERSsobrescreve com UA de navegador (mesma abordagem do ComunicaCNJ).page0-based no wire (a API publica do juscraper continua 1-based).tamanho_paginarestrito a{5, 10}para usuario nao autenticado; resultados limitados a 10000 por consulta.Testes
responses):tests/falcao/test_falcao_contract.py(schema do DataFrame, querystring com as guardas obrigatorias,TypeError/ValidationError) etest_falcao_granular.py(helpers de build/parse/paginacao). Samples por colecao emtests/falcao/samples/pesquisa/.@pytest.mark.integration):test_falcao_integration.py— busca real, 5 colecoes, filtros, download+parse. Todos passando contra o site ao vivo.Docs
Notebook de exemplo (
docs/notebooks/falcao.ipynb, em ingles), entradas em_quarto.ymleindex.qmd, e uma entrada consolidada noCHANGELOG.md.Notas para revisao
falcao= nome interno do sistema (backendbr.jus.trt9.falcao,client_idfalcao-trt9).precedentesnao tem numero CNJ de processo —processorecebe o numero do precedente (numero, ex.: numero da sumula/OJ). Documentado no schema.🤖 Generated with Claude Code