diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index adfc0e9..a73cfde 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,6 +1,10 @@ name: Test -on: [ push, pull_request ] +on: + push: + branches: + - develop + pull_request: jobs: test: diff --git a/README.md b/README.md index 8a4353c..17307e9 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,59 @@ isdn.jp が提供している書誌情報を取得 ([Web からの情報取得]( ISDN(code='2784702901978', prefix='278', group='4', registrant='702901', publication='97', check_digit='8') >>> record.product_name 'みほん同人誌' +>>> record +ISDNRecord( + isdn=ISDN( + code='2784702901978', + prefix='278', + group='4', + registrant='702901', + publication='97', + check_digit='8' + ), + region='日本', + class_='オリジナル', + type='同人誌', + rating_gender='区別なし', + rating_age='一般', + product_name='みほん同人誌', + product_yomi='みほんどうじんし', + publisher_code='14142139', + publisher_name='見本サークル', + publisher_yomi='みほんさーくる', + issue_date=datetime.date(2008, 3, 12), + genre_code='106', + genre_name='評論・情報', + genre_user=None, + c_code='C3055', + author='専門', + shape='単行本', + contents='電子通信', + price=Decimal('100'), + price_unit='JPY', + barcode2='2923055001007', + product_comment=None, + product_style=None, + product_size=None, + product_capacity=None, + product_capacity_unit=None, + sample_image_uri=HttpUrl('https://isdn.jp/images/thumbs/2784702901978.png', ), + useroptions=[ + UserOption(property='執筆者', value='みほん執筆者1'), + UserOption(property='執筆者', value='みほん執筆者2'), + UserOption(property='執筆者', value='みほん執筆者3'), + UserOption(property='執筆者', value='みほん執筆者4'), + UserOption(property='執筆者', value='みほん執筆者5'), + UserOption(property='執筆者', value='みほん執筆者6') + ], + external_links=[ + ExternalLink(title='国際標準同人誌番号', uri=HttpUrl('http://isdn.jp/', )), + ExternalLink( + title='mixiコミュニティ', + uri=HttpUrl('http://mixi.jp/view_community.pl?id=3188828', ) + ) + ] +) ``` ## CLI @@ -61,4 +114,5 @@ $ isdn list ``` $ isdn bulk-download /path/to/download +$ isdn bulk-download /path/to/download --write-image ``` diff --git a/isdn/__init__.py b/isdn/__init__.py index 532c622..70a1acd 100644 --- a/isdn/__init__.py +++ b/isdn/__init__.py @@ -1,150 +1,11 @@ import importlib.metadata -import re -from dataclasses import asdict, dataclass, field -from datetime import date __version__ = importlib.metadata.version("isdn") -class IsdnError(Exception): +class InvalidIsdnError(ValueError): pass -class InvalidIsdnError(IsdnError): - pass - - -@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 - - @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] - - def __post_init__(self): - if all(self.parts) and self.code != "".join(self.parts): - raise ValueError(f"ISDNs of arguments do not match: {self.code} != {''.join(self.parts)}") - - 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 - - @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) - - @classmethod - def from_disp_isdn(cls, disp_isdn: str) -> "ISDN": - parts = disp_isdn.lstrip("ISDN").split("-") - isdn = "".join(parts) - if len(parts) != 5: - raise InvalidIsdnError("ISDN must have 5 parts") - return cls(isdn, *parts) - - -@dataclass -class UserOption: - property: str - value: str - - -@dataclass -class ExternalLink: - uri: str - title: str | None - - -@dataclass -class ISDNRecord: - isdn: ISDN - disp_isdn: str - region: str - class_: str - type: str - rating_gender: str - rating_age: str - product_name: str - product_yomi: str | None - publisher_code: str - publisher_name: str - publisher_yomi: str | None - issue_date: date - genre_code: str | None - genre_name: str | None - genre_user: str | None - c_code: str | None - author: str | None - shape: str | None - contents: str | None - price: int | None - price_unit: str | None - barcode2: str | None - product_comment: str | None - product_style: str | None - product_size: str | None - product_capacity: int | None - product_capacity_unit: str | None - sample_image_uri: str - useroption: list[UserOption] = field(default_factory=list) - external_link: list[ExternalLink] = field(default_factory=list) - - def to_dict(self) -> dict: - return asdict(self) - - from .client import ISDNClient +from .model import ISDN, ISDNRecord diff --git a/isdn/client.py b/isdn/client.py index b35fd22..0f235e2 100644 --- a/isdn/client.py +++ b/isdn/client.py @@ -2,8 +2,9 @@ from typing import Iterator import requests -from . import ISDNRecord, __version__ -from .parser import ISDNJpXMLParser +from . import __version__ +from .model import ISDNRecord, ISDNRoot +from .parser import ISDNJpSitemapXMLParser ISDN_XML_ENDPOINT = "https://isdn.jp/xml/{isdn}" ISDN_IMAGE_ENDPOINT = "https://isdn.jp/images/thumbs/{isdn}.png" @@ -37,7 +38,7 @@ class ISDNClient: def get(self, isdn: str) -> ISDNRecord: r = self._get(isdn, self.xml_endpoint_url) - return ISDNJpXMLParser.parse_record(r.content) + return ISDNRoot.from_xml_first(r.content) def get_raw(self, isdn: str) -> bytes: r = self._get(isdn, self.xml_endpoint_url) @@ -54,4 +55,4 @@ class ISDNClient: def list(self) -> Iterator[str]: r = self._list() - return ISDNJpXMLParser.parse_list(r.raw) + return ISDNJpSitemapXMLParser.parse_list(r.raw) diff --git a/isdn/command.py b/isdn/command.py index 0c08b57..7f8bb40 100644 --- a/isdn/command.py +++ b/isdn/command.py @@ -1,13 +1,12 @@ -import json import os import time import click from requests.exceptions import HTTPError -from . import ISDN, __version__ +from . import __version__ from .client import ISDNClient -from .parser import ISDNJpXMLParser +from .model import ISDN, ISDNRoot @click.group() @@ -29,9 +28,9 @@ def get_isdn(isdn: str, format: str): case "xml": res = c.get_raw(isdn) case "dict": - res = c.get(isdn).to_dict() + res = c.get(isdn).dict() case "json": - res = json.dumps(c.get(isdn).to_dict(), ensure_ascii=False) + res = c.get(isdn).json(ensure_ascii=False) case _: raise NotImplementedError click.echo(res) @@ -72,7 +71,7 @@ def bulk_download( out.write(res) if write_image: - record = ISDNJpXMLParser.parse_record(res) + record = ISDNRoot.from_xml_first(res) if record.sample_image_uri: img = c.get_image(isdn) with open(image_path, "wb") as out: diff --git a/isdn/model.py b/isdn/model.py new file mode 100644 index 0000000..a334c39 --- /dev/null +++ b/isdn/model.py @@ -0,0 +1,166 @@ +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] diff --git a/isdn/parser.py b/isdn/parser.py index c7aba07..06acbd2 100644 --- a/isdn/parser.py +++ b/isdn/parser.py @@ -3,51 +3,10 @@ from typing import IO, Iterator from lxml import etree -from . import ISDN, ExternalLink, ISDNRecord, UserOption - -namespaces = {"isdn": "https://isdn.jp/schemas/0.1", "sitemap": "http://www.sitemaps.org/schemas/sitemap/0.9"} +namespaces = {"sitemap": "http://www.sitemaps.org/schemas/sitemap/0.9"} -class ISDNJpXMLParser: - @staticmethod - def convert_tag_name(tag: str) -> str: - key = etree.QName(tag).localname.replace("-", "_") - if key == "class": - key += "_" - return key - - @staticmethod - def parse_value(elem: etree.Element, key: str) -> str | UserOption | ExternalLink: - data = dict() - for child in elem: - data[ISDNJpXMLParser.convert_tag_name(child.tag)] = child.text - - match key: - case "useroption": - return UserOption(**data) - case "external_link": - return ExternalLink(**data) - case _: - return elem.text - - @staticmethod - def parse_record(doc: str | bytes) -> ISDNRecord: - root = etree.fromstring(doc) - item = root.find("./isdn:item", namespaces=namespaces) - data = dict() - for child in item: - key = ISDNJpXMLParser.convert_tag_name(child.tag) - value = ISDNJpXMLParser.parse_value(child, key) - - if key in {"useroption", "external_link"}: - if key not in data: - data[key] = [] - data[key].append(value) - else: - data[key] = value - - return ISDNRecord(isdn=ISDN.from_disp_isdn(data["disp_isdn"]), **data) - +class ISDNJpSitemapXMLParser: @staticmethod def parse_list(file: str | IO) -> Iterator[str]: for event, elm in etree.iterparse( diff --git a/poetry.lock b/poetry.lock index ed47910..2ef0f4d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -477,6 +477,79 @@ files = [ dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] +[[package]] +name = "pydantic" +version = "1.10.7" +description = "Data validation and settings management using python type hints" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pydantic-1.10.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e79e999e539872e903767c417c897e729e015872040e56b96e67968c3b918b2d"}, + {file = "pydantic-1.10.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:01aea3a42c13f2602b7ecbbea484a98169fb568ebd9e247593ea05f01b884b2e"}, + {file = "pydantic-1.10.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:516f1ed9bc2406a0467dd777afc636c7091d71f214d5e413d64fef45174cfc7a"}, + {file = "pydantic-1.10.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae150a63564929c675d7f2303008d88426a0add46efd76c3fc797cd71cb1b46f"}, + {file = "pydantic-1.10.7-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:ecbbc51391248116c0a055899e6c3e7ffbb11fb5e2a4cd6f2d0b93272118a209"}, + {file = "pydantic-1.10.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f4a2b50e2b03d5776e7f21af73e2070e1b5c0d0df255a827e7c632962f8315af"}, + {file = "pydantic-1.10.7-cp310-cp310-win_amd64.whl", hash = "sha256:a7cd2251439988b413cb0a985c4ed82b6c6aac382dbaff53ae03c4b23a70e80a"}, + {file = "pydantic-1.10.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:68792151e174a4aa9e9fc1b4e653e65a354a2fa0fed169f7b3d09902ad2cb6f1"}, + {file = "pydantic-1.10.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dfe2507b8ef209da71b6fb5f4e597b50c5a34b78d7e857c4f8f3115effaef5fe"}, + {file = "pydantic-1.10.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10a86d8c8db68086f1e30a530f7d5f83eb0685e632e411dbbcf2d5c0150e8dcd"}, + {file = "pydantic-1.10.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d75ae19d2a3dbb146b6f324031c24f8a3f52ff5d6a9f22f0683694b3afcb16fb"}, + {file = "pydantic-1.10.7-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:464855a7ff7f2cc2cf537ecc421291b9132aa9c79aef44e917ad711b4a93163b"}, + {file = "pydantic-1.10.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:193924c563fae6ddcb71d3f06fa153866423ac1b793a47936656e806b64e24ca"}, + {file = "pydantic-1.10.7-cp311-cp311-win_amd64.whl", hash = "sha256:b4a849d10f211389502059c33332e91327bc154acc1845f375a99eca3afa802d"}, + {file = "pydantic-1.10.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cc1dde4e50a5fc1336ee0581c1612215bc64ed6d28d2c7c6f25d2fe3e7c3e918"}, + {file = "pydantic-1.10.7-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e0cfe895a504c060e5d36b287ee696e2fdad02d89e0d895f83037245218a87fe"}, + {file = "pydantic-1.10.7-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:670bb4683ad1e48b0ecb06f0cfe2178dcf74ff27921cdf1606e527d2617a81ee"}, + {file = "pydantic-1.10.7-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:950ce33857841f9a337ce07ddf46bc84e1c4946d2a3bba18f8280297157a3fd1"}, + {file = "pydantic-1.10.7-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c15582f9055fbc1bfe50266a19771bbbef33dd28c45e78afbe1996fd70966c2a"}, + {file = "pydantic-1.10.7-cp37-cp37m-win_amd64.whl", hash = "sha256:82dffb306dd20bd5268fd6379bc4bfe75242a9c2b79fec58e1041fbbdb1f7914"}, + {file = "pydantic-1.10.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8c7f51861d73e8b9ddcb9916ae7ac39fb52761d9ea0df41128e81e2ba42886cd"}, + {file = "pydantic-1.10.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6434b49c0b03a51021ade5c4daa7d70c98f7a79e95b551201fff682fc1661245"}, + {file = "pydantic-1.10.7-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64d34ab766fa056df49013bb6e79921a0265204c071984e75a09cbceacbbdd5d"}, + {file = "pydantic-1.10.7-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:701daea9ffe9d26f97b52f1d157e0d4121644f0fcf80b443248434958fd03dc3"}, + {file = "pydantic-1.10.7-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:cf135c46099ff3f919d2150a948ce94b9ce545598ef2c6c7bf55dca98a304b52"}, + {file = "pydantic-1.10.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b0f85904f73161817b80781cc150f8b906d521fa11e3cdabae19a581c3606209"}, + {file = "pydantic-1.10.7-cp38-cp38-win_amd64.whl", hash = "sha256:9f6f0fd68d73257ad6685419478c5aece46432f4bdd8d32c7345f1986496171e"}, + {file = "pydantic-1.10.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c230c0d8a322276d6e7b88c3f7ce885f9ed16e0910354510e0bae84d54991143"}, + {file = "pydantic-1.10.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:976cae77ba6a49d80f461fd8bba183ff7ba79f44aa5cfa82f1346b5626542f8e"}, + {file = "pydantic-1.10.7-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d45fc99d64af9aaf7e308054a0067fdcd87ffe974f2442312372dfa66e1001d"}, + {file = "pydantic-1.10.7-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d2a5ebb48958754d386195fe9e9c5106f11275867051bf017a8059410e9abf1f"}, + {file = "pydantic-1.10.7-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:abfb7d4a7cd5cc4e1d1887c43503a7c5dd608eadf8bc615413fc498d3e4645cd"}, + {file = "pydantic-1.10.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:80b1fab4deb08a8292d15e43a6edccdffa5377a36a4597bb545b93e79c5ff0a5"}, + {file = "pydantic-1.10.7-cp39-cp39-win_amd64.whl", hash = "sha256:d71e69699498b020ea198468e2480a2f1e7433e32a3a99760058c6520e2bea7e"}, + {file = "pydantic-1.10.7-py3-none-any.whl", hash = "sha256:0cd181f1d0b1d00e2b705f1bf1ac7799a2d938cce3376b8007df62b29be3c2c6"}, + {file = "pydantic-1.10.7.tar.gz", hash = "sha256:cfc83c0678b6ba51b0532bea66860617c4cd4251ecf76e9846fa5a9f3454e97e"}, +] + +[package.dependencies] +typing-extensions = ">=4.2.0" + +[package.extras] +dotenv = ["python-dotenv (>=0.10.4)"] +email = ["email-validator (>=1.0.3)"] + +[[package]] +name = "pydantic-xml" +version = "0.6.0" +description = "pydantic xml extension" +category = "main" +optional = false +python-versions = ">=3.8,<4.0" +files = [ + {file = "pydantic_xml-0.6.0-py3-none-any.whl", hash = "sha256:7a0e4b8ea972b1425126e298c2572e4dbaecb82905b5d9b119a98b7d59fb0204"}, + {file = "pydantic_xml-0.6.0.tar.gz", hash = "sha256:ba7f47602187e18c5403376e51275d593833815517915202205223f33044967b"}, +] + +[package.dependencies] +lxml = {version = ">=4.9.1,<5.0.0", optional = true, markers = "extra == \"lxml\""} +pydantic = ">=1.9.0,<2.0.0" + +[package.extras] +docs = ["Sphinx (>=5.3.0,<6.0.0)", "furo (>=2022.12.7,<2023.0.0)", "sphinx-copybutton (>=0.5.1,<0.6.0)", "sphinx_design (>=0.3.0,<0.4.0)", "toml (>=0.10.2,<0.11.0)"] +lxml = ["lxml (>=4.9.1,<5.0.0)"] + [[package]] name = "pytest" version = "7.2.2" @@ -554,6 +627,18 @@ files = [ {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] +[[package]] +name = "typing-extensions" +version = "4.5.0" +description = "Backported and Experimental Type Hints for Python 3.7+" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "typing_extensions-4.5.0-py3-none-any.whl", hash = "sha256:fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4"}, + {file = "typing_extensions-4.5.0.tar.gz", hash = "sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb"}, +] + [[package]] name = "urllib3" version = "1.26.15" @@ -574,4 +659,4 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "c2d28a1355e6578e9415007ba906bfcab073adba9c398e82f45391989a4edc39" +content-hash = "25fb6ccc2bf825d93cd9f36ad58792f0da7f2bc1044b26a508fa4370956249c6" diff --git a/pyproject.toml b/pyproject.toml index 50d2f0c..e455bed 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "isdn" -version = "1.0.1" +version = "1.1.0" description = "Library for ISDN (International Standard Dojin Numbering)" authors = ["Babibubebon "] homepage = "https://github.com/Babibubebon/isdn-python" @@ -24,6 +24,7 @@ python = "^3.10" click = "^8.1.3" requests = "^2.28.2" lxml = "^4.9.2" +pydantic-xml = {extras = ["lxml"], version = "^0.6.0"} [tool.poetry.group.dev.dependencies] isort = "^5.12.0" diff --git a/tests/test_isdn.py b/tests/test_isdn.py index e5b9983..10ec61d 100644 --- a/tests/test_isdn.py +++ b/tests/test_isdn.py @@ -1,6 +1,6 @@ from unittest import TestCase -from isdn import ISDN, InvalidIsdnError +from isdn import ISDN class TestIsdn(TestCase): @@ -9,7 +9,12 @@ class TestIsdn(TestCase): ISDN(2784702901978, "278", "4", "702901", "97", "8") ISDN("278-4-702901-97-8", "278", "4", "702901", "97", "8") + disp_isdn = "ISDN278-4-702901-97-8" + isdn = ISDN(disp_isdn) + self.assertEqual(disp_isdn, isdn.to_disp_isdn()) + with self.assertRaises(ValueError): + ISDN("ISDN278-4-702901") ISDN("2784702901978", "278", "4", "000000", "97", "8") def test_parts(self): @@ -53,11 +58,3 @@ class TestIsdn(TestCase): self.assertEqual("7", ISDN.calc_check_digit("278470290107")) self.assertEqual("8", ISDN.calc_check_digit("278470290113")) self.assertEqual("9", ISDN.calc_check_digit("278470290103")) - - def test_from_disp_isdn(self): - disp_isdn = "ISDN278-4-702901-97-8" - isdn = ISDN.from_disp_isdn(disp_isdn) - self.assertEqual(disp_isdn, isdn.to_disp_isdn()) - - with self.assertRaises(InvalidIsdnError): - ISDN.from_disp_isdn("ISDN278-4-702901")