github jtrecenti/juscraper v0.4.0

4 hours ago

Added

  • Novo raspador stf (STFScraper) para a busca de jurisprudencia do STF (jurisprudencia.stf.jus.br), com listar_decisoes (acordaos ou decisoes monocraticas, paginado, ate 250 por pagina) e contar_decisoes (total e facetas do portal: base, classe, ministro, UF, orgao). Filtros: pesquisa na sintaxe do portal ($ como curinga, ou), base, classe, inteiro_teor e data_julgamento_*/data_publicacao_*. O portal fica atras de um desafio JavaScript do AWS WAF: o cookie aws-waf-token e obtido com Playwright, no novo extra pip install "juscraper[stf]" (seguido de playwright install chromium), ou pode ser passado em jus.scraper("stf", waf_token=...); as buscas seguem em requests e o cookie e renovado quando o WAF volta a desafiar. A API so entrega os 10.000 primeiros registros de uma busca: pagina alem disso levanta ValueError antes de qualquer requisicao, e paginas=None emite UserWarning e para no teto.
  • Contratos de teste offline para o agregador JusBR (auth, cpopg, download_documents). Suite em tests/jusbr/ cobre 18 cenarios: auth(token) (token valido, expirado -> ValueError, sem exp, malformado), cpopg (1 CNJ, list[str], lista vazia, CNJ invalido sem HTTP, sem auth previa) e download_documents (texto+binario, so texto, so binario, href malformado baixa so binario, ambos hrefs ausentes pulam, max_docs_per_process=1, sem auth). Mocks via responses + OrderedRegistry para o fluxo multi-step lista -> detalhes; samples capturados pelo backend real via tests/fixtures/capture/jusbr.py (depende de JUSBR_JWT/JUSBR_CNJ_1/JUSBR_CNJ_2 env vars) com sanitizacao agressiva pos-captura (CNJ neutro, PII redatada, regex defensivo para CPF/e-mail). auth_firefox() ficou fora — depende de cookies reais do Firefox, candidato a cassette VCR na Fase 4 da #113. Wiring de InputAuthJusBR/InputCPOPGJusBR/InputDownloadDocumentsJusBR segue como follow-up separado (regra do projeto: contrato e wiring nunca no mesmo PR). Refs #104, #113, #141.
  • Raspador TRF6 (cpopg — consulta pública de processos de 1º grau via eproc). Acessa o sistema eproc da Seção Judiciária de Minas Gerais em eproc1g.trf6.jus.br/eproc/. O formulário é gated por captcha de texto (imagem PNG embutida inline em base64 no HTML do form, validado server-side); o scraper resolve usando o pacote opcional txtcaptcha (CRNN pretrained do HuggingFace, baixado on-demand e cacheado). Cada captcha é vinculado ao cookie PHPSESSID, então cada nova tentativa após rejeição faz um GET fresco do form para obter um captcha novo (controlado por max_captcha_attempts, default 3). API: cpopg(id_cnj) aceita um CNJ ou lista; devolve pd.DataFrame com colunas id_cnj, processo, classe, data_autuacao, situacao, magistrado, orgao_julgador, assuntos, polo_ativo, polo_passivo, mpf, perito, movimentacoes. Implementação completamente independente em courts/trf6/ (client.py, download.py, parse.py, schemas.py) — sem infra compartilhada com TRF3/TRF5 ou outros tribunais (mesma justificativa: tribunais podem trocar de sistema). Schema pydantic com extra='forbid' no Input. Samples HTML em tests/trf6/samples/cpopg/ (form_initial, detail_normal, search_no_results, search_bad_captcha) capturados via tests/fixtures/capture/trf6.py. Cobertura: contrato offline com captcha solver mockado (5 testes incluindo retry após rejeição e fail após N tentativas), schema, integração (@pytest.mark.integration).
  • Parametros download_pecas: bool = False e diretorio: str | None = None em cpopg de TRF1Scraper, TRF3Scraper e TRF5Scraper. Quando download_pecas=True, cada peca (documento juntado) e baixada via documentoSemLoginHTML.seam para <diretorio>/<cnj>/<id_processo_doc>.html (XHTML auto-contido, imagens embarcadas como data: URLs) e o DataFrame ganha a coluna pecas com a lista de caminhos por processo. Default False -- comportamento atual de cpopg (so metadados + movimentacoes + lista de documentos) preservado. A flag vive no cpopg em vez de em um metodo separado porque os tokens ca que identificam cada peca estao amarrados a conversa Seam do detalhe -- pecas precisam ser baixadas na mesma requests.Session, entao isolar num metodo a parte exigiria refazer o GET do detalhe so para obter tokens validos. Refs #272.
  • cpopg em TRF1Scraper, TRF3Scraper e TRF5Scraper agora pagina tambem a tabela de documentos juntados (processoDocumentoGridTab), nao so a de movimentacoes. Antes, processos com mais de 15 documentos so devolviam a primeira pagina na coluna documentos -- e com download_pecas=True, so as 15 primeiras pecas eram baixadas. O paginador novo (extract_docs_pagination + merge_docs_pages, mesma forma do par movs ja existente) detecta o slider Richfaces do panel de docs e busca as paginas adicionais via POST AJAX dentro da mesma sessao. Refs #272.
  • Nova excecao juscraper.core.exceptions.BotChallengeBlockedError, levantada quando um portal devolve HTTP 403 com body Access Denied (tipico de bot manager Akamai). cpopg em TRF1Scraper, TRF3Scraper e TRF5Scraper detecta esse caso especifico e propaga a excecao em vez de engolir (como faz com erros transientes por item) -- um bloqueio Akamai e session-wide, nenhum CNJ do batch passaria. A mensagem orienta o usuario a aguardar alguns minutos ou trocar de IP (VPN, hotspot) e inclui a Reference #... da Akamai para suporte. 403 sem Access Denied no body não vira BotChallengeBlockedError: cai no fluxo de retry padrão do HTTPScraper (403 está em RETRYABLE_STATUSES) e, persistindo, esgota max_retries levantando RetryExhaustedError — ver a entrada correspondente em Changed.
  • Novo agregador pdpj (PdpjScraper) para a API DATALAKE - Processos do PDPJ (api-processo-integracao.data-lake.pdpj.jus.br). Autenticacao via JWT com auth(token) e endpoints publicos existe, cpopg, documentos, movimentos, partes, pesquisa, contar e download_documents (texto e/ou binario). Validacao via pydantic com extra="forbid".
  • Metodos listar_classes, listar_assuntos e listar_orgaos na familia eSAJ (TJAC, TJAL, TJAM, TJCE, TJMS, TJSP), mais listar_varas no TJSP, para descobrir os IDs internos aceitos pelos filtros classe/assunto/orgao_julgador/vara de cjsg/cjpg. Cada metodo baixa a arvore de selecao do eSAJ (endpoint *TreeSelect.do, a arvore inteira num unico GET) e devolve um pd.DataFrame achatado com as colunas id, nome, id_pai, nivel, selecionavel e caminho — so as linhas selecionavel=True valem como filtro. O argumento grau ("2" cjsg, default; "1" cjpg, so TJSP) seleciona o grau; pedir uma arvore inexistente no tribunal levanta ValueError. Resolve a dor da issue #228 (era pouco intuitivo descobrir os codigos de classe/assunto). Refs #228.
  • Parametro count_only=True em cjsg (TJAC, TJAL, TJAM, TJCE, TJMS, TJSP) e cjpg (TJSP). Quando passado, o metodo faz so a chamada inicial (POST + 1 GET no caso eSAJ; 1 GET no caso CJPG), extrai o total de resultados da primeira pagina e retorna int em vez do pd.DataFrame completo. Util para estimativa de wall-clock antes de coletas longas — ~2s/resultado em cjsg/cjpg, entao 5000 hits = ~3h. Com auto_chunk=True (default) e janela data_julgamento_* > 366 dias, itera as janelas disjuntas (iter_date_windows) e soma; e soma bruta (sem dedup por cd_acordao/id_processo), entao pode divergir ligeiramente de len(cjsg(...)) quando ha acordaos republicados em janelas diferentes. paginas e ignorado em count_only (emite UserWarning); auto_chunk=False + janela > 366d continua levantando ValueError. Refs #92.

Changed

  • pyarrow deixa de ser dependência obrigatória e passa para o extra docs (pip install "juscraper[docs]"). A biblioteca nunca importa pyarrow: as únicas chamadas de df.to_parquet(...) no repositório estão em notebooks de docs/. Com isso, instalar o juscraper deixa de impor uma faixa de versão de pyarrow ao ambiente de quem o instala — o que resolve duas dores de uma vez: o conflito de resolvedor com cudf-cu12 no Google Colab, que motivava o teto pyarrow<20.0.0 (refs #25), e o alerta de pip-audit sobre PYSEC-2026-113 em projetos downstream, que o teto tornava inescapável ao travar a resolução em pyarrow 19.x (refs #342). Migração: quem chamava df.to_parquet(...) sobre um DataFrame devolvido pelo juscraper contando com o pyarrow que vinha de carona precisa passar a instalar pyarrow (ou fastparquet) explicitamente. Refs #25, #342.
  • TRF1, TRF3 e TRF5: cpopg passa a usar core.http.HTTPScraper — respostas 5xx/429 agora sao retentadas com backoff exponencial (respeitando Retry-After) em vez de propagar requests.HTTPError na primeira ocorrencia; esgotar max_retries levanta RetryExhaustedError. O 403 Access Denied da Akamai continua virando BotChallengeBlockedError imediatamente, sem retentativas (bloqueio e session-wide — retentar so queimaria tempo). Mesmas colunas e mesmo payload do request. Refs #281, #294.
  • JusBR auth(token): agora valida exp explicitamente ("verify_exp": True nas options do jwt.decode). Antes, com verify_signature=False, o PyJWT desativava verify_exp por padrao e o ramo except jwt.ExpiredSignatureError era dead code — tokens expirados passavam silenciosamente. Tokens com exp no passado agora levantam ValueError("Token JWT expirado.") como ja documentado. Tokens sem exp continuam aceitos (PyJWT so valida o claim quando ele existe). Refs #141.
  • JusBR cpopg: linhas de fallback (CNJ Invalido / Nao encontrado na lista inicial / Erro ao obter ou parsear detalhes) agora populam tambem a coluna processo (canonico do projeto), alem de processo_pesquisado. Antes, happy-path emitia processo e fallbacks emitiam processo_pesquisado — DataFrame misto tinha NaN espalhado e o schema OutputCPOPGJusBR declarava processo_pesquisado como required, divergindo da realidade. OutputCPOPGJusBR agora declara processo: str (alinhado com OutputCJSGBase canonico); processo_pesquisado continua presente em rows de fallback como sinonimo historico via extra="allow". Refs #141.
  • JusBR download_documents: documentos com so hrefTexto ou so hrefBinario agora sao baixados parcialmente (texto ou binario sozinho), em vez de pulados. Documento e pulado apenas quando os dois hrefs faltam. Comportamento anterior fazia continue quando qualquer UUID nao podia ser extraido — usuario perdia silenciosamente documentos parciais. Linha de saida tem texto=None ou _raw_binary_api=None quando o href correspondente ausenta. Refs #141.
  • TJAPScraper.cjsg/cjsg_download agora falham alto quando o backend Tucujuris devolve um envelope de erro (HTTP-200 com status == "ERRO" e sem a chave dados), em vez de retornar um DataFrame vazio silenciosamente. O caso mais comum hoje é "A verificação de segurança falhou": desde ~2026 o TJAP passou a exigir um CAPTCHA Cloudflare Turnstile na busca de jurisprudência, validado server-side, que o raspador (HTTP puro, sem navegador) não consegue produzir — então cjsg levanta TJAPSecurityCheckError (subclasse de core.exceptions.HTTPSemanticError) com a explicação e a referência à issue. Não há solução pela API pública: a coleta de cjsg do TJAP está indisponível enquanto o Turnstile estiver ativo. Outros envelopes ERRO levantam TJAPApiError. Busca com zero resultados ("Nenhum resultado encontrado.") continua devolvendo DataFrame vazio. Refs #279.
  • TJRN, TJRO e TJRR: cjsg passa a usar core.http.HTTPScraper (backoff exponencial centralizado para 429/5xx, respeito a Retry-After) e os helpers de core.parse_utils (clean_html, coerce_date_columns em TJRN/TJRO) + utils.cnj.format_cnj(strict=False). Mesmas colunas e mesmo payload do request. Duas pequenas mudancas de comportamento herdadas da infra centralizada: (a) esgotar max_retries em status retryable agora levanta RetryExhaustedError em vez de propagar a ultima requests.HTTPError; (b) em TJRN/TJRO, resposta 200 com JSON corrompido deixa de fazer retry e passa a propagar ValueError na primeira ocorrencia (o _fetch_page antigo capturava ValueError no laco de retry; o _request_with_retry so retry-a 429/5xx). Refs #194, #202.
  • TJAP, TJRS e TJES: cjsg (e cjpg no TJES) passam a usar core.http.HTTPScraper e os helpers de core.parse_utils (clean_html em TJAP; coerce_date_columns nos tres). Mesmas colunas e mesmo payload do request. Mudancas de comportamento herdadas da infra centralizada: (a) esgotar max_retries em 429/5xx agora levanta RetryExhaustedError em vez de propagar a ultima requests.HTTPError (TJAP e TJES tinham retry local que propagava requests.RequestException apos MAX_RETRIES=3 com backoff 2 ** attempt; TJRS nao tinha retry algum e passa a ter max_retries=3 com mesmo backoff exponencial); (b) em TJAP e TJES, resposta 200 com JSON corrompido deixa de fazer retry e passa a propagar ValueError na primeira ocorrencia (o _fetch_page antigo capturava ValueError no laco de retry; o _request_with_retry so retry-a 429/5xx); (c) em TJRS, transitorios 429/5xx que antes propagavam imediatamente agora sao retentados ate max_retries=3. Breaking implicito em TJRSScraper.cjsg/cjsg_download: o parametro session: requests.Session | None = None sai da assinatura publica (nao estava documentado no schema pydantic — descrito como "dependencia de runtime, nao da API" — nem coberto por nenhum teste). Quem precisava de uma session customizada (proxies, cookies) deve mutar scraper.session apos o __init__. Refs #194, #202.
  • TJBA, TJPB e TJMT: cjsg passa a usar core.http.HTTPScraper (backoff exponencial centralizado para 429/5xx, respeito a Retry-After) em vez de duplicar retry local em cada download.py. TJBA e TJMT tambem migram o loop pd.to_datetime(...).dt.date para core.parse_utils.coerce_date_columns. TJPB mantem pd.to_datetime explicito com format="%d/%m/%Y" porque coerce_date_columns nao expoe format= (datas DD/MM/AAAA ambiguas sem hint, mesmo trade-off do TJRR). TJMT mantem _strip_html local porque clean_html substitui tags por espaco em vez de remover, alterando o output observavel em 100% das amostras versionadas (<p>foo</p><b>bar</b> -> "foo bar" em vez de "foobar"); a unificacao desse helper depende de uma variante clean_html(separator="") no core.parse_utils (fora do escopo desta migracao). Em TJBA, o parametro session foi removido da assinatura publica de cjsg_download e cjsg — passar session= agora cai em extra_forbidden via pydantic (TypeError claro em vez de injecao silenciosa de session). Mesmas mudancas de comportamento herdadas da infra centralizada do batch anterior: esgotar max_retries em status retryable levanta RetryExhaustedError; resposta 200 com JSON corrompido deixa de fazer retry e propaga ValueError/requests.HTTPError na primeira ocorrencia. Refs #194, #202.
  • TJPR e TJMG: cjsg passa a usar core.http.HTTPScraper (backoff exponencial centralizado para 429/5xx, respeito a Retry-After) e os helpers de core.parse_utils (TJMG migra _clean local para clean_html e o loop pd.to_datetime(...).dt.date para coerce_date_columns(date_format="%d/%m/%Y"); TJPR migra o loop pd.to_datetime para coerce_date_columns). Mesmas colunas e mesmo payload do request. As particularidades de cada um sao preservadas: TJPR continua hitando a home (get_initial_tokens -> populate_session) uma vez por chamada para popular JSESSIONID no cookie jar da session, mas o cookies={'JSESSIONID': ...} redundante saiu (a session ja carrega o cookie automaticamente); o tjpr.url.crypto token, que era extraido mas nunca consumido, deixa de ser parseado. TJMG mantem _solve_captcha local em download.py com o laco semantico de 3 tentativas (retry de OCR errado, distinto do retry transport-level centralizado em _request_with_retry); a session continua sendo passada explicitamente para _solve_captcha porque o body DWR le JSESSIONID direto do cookie jar. Como efeito colateral da migracao para _request_with_retry, o GET do PNG do captcha e o POST DWR de validacao agora abortam imediatamente em 4xx nao-retryable (antes o status era ignorado e o conteudo seguia para a OCR/parse, o que produzia uma falha tardia mascarada como "OCR errado"); para 429/5xx ha retry transport-level antes de entrar no laco semantico de OCR. User-Agent Chrome custom de ambos os tribunais (ha UA gating do portal) continua via override de _configure_session. Em TJPR, o try/except row-level de cjsg_parse (que degrada graciosamente quando a busca da ementa-completa de uma decisao falha) agora tambem captura RetryExhaustedError alem de requests.RequestException — sem isso, um 5xx persistente em uma unica ementa derrubaria o DataFrame inteiro no novo regime. Mudancas de comportamento herdadas da infra centralizada do batch anterior: esgotar max_retries em status retryable levanta RetryExhaustedError. Refs #194, #202, #248.
  • TJTO, TJPI e TJSC: cjsg (e cjpg/cjsg_ementa em TJTO) passa a usar core.http.HTTPScraper, substituindo o retry exponencial local por self._request_with_retry. Mesmas colunas e mesmo payload do request. Mudanca de comportamento herdada da infra centralizada: esgotar max_retries em status retryable agora levanta RetryExhaustedError em vez de propagar a ultima requests.RequestException; o escopo de retry passa de "qualquer RequestException" (incluindo ConnectionError/Timeout) para 429/5xx, com respeito a Retry-After numerico. Em TJTO, alem disso, o parametro publico session= foi removido dos metodos cjsg/cjsg_download/cjpg/cjpg_download — a sessao e gerenciada pelo HTTPScraper; sobreposicao continua possivel via _configure_session. Refs #194, #202.
  • TJDFT, TJGO, TJRJ, TJPA e TJPE: cjsg passa a usar core.http.HTTPScraper (backoff exponencial centralizado para 429/5xx, respeito a Retry-After). TJDFT e TJPA tambem migram o loop pd.to_datetime(...).dt.date para core.parse_utils.coerce_date_columns (TJPA preserva o format="%Y-%m-%d" original via date_format=; TJDFT seguia sem format= e mantem o mesmo comportamento). TJGO mantem o _clean privado em vez de migrar para core.parse_utils.clean_html porque o backend do Projudi envolve trechos de texto em tags inline (<b>, <i>) e clean_html substituiria as tags por espaco — inserindo espaco antes de pontuacao (<b>dano moral</b>, -> "dano moral ," em vez de "dano moral,"); mesmo trade-off ja documentado em TJMT no batch 3 e aguardando uma variante clean_html(separator="") no core.parse_utils (fora do escopo desta migracao). Mesmas mudancas de comportamento herdadas da infra centralizada do batch anterior: esgotar max_retries em status retryable levanta RetryExhaustedError em vez de propagar a ultima requests.HTTPError. Em TJPA, especificamente, o retry exponencial dentro de _fetch_page foi removido — agora so 429/5xx fazem retry (antes qualquer RequestException/ValueError retentava). Breaking change em TJPEScraper.cjsg/cjsg_download: o parametro session: requests.Session | None = None foi removido; o override de session passa pelo HTTPScraper._request_with_retry (consumido via self.session). Refs #194, #202.
  • TJSPScraper.cjsg aceita pesquisa="" por default — antes o argumento era obrigatorio e tjsp.cjsg(classe="...", assunto="...") levantava TypeError. Agora o usuario pode buscar so por filtros (sem termo textual), igualando o comportamento de cjpg. Refs #229.
  • EsajSearchScraper (base de TJAC/TJAL/TJAM/TJCE/TJMS/TJSP) herda de juscraper.core.http.HTTPScraper em vez de BaseScraper. A construção de self.session, o User-Agent padrão e o hook _configure_session passam a vir da base compartilhada — o override do TJCE para o adapter TLS continua válido sem alteração. Os GETs paginados em _esaj/download.py::download_cjsg_pages agora delegam ao retry centralizado HTTPScraper._request_with_retry: o escopo de retry passa a ser 429/5xx com backoff exponencial e suporte a Retry-After (antes: qualquer requests.RequestException, incluindo ConnectionError/Timeout, com backoff linear modulado por sleep_time). Quando esgota as tentativas, a exceção passa a ser juscraper.core.exceptions.RetryExhaustedError em vez da requests.RequestException original — usuários que capturavam a exceção antiga precisam atualizar. Refs #203, #194, #201.
  • ComunicaCNJScraper, JusbrScraper e DatajudScraper agora herdam de core.http.HTTPScraper (refs #204, Fase 3 de #194). A session/headers de cada um passa pelo hook compartilhado _configure_session (User-Agent, Origin/Referer no ComunicaCNJ; UA Chrome no JusBR; UA default do HTTPScraper no Datajud), e a validacao session= (TypeError quando nao for requests.Session) passa a ser garantida via heranca (cumpre #185 sem duplicar codigo). ComunicaCNJ ganha resiliencia a 429/5xx via self._request_with_retry (antes nao tinha retry algum); JusBR substitui o request_with_retry interno do download.py pelo mesmo, e os fetch_* agora recebem um request_fn (tipicamente self._request_with_retry) em vez da session crua — o contrato de "erro -> None" e preservado capturando RetryExhaustedError/RequestException. DatajudScraper herda HTTPScraper apenas para session/headers e cumprimento de #185: a funcao call_datajud_api continua intacta porque o retry especifico (504/Timeout -> reduz size por FALLBACK_DIVISOR, 1 retry) e incompativel com o backoff exponencial generico, e a refatoracao virou explicitamente fora de escopo no proprio guarda-chuva #194. Breaking change implicito em ComunicaCNJScraper.listar_comunicacoes: quando o servidor devolve 429/5xx persistente, a excecao propagada passa a ser juscraper.core.exceptions.RetryExhaustedError (apos esgotar max_retries) em vez de requests.HTTPError. Callers que faziam except requests.HTTPError para tratar indisponibilidade do CNJ devem passar a capturar RetryExhaustedError (ou ambas). 4xx nao-retryable continua propagando requests.HTTPError via raise_for_status() como antes. Em JusBR a mudanca fica encapsulada nos fetch_* (retornam None em qualquer falha), entao o contrato publico de cpopg/download_documents nao muda.
  • Filtros classe, assunto e orgao_julgador em cjsg (TJAC, TJAL, TJAM, TJCE, TJMS, TJSP) e em cjpg (TJSP) passam a aceitar int, str ou list[int | str]. Antes so aceitavam str. comarca em cjsg aceita int ou str (single-value, backend cdComarca). Listas viram CSV automaticamente; valores int viram str. A chamada tjsp.cjsg(classe=[417], assunto=[3607, 5885]) deixa de levantar ValidationError. Refs #232.
  • TJSPScraper.cjpg adota o nome canonico singular para IDs de filtro: classe/assunto/vara substituem classes/assuntos/varas. Os nomes plurais continuam funcionando como alias deprecados (com DeprecationWarning) por pelo menos um minor release. Passar plural e singular simultaneamente (cjpg(classe=12728, classes=[5885])) levanta ValueError. Refs #232.
  • TJBAScraper.cjsg/cjsg_download e DatajudScraper.listar_processos/contar_processos adotam o nome canonico singular: classe (TJBA) substitui classes; assunto (Datajud) substitui assuntos. Os plurais seguem funcionando como alias deprecados (DeprecationWarning) por pelo menos um minor release; plural + singular juntos -> ValueError. Refs #232.
  • TJPEScraper.cjsg/cjsg_download passam a validar filtros via InputCJSGTJPE (extra="forbid"). Kwargs desconhecidos passam a levantar TypeError (com a mensagem canonica de raise_on_extra_kwargs) em vez de serem silenciosamente descartados pelo **kwargs. Aliases deprecados (query, termo, classe_cnj, assunto_cnj, data_inicio/data_fim, data_julgamento_de/_ate) continuam aceitos com DeprecationWarning. Refs #93, #197.
  • TJRRScraper.cjsg/cjsg_download: o parametro relator muda de str | None para list[str] | None e passa a aceitar lista de nomes regimentais (ex.: relator=["ALMIRO PADILHA", "ERICK LINHARES"]). O scraper baixa o form GET inicial, extrai o mapa nomeRegimental -> bean Java opaco (menuinicial:relatorList) e injeta os valores resolvidos no body. Match insensivel a caixa e diacritico — "cristovao suter", "Cristóvão Suter" e "CRISTÓVÃO SUTER" resolvem para o mesmo magistrado; nomes desconhecidos levantam ValueError listando os disponiveis na forma canonica (UPPERCASE com acento). Mudanca breaking de tipo, mas o caminho anterior era no-op silencioso (o argumento str era descartado pelo backend) — nenhum codigo de usuario em producao dependia de uma string especifica ser respeitada. Refs #158.

Deprecated

  • TJSPScraper.cjpg: parametros classes/assuntos/varas emitem DeprecationWarning. Use os singulares canonicos classe/assunto/vara. Refs #232.
  • TJBAScraper.cjsg/cjsg_download: parametro classes emite DeprecationWarning. Use classe. Refs #232.
  • DatajudScraper.listar_processos/contar_processos: parametro assuntos emite DeprecationWarning. Use assunto. Refs #232.

Fixed

  • JusbrScraper.download_documents passa a tentar a próxima fonte documentada de metadados quando dadosBasicos.documentos ou documentos contêm um container malformado, aplica max_docs_per_process ao processo inteiro mesmo quando o mesmo numeroProcesso aparece em várias linhas e impede que metadados externos sobrescrevam numero_processo, texto e os campos brutos calculados pelo scraper. O método também passa a rejeitar base_df que não seja pd.DataFrame e max_docs_per_process negativo antes de iterar ou fazer chamadas de rede. Refs #312.
  • O contrato compartilhado de paginas em cjsg dos 25 tribunais estaduais, cjpg de TJSP/TJES/TJTO, DatajudScraper.listar_processos e ComunicaCNJScraper.listar_comunicacoes passa a rejeitar seleções vazias, inteiros/páginas menores ou iguais a zero e range descendente antes de qualquer chamada de rede. Esses valores contradiziam a paginação 1-based documentada; no DataJud, em particular, paginas=[] era convertido em None e baixava todas as páginas em vez de falhar.
  • DatajudScraper.listar_processos passa a respeitar a posição física solicitada quando paginas começa depois de 1 ou usa range com passo. O cursor search_after é sequencial e precisa partir da primeira página; antes, paginas=range(3, 6) enviava a primeira requisição sem cursor, mas rotulava e devolvia essa resposta como página 3, retornando na prática as páginas 1–3. Agora o scraper percorre o prefixo necessário, descarta as páginas não solicitadas e devolve apenas 3–5; listas esparsas continuam virando o intervalo contínuo entre mínimo e máximo, conforme o contrato público. Refs #315.
  • TJRRScraper.cjsg/cjsg_download: a paginação volta a avançar — cjsg("dano moral", paginas=range(1, 3)) traz processos novos na página 2, em vez de repetir a página 1. O POST AJAX de paginação enviava um payload mínimo (só os parâmetros do datatable + ViewState) que o backend PrimeFaces ignorava, devolvendo sempre a primeira página. Agora o scraper replica o que o navegador envia: ecoa o contexto completo do formulário de resultados (incluindo o termo de busca), dispara o evento de comportamento page do PrimeFaces, manda as flags de feature do datatable e o header Faces-Request: partial/ajax. Verificado ao vivo. Apenas a tabela de acórdãos é paginada; decisões monocráticas (segunda tabela, com paginador próprio) continuam vindo só da primeira página — paginação dessa tabela é follow-up. Refs #287.
  • TRF1, TRF3 e TRF5 (cpopg): as movimentações das páginas 2 em diante voltam a vir com acentuação correta. O fragmento AJAX (Richfaces) que pagina a tabela de movimentações é servido em UTF-8, mas o scraper o decodificava como latin-1 — o mesmo encoding da página de detalhe inicial, que de fato é latin-1. O resultado era double-encoding em toda movimentação paginada: "petição" virava "petição", "comunicação" virava "comunicação". Processos com até 15 movimentações (uma página) não eram afetados; só os paginados. Verificado ao vivo no TRF1 (processo com 55 movs em 4 páginas): zero mojibake após o fix; o TRF3 não pôde ser validado ao vivo por estar bloqueado por Akamai (#292), mas o sample capturado já está em UTF-8 e seu código é idêntico ao de TRF1/TRF5. A página de detalhe inicial segue em latin-1.
  • TJRJScraper.cjsg/cjsg_download chamados sem ano_inicio/ano_fim voltam a funcionar. O backend ASP.NET do TJRJ passou a exigir os campos cmbAnoInicio/cmbAnoFim nao-vazios no POST do formulario — enviar vazio (o default quando o usuario nao filtra por ano) fazia o tribunal responder HTTP 500 ja na submissao, abortando a coleta com RetryExhaustedError. Agora o scraper replica o padrao do site, preenchendo ambos com o ano corrente (a opcao mais nova do dropdown de anos). Buscas com ano explicito (cjsg(..., ano_inicio=2024, ano_fim=2024)) seguem inalteradas. Refs #278.
  • TJES (cjsg/cjpg): respostas HTTP 200 com corpo vazio/nao-JSON do backend Solr (intermitentes) deixavam de quebrar a coleta com um json.JSONDecodeError opaco (Expecting value: line 1 column 1). Agora sao tratadas como falha transitoria — retentadas com backoff — e, persistindo, levantam InvalidJSONResponseError com contexto da requisicao (URL, status, content-type). A validacao opcional de corpo JSON (expect_json) fica disponivel a todos os scrapers via _request_with_retry. Refs #275.
  • TJPRScraper.cjsg chamado sem o parametro paginas voltava apenas a primeira pagina de resultados. A extracao do total de paginas (extract_total_pages) buscava marcadores ("Pagina X de Y", links pageNumber=N) que nao existem no markup atual do portal TJPR, falhando silenciosamente e assumindo 1 pagina. Agora le o total a partir do link "Ultima Pagina" do paginador — cjsg("dano moral") sem paginas baixa todas as paginas, coerente com a convencao paginas=None. Refs #262.
  • core.http.HTTPScraper._request_with_retry passa a retentar HTTP 403 com backoff exponencial, alinhado com 429/5xx. No fluxo eSAJ cjsg (TJAC/TJAL/TJAM/TJCE/TJMS/TJSP), o POST inicial (resultadoCompleta.do) e o GET da primeira pagina (trocaDePagina.do?pagina=1, usado tambem para descobrir o total de paginas) tambem passam a usar o retry centralizado — antes ficavam fora, entao uma unica falha transitoria logo no comeco abortava a coleta. Motivado pelo TJSP eSAJ (#233): o WAF do eSAJ retorna 403 intermitente em raspagens longas mesmo sem o cliente bater no rate limit classico (429), e o refactor #203 tinha removido qualquer retry de 403, fazendo com que uma unica falha transitoria abortasse a coleta de centenas de paginas. 403 persistente (ex.: bloqueio por IP) ainda esgota max_retries e propaga RetryExhaustedError(status_code=403) — o WAF nao distingue transitorio e permanente pelo status code, entao o comportamento "correto" no caso permanente e o usuario receber a falha apos algumas tentativas. Mesmo backoff (base_backoff ** attempt = 2s, 4s por default — após as tentativas 1 e 2; a 3ª já levanta RetryExhaustedError sem dormir) e mesmo respeito a Retry-After. Em queries longas que envolvem bloqueio por IP institucional, a recomendacao continua sendo trocar para uma rede com IP residencial (ou aumentar sleep_time no construtor do scraper).
  • TJPE cjsg e TRF1/TRF3/TRF5/TRF6 cpopg voltam a funcionar em installs default (pip install juscraper). Antes os parsers exigiam lxml (BeautifulSoup(html, "lxml")), que nao esta declarado em pyproject.toml::dependencies — qualquer usuario sem lxml no ambiente recebia bs4.exceptions.FeatureNotFound. Trocado para html.parser (stdlib), padrao usado nos 22+ outros raspadores HTML do repo. Comportamento validado contra os samples versionados via contratos offline.
  • Endpoints com pydantic wired (TJSP cjsg/cjpg; familia eSAJ cjsg em TJAC/TJAL/TJAM/TJCE/TJMS; cjsg em TJDFT/TJES/TJBA/TJMT/TJAP/TJRS/TJPB/TJTO/TJPR/TJGO/TJRR/TJMG/TJRN/TJPA/TJRO/TJSC/TJPI; cjpg em TJES/TJTO) passam a autopreencher datas parciais. Quando o usuario informa apenas data_*_inicio, data_*_fim vira a data atual; quando informa apenas data_*_fim, data_*_inicio vira 01/01/1990 (e o auto-chunk divide a janela em chunks de 366 dias). Antes, o backend recebia uma data vazia em um dos lados e podia retornar resultado degenerado — no cjpg do TJSP, em particular, o paginador iterava sobre dezenas de milhares de paginas. UserWarning e emitido sinalizando o auto-fill e sugerindo passar a data explicitamente para restringir a janela.
  • TJRRScraper.cjsg(relator=...) voltou a funcionar. O backend TJRR migrou de um campo relator de texto livre para um SelectManyCheckbox (menuinicial:relatorList) cujos values sao beans Java serializados, e o scraper estava descartando o argumento silenciosamente (anotado # noqa: ARG001). Agora cjsg(relator=["ALMIRO PADILHA"]) parseia o form GET inicial, resolve o nome regimental para o bean correspondente e injeta a lista no body — o filtro chega ao backend e a busca e efetivamente restringida ao(s) relator(es). Refs #158.

Security

  • Hardening de logging nos agregadores JusBR e PDPJ: logs em modo verbose/DEBUG deixam de emitir credenciais. O header Authorization (e demais headers sensíveis) passa a ser redigido como [REDACTED] antes de ir para o log no download do JusBR; o dump claim-a-claim do JWT (verbose > 1) vira apenas a contagem de claims; e o sub do JWT deixa de ser logado no auth do PDPJ. Redação centralizada no novo helper juscraper.utils.logging_cfg.redact_headers, também adotado pelo Datajud. Refs #270.
  • TJMG (cjsg): a imagem do CAPTCHA passa a ser gravada com tempfile.NamedTemporaryFile (criacao atomica, nome imprevisivel) em vez de um caminho previsivel em /tmp (tjmg_captcha_<timestamp>.png). O caminho previsivel num diretorio compartilhado permitia, em host multiusuario, que um atacante local pre-criasse um symlink no caminho esperado e causasse overwrite de arquivo via TOCTOU (CWE-377). Severidade baixa — exige atacante local no mesmo host; nulo no uso single-user tipico, plausivel em CI/containers multi-tenant. Refs #271.
  • Downloaders do TJSP (cpopg via API, cposg via HTML e acordao) passam a validar os identificadores vindos do tribunal (cdProcesso, processo.codigo, cdAcordao) antes de construir o caminho de escrita. Um identificador com separador de path (/, \) ou segmento .. levanta ValueError em vez de gravar o arquivo fora do diretorio de download (path traversal / escrita arbitraria de arquivo). Em cpopg/cposg o processo afetado e logado e pulado (os callers ja capturam ValueError), e a coleta dos demais continua. Refs #269.

Don't miss a new juscraper release

NewReleases is sending notifications on new releases.