mirror of
https://github.com/Babibubebon/isdn-python.git
synced 2024-09-22 17:24:20 +09:00
Deserialize XML using pydantic-xml
This commit is contained in:
parent
6582f8d05c
commit
9ab036275e
10 changed files with 331 additions and 204 deletions
6
.github/workflows/test.yml
vendored
6
.github/workflows/test.yml
vendored
|
@ -1,6 +1,10 @@
|
|||
name: Test
|
||||
|
||||
on: [ push, pull_request ]
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
|
|
54
README.md
54
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=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
|
||||
```
|
||||
|
|
143
isdn/__init__.py
143
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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -5,9 +5,9 @@ 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 +29,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 +72,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:
|
||||
|
|
165
isdn/model.py
Normal file
165
isdn/model.py
Normal file
|
@ -0,0 +1,165 @@
|
|||
import re
|
||||
from datetime import date
|
||||
|
||||
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")
|
||||
|
||||
|
||||
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: int | 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: int | 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]
|
|
@ -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(
|
||||
|
|
87
poetry.lock
generated
87
poetry.lock
generated
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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")
|
||||
|
|
Loading…
Reference in a new issue