Skip to content

Commit e702cb2

Browse files
Corrige/Melhora PidProviderXML devido a um bug e uma melhoria packtools 4.16.11 (#1469)
* Corrigir extração de dados do XML para tolerar mudanças de versão do packtools Adiciona fix_get_article_data() e fix_get_data_to_compare() como camadas de compatibilidade em torno de xml_with_pre.get_article_data() e xml_adapter.get_data_to_compare(), já que o formato retornado por esses métodos mudou entre versões do packtools (partial_body -> body_fragment; z_partial_body -> body_fragment_fingerprint). fix_get_article_data() remove a chave legada 'partial_body' do dict retornado. fix_get_data_to_compare() força a chave 'z_partial_body' a vir de xml_with_pre.body_fragment_fingerprint, independente do que get_data_to_compare() retornar. Em QueryBuilderPidProviderXML: - z_partial_body agora é lido diretamente de xml_adapter.xml_with_pre.z_partial_body (hash legado), não mais de xml_adapter.data. - z_body (body_fingerprint, hash do corpo inteiro) deixou de ser usado — removido de __init__ e de partial_body_query. - partial_body_query agora combina apenas z_partial_body (legado) e z_body_fragment (body_fragment_fingerprint), como set, caindo em isnull=True quando nenhum dos dois existe. - validate_input_data passa a checar a chave 'body_fragment' (novo nome) em vez de 'partial_body'. * Usar as funções de compatibilidade do packtools em PidProviderXML Substitui as chamadas diretas a xml_with_pre.get_article_data() e xml_adapter.get_data_to_compare() por fix_get_article_data() e fix_get_data_to_compare(), garantindo que os dados usados em readable_data, no diff de conflito de pid v3 e nas comparações de registro fiquem normalizados entre versões do packtools. Também corrige get_registered_versions() para usar body_fragment_fingerprint (novo fingerprint) em vez do atributo antigo z_partial_body diretamente no dict de readable_data. * Realinhar testes de QueryBuilderPidProviderXML com o comportamento real do código O arquivo de testes anterior descrevia um refactor que não estava implementado no código-fonte atual, fazendo 13 de 55 testes falharem. Principais ajustes: - make_xml_adapter() passa a configurar z_partial_body diretamente em xml_with_pre (não mais em adapter.data), e usa 'body_fragment' como chave do dict de get_article_data(), incluindo uma chave 'partial_body' só para exercitar o pop feito por fix_get_article_data(). - Remove body_fingerprint (corpo inteiro) das comparações de partial_body_query e article_data_query, já que esse fingerprint não é mais usado. - Adiciona testes cobrindo validate_input_data com a chave 'body_fragment' e o caso de branco. - Ajusta CompareTests para refletir que compare() usa input_data.get(label) e não pula labels ausentes, incluindo o caso de ZeroDivisionError quando registered_items é vazio. * Ajustar asserção de select_record para o novo formato de dados de comparação get_best_match() agora recebe um dict de comparação que inclui a chave 'z_partial_body' (adicionada por fix_get_data_to_compare), então a asserção passa a usar ANY para esse valor em vez de comparar apenas por {'title': 'Foo'}. * Documentar funções e atributos de query_params.py Adiciona docstrings que faltavam (compare, compare_lists, compare_items, get_score, zero_to_none, fix_get_article_data) e corrige documentação desatualizada: - __init__ de QueryBuilderPidProviderXML: o comentário ainda descrevia um atributo z_body/body_fingerprint (fingerprint do corpo INTEIRO do artigo) que não é mais atribuído no código atual; substituído por uma docstring listando os atributos realmente definidos (z_body_fragment, z_partial_body, adapter_data, xml_with_pre_data). - partial_body_query: corrige a referência de onde vem o hash legado, que dizia vir de xml_adapter.z_partial_body mas na verdade é lido de xml_adapter.xml_with_pre.z_partial_body (via self.z_partial_body). - validate_input_data, pkg_name_list, article_data_query e get_article_data_query passam a ter docstrings explicando o comportamento. - Remove comentários de linha redundantes em pkg_name_list e identifier_queries que só repetiam o que o código já deixa claro, agora que a lógica está descrita na docstring do método. * Corrige o acesso a z_partial_body * Ajusta comparação de dados para maior precisão no pareamento de XML - Renomeia a chave 'z_partial_body' para 'body_fragment_fingerprint' em fix_get_data_to_compare, alinhando o nome ao valor efetivamente comparado. - fix_get_article_data passa a priorizar xml_with_pre.readable_data quando disponível, caindo para get_article_data(max_length=300) como fallback. - compare_items passa a incluir 'input_data' na resposta quando o score não é 1, permitindo rastrear o valor comparado em caso de divergência. * Aprimora rastreabilidade e robustez do processo de pareamento de PidProviderXML - Substitui os campos 'created'/'updated'/'record_status' fixos por uma property record_status, que calcula o estado do registro ('created' ou 'updated') com base na diferença de tempo entre criação e atualização. - Adiciona 'ppx_id' ao dicionário de dados retornado. - get_readable_data remove 'partial_body' do dado armazenado antes de retorná-lo. - data_to_compare passa a incluir article_titles e body_fragment apenas quando presentes, e usa body_fragment_fingerprint no lugar de z_partial_body. - get_best_match passa a usar um limiar de aceitação dinâmico (min_rate), reduzido para 0.49 quando há poucos dados disponíveis para comparação, e retorna também a resposta detalhada de cada comparação junto ao dado do candidato. - Evita processar atualização quando o registro correspondente não possui readable_data. - is_equal_to é considerado falso quando o registro não possui readable_data, evitando falso positivo de igualdade. - Melhora a mensagem de erro de PidProviderXMLPidV3ConflictError, tornando-a mais informativa sobre o resultado do pareamento. * Adiciona testes de configuração do Pid Provider Cobre PidProviderSetting (ex.: record_all_registration_events e demais opções configuráveis via Wagtail Admin). Mensagem gerada a partir do nome do arquivo — ajustar detalhes de implementação após revisão do conteúdo. * Adiciona testes de XMLVersion Cobre o modelo XMLVersion (versionamento de XML associado a PidProviderXML). Mensagem gerada a partir do nome do arquivo — ajustar detalhes de implementação após revisão do conteúdo. * Adiciona testes de URL de XML no Pid Provider Cobre geração/resolução de URLs associadas ao XML registrado. Mensagem gerada a partir do nome do arquivo — ajustar detalhes de implementação após revisão do conteúdo. * Adiciona testes de PIDs alternativos (other_pid) Cobre o relacionamento OtherPid/InlinePanel 'Other PID' em PidProviderXML. Mensagem gerada a partir do nome do arquivo — ajustar detalhes de implementação após revisão do conteúdo. * Adiciona testes de correção do pid v2 Cobre lógica de fix/normalização do PID v2 (ex.: CollectionPidV2 ou rotina de correção equivalente). Mensagem gerada a partir do nome do arquivo — ajustar detalhes de implementação após revisão do conteúdo. * Adiciona testes de funções auxiliares do PidProviderXML Cobre funções utilitárias usadas por PidProviderXML (possivelmente os wrappers fix_get_article_data/fix_get_data_to_compare ou equivalentes). Mensagem gerada a partir do nome do arquivo — ajustar detalhes de implementação após revisão do conteúdo. * Adiciona testes de manutenção de registros PidProviderXML Cobre rotinas de manutenção/limpeza sobre registros existentes de PidProviderXML. Mensagem gerada a partir do nome do arquivo — ajustar detalhes de implementação após revisão do conteúdo. * Adiciona testes de auditoria de registro (PidProviderXMLRegistration) Cobre o modelo PidProviderXMLRegistration e a lógica de quando gravar eventos de auditoria (erro, ambiguidade ou record_all_registration_events). Mensagem gerada a partir do nome do arquivo — ajustar detalhes de implementação após revisão do conteúdo. * Atualiza testes de query_params para tratamento de labels ausentes e z_partial_body Ajusta make_xml_adapter() para configurar explicitamente adapter.z_partial_body (agora lido como atributo direto pelo QueryBuilderPidProviderXML, não mais via xml_adapter.data.get()), evitando MagicMock não configurado no teste. CompareItemsTests: passa a esperar também a chave 'input_data' no retorno de compare_items() quando o score é != 1, além de 'registered'. CompareTests, reescrito para refletir que compare() NÃO pula mais labels ausentes em input_data (usa .get(label), tratando ausência como None): - test_missing_input_key_is_treated_as_none_not_skipped (renomeado de ...is_skipped_not_treated_as_none): label ausente com valor registrado falsy agora ENTRA em items com score 1, em vez de ser descartado. - test_missing_input_key_with_truthy_registered_value_lowers_score (novo): label ausente com valor registrado truthy cai no ramo how_similar (score < 1). - test_empty_registered_items_raises_zero_division_error (renomeado de test_all_labels_missing_raises_zero_division_error): único cenário que ainda levanta ZeroDivisionError é registered_items vazio, não mais 'todos os labels ausentes em input_data'. * Atualiza testes de get_best_match para novo formato de item com data/response get_best_match() passou a envolver cada candidato em {'data': ..., 'response': ...} nas listas 'matched'/'unmatched', em vez do dict .data cru. Ajusta as asserções de result['unmatched'][0]['id'] e result['matched'][N]['id'] para result[...][N]['data']['id'] nos quatro testes afetados. * Atualiza testes de select_record para uso de fix_get_data_to_compare select_record() não usa mais xml_adapter.get_data_to_compare() puro: passa a chamar fix_get_data_to_compare(xml_adapter), que acrescenta a chave 'body_fragment_fingerprint' (lida de xml_adapter.xml_with_pre.body_fragment_fingerprint) ao dict retornado por get_data_to_compare(). _make_xml_adapter() passa a aceitar body_fragment_fingerprint e configurá-lo no mock, para não ficar um MagicMock não configurado. test_select_record_passes_candidates_and_comparison_data_to_get_best_match atualizado para esperar que get_best_match seja chamado com o dict já acrescido de 'body_fragment_fingerprint', não mais o retorno cru de get_data_to_compare(). * Atualiza testes de register() para a migração readable_data Alinha os testes à mudança de register(), que agora faz input_data.update(xml_with_pre.readable_data) em vez de usar get_article_data(). O helper make_xml_with_pre() passa a configurar readable_data como um dict de verdade (com surnames, collab, links, article_titles, body_fragment) e body_fragment_fingerprint, já que um MagicMock não configurado quebra dict.update() com TypeError — erro que era silenciosamente capturado pelo except Exception de register() e mascarado como event_status='error'. Adiciona patch de PidProviderSetting.load no setUp da classe base (RegisterTestBase), desativado por padrão, para desacoplar os testes da configuração persistida e permitir reuso em RecordAllEventsSettingTest sem duplicar o patch. Adiciona RegisterResponseSchemaTest como teste de contrato: valida apenas as chaves presentes em cada nível do response de register() (sucesso limpo, erro, conflito, skipped), incluindo verificação explícita de que as chaves de readable_data substituem as antigas (ex.: ausência de 'partial_body'). Funciona como sentinela contra mudanças silenciosas de schema que testes anteriores, focados apenas em event_status/v3, não detectavam. * Corrige estrutura dos dados legíveis no dict de registro do PID Remove a chave 'finger_print' solta em record_data() e passa a aninhar o retorno de get_readable_data() sob a chave 'registered_data', em vez de mesclá-lo (update) diretamente no nível superior do dict. Isso evita colisão de chaves entre os dados legíveis e os demais campos do registro. * Adiciona pid_provider/tests/test_models.py
1 parent 7270626 commit e702cb2

15 files changed

Lines changed: 2332 additions & 157 deletions

pid_provider/models.py

Lines changed: 70 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@
3737
zero_to_none,
3838
compare,
3939
QueryBuilderPidProviderXML,
40+
fix_get_article_data,
41+
fix_get_data_to_compare,
4042
)
4143
from tracker.models import BaseEvent, UnexpectedEvent
4244

@@ -615,15 +617,33 @@ def data(self):
615617
"v2": self.v2,
616618
"aop_pid": self.aop_pid,
617619
"pkg_name": self.pkg_name,
618-
"finger_print": self.current_version and self.current_version.finger_print,
619-
"created": self.created and self.created.isoformat(),
620-
"updated": self.updated and self.updated.isoformat(),
621-
"record_status": "updated" if self.updated else "created",
622620
"registered_in_core": self.registered_in_core,
621+
"ppx_id": self.id
623622
}
624-
_data.update(self.get_readable_data())
623+
_data["registered_data"] = self.get_readable_data()
624+
_data.update(self.record_status)
625625
return _data
626626

627+
@property
628+
def record_status(self):
629+
"""Retorna os timestamps e o estado do registro ('created' ou 'updated').
630+
Calcula a variação entre `created` e `updated` para definir o estado de
631+
persistência. Utilizado para direcionar o código HTTP de resposta:
632+
- 'created' -> 201 Created
633+
- 'updated' -> 200 OK ou 204 No Content
634+
Returns:
635+
dict: Dicionário com `created`, `updated` em ISO 8601 e `record_status`.
636+
"""
637+
d = {}
638+
if self.created:
639+
d["created"] = self.created.isoformat()
640+
d["record_status"] = "created"
641+
if self.updated:
642+
d["updated"] = self.updated.isoformat()
643+
if (self.updated - self.created).total_seconds() > 1:
644+
d["record_status"] = "updated"
645+
return d
646+
627647
@classmethod
628648
@profile_classmethod
629649
def get_xml_with_pre(cls, v3):
@@ -657,25 +677,35 @@ def is_aop(self):
657677
return True
658678

659679
def get_readable_data(self):
660-
if self.readable_data:
661-
return self.readable_data
680+
readable_data = self.readable_data or {}
681+
if readable_data:
682+
try:
683+
readable_data.pop("partial_body")
684+
except KeyError:
685+
pass
686+
return readable_data
662687
if self.xml_with_pre:
663-
return self.xml_with_pre.get_article_data()
688+
return fix_get_article_data(self.xml_with_pre)
664689
return {}
665690

666691
@property
667692
def data_to_compare(self):
693+
data = {}
668694
readable = self.get_readable_data()
669-
titles = readable.get("article_titles")
670-
body_fragment = readable.get("body_fragment")
671-
return {
672-
"article_titles": titles or self.xml_with_pre.article_titles_texts,
695+
if readable:
696+
titles = readable.get("article_titles")
697+
body_fragment = readable.get("body_fragment")
698+
if titles:
699+
data["article_titles"] = titles
700+
if body_fragment:
701+
data["body_fragment"] = body_fragment
702+
data.update({
673703
"z_surnames": self.z_surnames,
674704
"z_collab": self.z_collab,
675705
"z_links": self.z_links,
676-
"z_partial_body": self.z_partial_body,
677-
"body_fragment": body_fragment or self.xml_with_pre.get_body_fragment(PARTIAL_BODY_MAX),
678-
}
706+
"body_fragment_fingerprint": self.z_partial_body,
707+
})
708+
return data
679709

680710
@classmethod
681711
@profile_classmethod
@@ -758,7 +788,7 @@ def register(
758788

759789
input_data = {}
760790
input_data.update(xml_with_pre.data)
761-
input_data.update(xml_with_pre.get_article_data())
791+
input_data.update(fix_get_article_data(xml_with_pre))
762792
input_data["origin"] = origin
763793
response["input_data"] = input_data
764794

@@ -997,6 +1027,9 @@ def is_updated(
9971027
)
9981028
return
9991029

1030+
if not registered.readable_data:
1031+
return
1032+
10001033
# verifica se é necessário atualizar
10011034
if registered.is_equal_to(xml_with_pre):
10021035
# XML fornecido é igual ao registrado, não precisa continuar
@@ -1074,12 +1107,12 @@ def select_record(xml_adapter, selection_results):
10741107
checamos truthiness (nunca .exists()/.count() sobre queryset).
10751108
"""
10761109
unmatched_items = {}
1077-
xml_adapter_data_to_compare = xml_adapter.get_data_to_compare()
1110+
xml_adapter_data_to_compare = fix_get_data_to_compare(xml_adapter)
10781111
for label, results in selection_results:
10791112
if not results:
10801113
continue
10811114

1082-
result = PidProviderXML.get_best_match(results, xml_adapter_data_to_compare)
1115+
result = PidProviderXML.get_best_match(results, xml_adapter_data_to_compare)
10831116

10841117
matched = result.get("matched")
10851118
unmatched = result.get("unmatched")
@@ -1117,14 +1150,15 @@ def get_record_by_pid_v3(cls, xml_adapter):
11171150
# pid v3 é inédito
11181151
raise cls.DoesNotExist
11191152

1120-
xml_adapter_data_to_compare = xml_adapter.get_data_to_compare()
1153+
xml_adapter_data_to_compare = fix_get_data_to_compare(xml_adapter)
11211154
result = PidProviderXML.get_best_match(results, xml_adapter_data_to_compare)
1155+
11221156
registered = result.get("registered")
11231157
if not registered:
1124-
xml_data = xml_adapter.xml_with_pre.get_article_data(PARTIAL_BODY_MAX)
1125-
items = [item.data for item in results]
11261158
raise PidProviderXMLPidV3ConflictError(
1127-
_(f"{xml_pid_v3} belongs to {items}, not to {xml_data}")
1159+
_("{} do not belong to {}. Result: {}").format(
1160+
xml_pid_v3, xml_adapter_data_to_compare, result,
1161+
)
11281162
)
11291163
return registered
11301164

@@ -1150,10 +1184,10 @@ def get_best_match(results, xml_adapter_data):
11501184
ao consumir o retorno, nunca acesso direto.
11511185
11521186
- ``"unmatched"``: presente apenas se houver ao menos 1
1153-
candidato com ``percentual_score`` <= 0.6. Lista de
1187+
candidato com ``percentual_score`` <= min_rate. Lista de
11541188
``item.data`` desses candidatos.
11551189
- ``"registered"``: presente apenas se houver ao menos 1
1156-
candidato aprovado (score > 0.6). Contém o OBJETO
1190+
candidato aprovado (score > min_rate). Contém o OBJETO
11571191
``PidProviderXML`` (não o dict ``.data``) do candidato com
11581192
maior score — em caso de empate, o critério de desempate é
11591193
``updated`` mais recente e, em seguida, maior ``id``.
@@ -1166,20 +1200,26 @@ def get_best_match(results, xml_adapter_data):
11661200
detail = {}
11671201
found = []
11681202
items = {}
1203+
responses = {}
1204+
min_rate = 0.6
1205+
if len(xml_adapter_data) <= 4:
1206+
min_rate = 0.49
11691207
for item in results:
11701208
item_data = item.data_to_compare
11711209
response = compare(item_data, xml_adapter_data)
11721210
items[item.id] = item
1211+
responses[item.id] = response
11731212
found.append((response["percentual_score"], item.updated.isoformat(), item.id))
11741213

11751214
found = sorted(found, reverse=True)
11761215
matched = []
11771216
unmatched = []
11781217
for percentual_score, updated, item_id in found:
1179-
if percentual_score > 0.6:
1180-
matched.append(items[item_id].data)
1218+
data = {"data": items[item_id].data, "response": responses[item_id]}
1219+
if percentual_score > min_rate:
1220+
matched.append(data)
11811221
else:
1182-
unmatched.append(items[item_id].data)
1222+
unmatched.append(data)
11831223
if matched:
11841224
detail["registered"] = items[found[0][-1]]
11851225
if len(matched) > 1:
@@ -1210,7 +1250,7 @@ def _add_data(self, xml_adapter, registered_in_core):
12101250
self.z_links = xml_adapter.z_links
12111251
self.z_partial_body = xml_adapter.xml_with_pre.body_fragment_fingerprint
12121252

1213-
self.readable_data = xml_adapter.xml_with_pre.get_article_data()
1253+
self.readable_data = fix_get_article_data(xml_adapter.xml_with_pre)
12141254

12151255
@profile_method
12161256
def _add_dates(self, xml_adapter, origin_date, available_since):
@@ -1382,6 +1422,8 @@ def is_registered(
13821422
response["registered"] = True
13831423
response.update(registered.data)
13841424
response["is_equal"] = registered.is_equal_to(xml_with_pre)
1425+
if not registered.readable_data:
1426+
response["is_equal"] = False
13851427
return response
13861428
except Exception as e:
13871429
exc_type, exc_value, exc_traceback = sys.exc_info()

0 commit comments

Comments
 (0)