isdn-python/isdn/model.py
2023-04-08 04:13:56 +09:00

166 lines
6 KiB
Python

import re
from datetime import date
from decimal import Decimal
from pydantic import Field, HttpUrl, root_validator, validator
from pydantic.dataclasses import dataclass
from pydantic_xml import BaseXmlModel, element
from . import InvalidIsdnError
NSMAP = {"": "https://isdn.jp/schemas/0.1"}
@dataclass
class ISDN:
code: str
prefix: str | None = None
group: str | None = None
registrant: str | None = None
publication: str | None = None
check_digit: str | None = None
@root_validator(pre=True)
def validate_code(cls, values):
code = str(values["code"])
if code.startswith("ISDN") and "-" in code:
parts = code.lstrip("ISDN").split("-")
code = "".join(parts)
if len(parts) != 5:
raise InvalidIsdnError("ISDN must have 5 parts")
values.update(
{
"prefix": parts[0],
"group": parts[1],
"registrant": parts[2],
"publication": parts[3],
"check_digit": parts[4],
}
)
else:
code = cls.normalize(code)
arg_parts = [values.get(k) for k in ["prefix", "group", "registrant", "publication", "check_digit"]]
if all(arg_parts) and code != "".join(arg_parts):
raise ValueError(f"ISDNs of arguments do not match: {code} != {''.join(arg_parts)}")
return values | {"code": code}
@property
def parts(self) -> list[str | None]:
self.code = self.normalize(self.code)
return [self.prefix, self.group, self.registrant, self.publication, self.check_digit]
@staticmethod
def normalize(isdn: int | str) -> str:
return str(isdn).replace("-", "").strip()
@staticmethod
def calc_check_digit(isdn: str) -> str:
isdn = [int(n) for n in isdn]
cd = 10 - (sum([(n if i % 2 == 0 else n * 3) for i, n in enumerate(isdn[:12])]) % 10)
return str(cd % 10)
def to_disp_isdn(self) -> str | None:
if not all(self.parts):
return None
return f"ISDN{self.prefix}-{self.group}-{self.registrant}-{self.publication}-{self.check_digit}"
def validate(self, raise_error: bool = False) -> bool:
if not re.fullmatch(r"\d+", self.code):
if raise_error:
raise InvalidIsdnError("Contains non-numeric characters")
else:
return False
if len(self.code) != 13:
if raise_error:
raise InvalidIsdnError("ISDN must be 13 digits")
else:
return False
if not (self.code.startswith("278") or self.code.startswith("279")):
if raise_error:
raise InvalidIsdnError("ISDN prefix must be 278 or 279")
else:
return False
if self.calc_check_digit(self.code) != self.code[12]:
if raise_error:
raise InvalidIsdnError("Invalid check digit")
else:
return False
# Validate parts
if self.group and not (1 <= len(self.group) <= 5):
if raise_error:
raise InvalidIsdnError("Group part must be 1 to 5 digits")
else:
return False
if self.registrant and not (1 <= len(self.registrant) <= 7):
if raise_error:
raise InvalidIsdnError("Publisher part must be 1 to 7 digits")
else:
return False
if self.publication and not (1 <= len(self.publication) <= 2):
if raise_error:
raise InvalidIsdnError("Publication part must be 1 to 2 digits")
else:
return False
return True
class UserOption(BaseXmlModel, tag="useroption", nsmap=NSMAP):
property: str = element(tag="property", default="")
value: str = element(tag="value", default="")
class ExternalLink(BaseXmlModel, tag="external-link", nsmap=NSMAP):
title: str | None = element(tag="title")
uri: HttpUrl = element(tag="uri")
class ISDNRecord(BaseXmlModel, nsmap=NSMAP):
isdn: ISDN = element(tag="disp-isdn")
region: str = element(tag="region")
class_: str = element(tag="class")
type: str = element(tag="type")
rating_gender: str = element(tag="rating_gender")
rating_age: str = element(tag="rating_age")
product_name: str = element(tag="product-name")
product_yomi: str | None = element(tag="product-yomi")
publisher_code: str = element(tag="publisher-code")
publisher_name: str = element(tag="publisher-name")
publisher_yomi: str | None = element(tag="publisher-yomi")
issue_date: date = element(tag="issue-date")
genre_code: str | None = element(tag="genre-code")
genre_name: str | None = element(tag="genre-name")
genre_user: str | None = element(tag="genre-user")
c_code: str | None = element(tag="c-code")
author: str | None = element(tag="author")
shape: str | None = element(tag="shape")
contents: str | None = element(tag="contents")
price: Decimal | None = element(tag="price")
price_unit: str | None = element(tag="price-unit")
barcode2: str | None = element(tag="barcode2")
product_comment: str | None = element(tag="product-comment")
product_style: str | None = element(tag="product-style")
product_size: str | None = element(tag="product-size")
product_capacity: Decimal | None = element(tag="product-capacity")
product_capacity_unit: str | None = element(tag="product-capacity-unit")
sample_image_uri: HttpUrl | None = element(tag="sample-image-uri")
useroptions: list[UserOption] = Field(default_factory=list)
external_links: list[ExternalLink] = Field(default_factory=list)
@validator("isdn", pre=True)
def parse_disp_isdn(cls, isdn: str) -> ISDN:
return ISDN(code=isdn)
class ISDNRoot(BaseXmlModel, tag="isdn", nsmap=NSMAP):
records: list[ISDNRecord] = element(tag="item")
@classmethod
def from_xml_first(cls, source: str | bytes) -> ISDNRecord:
isdn_root = cls.from_xml(source)
return isdn_root.records[0]