From 5a49957f6a2c05949ccef39e50ab95b0bd911111 Mon Sep 17 00:00:00 2001 From: Brian Pepple Date: Tue, 17 Sep 2024 10:03:54 -0400 Subject: [PATCH] Add support for Series AlternativeNames --- darkseid/metadata.py | 57 +++++++++++++++++++++++++++++++++ darkseid/metroninfo.py | 23 +++++++++++++ tests/conftest.py | 10 ++++-- tests/test_files/MetronInfo.xsd | 17 ++++++++++ tests/test_metadata.py | 4 +++ tests/test_metroninfo.py | 34 ++++++++++++++++++-- 6 files changed, 141 insertions(+), 4 deletions(-) diff --git a/darkseid/metadata.py b/darkseid/metadata.py index b69840d..68a139f 100644 --- a/darkseid/metadata.py +++ b/darkseid/metadata.py @@ -189,6 +189,61 @@ class Role(Basic): primary: bool = False +@dataclass +class AlternativeNames(Basic): + """ + A data class representing an alternative name for a series with basic information and validations. + + Attributes: + name (str): The alternative name for a series. + id_ (int | None): The ID associated with the alternative name, defaults to None. + language (str | None): The 2-letter ISO code of the language, defaults to None. + + Static Methods: + validate_language(value: str, **_: any) -> str | None: Validates a language value. + """ + + language: str | None = None + + @staticmethod + def validate_language(value: str, **_: any) -> str | None: + """ + Validates a language value. + + If the value is empty, it returns None. Otherwise, it strips any leading or trailing + whitespace from the value. If the length of the value is 2, it tries to find the + language object using the alpha-2 code. Otherwise, it tries to look up the language + object using the value. If the language object is not found, it raises a ValueError. + + Args: + value (str): The language value to validate. + **_ (any): Additional keyword arguments (ignored). + + Returns: + Optional[str]: The validated language code, or None if the value is empty. + + Raises: + ValueError: Raised when the language object cannot be found. + """ + + if not value: + return None + value = value.strip() + + if len(value) == COUNTRY_LEN: + obj = pycountry.languages.get(alpha_2=value) + else: + try: + obj = pycountry.languages.lookup(value) + except LookupError as e: + msg = f"Couldn't find language {value}" + raise ValueError(msg) from e + if obj is None: + msg = f"Couldn't find language {value}" + raise ValueError(msg) + return obj.alpha_2 + + @dataclass class Series(Basic, Validations): """ @@ -200,6 +255,7 @@ class Series(Basic, Validations): sort_name (str | None): The sort name of the series, defaults to None. volume (int | None): The volume of the series, defaults to None. format (str | None): The format of the series, defaults to None. + alternative_names: list[AlternativeNames]: A list of alternative names for series. language (str | None): The 2-letter ISO code of the language, defaults to None. Static Methods: @@ -209,6 +265,7 @@ class Series(Basic, Validations): sort_name: str | None = None volume: int | None = None format: str | None = None + alternative_names: list[AlternativeNames] = field(default_factory=list) language: str | None = None # 2-letter iso code @staticmethod diff --git a/darkseid/metroninfo.py b/darkseid/metroninfo.py index 7939648..00b07c6 100644 --- a/darkseid/metroninfo.py +++ b/darkseid/metroninfo.py @@ -14,6 +14,7 @@ from darkseid.issue_string import IssueString from darkseid.metadata import ( GTIN, + AlternativeNames, Arc, Basic, Credit, @@ -220,6 +221,18 @@ def assign_series(root: ET.Element, series: Series) -> None: ET.SubElement(series_node, "Format").text = ( series.format if series.format in MetronInfo.mix_series_format else "Single Issue" ) + if series.alternative_names: + alt_names_node = ET.SubElement(series_node, "AlternativeNames") + for alt_name in series.alternative_names: + name_node = ET.SubElement(alt_names_node, "Name") + name_node.text = alt_name.name + alt_attrib = {} + if alt_name.id_: + alt_attrib["id"] = str(alt_name.id_) + if alt_name.language: + alt_attrib["lang"] = alt_name.language + if alt_attrib: + name_node.attrib = alt_attrib @staticmethod def assign_info_source(root: ET.Element, primary: Basic, alt_lst: list[Basic]) -> None: @@ -406,6 +419,14 @@ def get_prices() -> list[Price]: Price(Decimal(item.text), item.attrib.get("country", "US")) for item in resource ] + def _create_alt_name_list(element: ET.Element) -> list[AlternativeNames]: + return [ + AlternativeNames( + name.text, get_id_from_attrib(name.attrib), name.attrib.get("lang") + ) + for name in element.findall("Name") + ] + def get_series() -> Series | None: resource = root.find("Series") if resource is None: @@ -427,6 +448,8 @@ def get_series() -> Series | None: series_md.volume = int(item.text) case "Format": series_md.format = item.text + case "AlternativeNames": + series_md.alternative_names = _create_alt_name_list(item) case _: pass diff --git a/tests/conftest.py b/tests/conftest.py index ecd9073..c2934c1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,7 +3,7 @@ import pytest -from darkseid.metadata import Arc, Basic, Metadata, Price, Series, Universe +from darkseid.metadata import AlternativeNames, Arc, Basic, Metadata, Price, Series, Universe @pytest.fixture(scope="module") @@ -38,7 +38,13 @@ def fake_metadata() -> Metadata: @pytest.fixture(scope="session") def fake_overlay_metadata() -> Metadata: overlay_md = Metadata() - overlay_md.series = Series(name="Aquaman", sort_name="Aquaman", volume=1, format="Annual") + overlay_md.series = Series( + name="Aquaman", + sort_name="Aquaman", + volume=1, + format="Annual", + alternative_names=[AlternativeNames("Water Boy"), AlternativeNames("Fishy", 60, "de")], + ) overlay_md.cover_date = date(1994, 10, 1) overlay_md.reprints = [Basic("Aquaman (1964) #64", 12345)] overlay_md.prices = [Price(Decimal("3.99")), Price(Decimal("1.5"), "CA")] diff --git a/tests/test_files/MetronInfo.xsd b/tests/test_files/MetronInfo.xsd index 89259c3..3b5538e 100644 --- a/tests/test_files/MetronInfo.xsd +++ b/tests/test_files/MetronInfo.xsd @@ -80,11 +80,28 @@ + + + + + + + + + + + + + + + + + diff --git a/tests/test_metadata.py b/tests/test_metadata.py index 4617a0a..1a26eb2 100644 --- a/tests/test_metadata.py +++ b/tests/test_metadata.py @@ -42,6 +42,10 @@ def test_metadata_overlay(fake_metadata: Metadata, fake_overlay_metadata: Metada md.overlay(fake_overlay_metadata) assert md.series.name == "Aquaman" + assert len(md.series.alternative_names) == 2 + assert md.series.alternative_names[1].name == "Fishy" + assert md.series.alternative_names[1].id_ == 60 + assert md.series.alternative_names[1].language == "de" assert md.issue == "0" assert md.stories == fake_metadata.stories assert md.cover_date == date(1994, 10, 1) diff --git a/tests/test_metroninfo.py b/tests/test_metroninfo.py index d97cfdc..be84f99 100644 --- a/tests/test_metroninfo.py +++ b/tests/test_metroninfo.py @@ -6,7 +6,18 @@ import pytest from lxml import etree -from darkseid.metadata import GTIN, Arc, Basic, Credit, Metadata, Price, Role, Series, Universe +from darkseid.metadata import ( + GTIN, + AlternativeNames, + Arc, + Basic, + Credit, + Metadata, + Price, + Role, + Series, + Universe, +) from darkseid.metroninfo import MetronInfo MI_XSD = "tests/test_files/MetronInfo.xsd" @@ -101,7 +112,17 @@ def test_convert_metadata_to_xml(metron_info): info_source=Basic("Metron", id_=54), alt_sources=[Basic("Comic Vine", id_=90)], publisher=Basic("Marvel", id_=1), - series=Series(name="Spider-Man", volume=1, format="Single Issue", id_=50, language="en"), + series=Series( + name="Spider-Man", + volume=1, + format="Single Issue", + id_=50, + language="en", + alternative_names=[ + AlternativeNames("Bug Boy", 50), + AlternativeNames("Spider", language="de"), + ], + ), issue="50", story_arcs=[Arc("Final Crisis, Inc", id_=80, number=1)], cover_date=date(2020, 1, 1), @@ -141,6 +162,10 @@ def test_metadata_from_string(metron_info): Spider-Man 1 Omnibus + + Foo + Hüsker Dü + 3.99 @@ -183,6 +208,11 @@ def test_metadata_from_string(metron_info): assert result.publisher.name == "Marvel" assert result.series.name == "Spider-Man" assert result.series.format == "Omnibus" + assert len(result.series.alternative_names) == 2 + assert result.series.alternative_names[0].name == "Foo" + assert result.series.alternative_names[0].id_ == 1234 + assert result.series.alternative_names[1].language == "de" + assert result.series.alternative_names[1].name == "Hüsker Dü" assert result.prices[0].amount == Decimal("3.99") assert result.gtin.isbn == 1234567890123 assert result.gtin.upc == 76194130593600111