From c318355f749fd0c1366175ad35264947935f8977 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pasternak?= Date: Tue, 14 Jul 2026 19:06:22 +0200 Subject: [PATCH] =?UTF-8?q?Dodaj=20normalize=5Fauthor=5Fname=20=E2=80=94?= =?UTF-8?q?=20normalizacja=20autora=20z=20JSON-=C3=B3w=20PBN?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PBN podaje dane osobowe autora niespójnie zależnie od endpointu: nazwisko jako lastName albo familyName, imię jako firstName, givenNames albo name. Nowa czysta funkcja normalize_author_name scala oba warianty spotykane w hostach (unia łańcuchów fallbacków): - lastName: familyName → lastName → None - firstName: firstName → givenNames → name → None Puste stringi traktowane jak brak wartości; wejście nie-dict (np. goły PBN UID jako string) daje oba pola None. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/pbn_client/__init__.py | 2 + src/pbn_client/authors.py | 40 +++++++++++++++++ tests/test_authors.py | 88 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 130 insertions(+) create mode 100644 src/pbn_client/authors.py create mode 100644 tests/test_authors.py diff --git a/src/pbn_client/__init__.py b/src/pbn_client/__init__.py index 5efa056..8c3c1e3 100644 --- a/src/pbn_client/__init__.py +++ b/src/pbn_client/__init__.py @@ -6,6 +6,7 @@ """ from .auth import OAuthMixin +from .authors import normalize_author_name from .client import PBNClient from .identifiers import is_valid_object_id, parse_publication_id from .mixins import ( @@ -53,4 +54,5 @@ "smart_content", "is_valid_object_id", "parse_publication_id", + "normalize_author_name", ] diff --git a/src/pbn_client/authors.py b/src/pbn_client/authors.py new file mode 100644 index 0000000..a105c94 --- /dev/null +++ b/src/pbn_client/authors.py @@ -0,0 +1,40 @@ +"""Normalizacja danych osobowych autora z niespójnych JSON-ów PBN.""" + +#: Klucze imienia w kolejności preferencji (unia wariantów spotykanych w PBN). +_FIRST_NAME_KEYS = ("firstName", "givenNames", "name") + +#: Klucze nazwiska w kolejności preferencji. +_LAST_NAME_KEYS = ("familyName", "lastName") + + +def _first_present(data, keys): + """Zwróć pierwszą niepustą wartość spod ``keys`` w ``data`` (albo None).""" + for key in keys: + value = data.get(key) + if value: + return value + return None + + +def normalize_author_name(author): + """Sprowadź autora z PBN do ``{"lastName": ..., "firstName": ...}``. + + PBN podaje dane osobowe niespójnie, zależnie od endpointu: nazwisko + jako ``lastName`` albo ``familyName``, imię jako ``firstName``, + ``givenNames`` albo ``name``. Czasem zamiast słownika przychodzi + goły PBN UID (string) — wtedy nie mamy danych osobowych. + + Kolejność preferencji: + + - ``lastName``: ``familyName`` → ``lastName`` → ``None`` + - ``firstName``: ``firstName`` → ``givenNames`` → ``name`` → ``None`` + + Puste stringi traktowane są jak brak wartości. Wejście inne niż dict + (None, string-UID, cokolwiek) daje oba pola ``None``. + """ + if not isinstance(author, dict): + return {"lastName": None, "firstName": None} + return { + "lastName": _first_present(author, _LAST_NAME_KEYS), + "firstName": _first_present(author, _FIRST_NAME_KEYS), + } diff --git a/tests/test_authors.py b/tests/test_authors.py new file mode 100644 index 0000000..a18aa70 --- /dev/null +++ b/tests/test_authors.py @@ -0,0 +1,88 @@ +"""Testy normalizacji autora z niespójnych JSON-ów PBN. + +PBN podaje nazwisko raz jako ``lastName``, raz jako ``familyName``, +a imię jako ``firstName``, ``givenNames`` albo ``name`` — w zależności +od endpointu. ``normalize_author_name`` sprowadza wszystkie warianty +do jednego kształtu ``{"lastName": ..., "firstName": ...}``. +""" + +import pytest + +from pbn_client import normalize_author_name + + +def test_firstname_lastname_payload(): + assert normalize_author_name({"firstName": "Jan", "lastName": "Kowalski"}) == { + "lastName": "Kowalski", + "firstName": "Jan", + } + + +def test_familyname_givennames_payload(): + assert normalize_author_name( + {"familyName": "Nowak", "givenNames": "Anna Maria"} + ) == {"lastName": "Nowak", "firstName": "Anna Maria"} + + +def test_name_only_payload(): + assert normalize_author_name({"name": "Zofia"}) == { + "lastName": None, + "firstName": "Zofia", + } + + +def test_givennames_only_payload(): + assert normalize_author_name({"givenNames": "Piotr"}) == { + "lastName": None, + "firstName": "Piotr", + } + + +def test_lastname_only_payload(): + assert normalize_author_name({"lastName": "Wiśniewska"}) == { + "lastName": "Wiśniewska", + "firstName": None, + } + + +def test_missing_everything(): + assert normalize_author_name({}) == {"lastName": None, "firstName": None} + + +def test_irrelevant_keys_only(): + assert normalize_author_name({"orcid": "0000-0001-2345-6789"}) == { + "lastName": None, + "firstName": None, + } + + +@pytest.mark.parametrize("value", [None, "5e70930d878c28a04b8efd23", 42, ["x"]]) +def test_non_dict_input(value): + assert normalize_author_name(value) == {"lastName": None, "firstName": None} + + +def test_union_familyname_wins_over_lastname(): + assert normalize_author_name({"familyName": "Wolski", "lastName": "Kowalski"}) == { + "lastName": "Wolski", + "firstName": None, + } + + +def test_union_firstname_wins_over_givennames_and_name(): + assert normalize_author_name( + {"firstName": "Jan", "givenNames": "Janusz", "name": "Jasiek"} + ) == {"lastName": None, "firstName": "Jan"} + + +def test_union_givennames_wins_over_name(): + assert normalize_author_name({"givenNames": "Ewa", "name": "Ewelina"}) == { + "lastName": None, + "firstName": "Ewa", + } + + +def test_empty_string_values_fall_through(): + # PBN potrafi przysłać puste stringi — traktujemy je jak brak wartości. + assert normalize_author_name( + {"firstName": "", "givenNames": "Adam", "lastName": "", "familyName": ""} + ) == {"lastName": None, "firstName": "Adam"}