Skip to content

feat(pubmed): processamento em lote (.zip) na CLI + registro como comando instalável#1249

Open
Rossi-Luciano wants to merge 8 commits into
scieloorg:masterfrom
Rossi-Luciano:feat/1241-pubmed-batch-cli
Open

feat(pubmed): processamento em lote (.zip) na CLI + registro como comando instalável#1249
Rossi-Luciano wants to merge 8 commits into
scieloorg:masterfrom
Rossi-Luciano:feat/1241-pubmed-batch-cli

Conversation

@Rossi-Luciano

Copy link
Copy Markdown
Contributor

Nota: esta branch parte de feat/1234-pubmed-article-set-envelope (PR #1243, ainda não mergeado), porque depende de pipeline_pubmed_set para agrupar múltiplos artigos num único ArticleSet. O diff inclui os commits do #1243 até ele ser mergeado em master.

Este PR fecha a última sub-issue da quebra da #1226finaliza a solicitação original da issue-mãe.

O que esse PR faz?

A issue #1226 pede: "Utilitário de CLI: [...] aceitando argumentos de entrada (--input/-i) e saída (--output/-o), além de permitir o processamento em lote." Hoje pubmed_generator.py só processa um XML por vez e nem está registrado como comando instalável (diferente de pdf_generator, que já está).

  • get_xml_trees_and_errors(path) (nova, testável isoladamente): usa packtools.sps.pid_provider.xml_sps_lib.get_xml_items, que já trata .xml ou .zip de forma transparente — é o mesmo componente usado pelo spsvalidator para iterar pacotes SciELO. Retorna a lista de xmltree válidos e a lista de erros por arquivo, para que um XML corrompido dentro de um .zip não derrube o lote inteiro.
  • main() reescrito: usa pubmed.pipeline_pubmed_set (da Pubmed XML: adicionar envelope ArticleSet/FileHeader + DOCTYPE #1234) para gerar um único <ArticleSet> com um <Article> por XML de entrada (seja de um .xml só ou de todos os XMLs de um .zip). Avisos de arquivos com erro vão para stderr; exit code 1 só se nenhum artigo foi processado com sucesso.
  • setup.py: registrado pubmed_generator=packtools.sps.formats.pubmed_generator:main em entry_points.console_scripts, no mesmo padrão de pdf_generator.

Onde a revisão poderia começar?

packtools/sps/formats/pubmed_generator.py (reescrito quase por completo) e a entrada nova em setup.py.

Como usar a funcionalidade

Depois de instalado (pip install -e . neste repo, ou após publicado o pacote), o comando fica disponível direto no terminal:

# um XML só
pubmed_generator -i artigo.xml -o saida.xml

# um pacote .zip com vários XMLs -> gera 1 ArticleSet com 1 Article por XML
pubmed_generator -i pacote.zip -o saida.xml

Sem instalar o pacote, funciona do mesmo jeito via python -m:

python -m packtools.sps.formats.pubmed_generator -i artigo.xml -o saida.xml
python -m packtools.sps.formats.pubmed_generator -i pacote.zip -o saida.xml

Se algum XML do .zip estiver corrompido, o utilitário não para o lote inteiro — processa os válidos e avisa no stderr quais falharam:

Aviso: falha ao processar corrupt.xml: Unable to get xml with pre StartTag: invalid element name...
Arquivo criado em: saida.xml (1 artigo(s), 1 erro(s))

Como testar localmente (passo a passo)

  1. Ambiente:

    cd packtools
    source .venv/bin/activate  # ou seu virtualenv
  2. Suíte automatizada:

    python -m pytest tests/sps/formats/test_pubmed.py tests/sps/formats/test_pubmed_generator.py -v

    Esperado: 67 passed (suíte combinada de todas as sub-issues da Implementar função e utilitário de CLI para conversão de SciELO XML (SPS 1.10) para PubMed XML #1226 já mergeadas neste branch + as 5 novas em test_pubmed_generator.py: XML único, .zip com 2 artigos, .zip com 1 artigo inválido + 1 válido, e 2 testes end-to-end via main()).

  3. Rodar a CLI manualmente com artigos reais do repositório (não precisa instalar nada, roda com python -m):

    python3 - <<'PY'
    import tempfile, zipfile, os, subprocess, sys
    
    sample1 = "tests/samples/0034-7094-rba-69-03-0227.xml"
    sample2 = "tests/samples/example.xml"
    
    with tempfile.TemporaryDirectory() as tmp_dir:
        zip_path = os.path.join(tmp_dir, "package.zip")
        with zipfile.ZipFile(zip_path, "w") as zf:
            zf.write(sample1, arcname="article1.xml")
            zf.write(sample2, arcname="article2.xml")
    
        out_path = os.path.join(tmp_dir, "out.xml")
        subprocess.run(
            [sys.executable, "-m", "packtools.sps.formats.pubmed_generator", "-i", zip_path, "-o", out_path],
            check=True,
        )
        print(open(out_path, encoding="utf-8").read())
    PY

    Esperado: saída com <!DOCTYPE ArticleSet ...> + <ArticleSet> contendo 2 <Article>.

  4. Validar contra a DTD oficial do PubMed (critério de aceitação original da Implementar função e utilitário de CLI para conversão de SciELO XML (SPS 1.10) para PubMed XML #1226: "O XML de saída gerado pela ferramenta deve ser validado com sucesso [...], retornando zero erros de DTD" — o validador web citado na issue-mãe não serve para este formato, ver comentário em Implementar função e utilitário de CLI para conversão de SciELO XML (SPS 1.10) para PubMed XML #1226):

    curl -o /tmp/PubMed.dtd https://dtd.nlm.nih.gov/ncbi/pubmed/in/PubMed.dtd
    python3 - <<'PY'
    from lxml import etree as ET
    dtd = ET.DTD("/tmp/PubMed.dtd")
    doc = ET.parse("<caminho do out.xml gerado no passo 3>")
    print("VALID:", dtd.validate(doc))
    for err in dtd.error_log:
        print(err)
    PY

    Resultado obtido nesta implementação: VALID: True, zero erros, tanto para o artigo único quanto para o lote de 2 artigos.

  5. Testar resiliência a erro (um XML corrompido dentro do .zip não deve derrubar o lote):

    python3 - <<'PY'
    import tempfile, zipfile, os, subprocess, sys
    
    with tempfile.TemporaryDirectory() as tmp_dir:
        zip_path = os.path.join(tmp_dir, "package.zip")
        with zipfile.ZipFile(zip_path, "w") as zf:
            zf.write("tests/samples/0034-7094-rba-69-03-0227.xml", arcname="valid.xml")
            zf.writestr("corrupt.xml", "not valid xml at all <<<")
    
        out_path = os.path.join(tmp_dir, "out.xml")
        result = subprocess.run(
            [sys.executable, "-m", "packtools.sps.formats.pubmed_generator", "-i", zip_path, "-o", out_path],
            capture_output=True, text=True,
        )
        print("stdout:", result.stdout)
        print("stderr:", result.stderr)
        print("returncode:", result.returncode)
    PY

    Esperado: returncode == 0, 1 artigo processado, 1 erro reportado em stderr sem interromper o restante.

Algum cenário de contexto que queira dar?

Esta é a última sub-issue da quebra da #1226. Durante a validação end-to-end deste PR contra a DTD oficial, usando o artigo real tests/samples/example.xml, encontrei mais um bug (fora do escopo direto da #1241, mas que bloqueava a validação real do lote): um contrib desse artigo tem <given-names> sem <surname>, e a DTD do PubMed exige FirstName+LastName sempre juntos em Author — gerando Author inválido. Corrigi isso no PR #1245 (#1236), que já foi atualizado e re-testado; a #1246 (#1237) também foi atualizada para incorporar esse fix, já que reaproveita as mesmas funções.

Depois dessa correção, validei todos os 7 PRs combinados (#1243, #1244, #1245, #1246, #1247, #1248 e este) localmente — merge temporário de todas as branches, sem alterar nenhuma delas — e o resultado final é: 67 testes passando, VALID: True contra a DTD oficial tanto para artigos individuais quanto para o lote gerado via .zip, cobrindo todas as features implementadas na quebra da #1226 (ArticleSet, JournalTitle correto, Suffix/CollectiveName, GroupList, CopyrightInformation/CoiStatement, Object grant, e agora o processamento em lote).

Quais são tickets relevantes?

Closes #1241. Depende de #1243 (#1234). Relacionado a #1226 (issue-mãe, que esta PR finaliza a quebra de sub-issues).

Referências

…os elementos

pipeline_pubmed() gerava um <Article> solto, sem o envelope <ArticleSet>
e DOCTYPE exigidos pela DTD do PubMed. Extrai a construção do <Article>
para build_pubmed_article() e adiciona pipeline_pubmed_set() para
agrupar múltiplos artigos num único <ArticleSet> (pré-requisito da scieloorg#1241).

Validação contra a DTD oficial (lxml.etree.DTD +
https://dtd.nlm.nih.gov/ncbi/pubmed/in/PubMed.dtd) expôs 2 bugs no
pipeline, corrigidos aqui: Abstract/OtherAbstract precisavam vir antes
de CopyrightInformation/CoiStatement/ObjectList/ReferenceList, e
VernacularTitle nunca era chamado apesar de implementado e testado.

Refs scieloorg#1226, scieloorg#1234
Reconcilia scieloorg#1242 com o envelope ArticleSet desta branch. Journal/PubDate
é obrigatório na DTD do PubMed; build_pubmed_article agora levanta
MissingRequiredPubDateError quando não encontra data, em vez de gerar
um Journal inválido. pipeline_pubmed_set continua estrito (propaga o
erro para qualquer artigo inválido do lote).

Extrai build_article_set_xml (wrap de <Article>s já construídos em
<ArticleSet> + DOCTYPE) de dentro de pipeline_pubmed_set, para uso por
chamadores que precisem filtrar artigos inválidos antes de montar o
documento final (caso do CLI em lote da scieloorg#1241).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@Rossi-Luciano Rossi-Luciano force-pushed the feat/1241-pubmed-batch-cli branch from bf3a067 to 7a91eee Compare July 6, 2026 13:39
…el error

Per review on scieloorg#1250: the required-element check belongs to the pipe
function that builds the element, not bolted on after the fact in
build_pubmed_article -- and a missing element reflects incomplete
source data, not a transform bug. MissingRequiredPubDateError becomes
MissingRequiredElementError(element_path), raised directly inside
xml_pubmed_pub_date_pipe, ready to be reused for the other DTD-required
Journal children (PublisherName, JournalTitle, Issn) in a follow-up.
@Rossi-Luciano Rossi-Luciano force-pushed the feat/1241-pubmed-batch-cli branch from 7a91eee to bfdd2dd Compare July 10, 2026 16:32
…perdida

Encontrados ao validar as 26 amostras reais de tests/samples/ contra a
DTD real do PubMed em vez de só 1-2 arquivos escolhidos a dedo.

1. get_date() só olhava article_date (epub), ignorando collection_date
   (pub-type="epub-ppub"/date-type="collection") -- o padrão mais comum
   em artigos SciELO mais antigos. Isso descartava 21 de 26 (81%) das
   amostras reais do lote inteiro por "falta de PubDate", quando na
   verdade tinham data. references.py e oai_dc.py já fazem esse fallback
   manualmente; só pubmed.py não fazia.

2. xml_pubmed_citations quebrava com AttributeError quando <ref-list>
   não tem <title> (opcional no JATS), pois presumia que
   xml_pubmed_title_reference_list já tinha criado <ReferenceList>.
   Agora cria o elemento se necessário -- válido pela DTD
   (ReferenceList (Title?, Reference*, ReferenceList*)).

3. get_first_page só olhava elocation-id, nunca fpage; LastPage não
   existia. Artigos com paginação tradicional (fpage/lpage) perdiam
   essa informação por completo. Agora FirstPage prefere fpage e cai
   para elocation-id só quando não há paginação (fluxo de Publicação
   Contínua); LastPage foi implementado.

Validado: 24/26 amostras reais passam na DTD real (as 2 restantes
dependem do fix de Author FirstName/LastName de scieloorg#1236/scieloorg#1245, ainda não
mesclado nesta branch -- não são regressão destes fixes).
@Rossi-Luciano Rossi-Luciano force-pushed the feat/1241-pubmed-batch-cli branch from bfdd2dd to 668d4a5 Compare July 10, 2026 18:16
Rossi-Luciano and others added 4 commits July 10, 2026 15:40
…ra a DTD

Diferente de test_pubmed.py (uma pipe por vez, XML sintético mínimo),
este teste roda o pipeline completo (build_pubmed_article +
build_article_set_xml) em cada amostra real de tests/samples/*.xml e
valida o resultado contra a DTD real do PubMed.

A DTD foi vendorizada em tests/fixtures/pubmed/PubMed.dtd para o teste
não depender de rede (CI sem acesso à internet, ou instabilidade do
servidor da NLM não devem quebrar a suíte).

Essa é exatamente a lacuna que deixou passar despercebidos os 3 bugs
corrigidos no commit anterior (PubDate/collection_date, crash em
citations, FirstPage/LastPage) por 8 PRs já abertos.

Duas amostras (example.xml, 0034-8910-rsp-48-2-0249.xml) ainda
dependem de fixes que vivem em scieloorg#1236/PR scieloorg#1245 (Author FirstName/
LastName pairing e fallback `or []` em get_affiliations), ainda não
mesclados nesta branch -- marcadas em KNOWN_FAILURES com asserção
inversa, para que a suíte avise (ao invés de silenciosamente continuar
verde) quando alguém puder remover a entrada.
…ando instalável

pubmed_generator.py hoje só processa um XML por vez e nem está
registrado como comando instalável (diferente de pdf_generator).

- get_xml_trees_and_errors(path) usa
  packtools.sps.pid_provider.xml_sps_lib.get_xml_items, que já trata
  .xml e .zip de forma transparente (mesmo componente usado pelo
  spsvalidator) — retorna a lista de xmltree válidos e os erros por
  arquivo, sem que um XML corrompido no pacote derrube o lote inteiro.
- main() usa pubmed.pipeline_pubmed_set (da scieloorg#1234) para gerar um único
  <ArticleSet> com um <Article> por XML de entrada, avisos de erro em
  stderr, exit code 1 só se nenhum artigo foi processado com sucesso.
- Registrado pubmed_generator=packtools.sps.formats.pubmed_generator:main
  em setup.py, no mesmo padrão de pdf_generator.

Refs scieloorg#1226, scieloorg#1241
… lote

Reconcilia scieloorg#1242 com o CLI de processamento em lote. build_pubmed_article
agora levanta MissingRequiredPubDateError (da branch scieloorg#1234) quando um
artigo não tem data de publicação; antes isso derrubaria a geração do
<ArticleSet> inteiro via pipeline_pubmed_set.

main() agora usa build_articles_and_errors, que constrói cada <Article>
individualmente e trata essa exceção do mesmo jeito que os erros de
parsing já tratados por get_xml_trees_and_errors: aviso em stderr
identificando o artigo (por PII/DOI via get_elocation) e segue
processando o resto do lote. build_article_set_xml (extraída na scieloorg#1234)
monta o <ArticleSet> final só com os artigos que puderam ser gerados.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…mentError

Segue o refactor de scieloorg#1250/scieloorg#1234 que generalizou a exceção de
required-PubDate. Sem mudança de comportamento.
@Rossi-Luciano Rossi-Luciano force-pushed the feat/1241-pubmed-batch-cli branch from 668d4a5 to e6f0482 Compare July 10, 2026 18:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Pubmed XML: CLI em lote (.zip) e registro como comando instalável

2 participants