Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 0 additions & 34 deletions .github/release-drafter.yml

This file was deleted.

23 changes: 0 additions & 23 deletions .github/workflows/release-drafter.yml

This file was deleted.

2 changes: 2 additions & 0 deletions darkseid/metadata/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
Role,
Series,
Universe,
currency_to_country,
)
from darkseid.metadata.metroninfo import MetronInfo

Expand All @@ -45,4 +46,5 @@
"Series",
"Universe",
"XmlError",
"currency_to_country",
]
86 changes: 57 additions & 29 deletions darkseid/metadata/data_classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@
MAX_UPC = 17
MAX_ISBN = 13
COUNTRY_LEN = 2
CURRENCY_LEN = 3
YEAR_LEN = 4

# __str__ constants
COMMENT_LEN = 50
MAX_COMMENT_LEN = 100
Expand All @@ -37,6 +39,43 @@
MAX_NUMBER_OF_STORIES = 3
MAX_NUMBER_OF_TAGS = 5

_CURRENCY_TO_COUNTRY: dict[str, str] = {
"AUD": "AU",
"CAD": "CA",
"EUR": "DE",
"GBP": "GB",
"NZD": "NZ",
"USD": "US",
}

_COUNTRY_TO_CURRENCY: dict[str, str] = {v: k for k, v in _CURRENCY_TO_COUNTRY.items()}


def currency_to_country(currency_code: str) -> str | None:
"""Convert an ISO 4217 currency code to an ISO 3166-1 alpha-2 country code.

Args:
currency_code: ISO 4217 currency code (e.g., "USD").

Returns:
ISO 3166-1 alpha-2 country code, or None if not mapped.

"""
return _CURRENCY_TO_COUNTRY.get(currency_code)


def country_to_currency(country_code: str) -> str | None:
"""Convert an ISO 3166-1 alpha-2 country code to an ISO 4217 currency code.

Args:
country_code: ISO 3166-1 alpha-2 country code (e.g., "US").

Returns:
ISO 4217 currency code, or None if not mapped.

"""
return _COUNTRY_TO_CURRENCY.get(country_code)


class Validations:
"""A base class for data validation in dataclasses.
Expand Down Expand Up @@ -116,56 +155,45 @@ class Price(Validations):

Attributes:
amount (Decimal): The amount associated with the price.
country (str): The country associated with the price, defaults to "US".
currency (str): The ISO 4217 currency code, defaults to "USD".

"""

amount: Decimal
country: str = field(default="US")
currency: str = field(default="USD")

@staticmethod
def validate_country(value: str, **_: any) -> str:
"""Validate a country value.
def validate_currency(value: str, **_: object) -> str:
"""Validate an ISO 4217 currency code.

If the value is None, it returns the default country code "US". Otherwise, it strips
any leading or trailing whitespace from the value. If the value is empty after
stripping, it raises a ValueError.

If the length of the value is 2, it tries to find the country object using the alpha-2
code. Otherwise, it tries to look up the country object using the value. If the country
object is not found, it raises a ValueError.
If the value is None, it returns the default currency code "USD". Otherwise, it strips
any leading or trailing whitespace and uppercases the value. If the value is empty after
stripping, it raises a ValueError. The currency code is validated against pycountry's
ISO 4217 currency database.

Args:
value (str): The country value to validate.
value (str): The currency code to validate.
**_ (any): Additional keyword arguments (ignored).

Returns:
str: The validated country code.
str: The validated ISO 4217 currency code.

Raises:
ValueError: Raised when the country code cannot be found or when no value is given for the country.
ValueError: Raised when the currency code is invalid or no value is given.

"""
if value is None:
return "US"
value = value.strip()
return "USD"
value = value.strip().upper()
if not value:
msg = "No value given for country"
msg = "No value given for currency"
raise ValueError(msg)

if len(value) == COUNTRY_LEN:
obj = pycountry.countries.get(alpha_2=value)
else:
try:
obj = pycountry.countries.lookup(value)
except LookupError as e:
msg = f"Couldn't find country for {value}"
raise ValueError(msg) from e

obj = pycountry.currencies.get(alpha_3=value)
if obj is None:
msg = f"Couldn't get country code for {value}"
msg = f"Couldn't find currency for {value}"
raise ValueError(msg)
return obj.alpha_2
return obj.alpha_3


@dataclass
Expand Down Expand Up @@ -1011,7 +1039,7 @@ def __str__(self: Metadata) -> str: # noqa: PLR0912

# Pricing and identification
if self.prices:
price_strs = [f"${price.amount} ({price.country})" for price in self.prices]
price_strs = [f"${price.amount} ({price.currency})" for price in self.prices]
lines.append(f"{indent}Prices: {', '.join(price_strs)}")

if self.gtin:
Expand Down
9 changes: 7 additions & 2 deletions darkseid/metadata/metroninfo.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
Role,
Series,
Universe,
country_to_currency,
currency_to_country,
)

if TYPE_CHECKING:
Expand All @@ -40,6 +42,7 @@
EARLIEST_YEAR = 1900
VOLUME_THRESHOLD = 1000
DEFAULT_COUNTRY = "US"
DEFAULT_CURRENCY = "USD"
DEFAULT_SERIES_NAME = "None" # Placeholder for Series initialization; actual name set from XML

# Validation sets
Expand Down Expand Up @@ -510,7 +513,8 @@ def _add_prices(self, root: ET.Element, prices: list[Price]) -> None:

price_node = self._get_or_create_element(root, "Prices")
for price in prices:
child_node = ET.SubElement(price_node, "Price", attrib={"country": price.country})
country = currency_to_country(price.currency) or DEFAULT_COUNTRY
child_node = ET.SubElement(price_node, "Price", attrib={"country": country})
child_node.text = str(price.amount)

def _add_universes(self, root: ET.Element, universes: list[Universe]) -> None:
Expand Down Expand Up @@ -669,7 +673,8 @@ def _parse_prices_element(self, prices_element: ET.Element | None) -> list[Price
try:
amount = Decimal(item.text)
country = item.attrib.get("country", DEFAULT_COUNTRY)
prices.append(Price(amount, country))
currency = country_to_currency(country) or DEFAULT_CURRENCY
prices.append(Price(amount, currency))
except (ValueError, TypeError):
continue
return prices
Expand Down
22 changes: 11 additions & 11 deletions tests/test_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -422,21 +422,21 @@ def test_gtin_isbn_valid_values():
assert gtin.upc is None


def test_price_none_country():
"""Test Price with None country."""
def test_price_none_currency():
"""Test Price with None currency."""
price = Price(Decimal("2.99"), None) # type: ignore
assert price.country == "US" # Should default to US
assert price.currency == "USD" # Should default to USD


def test_price_whitespace_country():
"""Test Price with whitespace-only country."""
with pytest.raises(ValueError, match="No value given for country"):
def test_price_whitespace_currency():
"""Test Price with whitespace-only currency."""
with pytest.raises(ValueError, match="No value given for currency"):
Price(Decimal("2.99"), " ")


def test_price_with_bad_country():
"""Test Price with bad country."""
with pytest.raises(ValueError, match="Couldn't find country for"):
def test_price_with_bad_currency():
"""Test Price with invalid currency code."""
with pytest.raises(ValueError, match="Couldn't find currency for"):
Price(Decimal("2.99"), "xyz")


Expand Down Expand Up @@ -586,7 +586,7 @@ def test_metadata_comprehensive_str():
# Create a metadata object with many fields populated
series = Series("Superman", volume=1, start_year=1938)
publisher = Publisher("DC Comics", imprint=Basic("Vertigo"))
prices = [Price(Decimal("2.99"), "US"), Price(Decimal("3.99"), "CA")]
prices = [Price(Decimal("2.99"), "USD"), Price(Decimal("3.99"), "CAD")]
gtin = GTIN(upc=123456789012345, isbn=9781234567890)
credits_ = [
Credit("Jerry Siegel", [Role("Writer", primary=True)]),
Expand Down Expand Up @@ -641,7 +641,7 @@ def test_metadata_comprehensive_str():
assert "Superman (v1) [1938]" in result
assert "DC Comics (Vertigo)" in result
assert "Cover: 1938-06-01 | Store: 1938-05-15" in result
assert "$2.99 (US), $3.99 (CA)" in result
assert "$2.99 (USD), $3.99 (CAD)" in result
assert "22 pages | Color" in result
assert "and 5 more" in result # Character truncation
assert "..." in result # Comment truncation
Expand Down
4 changes: 2 additions & 2 deletions tests/test_metroninfo.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,8 @@ def complex_metadata():

# Pricing and GTIN
metadata.prices = [
Price(Decimal("0.12"), "US"),
Price(Decimal("0.15"), "CA"),
Price(Decimal("0.12"), "USD"),
Price(Decimal("0.15"), "CAD"),
]
metadata.gtin = GTIN(isbn=1234567890123, upc=76194130593600111)

Expand Down
Loading