From 003ebacba57c0da582689227f42413bedf66cb6d Mon Sep 17 00:00:00 2001 From: Rossi-Luciano Date: Fri, 3 Jul 2026 21:33:30 -0300 Subject: [PATCH 1/2] feat(pubmed): adicionar Suffix e CollectiveName em AuthorList MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implementa add_suffix e add_collective_name em packtools/sps/formats/pubmed.py, usando dados já expostos pelo model article_contribs.Contrib (contrib_name["suffix"] e collab). A DTD do PubMed exige que tenha nome pessoal (FirstName/MiddleName/LastName/Suffix) OU CollectiveName, nunca os dois: (((FirstName, MiddleName?, LastName, Suffix?, Initials?) | CollectiveName), affiliation, Identifier*). xml_pubmed_author_list agora respeita essa exclusividade: contribs com (sem ) geram só CollectiveName, os demais geram FirstName/LastName/Suffix como antes. Corrige também um bug em get_affiliations: contribs sem afiliação vinculada (comum no caso de CollectiveName, cujo único xref costuma ser para author-notes) não tinham a chave "affs" no dict, causando TypeError ao iterar author_reg.get("affs"). Agora usa `or []` como fallback. Refs #1226, #1236 --- packtools/sps/formats/pubmed.py | 60 ++++++++++++----------- tests/sps/formats/test_pubmed.py | 82 ++++++++++++++++++++++++++++++++ 2 files changed, 115 insertions(+), 27 deletions(-) diff --git a/packtools/sps/formats/pubmed.py b/packtools/sps/formats/pubmed.py index f99b2651d..634fd94f1 100644 --- a/packtools/sps/formats/pubmed.py +++ b/packtools/sps/formats/pubmed.py @@ -305,10 +305,26 @@ def add_last_name(author_reg, author_tag): author_tag.append(last) +def add_suffix(author_reg, author_tag): + author_suffix = author_reg.get("contrib_name", {}).get("suffix") + if author_suffix: + suffix = ET.Element("Suffix") + suffix.text = author_suffix + author_tag.append(suffix) + + +def add_collective_name(author_reg, author_tag): + collab = author_reg.get("collab") + if collab: + collective_name = ET.Element("CollectiveName") + collective_name.text = collab + author_tag.append(collective_name) + + def get_affiliations(author_reg, xml_tree): affiliations = aff.AffiliationExtractor(xml_tree).get_affiliation_dict(subtag=False) affiliation_list = [] - for item in author_reg.get("affs"): + for item in author_reg.get("affs") or []: affiliation_list.append( affiliations.get(item.get("id"), {}).get("institution", {})[0].get("original") ) @@ -342,33 +358,23 @@ def xml_pubmed_author_list(xml_pubmed, xml_tree): author_list_tag = ET.Element("AuthorList") for author_reg in authors: author_tag = ET.Element("Author") - add_first_name(author_reg, author_tag) - - # TODO - # add_middle_name(author_reg, author_tag) - # The Author’s full middle name, or initial if the full name is not available. - # Multiple names are allowed in this tag. - # There is no example of using this value in the files. - add_last_name(author_reg, author_tag) - - # TODO - # add_suffix(author_reg, author_tag) - # The Author's suffix, if any, e.g. "Jr", "Sr", "II", "IV". Do not include honorific titles, - # e.g. "M.D.", "Ph.D.". - # There is no example of using this value in the files - - # TODO - # add_collective_name(author_reg, author_tag) - # The name of the authoring committee or organization. The CollectiveName tag should be placed within - # an Author tag. Omit extraneous text like, “on behalf of.” - # Please see the following example: - # - # - # Plastic Surgery Educational Foundation DATA Committee - # - # - # There is no example of using this value in the files + # A DTD do PubMed exige que Author tenha nome pessoal + # (FirstName/MiddleName/LastName/Suffix) OU CollectiveName, + # nunca os dois ao mesmo tempo. + if author_reg.get("collab"): + add_collective_name(author_reg, author_tag) + else: + add_first_name(author_reg, author_tag) + + # TODO + # add_middle_name(author_reg, author_tag) + # The Author’s full middle name, or initial if the full name is not available. + # Multiple names are allowed in this tag. + # There is no example of using this value in the files. + + add_last_name(author_reg, author_tag) + add_suffix(author_reg, author_tag) affiliations = get_affiliations(author_reg, xml_tree) add_affiliations(affiliations, author_tag) diff --git a/tests/sps/formats/test_pubmed.py b/tests/sps/formats/test_pubmed.py index a8afad598..1fdfb2dda 100644 --- a/tests/sps/formats/test_pubmed.py +++ b/tests/sps/formats/test_pubmed.py @@ -1012,6 +1012,88 @@ def test_xml_pubmed_author_list_without_author(self): self.assertEqual(obtained, expected) + def test_xml_pubmed_author_list_with_suffix(self): + expected = ( + '
' + '' + '' + 'Rogerio' + 'Meneghini' + 'Junior' + 'Some University' + '' + '' + '
' + ) + xml_pubmed = ET.fromstring( + '
' + ) + xml_tree = ET.fromstring( + '
' + '' + '' + '' + '' + '' + 'Meneghini' + 'Rogerio' + 'Junior' + '' + '1' + '' + '' + '' + 'Some University' + '' + '' + '' + '
' + ) + + xml_pubmed_author_list(xml_pubmed, xml_tree) + + obtained = ET.tostring(xml_pubmed, encoding="utf-8").decode("utf-8") + + self.assertEqual(obtained, expected) + + def test_xml_pubmed_author_list_with_collective_name(self): + expected = ( + '
' + '' + '' + 'The SciELO Group' + '' + '' + '
' + ) + xml_pubmed = ET.fromstring( + '
' + ) + xml_tree = ET.fromstring( + '
' + '' + '' + '' + '' + 'The SciELO Group' + '1' + '' + '' + '' + '' + '
' + ) + + xml_pubmed_author_list(xml_pubmed, xml_tree) + + obtained = ET.tostring(xml_pubmed, encoding="utf-8").decode("utf-8") + + self.assertEqual(obtained, expected) + def test_xml_pubmed_publication_type(self): # TODO # Originalmente, espera-se que o valor da tag seja Journal Article From 755830d8aacc6d2017c09f4fb6a0453d2ed8db02 Mon Sep 17 00:00:00 2001 From: Rossi-Luciano Date: Fri, 3 Jul 2026 23:02:01 -0300 Subject: [PATCH 2/2] =?UTF-8?q?fix(pubmed):=20duplicar=20nome=20quando=20s?= =?UTF-8?q?urname=20ou=20given-names=20est=C3=A1=20ausente?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A DTD do PubMed exige FirstName e LastName sempre juntos em Author (nenhum dos dois é opcional isoladamente): Author (((FirstName, MiddleName?, LastName, Suffix?, Initials?) | CollectiveName), ...) Encontrado ao validar a saída da CLI em lote (#1241) contra a DTD real usando tests/samples/example.xml: um contrib tem mas não tem , gerando ...... sem — inválido segundo a DTD (confirmado empiricamente: Author só com FirstName ou só com LastName sempre falha a validação). add_first_name e add_last_name agora caem para o outro campo quando o esperado está ausente, duplicando o único nome disponível em ambos — preserva a identificação do autor em vez de descartá-lo ou gerar XML inválido. Refs #1226, #1236 --- packtools/sps/formats/pubmed.py | 10 ++++++-- tests/sps/formats/test_pubmed.py | 41 ++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/packtools/sps/formats/pubmed.py b/packtools/sps/formats/pubmed.py index 634fd94f1..89f26ddb3 100644 --- a/packtools/sps/formats/pubmed.py +++ b/packtools/sps/formats/pubmed.py @@ -290,7 +290,12 @@ def get_authors(xml_tree): def add_first_name(author_reg, author_tag): - author_first_name = author_reg.get("contrib_name", {}).get("given-names") + contrib_name = author_reg.get("contrib_name", {}) + # A DTD do PubMed exige FirstName e LastName juntos (nenhum dos dois é + # opcional isoladamente). Quando o XML fonte só tem um dos dois campos + # (autores sem , por exemplo), duplicamos o valor disponível + # em ambos para gerar um Author válido em vez de descartar o autor. + author_first_name = contrib_name.get("given-names") or contrib_name.get("surname") if author_first_name: first = ET.Element("FirstName") first.text = author_first_name @@ -298,7 +303,8 @@ def add_first_name(author_reg, author_tag): def add_last_name(author_reg, author_tag): - author_last_name = author_reg.get("contrib_name", {}).get("surname") + contrib_name = author_reg.get("contrib_name", {}) + author_last_name = contrib_name.get("surname") or contrib_name.get("given-names") if author_last_name: last = ET.Element("LastName") last.text = author_last_name diff --git a/tests/sps/formats/test_pubmed.py b/tests/sps/formats/test_pubmed.py index 1fdfb2dda..98d70328a 100644 --- a/tests/sps/formats/test_pubmed.py +++ b/tests/sps/formats/test_pubmed.py @@ -1058,6 +1058,47 @@ def test_xml_pubmed_author_list_with_suffix(self): self.assertEqual(obtained, expected) + def test_xml_pubmed_author_list_without_surname_duplicates_given_names(self): + # A DTD do PubMed exige FirstName e LastName juntos; quando o XML + # fonte só tem (sem ), duplicamos o valor + # em ambos os campos em vez de gerar um Author inválido. + expected = ( + '
' + '' + '' + 'Viviana Alder' + 'Viviana Alder' + '' + '' + '
' + ) + xml_pubmed = ET.fromstring( + '
' + ) + xml_tree = ET.fromstring( + '
' + '' + '' + '' + '' + '' + 'Viviana Alder' + '' + '' + '' + '' + '' + '
' + ) + + xml_pubmed_author_list(xml_pubmed, xml_tree) + + obtained = ET.tostring(xml_pubmed, encoding="utf-8").decode("utf-8") + + self.assertEqual(obtained, expected) + def test_xml_pubmed_author_list_with_collective_name(self): expected = ( '
'