diff --git a/.gitignore b/.gitignore index f462cf5..7106aac 100644 --- a/.gitignore +++ b/.gitignore @@ -1,14 +1,2 @@ -*.log -*.csv -*.html -*.ipynb -Pipfile.lock -.venv/ -__pycache__/ -JianshuResearchTools/__pycache__/ -.vscode/ -build/ -dist/ -JianshuResearchTools.egg-info/ -assets/ -.pytest_cache/ \ No newline at end of file +**/__pycache__/ +dist/ \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c481cae..a451ce6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -11,11 +11,11 @@ 以下是一个示例: - 操作系统:Windows 10 专业版 内部版本号 2004 -- Python :Python 3.8.10 64-bit +- Python :Python 3.10.10 64-bit - JRT:v2.0.0 - 依赖库: - - requests 2.25.1 - - lxml 4.6.3 + - httpx 0.24.0 + - lxml 4.9.2 **您执行的代码** @@ -139,13 +139,13 @@ ValueError: check_hostname requires server_hostname 示例: -增加获取贝壳小岛挂单信息的函数 +支持从 collection_url 获取 collection_id **提出该建议的原因**(可选) 示例: -助力对简书资产交易的分析 +简化专题信息获取 **实现思路**(可选) @@ -158,23 +158,6 @@ ValueError: check_hostname requires server_hostname - 参数 - 返回值 -示例: - -思路:通过解析接口返回的 Json 数据实现。 - -接口:https://www.beikeisland.com/api/Trade/getTradeList - -请求方式:POST - -参数: - -- pageIndex:整数,页码 -- retype:整数,1 为卖单,2 为买单 - -返回值: - -(略) - **联系方式** 可选的联系方式如下: @@ -199,7 +182,7 @@ ValueError: check_hostname requires server_hostname 存储库下载到本地后,请执行 `git switch dev` 切换到开发分支,在主分支上进行开发的 PR 将被拒绝。 -开发时请注意遵守代码规范。本项目基本遵循 PEP8 规范,对单行字符数的限制除外。 +开发时请注意遵守代码规范。本项目遵守 PEP8 规范,对单行字符数的限制除外。 请书写与现有注释格式一致的函数注释。如果您使用 VS Code 进行开发,建议下载 Python Docstring Generator 扩展。 diff --git a/JianshuResearchTools/__init__.py b/JianshuResearchTools/__init__.py index f431ded..e170956 100644 --- a/JianshuResearchTools/__init__.py +++ b/JianshuResearchTools/__init__.py @@ -1,15 +1,11 @@ -__version__ = "2.10.1" +__version__ = "2.11.0" -from . import (article, beikeisland, collection, island, notebook, objects, - rank, user) +from . import article, collection, island, notebook, objects, rank, user -__all__ = [ - "article", "beikeisland", "collection", "island", "notebook", "objects", - "rank", "user" -] +__all__ = ["article", "collection", "island", "notebook", "objects", "rank", "user"] -def future(): +def future() -> None: """彩蛋 在 JRT 2.0 版本中加入 diff --git a/JianshuResearchTools/article.py b/JianshuResearchTools/article.py index 9475c53..0124f87 100644 --- a/JianshuResearchTools/article.py +++ b/JianshuResearchTools/article.py @@ -1,27 +1,41 @@ +from contextlib import suppress from datetime import datetime from re import findall, sub -from typing import Dict, Generator, List +from typing import Dict, Generator, List, Literal, Optional from lxml import etree from .assert_funcs import AssertArticleStatusNormal, AssertArticleUrl -from .basic_apis import (GetArticleCommentsJsonDataApi, - GetArticleHtmlJsonDataApi, GetArticleJsonDataApi) +from .basic_apis import ( + GetArticleCommentsJsonDataApi, + GetArticleHtmlJsonDataApi, + GetArticleJsonDataApi, +) -try: +with suppress(ImportError): from tomd import convert as html2md -except ImportError: - pass __all__ = [ - "GetArticleTitle", "GetArticleAuthorName", "GetArticleReadsCount", - "GetArticleWordage", "GetArticleLikesCount", "GetArticleCommentsCount", - "GetArticleMostValuableCommentsCount", "GetArticleTotalFPCount", - "GetArticleDescription", "GetArticlePublishTime", "GetArticleUpdateTime", - "GetArticlePaidStatus", "GetArticleReprintStatus", - "GetArticleCommentStatus", "GetArticleHtml", "GetArticleText", - "GetArticleMarkdown", "GetArticleCommentsData", "GetArticleAllBasicData", - "GetArticleAllCommentsData" + "GetArticleTitle", + "GetArticleAuthorName", + "GetArticleReadsCount", + "GetArticleWordage", + "GetArticleLikesCount", + "GetArticleCommentsCount", + "GetArticleMostValuableCommentsCount", + "GetArticleTotalFPCount", + "GetArticleDescription", + "GetArticlePublishTime", + "GetArticleUpdateTime", + "GetArticlePaidStatus", + "GetArticleReprintStatus", + "GetArticleCommentStatus", + "GetArticleHtml", + "GetArticleText", + "GetArticleMarkdown", + "GetArticleCommentsData", + "GetArticleAllBasicData", + "GetArticleAllCommentsData", ] @@ -39,8 +53,7 @@ def GetArticleTitle(article_url: str, disable_check: bool = False) -> str: AssertArticleUrl(article_url) AssertArticleStatusNormal(article_url) json_obj = GetArticleJsonDataApi(article_url) - result = json_obj["public_title"] - return result + return json_obj["public_title"] def GetArticleAuthorName(article_url: str, disable_check: bool = False) -> str: @@ -57,8 +70,7 @@ def GetArticleAuthorName(article_url: str, disable_check: bool = False) -> str: AssertArticleUrl(article_url) AssertArticleStatusNormal(article_url) json_obj = GetArticleHtmlJsonDataApi(article_url) - result = json_obj["props"]["initialState"]["note"]["data"]["user"]["nickname"] - return result + return json_obj["props"]["initialState"]["note"]["data"]["user"]["nickname"] def GetArticleReadsCount(article_url: str, disable_check: bool = False) -> int: @@ -75,8 +87,7 @@ def GetArticleReadsCount(article_url: str, disable_check: bool = False) -> int: AssertArticleUrl(article_url) AssertArticleStatusNormal(article_url) json_obj = GetArticleHtmlJsonDataApi(article_url) - result = json_obj["props"]["initialState"]["note"]["data"]["views_count"] - return result + return json_obj["props"]["initialState"]["note"]["data"]["views_count"] def GetArticleWordage(article_url: str, disable_check: bool = False) -> int: @@ -93,8 +104,7 @@ def GetArticleWordage(article_url: str, disable_check: bool = False) -> int: AssertArticleUrl(article_url) AssertArticleStatusNormal(article_url) json_obj = GetArticleHtmlJsonDataApi(article_url) - result = json_obj["props"]["initialState"]["note"]["data"]["wordage"] - return result + return json_obj["props"]["initialState"]["note"]["data"]["wordage"] def GetArticleLikesCount(article_url: str, disable_check: bool = False) -> int: @@ -111,8 +121,7 @@ def GetArticleLikesCount(article_url: str, disable_check: bool = False) -> int: AssertArticleUrl(article_url) AssertArticleStatusNormal(article_url) json_obj = GetArticleJsonDataApi(article_url) - result = json_obj["likes_count"] - return result + return json_obj["likes_count"] def GetArticleCommentsCount(article_url: str, disable_check: bool = False) -> int: @@ -129,11 +138,12 @@ def GetArticleCommentsCount(article_url: str, disable_check: bool = False) -> in AssertArticleUrl(article_url) AssertArticleStatusNormal(article_url) json_obj = GetArticleJsonDataApi(article_url) - result = json_obj["public_comment_count"] - return result + return json_obj["public_comment_count"] -def GetArticleMostValuableCommentsCount(article_url: str, disable_check: bool = False) -> int: +def GetArticleMostValuableCommentsCount( + article_url: str, disable_check: bool = False +) -> int: """获取文章精选评论数量 Args: @@ -147,8 +157,7 @@ def GetArticleMostValuableCommentsCount(article_url: str, disable_check: bool = AssertArticleUrl(article_url) AssertArticleStatusNormal(article_url) json_obj = GetArticleJsonDataApi(article_url) - result = json_obj["featured_comments_count"] - return result + return json_obj["featured_comments_count"] def GetArticleTotalFPCount(article_url: str, disable_check: bool = False) -> float: @@ -165,8 +174,7 @@ def GetArticleTotalFPCount(article_url: str, disable_check: bool = False) -> flo AssertArticleUrl(article_url) AssertArticleStatusNormal(article_url) json_obj = GetArticleJsonDataApi(article_url) - result = json_obj["total_fp_amount"] / 1000 - return result + return json_obj["total_fp_amount"] / 1000 def GetArticleDescription(article_url: str, disable_check: bool = False) -> str: @@ -183,8 +191,7 @@ def GetArticleDescription(article_url: str, disable_check: bool = False) -> str: AssertArticleUrl(article_url) AssertArticleStatusNormal(article_url) json_obj = GetArticleJsonDataApi(article_url) - result = json_obj["description"] - return result + return json_obj["description"] def GetArticlePublishTime(article_url: str, disable_check: bool = False) -> datetime: @@ -201,8 +208,7 @@ def GetArticlePublishTime(article_url: str, disable_check: bool = False) -> date AssertArticleUrl(article_url) AssertArticleStatusNormal(article_url) json_obj = GetArticleJsonDataApi(article_url) - result = datetime.fromisoformat(json_obj["first_shared_at"]) - return result + return datetime.fromisoformat(json_obj["first_shared_at"]).replace(tzinfo=None) def GetArticleUpdateTime(article_url: str, disable_check: bool = False) -> datetime: @@ -219,8 +225,7 @@ def GetArticleUpdateTime(article_url: str, disable_check: bool = False) -> datet AssertArticleUrl(article_url) AssertArticleStatusNormal(article_url) json_obj = GetArticleJsonDataApi(article_url) - result = datetime.fromtimestamp(json_obj["last_updated_at"]) - return result + return datetime.fromtimestamp(json_obj["last_updated_at"]) def GetArticlePaidStatus(article_url: str, disable_check: bool = False) -> bool: @@ -238,15 +243,14 @@ def GetArticlePaidStatus(article_url: str, disable_check: bool = False) -> bool: AssertArticleStatusNormal(article_url) json_obj = GetArticleJsonDataApi(article_url) paid_type = { - "free": False, # 免费文章 - "fbook_free": False, # 免费连载中的免费文章 - "pbook_free": False, # 付费连载中的免费文章 - "paid": True, # 付费文章 - "fbook_paid": True, # 免费连载中的付费文章 - "pbook_paid": True # 付费连载中的付费文章 + "free": False, # 免费文章 + "fbook_free": False, # 免费连载中的免费文章 + "pbook_free": False, # 付费连载中的免费文章 + "paid": True, # 付费文章 + "fbook_paid": True, # 免费连载中的付费文章 + "pbook_paid": True, # 付费连载中的付费文章 } - result = paid_type[json_obj["paid_type"]] - return result + return paid_type[json_obj["paid_type"]] def GetArticleReprintStatus(article_url: str, disable_check: bool = False) -> bool: @@ -263,8 +267,7 @@ def GetArticleReprintStatus(article_url: str, disable_check: bool = False) -> bo AssertArticleUrl(article_url) AssertArticleStatusNormal(article_url) json_obj = GetArticleJsonDataApi(article_url) - result = json_obj["reprintable"] - return result + return json_obj["reprintable"] def GetArticleCommentStatus(article_url: str, disable_check: bool = False) -> bool: @@ -281,8 +284,7 @@ def GetArticleCommentStatus(article_url: str, disable_check: bool = False) -> bo AssertArticleUrl(article_url) AssertArticleStatusNormal(article_url) json_obj = GetArticleJsonDataApi(article_url) - result = json_obj["commentable"] - return result + return json_obj["commentable"] def GetArticleHtml(article_url: str, disable_check: bool = False) -> str: @@ -309,11 +311,13 @@ def GetArticleHtml(article_url: str, disable_check: bool = False) -> str: # 去除 image-package html_text = html_text.replace('
', "") - old_img_blocks = findall(r'', html_text) # 匹配旧的 img 标签 + old_img_blocks = findall(r"", html_text) # 匹配旧的 img 标签 if not old_img_blocks: # 文章中没有图片块 return html_text - img_urls = [findall(r'', i)[0] for i in old_img_blocks] + img_urls = [ + findall(r'', i)[0] for i in old_img_blocks + ] new_img_blocks = [f'' for img_url in img_urls] for old_img_block, new_img_block in zip(old_img_blocks, new_img_blocks): @@ -340,7 +344,7 @@ def GetArticleText(article_url: str, disable_check: bool = False) -> str: AssertArticleUrl(article_url) AssertArticleStatusNormal(article_url) json_obj = GetArticleJsonDataApi(article_url) - html_obj = etree.HTML(json_obj["free_content"]) + html_obj = etree.HTML(json_obj["free_content"]) # type: ignore result = "".join(html_obj.itertext()) result = sub(r"\s{3,}", "", result) # 去除多余的空行 return result @@ -360,35 +364,57 @@ def GetArticleMarkdown(article_url: str, disable_check: bool = False) -> str: str: Markdown 格式的文章内容 """ try: - html2md + html2md # noqa: B018 # type: ignore except NameError: - raise ImportError("未安装 html2md 模块,该函数不可用") + raise ImportError("未安装 html2md 模块,该函数不可用") from None if not disable_check: AssertArticleUrl(article_url) AssertArticleStatusNormal(article_url) html_text = GetArticleHtml(article_url, disable_check=True) - image_descriptions = [description for description in findall(r'class="image-caption">.+
', html_text)] # 获取图片描述块 - image_descriptions_text = [description.replace('class="image-caption">', "").replace("", "") - for description in findall(r'class="image-caption">.+', html_text)] # 获取图片描述文本 + image_descriptions = list( + findall(r'class="image-caption">.+', html_text) + ) # 获取图片描述块 + image_descriptions_text = [ + description.replace('class="image-caption">', "").replace("", "") + for description in findall(r'class="image-caption">.+', html_text) + ] # 获取图片描述文本 for index in range(len(image_descriptions)): - html_text = html_text.replace(image_descriptions[index], "

&&" + image_descriptions_text[index] + "&&

") # 将图片描述替换成带有标记符的文本 + html_text = html_text.replace( + image_descriptions[index], + "

&&" + image_descriptions_text[index] + "&&

", + ) # 将图片描述替换成带有标记符的文本 images = findall(r'', html_text) # 获取图片块 for image in images: html_text = html_text.replace(image, f"

{image}

") # 处理图片块 markdown = html2md(html_text) # 将 HTML 格式的文章转换成 Markdown 格式 - md_images_and_description = findall(r'!\[.*\]\(.+\)\n\n&&.+&&', markdown) # 获取 Markdown 中图片语法和对应描述的部分 - md_images_url = [findall(r'https://.+\)', item)[0].replace(")", "") for item in md_images_and_description] # 获取所有图片链接 - md_image_descriptions = [findall(r'&&.+&&', item)[0].replace("&&", "") for item in md_images_and_description] # 获取所有图片描述 + md_images_and_description = findall( + r"!\[.*\]\(.+\)\n\n&&.+&&", markdown + ) # 获取 Markdown 中图片语法和对应描述的部分 + md_images_url = [ + findall(r"https://.+\)", item)[0].replace(")", "") + for item in md_images_and_description + ] # 获取所有图片链接 + md_image_descriptions = [ + findall(r"&&.+&&", item)[0].replace("&&", "") + for item in md_images_and_description + ] # 获取所有图片描述 for index, item in enumerate(md_images_and_description): - markdown = markdown.replace(item, f"![{md_image_descriptions[index]}]({md_images_url[index]})") # 拼接 Markdown 语法并进行替换 + markdown = markdown.replace( + item, f"![{md_image_descriptions[index]}]({md_images_url[index]})" + ) # 拼接 Markdown 语法并进行替换 return markdown -def GetArticleCommentsData(article_id: int, page: int = 1, count: int = 10, - author_only: bool = False, sorting_method: str = "positive") -> List[Dict]: +def GetArticleCommentsData( + article_id: int, + page: int = 1, + count: int = 10, + author_only: bool = False, + sorting_method: Literal["positive", "reverse"] = "positive", +) -> List[Dict]: """获取文章评论信息 Args: @@ -396,16 +422,18 @@ def GetArticleCommentsData(article_id: int, page: int = 1, count: int = 10, page (int, optional): 页码. Defaults to 1. count (int, optional): 每次获取的评论数(不包含子评论). Defaults to 10. author_only (bool, optional): 为 True 时只获取作者发布的评论,包含作者发布的子评论及其父评论. Defaults to False. - sorting_method (str, optional): 排序方式,为”positive“时按时间正序排列,为”reverse“时按时间倒序排列. Defaults to "positive". + sorting_method (Literal["positive", "reverse"], optional): 排序方式,为”positive“时按时间正序排列,为”reverse“时按时间倒序排列. Defaults to "positive". Returns: List[Dict]: 文章评论信息 """ order_by = { - "positive": "asc", # 正序 - "reverse": "desc" # 倒序 + "positive": "asc", + "reverse": "desc", }[sorting_method] - json_obj = GetArticleCommentsJsonDataApi(article_id, page, count, author_only, order_by) + json_obj = GetArticleCommentsJsonDataApi( + article_id, page, count, author_only, order_by + ) result = [] for item in json_obj["comments"]: item_data = { @@ -420,8 +448,8 @@ def GetArticleCommentsData(article_id: int, page: int = 1, count: int = 10, "uid": item["user"]["id"], "name": item["user"]["nickname"], "uslug": item["user"]["slug"], - "avatar_url": item["user"]["avatar"] - } + "avatar_url": item["user"]["avatar"], + }, } try: item["user"]["member"] @@ -434,9 +462,11 @@ def GetArticleCommentsData(article_id: int, page: int = 1, count: int = 10, "gold": "黄金", "platina": "白金", "ordinary": "普通(旧会员)", - "distinguished": "至尊(旧会员)" + "distinguished": "至尊(旧会员)", }[item["user"]["member"]["type"]] - item_data["user"]["vip_expire_date"] = datetime.fromtimestamp(item["user"]["member"]["expires_at"]) + item_data["user"]["vip_expire_date"] = datetime.fromtimestamp( + item["user"]["member"]["expires_at"] + ) try: item["children"] @@ -455,8 +485,8 @@ def GetArticleCommentsData(article_id: int, page: int = 1, count: int = 10, "uid": sub_comment["user"]["id"], "name": sub_comment["user"]["nickname"], "uslug": sub_comment["user"]["slug"], - "avatar_url": sub_comment["user"]["avatar"] - } + "avatar_url": sub_comment["user"]["avatar"], + }, } try: @@ -470,9 +500,13 @@ def GetArticleCommentsData(article_id: int, page: int = 1, count: int = 10, "gold": "黄金", "platina": "白金", "ordinary": "普通(旧会员)", - "distinguished": "至尊(旧会员)" + "distinguished": "至尊(旧会员)", }[sub_comment["user"]["member"]["type"]] - sub_comment_data["user"]["vip_expire_date"] = datetime.fromtimestamp(sub_comment["user"]["member"]["expires_at"]) + sub_comment_data["user"][ + "vip_expire_date" + ] = datetime.fromtimestamp( + sub_comment["user"]["member"]["expires_at"] + ) item_data["sub_comments"].append(sub_comment_data) @@ -498,12 +532,18 @@ def GetArticleAllBasicData(article_url: str, disable_check: bool = False) -> Dic html_json_obj = GetArticleHtmlJsonDataApi(article_url) result["title"] = json_obj["public_title"] - result["author_name"] = html_json_obj["props"]["initialState"]["note"]["data"]["user"]["nickname"] - result["reads_count"] = html_json_obj["props"]["initialState"]["note"]["data"]["views_count"] + result["author_name"] = html_json_obj["props"]["initialState"]["note"]["data"][ + "user" + ]["nickname"] + result["reads_count"] = html_json_obj["props"]["initialState"]["note"]["data"][ + "views_count" + ] result["likes_count"] = json_obj["likes_count"] result["comments_count"] = json_obj["public_comment_count"] result["most_valuable_comments_count"] = json_obj["featured_comments_count"] - result["wordage"] = html_json_obj["props"]["initialState"]["note"]["data"]["wordage"] + result["wordage"] = html_json_obj["props"]["initialState"]["note"]["data"][ + "wordage" + ] result["FP_count"] = json_obj["total_fp_amount"] / 1000 result["description"] = json_obj["description"] result["publish_time"] = datetime.fromisoformat(json_obj["first_shared_at"]) @@ -514,22 +554,27 @@ def GetArticleAllBasicData(article_url: str, disable_check: bool = False) -> Dic "pbook_free": False, "paid": True, "fbook_paid": True, - "pbook_paid": True + "pbook_paid": True, }[json_obj["paid_type"]] result["reprint_status"] = json_obj["reprintable"] result["comment_status"] = json_obj["commentable"] return result -def GetArticleAllCommentsData(article_id: int, count: int = 10, author_only: bool = False, - sorting_method: str = "positive", max_count: int = None) -> Generator[Dict, None, None]: +def GetArticleAllCommentsData( + article_id: int, + count: int = 10, + author_only: bool = False, + sorting_method: Literal["positive", "reverse"] = "positive", + max_count: Optional[int] = None, +) -> Generator[Dict, None, None]: """获取文章的全部评论信息 Args: article_id (int): 文章 ID count (int, optional): 单次获取的数据数量,会影响性能. Defaults to 10. author_only (bool, optional): 为 True 时只获取作者发布的评论,包含作者发布的子评论及其父评论. Defaults to False. - sorting_method (str, optional): 排序方式,为”positive“时按时间正序排列,为”reverse“时按时间倒序排列. Defaults to "positive". + sorting_method (Literal["positive", "reverse"], optional): 排序方式,为”positive“时按时间正序排列,为”reverse“时按时间倒序排列. Defaults to "positive". max_count (int, optional): 获取的文章评论信息数量上限,Defaults to None. Yields: @@ -538,7 +583,9 @@ def GetArticleAllCommentsData(article_id: int, count: int = 10, author_only: boo page = 1 now_count = 0 while True: - result = GetArticleCommentsData(article_id, page, count, author_only, sorting_method) + result = GetArticleCommentsData( + article_id, page, count, author_only, sorting_method + ) if result: page += 1 else: diff --git a/JianshuResearchTools/assert_funcs.py b/JianshuResearchTools/assert_funcs.py index fa71a26..23710b5 100644 --- a/JianshuResearchTools/assert_funcs.py +++ b/JianshuResearchTools/assert_funcs.py @@ -2,20 +2,34 @@ from re import compile as re_compile from typing import Any -from .basic_apis import (GetArticleJsonDataApi, GetCollectionJsonDataApi, - GetIslandJsonDataApi, GetNotebookJsonDataApi, - GetUserJsonDataApi) +from .basic_apis import ( + GetArticleJsonDataApi, + GetCollectionJsonDataApi, + GetIslandJsonDataApi, + GetNotebookJsonDataApi, + GetUserJsonDataApi, +) from .exceptions import InputError, ResourceError __all__ = [ - "JIANSHU_URL_REGEX", "JIANSHU_USER_URL_REGEX", - "JIANSHU_ARTICLES_URL_REGEX", "JIANSHU_NOTEBOOK_URL_REGEX", - "JIANSHU_COLLECTION_URL_REGEX", "JIANSHU_ISLAND_URL_REGEX", - "JIANSHU_ISLAND_POST_URL_REGEX", "AssertType", "AssertJianshuUrl", - "AssertUserUrl", "AssertUserStatusNormal", "AssertArticleUrl", - "AssertArticleStatusNormal", "AssertCollectionUrl", - "AssertCollectionStatusNormal", "AssertIslandUrl", - "AssertIslandStatusNormal", "AssertIslandPostUrl" + "JIANSHU_URL_REGEX", + "JIANSHU_USER_URL_REGEX", + "JIANSHU_ARTICLES_URL_REGEX", + "JIANSHU_NOTEBOOK_URL_REGEX", + "JIANSHU_COLLECTION_URL_REGEX", + "JIANSHU_ISLAND_URL_REGEX", + "JIANSHU_ISLAND_POST_URL_REGEX", + "AssertType", + "AssertJianshuUrl", + "AssertUserUrl", + "AssertUserStatusNormal", + "AssertArticleUrl", + "AssertArticleStatusNormal", + "AssertCollectionUrl", + "AssertCollectionStatusNormal", + "AssertIslandUrl", + "AssertIslandStatusNormal", + "AssertIslandPostUrl", ] @@ -82,7 +96,7 @@ def AssertUserStatusNormal(user_url: str) -> None: try: user_json_data["nickname"] except KeyError: - raise ResourceError(f"用户 {user_url} 账号状态异常") + raise ResourceError(f"用户 {user_url} 账号状态异常") from None def AssertArticleUrl(string: str) -> None: @@ -113,7 +127,7 @@ def AssertArticleStatusNormal(article_url: str) -> None: try: json_obj["show_ad"] except KeyError: - raise ResourceError(f"文章 {article_url} 状态异常") + raise ResourceError(f"文章 {article_url} 状态异常") from None def AssertNotebookUrl(string: str) -> None: @@ -143,7 +157,7 @@ def AssertNotebookStatusNormal(notebook_url: str) -> None: try: json_obj["name"] except KeyError: - raise ResourceError(f"文集 {notebook_url} 状态异常") + raise ResourceError(f"文集 {notebook_url} 状态异常") from None def AssertCollectionUrl(string: str) -> None: @@ -173,7 +187,7 @@ def AssertCollectionStatusNormal(collection_url: str) -> None: try: collection_json_data["title"] except KeyError: - raise ResourceError(f"专题 {collection_url} 状态异常") + raise ResourceError(f"专题 {collection_url} 状态异常") from None def AssertIslandUrl(string: str) -> None: @@ -195,7 +209,7 @@ def AssertIslandStatusNormal(island_url: str) -> None: try: island_json_data["name"] except KeyError: - raise ResourceError(f"小岛 {island_url} 状态异常") + raise ResourceError(f"小岛 {island_url} 状态异常") from None def AssertIslandPostUrl(string: str) -> None: diff --git a/JianshuResearchTools/basic_apis.py b/JianshuResearchTools/basic_apis.py index f952f99..18ba6c3 100644 --- a/JianshuResearchTools/basic_apis.py +++ b/JianshuResearchTools/basic_apis.py @@ -1,12 +1,13 @@ -from typing import Dict, List, Union, Optional +from typing import Dict, Optional -from httpx import get as httpx_get -from httpx import post as httpx_post from lxml import etree from lxml.etree import _Element -from .headers import (BeikeIsland_request_header, PC_header, - api_request_header, mobile_header) +from .httpx_client import ( + JIANSHU_API_CLIENT, + JIANSHU_MOBILE_CLIENT, + JIANSHU_PC_CLIENT, +) try: from ujson import loads as json_loads @@ -14,268 +15,271 @@ from json import loads as json_loads __all__ = [ - "GetArticleJsonDataApi", "GetArticleHtmlJsonDataApi", - "GetArticleCommentsJsonDataApi", "GetBeikeIslandTradeRankListJsonDataApi", - "GetBeikeIslandTradeListJsonDataApi", "GetCollectionJsonDataApi", + "GetArticleJsonDataApi", + "GetArticleHtmlJsonDataApi", + "GetArticleCommentsJsonDataApi", + "GetCollectionJsonDataApi", "GetCollectionEditorsJsonDataApi", "GetCollectionRecommendedWritersJsonDataApi", - "GetCollectionSubscribersJsonDataApi", "GetCollectionArticlesJsonDataApi", - "GetIslandJsonDataApi", "GetIslandPostsJsonDataApi", - "GetNotebookJsonDataApi", "GetDailyArticleRankListJsonDataApi", - "GetArticlesFPRankListJsonDataApi", "GetUserJsonDataApi", - "GetUserPCHtmlDataApi", "GetUserCollectionsAndNotebooksJsonDataApi", - "GetUserArticlesListJsonDataApi", "GetUserFollowingListHtmlDataApi", - "GetUserFollowersListHtmlDataApi", "GetUserNextAnniversaryDayHtmlDataApi", - "GetIslandPostJsonDataApi", "GetUserTimelineHtmlDataApi" + "GetCollectionSubscribersJsonDataApi", + "GetCollectionArticlesJsonDataApi", + "GetIslandJsonDataApi", + "GetIslandPostsJsonDataApi", + "GetNotebookJsonDataApi", + "GetDailyArticleRankListJsonDataApi", + "GetArticlesFPRankListJsonDataApi", + "GetUserJsonDataApi", + "GetUserPCHtmlDataApi", + "GetUserCollectionsAndNotebooksJsonDataApi", + "GetUserArticlesListJsonDataApi", + "GetUserFollowingListHtmlDataApi", + "GetUserFollowersListHtmlDataApi", + "GetUserNextAnniversaryDayHtmlDataApi", + "GetIslandPostJsonDataApi", + "GetUserTimelineHtmlDataApi", ] def GetArticleJsonDataApi(article_url: str) -> Dict: - request_url = article_url.replace("https://www.jianshu.com/", - "https://www.jianshu.com/asimov/") - source = httpx_get(request_url, headers=api_request_header).content - json_obj = json_loads(source) - return json_obj + request_url = article_url.replace("https://www.jianshu.com", "/asimov") + source = JIANSHU_API_CLIENT.get(request_url).content + return json_loads(source) -def GetArticleHtmlJsonDataApi(article_url: str) -> _Element: - source = httpx_get(article_url, headers=PC_header).content - html_obj = etree.HTML(source) - json_obj = json_loads(html_obj.xpath("//script[@id='__NEXT_DATA__']/text()")[0]) - return json_obj +def GetArticleHtmlJsonDataApi(article_url: str) -> Dict: + request_url = article_url.replace("https://www.jianshu.com", "") + source = JIANSHU_PC_CLIENT.get(request_url).content + html_obj = etree.HTML(source) # type: ignore + return json_loads(html_obj.xpath("//script[@id='__NEXT_DATA__']/text()")[0]) -def GetArticleCommentsJsonDataApi(article_id: int, page: int, count: int, - author_only: bool, order_by: str) -> Dict: +def GetArticleCommentsJsonDataApi( + article_id: int, page: int, count: int, author_only: bool, order_by: str +) -> Dict: params = { "page": page, "count": count, "author_only": author_only, - "order_by": order_by - } - request_url = f"https://www.jianshu.com/shakespeare/notes/{article_id}/comments" - source = httpx_get(request_url, params=params, headers=api_request_header).content - json_obj = json_loads(source) - return json_obj - - -def GetBeikeIslandTradeRankListJsonDataApi(ranktype: Union[int, None], pageIndex: Union[int, None]) -> Dict: - params = { - "ranktype": ranktype, - "pageIndex": pageIndex - } - source = httpx_post("https://www.beikeisland.com/api/Trade/getTradeRankList", - headers=BeikeIsland_request_header, json=params).content - json_obj = json_loads(source) - return json_obj - - -def GetBeikeIslandTradeListJsonDataApi(pageIndex: int, retype: int): - params = { - "pageIndex": pageIndex, - "retype": retype + "order_by": order_by, } - source = httpx_post("https://www.beikeisland.com/api/Trade/getTradeList", - headers=BeikeIsland_request_header, json=params).content - json_obj = json_loads(source) - return json_obj + request_url = f"shakespeare/notes/{article_id}/comments" + source = JIANSHU_API_CLIENT.get(request_url, params=params).content + return json_loads(source) def GetCollectionJsonDataApi(collection_url: str) -> Dict: - request_url = collection_url.replace("https://www.jianshu.com/c/", "https://www.jianshu.com/asimov/collections/slug/") - source = httpx_get(request_url, headers=api_request_header).content - json_obj = json_loads(source) - return json_obj + request_url = collection_url.replace( + "https://www.jianshu.com/c/", "asimov/collections/slug/" + ) + source = JIANSHU_API_CLIENT.get(request_url).content + return json_loads(source) def GetCollectionEditorsJsonDataApi(collection_id: int, page: int) -> Dict: - request_url = f"https://www.jianshu.com/collections/{collection_id}/editors" + request_url = f"collections/{collection_id}/editors" params = { - "page": page + "page": page, } - source = httpx_get(request_url, params=params, headers=api_request_header).content - json_obj = json_loads(source) - return json_obj + source = JIANSHU_API_CLIENT.get(request_url, params=params).content + return json_loads(source) -def GetCollectionRecommendedWritersJsonDataApi(collection_id: int, page: int, count: int) -> Dict: +def GetCollectionRecommendedWritersJsonDataApi( + collection_id: int, page: int, count: int +) -> Dict: params = { "collection_id": collection_id, "page": page, - "count": count + "count": count, } - source = httpx_get("https://www.jianshu.com/collections/recommended_users", - params=params, headers=api_request_header).content - json_obj = json_loads(source) - return json_obj + source = JIANSHU_API_CLIENT.get( + "/collections/recommended_users", + params=params, + ).content + return json_loads(source) -def GetCollectionSubscribersJsonDataApi(collection_id: int, max_sort_id: int) -> Dict: - request_url = f"https://www.jianshu.com/collection/{collection_id}/subscribers" +def GetCollectionSubscribersJsonDataApi( + collection_id: int, max_sort_id: Optional[int] +) -> Dict: + request_url = f"/collection/{collection_id}/subscribers" params = { - "max_sort_id": max_sort_id + "max_sort_id": max_sort_id, } - source = httpx_get(request_url, params=params, headers=api_request_header).content - json_obj = json_loads(source) - return json_obj + source = JIANSHU_API_CLIENT.get(request_url, params=params).content + return json_loads(source) -def GetCollectionArticlesJsonDataApi(collection_slug: str, page: int, count: int, order_by: str) -> Dict: - request_url = f"https://www.jianshu.com/asimov/collections/slug/{collection_slug}/public_notes" +def GetCollectionArticlesJsonDataApi( + collection_slug: str, page: int, count: int, order_by: str +) -> Dict: + request_url = f"/asimov/collections/slug/{collection_slug}/public_notes" params = { "page": page, "count": count, - "order_by": order_by + "order_by": order_by, } - source = httpx_get(request_url, params=params, headers=api_request_header).content - json_obj = json_loads(source) - return json_obj + source = JIANSHU_API_CLIENT.get(request_url, params=params).content + return json_loads(source) def GetIslandJsonDataApi(island_url: str) -> Dict: - request_url = island_url.replace("https://www.jianshu.com/g/", "https://www.jianshu.com/asimov/groups/") - source = httpx_get(request_url, headers=api_request_header).content - json_obj = json_loads(source) - return json_obj - - -def GetIslandPostsJsonDataApi(group_slug: str, max_id: int, - count: int, topic_id: int, order_by: str): + request_url = island_url.replace("https://www.jianshu.com/g/", "/asimov/groups/") + source = JIANSHU_API_CLIENT.get(request_url).content + return json_loads(source) + + +def GetIslandPostsJsonDataApi( + group_slug: str, + max_id: Optional[int], + count: int, + topic_id: Optional[int], + order_by: str, +) -> Dict: params = { "group_slug": group_slug, "order_by": order_by, "max_id": max_id, "count": count, - "topic_id": topic_id + "topic_id": topic_id, } - source = httpx_get("https://www.jianshu.com/asimov/posts", - params=params, headers=api_request_header).content - json_obj = json_loads(source) - return json_obj + source = JIANSHU_API_CLIENT.get( + "/asimov/posts", + params=params, + ).content + return json_loads(source) def GetNotebookJsonDataApi(notebook_url: str) -> Dict: - request_url = notebook_url.replace("https://www.jianshu.com/", "https://www.jianshu.com/asimov/") - source = httpx_get(request_url, headers=api_request_header).content - json_obj = json_loads(source) - return json_obj - - -def GetNotebookArticlesJsonDataApi(notebook_url: str, page: int, - count: int, order_by: str) -> Dict: - request_url = notebook_url.replace("https://www.jianshu.com/nb/", - "https://www.jianshu.com/asimov/notebooks/") + "/public_notes/" + request_url = notebook_url.replace("https://www.jianshu.com/", "/asimov/") + source = JIANSHU_API_CLIENT.get(request_url).content + return json_loads(source) + + +def GetNotebookArticlesJsonDataApi( + notebook_url: str, page: int, count: int, order_by: str +) -> Dict: + request_url = ( + notebook_url.replace("https://www.jianshu.com/nb/", "/asimov/notebooks/") + + "/public_notes/" + ) params = { "page": page, "count": count, - "order_by": order_by + "order_by": order_by, } - source = httpx_get(request_url, params=params, headers=api_request_header).content - json_obj = json_loads(source) - return json_obj + source = JIANSHU_API_CLIENT.get(request_url, params=params).content + return json_loads(source) def GetAssetsRankJsonDataApi(max_id: int, since_id: int) -> Dict: params = { "max_id": max_id, - "since_id": since_id + "since_id": since_id, } - source = httpx_get("https://www.jianshu.com/asimov/fp_rankings", params=params, headers=api_request_header).content - json_obj = json_loads(source) - return json_obj + source = JIANSHU_API_CLIENT.get( + "/asimov/fp_rankings", + params=params, + ).content + return json_loads(source) def GetDailyArticleRankListJsonDataApi() -> Dict: - source = httpx_get("https://www.jianshu.com/asimov/daily_activity_participants/rank", headers=api_request_header).content - json_obj = json_loads(source) - return json_obj + source = JIANSHU_API_CLIENT.get( + "/asimov/daily_activity_participants/rank", + ).content + return json_loads(source) -def GetArticlesFPRankListJsonDataApi(date: str, type_: Optional[str]) -> Dict: # 避免覆盖内置函数 +def GetArticlesFPRankListJsonDataApi(date: str, type_: Optional[str]) -> Dict: params = { "date": date, - "type": type_ + "type": type_, } - source = httpx_get("https://www.jianshu.com/asimov/fp_rankings/voter_notes", params=params, headers=api_request_header).content - json_obj = json_loads(source) - return json_obj + source = JIANSHU_API_CLIENT.get( + "/asimov/fp_rankings/voter_notes", + params=params, + ).content + return json_loads(source) def GetUserJsonDataApi(user_url: str) -> Dict: - request_url = user_url.replace("https://www.jianshu.com/u/", "https://www.jianshu.com/asimov/users/slug/") - source = httpx_get(request_url, headers=api_request_header).content - json_obj = json_loads(source) - return json_obj + request_url = user_url.replace("https://www.jianshu.com/u/", "/asimov/users/slug/") + source = JIANSHU_API_CLIENT.get(request_url).content + return json_loads(source) def GetUserPCHtmlDataApi(user_url: str) -> _Element: - source = httpx_get(user_url, headers=PC_header).content - html_obj = etree.HTML(source) - return html_obj + source = JIANSHU_PC_CLIENT.get(user_url).content + return etree.HTML(source) # type: ignore def GetUserCollectionsAndNotebooksJsonDataApi(user_url: str, user_slug: str) -> Dict: - request_url = user_url.replace("/u/", "/users/") + "/collections_and_notebooks" + request_url = ( + user_url.replace("https://www.jianshu.com/u/", "/users/") + + "/collections_and_notebooks" + ) params = { - "slug": user_slug + "slug": user_slug, } - source = httpx_get(request_url, headers=api_request_header, params=params).content - json_obj = json_loads(source) - return json_obj + source = JIANSHU_API_CLIENT.get(request_url, params=params).content + return json_loads(source) -def GetUserArticlesListJsonDataApi(user_url: str, page: int, - count: int, order_by: str) -> Dict: - request_url = user_url.replace("/u/", "/asimov/users/slug/") + "/public_notes" +def GetUserArticlesListJsonDataApi( + user_url: str, page: int, count: int, order_by: str +) -> Dict: + request_url = ( + user_url.replace("https://www.jianshu.com/u/", "/asimov/users/slug/") + + "/public_notes" + ) params = { "page": page, "count": count, - "order_by": order_by + "order_by": order_by, } - source = httpx_get(request_url, headers=api_request_header, params=params).content - json_obj = json_loads(source) - return json_obj + source = JIANSHU_API_CLIENT.get(request_url, params=params).content + return json_loads(source) def GetUserFollowingListHtmlDataApi(user_url: str, page: int) -> _Element: - request_url = user_url.replace("/u/", "/users/") + "/following" + request_url = ( + user_url.replace("https://www.jianshu.com/u/", "/users/") + "/following" + ) params = { - "page": page + "page": page, } - source = httpx_get(request_url, headers=PC_header, params=params).content - html_obj = etree.HTML(source) - return html_obj + source = JIANSHU_PC_CLIENT.get(request_url, params=params).content + return etree.HTML(source) # type: ignore def GetUserFollowersListHtmlDataApi(user_url: str, page: int) -> _Element: - request_url = user_url.replace("/u/", "/users/") + "/followers" + request_url = ( + user_url.replace("https://www.jianshu.com/u/", "/users/") + "/followers" + ) params = { - "page": page + "page": page, } - source = httpx_get(request_url, headers=PC_header, params=params).content - html_obj = etree.HTML(source) - return html_obj + source = JIANSHU_PC_CLIENT.get(request_url, params=params).content + return etree.HTML(source) # type: ignore def GetUserNextAnniversaryDayHtmlDataApi(user_slug: str) -> _Element: - request_url = f"https://www.jianshu.com/mobile/u/{user_slug}/anniversary" - source = httpx_get(request_url, headers=mobile_header).content - html_obj = etree.HTML(source) - return html_obj + request_url = f"/mobile/u/{user_slug}/anniversary" + source = JIANSHU_MOBILE_CLIENT.get(request_url).content + return etree.HTML(source) # type: ignore -def GetIslandPostJsonDataApi(post_slug: str) -> List[Dict]: - request_url = f"https://www.jianshu.com/asimov/posts/{post_slug}" - source = httpx_get(request_url, headers=api_request_header).content - json_obj = json_loads(source) - return json_obj +def GetIslandPostJsonDataApi(post_slug: str) -> Dict: + request_url = f"/asimov/posts/{post_slug}" + source = JIANSHU_API_CLIENT.get(request_url).content + return json_loads(source) -def GetUserTimelineHtmlDataApi(uslug: str, max_id: int) -> _Element: - request_url = f"https://www.jianshu.com/users/{uslug}/timeline" +def GetUserTimelineHtmlDataApi(uslug: str, max_id: Optional[int]) -> _Element: + request_url = f"/users/{uslug}/timeline" params = { - "max_id": max_id + "max_id": max_id, } - source = httpx_get(request_url, headers=PC_header, params=params).content - html_obj = etree.HTML(source) - return html_obj + source = JIANSHU_PC_CLIENT.get(request_url, params=params).content + return etree.HTML(source) # type: ignore diff --git a/JianshuResearchTools/beikeisland.py b/JianshuResearchTools/beikeisland.py deleted file mode 100644 index 25931e3..0000000 --- a/JianshuResearchTools/beikeisland.py +++ /dev/null @@ -1,194 +0,0 @@ -from datetime import datetime -from typing import Dict, List - -from .basic_apis import (GetBeikeIslandTradeListJsonDataApi, - GetBeikeIslandTradeRankListJsonDataApi) -from .convert import UserUrlToUserSlug -from .exceptions import ResourceError - -__all__ = [ - "GetBeikeIslandTotalTradeAmount", "GetBeikeIslandTotalTradeCount", - "GetBeikeIslandTotalTradeRankData", "GetBeikeIslandBuyTradeRankData", - "GetBeikeIslandSellTradeRankData", "GetBeikeIslandTradeOrderInfo", - "GetBeikeIslandTradePrice" -] - - -def GetBeikeIslandTotalTradeAmount() -> int: - """获取贝壳小岛总交易量 - - Returns: - int: 总交易量 - """ - json_obj = GetBeikeIslandTradeRankListJsonDataApi(ranktype=None, pageIndex=None) - result = json_obj["data"]["totalcount"] - return result - - -def GetBeikeIslandTotalTradeCount() -> int: - """获取贝壳小岛总交易笔数 - - Returns: - int: 总交易笔数 - """ - json_obj = GetBeikeIslandTradeRankListJsonDataApi(ranktype=None, pageIndex=None) - result = json_obj["data"]["totaltime"] - return result - - -def GetBeikeIslandTotalTradeRankData(page: int = 1) -> List[Dict]: - """获取贝壳小岛总交易排行榜中的用户信息 - - Args: - page (int, optional): 页码. Defaults to 1. - - Returns: - List: 总交易排行榜的用户信息 - """ - json_obj = GetBeikeIslandTradeRankListJsonDataApi(ranktype=3, pageIndex=page) - result = [] - for item in json_obj["data"]["ranklist"]: - item_data = { - "bkuid": item["userid"], - "jianshuname": item["jianshuname"], - "avatar_url": item["avatarurl"], - "userurl": item["jianshupath"], - "uslug": UserUrlToUserSlug(item["jianshupath"]), - "total_trade_amount": item["totalamount"], - "total_trade_times": item["totaltime"] - } - result.append(item_data) - return result - - -def GetBeikeIslandBuyTradeRankData(page: int = 1) -> List[Dict]: - """获取贝壳小岛买贝排行榜中的用户信息 - - Args: - page (int, optional): 页码. Defaults to 1. - - Returns: - List: 买贝榜的用户信息 - """ - json_obj = GetBeikeIslandTradeRankListJsonDataApi(ranktype=1, pageIndex=page) - result = [] - for item in json_obj["data"]["ranklist"]: - item_data = { - "bkuid": item["userid"], - "jianshuname": item["jianshuname"], - "avatar_url": item["avatarurl"], - "userurl": item["jianshupath"], - "uslug": UserUrlToUserSlug(item["jianshupath"]), - "total_trade_amount": item["totalamount"], - "total_trade_times": item["totaltime"] - } - result.append(item_data) - return result - - -def GetBeikeIslandSellTradeRankData(page: int = 1) -> List[Dict]: - """获取贝壳小岛卖贝排行榜中的用户信息 - - Args: - page (int, optional): 页码. Defaults to 1. - - Returns: - List: 卖贝榜的用户信息 - """ - json_obj = GetBeikeIslandTradeRankListJsonDataApi(ranktype=2, pageIndex=page) - result = [] - for item in json_obj["data"]["ranklist"]: - item_data = { - "bkuid": item["userid"], - "jianshuname": item["jianshuname"], - "avatar_url": item["avatarurl"], - "userurl": item["jianshupath"], - "uslug": UserUrlToUserSlug(item["jianshupath"]), - "total_trade_amount": item["totalamount"], - "total_trade_times": item["totaltime"] - } - result.append(item_data) - return result - - -def GetBeikeIslandTradeOrderInfo(trade_type: str, page: int = 1) -> List[Dict]: - """获取贝壳小岛的挂单信息 - - Args: - trade_type (str): 为 "buy" 时获取买单信息,为 "sell" 时获取卖单信息 - page (int, optional): 页码. Defaults to 1. - - Returns: - List: 挂单数据 - """ - # 通过 trade_type 构建 retype - retype = { - "buy": 2, - "sell": 1 - }[trade_type] - json_obj = GetBeikeIslandTradeListJsonDataApi(pageIndex=page, - retype=retype) - result = [] - for item in json_obj["data"]["tradelist"]: - item_data = { - "trade_id": item["id"], - "trade_slug": item["tradeno"], - "publish_time": datetime.fromisoformat(item["releasetime"]), - "status": { - "code": item["statuscode"], - "text": item["statustext"] - }, - "trade": { - "total": item["recount"], - "traded": item["recount"] - item["cantradenum"], - "remaining": item["cantradenum"], - "minimum_trade_limit": item["minlimit"], - "traded_percentage": round( - float(item["compeletper"]) / 100, 3 - ), - "price": item["reprice"], - } - } - - if item["anonymity"]: - item_data["user"] = { - "is_anonymity": True - } - else: - item_data["user"] = { - "is_anonymity": False, - "name": item["reusername"], - "avatar_url": item["avatarurl"], - "level": { - "code": item["levelnum"], - "text": item["userlevel"] - } - } - - result.append(item_data) - return result - - -def GetBeikeIslandTradePrice(trade_type: str, rank: int = 1) -> float: - """获取特定位置交易单的价格 - - Args: - trade_type (str): trade_type (str): 为 "buy" 时获取买单信息,为 "sell" 时获取卖单信息 - rank (int, optional): 自最低 / 最高价开始,需要获取的价格所在的位置. Defaults to 1. - - Returns: - float: 交易单的价格 - """ - pageIndex = int(rank / 10) # 确定需要请求的页码 - # 通过 trade_type 构建 retype - retype = { - "buy": 2, - "sell": 1 - }[trade_type] - json_obj = GetBeikeIslandTradeListJsonDataApi(pageIndex=pageIndex, retype=retype) - rank_in_this_page = rank % 10 - 1 # 本页信息中目标交易单的位置,考虑索引下标起始值为 0 问题 - try: - result = json_obj["data"]["tradelist"][rank_in_this_page]["reprice"] - except IndexError: - raise ResourceError("该排名没有对应的交易单") - return result diff --git a/JianshuResearchTools/collection.py b/JianshuResearchTools/collection.py index d26f647..8e1fcbc 100644 --- a/JianshuResearchTools/collection.py +++ b/JianshuResearchTools/collection.py @@ -1,24 +1,34 @@ from datetime import datetime -from typing import Dict, Generator, List +from typing import Dict, Generator, List, Literal, Optional from .assert_funcs import AssertCollectionStatusNormal, AssertCollectionUrl -from .basic_apis import (GetCollectionArticlesJsonDataApi, - GetCollectionEditorsJsonDataApi, - GetCollectionJsonDataApi, - GetCollectionRecommendedWritersJsonDataApi, - GetCollectionSubscribersJsonDataApi) +from .basic_apis import ( + GetCollectionArticlesJsonDataApi, + GetCollectionEditorsJsonDataApi, + GetCollectionJsonDataApi, + GetCollectionRecommendedWritersJsonDataApi, + GetCollectionSubscribersJsonDataApi, +) from .convert import CollectionUrlToCollectionSlug __all__ = [ - "GetCollectionName", "GetCollectionAvatarUrl", - "GetCollectionIntroductionText", "GetCollectionIntroductionHtml", - "GetCollectionArticlesCount", "GetCollectionSubscribersCount", - "GetCollectionArticlesUpdateTime", "GetCollectionInformationUpdateTime", - "GetCollectionOwnerInfo", "GetCollectionEditorsInfo", - "GetCollectionRecommendedWritersInfo", "GetCollectionSubscribersInfo", - "GetCollectionAllBasicData", "GetCollectionAllEditorsInfo", + "GetCollectionName", + "GetCollectionAvatarUrl", + "GetCollectionIntroductionText", + "GetCollectionIntroductionHtml", + "GetCollectionArticlesCount", + "GetCollectionSubscribersCount", + "GetCollectionArticlesUpdateTime", + "GetCollectionInformationUpdateTime", + "GetCollectionOwnerInfo", + "GetCollectionEditorsInfo", + "GetCollectionRecommendedWritersInfo", + "GetCollectionSubscribersInfo", + "GetCollectionAllBasicData", + "GetCollectionAllEditorsInfo", "GetCollectionAllRecommendedWritersInfo", - "GetCollectionAllSubscribersInfo", "GetCollectionAllArticlesInfo" + "GetCollectionAllSubscribersInfo", + "GetCollectionAllArticlesInfo", ] @@ -36,8 +46,7 @@ def GetCollectionName(collection_url: str, disable_check: bool = False) -> str: AssertCollectionUrl(collection_url) AssertCollectionStatusNormal(collection_url) json_obj = GetCollectionJsonDataApi(collection_url) - result = json_obj["title"] - return result + return json_obj["title"] def GetCollectionAvatarUrl(collection_url: str, disable_check: bool = False) -> str: @@ -54,11 +63,12 @@ def GetCollectionAvatarUrl(collection_url: str, disable_check: bool = False) -> AssertCollectionUrl(collection_url) AssertCollectionStatusNormal(collection_url) json_obj = GetCollectionJsonDataApi(collection_url) - result = json_obj["image"] - return result + return json_obj["image"] -def GetCollectionIntroductionText(collection_url: str, disable_check: bool = False) -> str: +def GetCollectionIntroductionText( + collection_url: str, disable_check: bool = False +) -> str: """获取纯文本格式的专题简介 Args: @@ -72,11 +82,12 @@ def GetCollectionIntroductionText(collection_url: str, disable_check: bool = Fal AssertCollectionUrl(collection_url) AssertCollectionStatusNormal(collection_url) json_obj = GetCollectionJsonDataApi(collection_url) - result = json_obj["content_without_html"] - return result + return json_obj["content_without_html"] -def GetCollectionIntroductionHtml(collection_url: str, disable_check: bool = False) -> str: +def GetCollectionIntroductionHtml( + collection_url: str, disable_check: bool = False +) -> str: """获取 Html 格式的专题简介 Args: @@ -90,8 +101,7 @@ def GetCollectionIntroductionHtml(collection_url: str, disable_check: bool = Fal AssertCollectionUrl(collection_url) AssertCollectionStatusNormal(collection_url) json_obj = GetCollectionJsonDataApi(collection_url) - result = json_obj["content_in_full"] - return result + return json_obj["content_in_full"] def GetCollectionArticlesCount(collection_url: str, disable_check: bool = False) -> int: @@ -108,11 +118,12 @@ def GetCollectionArticlesCount(collection_url: str, disable_check: bool = False) AssertCollectionUrl(collection_url) AssertCollectionStatusNormal(collection_url) json_obj = GetCollectionJsonDataApi(collection_url) - result = json_obj["notes_count"] - return result + return json_obj["notes_count"] -def GetCollectionSubscribersCount(collection_url: str, disable_check: bool = False) -> int: +def GetCollectionSubscribersCount( + collection_url: str, disable_check: bool = False +) -> int: """获取专题的订阅者数量 Args: @@ -126,11 +137,12 @@ def GetCollectionSubscribersCount(collection_url: str, disable_check: bool = Fal AssertCollectionUrl(collection_url) AssertCollectionStatusNormal(collection_url) json_obj = GetCollectionJsonDataApi(collection_url) - result = json_obj["subscribers_count"] - return result + return json_obj["subscribers_count"] -def GetCollectionArticlesUpdateTime(collection_url: str, disable_check: bool = False) -> datetime: +def GetCollectionArticlesUpdateTime( + collection_url: str, disable_check: bool = False +) -> datetime: """获取专题文章更新时间 Args: @@ -144,11 +156,12 @@ def GetCollectionArticlesUpdateTime(collection_url: str, disable_check: bool = F AssertCollectionUrl(collection_url) AssertCollectionStatusNormal(collection_url) json_obj = GetCollectionJsonDataApi(collection_url) - result = datetime.fromtimestamp(json_obj["newly_added_at"]) - return result + return datetime.fromtimestamp(json_obj["newly_added_at"]) -def GetCollectionInformationUpdateTime(collection_url: str, disable_check: bool = False) -> datetime: +def GetCollectionInformationUpdateTime( + collection_url: str, disable_check: bool = False +) -> datetime: """获取专题信息更新时间 Args: @@ -162,8 +175,7 @@ def GetCollectionInformationUpdateTime(collection_url: str, disable_check: bool AssertCollectionUrl(collection_url) AssertCollectionStatusNormal(collection_url) json_obj = GetCollectionJsonDataApi(collection_url) - result = datetime.fromtimestamp(json_obj["last_updated_at"]) - return result + return datetime.fromtimestamp(json_obj["last_updated_at"]) def GetCollectionOwnerInfo(collection_url: str, disable_check: bool = False) -> Dict: @@ -180,12 +192,11 @@ def GetCollectionOwnerInfo(collection_url: str, disable_check: bool = False) -> AssertCollectionUrl(collection_url) AssertCollectionStatusNormal(collection_url) json_obj = GetCollectionJsonDataApi(collection_url) - result = { + return { "uid": json_obj["owner"]["id"], "name": json_obj["owner"]["nickname"], - "uslug": json_obj["owner"]["slug"] + "uslug": json_obj["owner"]["slug"], } - return result def GetCollectionEditorsInfo(collection_id: int, page: int = 1) -> List[Dict]: @@ -204,13 +215,15 @@ def GetCollectionEditorsInfo(collection_id: int, page: int = 1) -> List[Dict]: item_data = { "uslug": item["slug"], "name": item["nickname"], - "avatar_url": item["avatar_source"] + "avatar_url": item["avatar_source"], } result.append(item_data) return result -def GetCollectionRecommendedWritersInfo(collection_id: int, page: int = 1, count: int = 20) -> List[Dict]: +def GetCollectionRecommendedWritersInfo( + collection_id: int, page: int = 1, count: int = 20 +) -> List[Dict]: """获取专题推荐作者信息 Args: @@ -221,7 +234,9 @@ def GetCollectionRecommendedWritersInfo(collection_id: int, page: int = 1, count Returns: List[Dict]: 专题推荐作者信息 """ - json_obj = GetCollectionRecommendedWritersJsonDataApi(collection_id, page=page, count=count) + json_obj = GetCollectionRecommendedWritersJsonDataApi( + collection_id, page=page, count=count + ) result = [] for item in json_obj["users"]: item_data = { @@ -231,13 +246,15 @@ def GetCollectionRecommendedWritersInfo(collection_id: int, page: int = 1, count "avatar_url": item["avatar_source"], "collection_name": item["collection_name"], "likes_count": item["total_likes_count"], - "words_count": item["total_wordage"] + "words_count": item["total_wordage"], } result.append(item_data) return result -def GetCollectionSubscribersInfo(collection_id: int, start_sort_id: int = None) -> List[Dict]: +def GetCollectionSubscribersInfo( + collection_id: int, start_sort_id: Optional[int] = None +) -> List[Dict]: """获取专题关注者信息 Args: @@ -247,7 +264,9 @@ def GetCollectionSubscribersInfo(collection_id: int, start_sort_id: int = None) Returns: List[Dict]: 关注者信息 """ - json_obj = GetCollectionSubscribersJsonDataApi(collection_id, max_sort_id=start_sort_id) + json_obj = GetCollectionSubscribersJsonDataApi( + collection_id, max_sort_id=start_sort_id + ) result = [] for item in json_obj: item_data = { @@ -255,22 +274,26 @@ def GetCollectionSubscribersInfo(collection_id: int, start_sort_id: int = None) "name": item["nickname"], "avatar_url": item["avatar_source"], "sort_id": item["like_id"], - "subscribe_time": datetime.fromisoformat(item["subscribed_at"]) + "subscribe_time": datetime.fromisoformat(item["subscribed_at"]), } result.append(item_data) return result -def GetCollectionArticlesInfo(collection_url: str, page: int = 1, - count: int = 10, sorting_method: str = "time", - disable_check: bool = False) -> List[Dict]: +def GetCollectionArticlesInfo( + collection_url: str, + page: int = 1, + count: int = 10, + sorting_method: Literal["time", "comment_time", "hot"] = "time", + disable_check: bool = False, +) -> List[Dict]: """获取专题文章信息 Args: collection_url (str): 专题 URL page (int, optional): 页码. Defaults to 1. count (int, optional): 每次返回的数据数量. Defaults to 10. - sorting_method (str, optional): 排序方法,"time" 为按照发布时间排序, + sorting_method (Literal["time", "comment_time", "hot"], optional): 排序方法,"time" 为按照发布时间排序, "comment_time" 为按照最近评论时间排序,"hot" 为按照热度排序. Defaults to "time". disable_check (bool): 禁用参数有效性检查. Defaults to False. @@ -283,17 +306,23 @@ def GetCollectionArticlesInfo(collection_url: str, page: int = 1, order_by = { "time": "added_at", "comment_time": "commented_at", - "hot": "top" + "hot": "top", }[sorting_method] - json_obj = GetCollectionArticlesJsonDataApi(CollectionUrlToCollectionSlug(collection_url), - page=page, count=count, order_by=order_by) + json_obj = GetCollectionArticlesJsonDataApi( + CollectionUrlToCollectionSlug(collection_url), + page=page, + count=count, + order_by=order_by, + ) result = [] for item in json_obj: item_data = { "aid": item["object"]["data"]["id"], "title": item["object"]["data"]["title"], "aslug": item["object"]["data"]["slug"], - "release_time": datetime.fromisoformat(item["object"]["data"]["first_shared_at"]), + "release_time": datetime.fromisoformat( + item["object"]["data"]["first_shared_at"] + ).replace(tzinfo=None), "first_image_url": item["object"]["data"]["list_image_url"], "summary": item["object"]["data"]["public_abbr"], "views_count": item["object"]["data"]["views_count"], @@ -304,11 +333,11 @@ def GetCollectionArticlesInfo(collection_url: str, page: int = 1, "uid": item["object"]["data"]["user"]["id"], "name": item["object"]["data"]["user"]["nickname"], "uslug": item["object"]["data"]["user"]["slug"], - "avatar_url": item["object"]["data"]["user"]["avatar"] + "avatar_url": item["object"]["data"]["user"]["avatar"], }, "total_fp_amount": item["object"]["data"]["total_fp_amount"] / 1000, "comments_count": item["object"]["data"]["public_comments_count"], - "rewards_count": item["object"]["data"]["total_rewards_count"] + "rewards_count": item["object"]["data"]["total_rewards_count"], } result.append(item_data) return result @@ -337,16 +366,20 @@ def GetCollectionAllBasicData(collection_url: str, disable_check: bool = False) result["articles_count"] = json_obj["notes_count"] result["subscribers_count"] = json_obj["subscribers_count"] result["articles_update_time"] = datetime.fromtimestamp(json_obj["newly_added_at"]) - result["information_update_time"] = datetime.fromtimestamp(json_obj["last_updated_at"]) + result["information_update_time"] = datetime.fromtimestamp( + json_obj["last_updated_at"] + ) result["owner_info"] = { "uid": json_obj["owner"]["id"], "name": json_obj["owner"]["nickname"], - "uslug": json_obj["owner"]["slug"] + "uslug": json_obj["owner"]["slug"], } return result -def GetCollectionAllEditorsInfo(collection_id: int, max_count: int = None) -> Generator[Dict, None, None]: +def GetCollectionAllEditorsInfo( + collection_id: int, max_count: Optional[int] = None +) -> Generator[Dict, None, None]: """获取专题的所有编辑信息 Args: @@ -372,7 +405,9 @@ def GetCollectionAllEditorsInfo(collection_id: int, max_count: int = None) -> Ge return -def GetCollectionAllRecommendedWritersInfo(collection_id: int, count: int = 20, max_count: int = None) -> Generator[Dict, None, None]: +def GetCollectionAllRecommendedWritersInfo( + collection_id: int, count: int = 20, max_count: Optional[int] = None +) -> Generator[Dict, None, None]: """获取专题的所有推荐作者信息 Args: @@ -399,7 +434,9 @@ def GetCollectionAllRecommendedWritersInfo(collection_id: int, count: int = 20, return -def GetCollectionAllSubscribersInfo(collection_id: int, max_count: int = None) -> Generator[Dict, None, None]: +def GetCollectionAllSubscribersInfo( + collection_id: int, max_count: Optional[int] = None +) -> Generator[Dict, None, None]: """获取专题的所有关注者信息 Args: @@ -425,15 +462,19 @@ def GetCollectionAllSubscribersInfo(collection_id: int, max_count: int = None) - return -def GetCollectionAllArticlesInfo(collection_url: str, count: int = 10, - sorting_method: str = "time", max_count: int = None, - disable_check: bool = False) -> Generator[Dict, None, None]: +def GetCollectionAllArticlesInfo( + collection_url: str, + count: int = 10, + sorting_method: Literal["time", "comment_time", "hot"] = "time", + max_count: Optional[int] = None, + disable_check: bool = False, +) -> Generator[Dict, None, None]: """获取专题的所有文章信息 Args: collection_url (str): 专题 URL count (int, optional): 单次获取的数据数量,会影响性能. Defaults to 10. - sorting_method (str, optional): 排序方法,"time" 为按照发布时间排序, + sorting_method (Literal["time", "comment_time", "hot"], optional): 排序方法,"time" 为按照发布时间排序, "comment_time" 为按照最近评论时间排序,"hot" 为按照热度排序. Defaults to "time". max_count (int, optional): 获取的专题文章信息数量上限,Defaults to None. disable_check (bool): 禁用参数有效性检查. Defaults to False. @@ -447,7 +488,9 @@ def GetCollectionAllArticlesInfo(collection_url: str, count: int = 10, page = 1 now_count = 0 while True: - result = GetCollectionArticlesInfo(collection_url, page, count, sorting_method, disable_check=True) + result = GetCollectionArticlesInfo( + collection_url, page, count, sorting_method, disable_check=True + ) if result: page += 1 else: diff --git a/JianshuResearchTools/convert.py b/JianshuResearchTools/convert.py index b33e5f2..88b2a62 100644 --- a/JianshuResearchTools/convert.py +++ b/JianshuResearchTools/convert.py @@ -1,20 +1,39 @@ -from .assert_funcs import (AssertArticleStatusNormal, AssertArticleUrl, - AssertCollectionUrl, AssertIslandPostUrl, - AssertIslandUrl, AssertNotebookUrl, AssertType, - AssertUserUrl) -from .basic_apis import (GetArticleJsonDataApi, GetCollectionJsonDataApi, - GetUserJsonDataApi) +from .assert_funcs import ( + AssertArticleStatusNormal, + AssertArticleUrl, + AssertCollectionUrl, + AssertIslandPostUrl, + AssertIslandUrl, + AssertNotebookUrl, + AssertType, + AssertUserUrl, +) +from .basic_apis import ( + GetArticleJsonDataApi, + GetCollectionJsonDataApi, + GetUserJsonDataApi, +) __all__ = [ - "UserUrlToUserId", "UserSlugToUserId", "UserUrlToUserSlug", - "ArticleUrlToArticleSlug", "ArticleSlugToArticleUrl", - "ArticleSlugToArticleId", "ArticleUrlToArticleId", - "NotebookUrlToNotebookId", "NotebookUrlToNotebookSlug", - "CollectionSlugToCollectionUrl", "CollectionUrlToCollectionId", - "IslandUrlToIslandSlug", "IslandSlugToIslandUrl", "UserUrlToUserUrlScheme", - "ArticleUrlToArticleUrlScheme", "NotebookUrlToNotebookUrlScheme", - "CollectionUrlToCollectionUrlScheme", "IslandPostUrlToIslandPostSlug", - "IslandPostSlugToIslandPostUrl" + "UserUrlToUserId", + "UserSlugToUserId", + "UserUrlToUserSlug", + "ArticleUrlToArticleSlug", + "ArticleSlugToArticleUrl", + "ArticleSlugToArticleId", + "ArticleUrlToArticleId", + "NotebookUrlToNotebookId", + "NotebookUrlToNotebookSlug", + "CollectionSlugToCollectionUrl", + "CollectionUrlToCollectionId", + "IslandUrlToIslandSlug", + "IslandSlugToIslandUrl", + "UserUrlToUserUrlScheme", + "ArticleUrlToArticleUrlScheme", + "NotebookUrlToNotebookUrlScheme", + "CollectionUrlToCollectionUrlScheme", + "IslandPostUrlToIslandPostSlug", + "IslandPostSlugToIslandPostUrl", ] @@ -30,8 +49,7 @@ def UserUrlToUserId(user_url: str) -> int: AssertType(user_url, str) AssertUserUrl(user_url) json_obj = GetUserJsonDataApi(user_url) - result = json_obj["id"] - return result + return json_obj["id"] def UserSlugToUserId(user_slug: str) -> int: @@ -46,8 +64,7 @@ def UserSlugToUserId(user_slug: str) -> int: AssertType(user_slug, str) user_url = UserSlugToUserUrl(user_slug) AssertUserUrl(user_url) - result = UserUrlToUserId(user_url) - return result + return UserUrlToUserId(user_url) def UserUrlToUserSlug(user_url: str) -> str: @@ -119,8 +136,7 @@ def ArticleSlugToArticleId(article_slug: str) -> int: """ AssertType(article_slug, str) json_obj = GetArticleJsonDataApi(ArticleSlugToArticleUrl(article_slug)) - result = json_obj["id"] - return result + return json_obj["id"] def ArticleUrlToArticleId(article_url: str) -> int: @@ -136,8 +152,7 @@ def ArticleUrlToArticleId(article_url: str) -> int: AssertArticleUrl(article_url) AssertArticleStatusNormal(article_url) json_obj = GetArticleJsonDataApi(article_url) - result = json_obj["id"] - return result + return json_obj["id"] def NotebookUrlToNotebookId(notebook_url: str) -> int: @@ -152,8 +167,7 @@ def NotebookUrlToNotebookId(notebook_url: str) -> int: AssertType(notebook_url, str) AssertNotebookUrl(notebook_url) json_obj = GetArticleJsonDataApi(notebook_url) - result = json_obj["id"] - return result + return json_obj["id"] def NotebookUrlToNotebookSlug(notebook_url: str) -> str: @@ -225,8 +239,7 @@ def CollectionUrlToCollectionId(collection_url: str) -> int: """ AssertType(collection_url, str) AssertCollectionUrl(collection_url) - result = GetCollectionJsonDataApi(collection_url)["id"] - return result + return GetCollectionJsonDataApi(collection_url)["id"] def IslandUrlToIslandSlug(island_url: str) -> str: @@ -268,8 +281,7 @@ def UserUrlToUserUrlScheme(user_url: str) -> str: """ AssertType(user_url, str) AssertUserUrl(user_url) - result = user_url.replace("https://www.jianshu.com/u/", "jianshu://u/") - return result + return user_url.replace("https://www.jianshu.com/u/", "jianshu://u/") def ArticleUrlToArticleUrlScheme(article_url: str) -> str: @@ -282,8 +294,7 @@ def ArticleUrlToArticleUrlScheme(article_url: str) -> str: """ AssertType(article_url, str) AssertArticleUrl(article_url) - result = article_url.replace("https://www.jianshu.com/p/", "jianshu://notes/") - return result + return article_url.replace("https://www.jianshu.com/p/", "jianshu://notes/") def NotebookUrlToNotebookUrlScheme(notebook_url: str) -> str: @@ -296,8 +307,7 @@ def NotebookUrlToNotebookUrlScheme(notebook_url: str) -> str: """ AssertType(notebook_url, str) AssertNotebookUrl(notebook_url) - result = notebook_url.replace("https://www.jianshu.com/nb/", "jianshu://nb/") - return result + return notebook_url.replace("https://www.jianshu.com/nb/", "jianshu://nb/") def CollectionUrlToCollectionUrlScheme(collection_url: str) -> str: @@ -310,8 +320,7 @@ def CollectionUrlToCollectionUrlScheme(collection_url: str) -> str: """ AssertType(collection_url, str) AssertCollectionUrl(collection_url) - result = collection_url.replace("https://www.jianshu.com/c/", "jianshu://c/") - return result + return collection_url.replace("https://www.jianshu.com/c/", "jianshu://c/") def IslandPostUrlToIslandPostSlug(post_url: str) -> str: @@ -325,8 +334,7 @@ def IslandPostUrlToIslandPostSlug(post_url: str) -> str: """ AssertType(post_url, str) AssertIslandPostUrl(post_url) - result = post_url.replace("https://www.jianshu.com/gp/", "") - return result + return post_url.replace("https://www.jianshu.com/gp/", "") def IslandPostSlugToIslandPostUrl(post_slug: str) -> str: diff --git a/JianshuResearchTools/exceptions.py b/JianshuResearchTools/exceptions.py index 9439dbf..48d887f 100644 --- a/JianshuResearchTools/exceptions.py +++ b/JianshuResearchTools/exceptions.py @@ -2,17 +2,16 @@ class InputError(Exception): - """函数参数错误时抛出此异常 - """ + """函数参数错误时抛出此异常""" + pass class APIError(Exception): - """由于简书 API 受限导致无法获取数据时抛出此异常 - """ + """由于简书 API 受限导致无法获取数据时抛出此异常""" + pass class ResourceError(Exception): - """访问的资源不存在或无法正常访问时抛出此异常 - """ + """访问的资源不存在或无法正常访问时抛出此异常""" diff --git a/JianshuResearchTools/headers.py b/JianshuResearchTools/headers.py index 1058a48..748b44d 100644 --- a/JianshuResearchTools/headers.py +++ b/JianshuResearchTools/headers.py @@ -1,25 +1,15 @@ -__all__ = [ - "PC_header", "api_request_header", "BeikeIsland_request_header", - "mobile_header" -] +__all__ = ["PC_HEADER", "API_HEADER", "MOBILE_HEADER"] -PC_header = { +PC_HEADER = { "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36" } -api_request_header = { +API_HEADER = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36 Edg/89.0.774.57", "X-INFINITESCROLL": "true", - "X-Requested-With": "XMLHttpRequest" + "X-Requested-With": "XMLHttpRequest", } -BeikeIsland_request_header = { - "Host": "www.beikeisland.com", - "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36 Edg/89.0.774.57", - "Content-Type": "application/json", - "Version": "v2.0" -} - -mobile_header = { +MOBILE_HEADER = { "User-Agent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.134 Mobile Safari/537.36" } diff --git a/JianshuResearchTools/httpx_client.py b/JianshuResearchTools/httpx_client.py new file mode 100644 index 0000000..9ecb48f --- /dev/null +++ b/JianshuResearchTools/httpx_client.py @@ -0,0 +1,22 @@ +from httpx import Client + +from JianshuResearchTools.headers import API_HEADER, MOBILE_HEADER, PC_HEADER + +JIANSHU_API_CLIENT = Client( + http2=True, + timeout=5, + base_url="https://www.jianshu.com", + headers=API_HEADER, +) +JIANSHU_PC_CLIENT = Client( + http2=True, + timeout=5, + base_url="https://www.jianshu.com", + headers=PC_HEADER, +) +JIANSHU_MOBILE_CLIENT = Client( + http2=True, + timeout=5, + base_url="https://www.jianshu.com", + headers=MOBILE_HEADER, +) diff --git a/JianshuResearchTools/island.py b/JianshuResearchTools/island.py index 207c581..d76d0d7 100644 --- a/JianshuResearchTools/island.py +++ b/JianshuResearchTools/island.py @@ -1,18 +1,30 @@ +from contextlib import suppress from datetime import datetime -from typing import Dict, Generator, List - -from .assert_funcs import (AssertIslandPostUrl, AssertIslandStatusNormal, - AssertIslandUrl) -from .basic_apis import (GetIslandJsonDataApi, GetIslandPostJsonDataApi, - GetIslandPostsJsonDataApi) -from .convert import (IslandPostSlugToIslandPostUrl, - IslandPostUrlToIslandPostSlug, IslandUrlToIslandSlug) +from typing import Dict, Generator, List, Literal, Optional + +from .assert_funcs import AssertIslandPostUrl, AssertIslandStatusNormal, AssertIslandUrl +from .basic_apis import ( + GetIslandJsonDataApi, + GetIslandPostJsonDataApi, + GetIslandPostsJsonDataApi, +) +from .convert import ( + IslandPostSlugToIslandPostUrl, + IslandPostUrlToIslandPostSlug, + IslandUrlToIslandSlug, +) __all__ = [ - "GetIslandName", "GetIslandAvatarUrl", "GetIslandIntroduction", - "GetIslandMembersCount", "GetIslandPostsCount", "GetIslandCategory", - "GetIslandPostFullContent", "GetIslandPosts", "GetIslandAllBasicData", - "GetIslandAllPostsData" + "GetIslandName", + "GetIslandAvatarUrl", + "GetIslandIntroduction", + "GetIslandMembersCount", + "GetIslandPostsCount", + "GetIslandCategory", + "GetIslandPostFullContent", + "GetIslandPosts", + "GetIslandAllBasicData", + "GetIslandAllPostsData", ] @@ -30,8 +42,7 @@ def GetIslandName(island_url: str, disable_check: bool = False) -> str: AssertIslandUrl(island_url) AssertIslandStatusNormal(island_url) json_obj = GetIslandJsonDataApi(island_url) - result = json_obj["name"] - return result + return json_obj["name"] def GetIslandAvatarUrl(island_url: str, disable_check: bool = False) -> str: @@ -48,8 +59,7 @@ def GetIslandAvatarUrl(island_url: str, disable_check: bool = False) -> str: AssertIslandUrl(island_url) AssertIslandStatusNormal(island_url) json_obj = GetIslandJsonDataApi(island_url) - result = json_obj["image"] - return result + return json_obj["image"] def GetIslandIntroduction(island_url: str, disable_check: bool = False) -> str: @@ -66,8 +76,7 @@ def GetIslandIntroduction(island_url: str, disable_check: bool = False) -> str: AssertIslandUrl(island_url) AssertIslandStatusNormal(island_url) json_obj = GetIslandJsonDataApi(island_url) - result = json_obj["intro"] - return result + return json_obj["intro"] def GetIslandMembersCount(island_url: str, disable_check: bool = False) -> int: @@ -84,8 +93,7 @@ def GetIslandMembersCount(island_url: str, disable_check: bool = False) -> int: AssertIslandUrl(island_url) AssertIslandStatusNormal(island_url) json_obj = GetIslandJsonDataApi(island_url) - result = json_obj["members_count"] - return result + return json_obj["members_count"] def GetIslandPostsCount(island_url: str, disable_check: bool = False) -> int: @@ -102,8 +110,7 @@ def GetIslandPostsCount(island_url: str, disable_check: bool = False) -> int: AssertIslandUrl(island_url) AssertIslandStatusNormal(island_url) json_obj = GetIslandJsonDataApi(island_url) - result = json_obj["posts_count"] - return result + return json_obj["posts_count"] def GetIslandCategory(island_url: str, disable_check: bool = False) -> str: @@ -120,8 +127,7 @@ def GetIslandCategory(island_url: str, disable_check: bool = False) -> str: AssertIslandUrl(island_url) AssertIslandStatusNormal(island_url) json_obj = GetIslandJsonDataApi(island_url) - result = json_obj["category"]["name"] - return result + return json_obj["category"]["name"] def GetIslandPostFullContent(post_url: str, disable_check: bool = False) -> str: @@ -138,28 +144,33 @@ def GetIslandPostFullContent(post_url: str, disable_check: bool = False) -> str: AssertIslandPostUrl(post_url) AssertIslandStatusNormal(post_url) json_obj = GetIslandPostJsonDataApi(IslandPostUrlToIslandPostSlug(post_url)) - result = json_obj["content"] - return result - - -def GetIslandPosts(island_url: str, start_sort_id: int = None, count: int = 10, - topic_id: int = None, sorting_method: str = "time", - get_full_content: bool = False, disable_check: bool = False) -> List[Dict]: + return json_obj["content"] + + +def GetIslandPosts( + island_url: str, + start_sort_id: Optional[int] = None, + count: int = 10, + topic_id: Optional[int] = None, + sorting_method: Literal["time", "comment_time", "hot"] = "time", + get_full_content: bool = False, + disable_check: bool = False, +) -> List[Dict]: """获取小岛帖子信息 - Args: - island_url (str): 小岛 URL - start_sort_id (int, optional): 起始序号,等于上一条数据的序号. Defaults to None. - count (int, optional): 每次返回的数据数量. Defaults to 10. - topic_id (int, optional): 话题 ID. Defaults to None. - sorting_method (str, optional): 排序方法,"time" 为按照发布时间排序, - "comment_time" 为按照最近评论时间排序,"hot" 为按照热度排序. Defaults to "time". - get_full_content (bool, optional): 为 True 时,当检测到获取的帖子内容不全时, - 自动调用 GetIslandPostFullContent 函数获取完整内容并替换. Defaults to False. - disable_check (bool): 禁用参数有效性检查. Defaults to False. - - Returns: - List[Dict]: 帖子信息 + Args: + island_url (str): 小岛 URL + start_sort_id (int, optional): 起始序号,等于上一条数据的序号. Defaults to None. + count (int, optional): 每次返回的数据数量. Defaults to 10. + topic_id (int, optional): 话题 ID. Defaults to None. + sorting_method (Literal["time", "comment_time", "hot"], optional): 排序方法,"time" 为按照发布时间排序, + "comment_time" 为按照最近评论时间排序,"hot" 为按照热度排序. Defaults to "time". + get_full_content (bool, optional): 为 True 时,当检测到获取的帖子内容不全时, + 自动调用 GetIslandPostFullContent 函数获取完整内容并替换. Defaults to False. + disable_check (bool): 禁用参数有效性检查. Defaults to False. + + Returns: + List[Dict]: 帖子信息 """ if not disable_check: AssertIslandUrl(island_url) @@ -167,11 +178,15 @@ def GetIslandPosts(island_url: str, start_sort_id: int = None, count: int = 10, order_by = { "time": "latest", "hot": "hot", - "most_valuable": "best" - }[sorting_method], - json_obj = GetIslandPostsJsonDataApi(group_slug=IslandUrlToIslandSlug(island_url), - max_id=start_sort_id, count=count, topic_id=topic_id, - order_by=order_by) + "most_valuable": "best", + }[sorting_method] + json_obj = GetIslandPostsJsonDataApi( + group_slug=IslandUrlToIslandSlug(island_url), + max_id=start_sort_id, + count=count, + topic_id=topic_id, + order_by=order_by, + ) result = [] for item in json_obj: @@ -192,7 +207,7 @@ def GetIslandPosts(island_url: str, start_sort_id: int = None, count: int = 10, "island": { "iid": item["group"]["id"], "islug": item["group"]["slug"], - "island_name": item["group"]["name"] + "island_name": item["group"]["name"], }, "user": { "uid": item["user"]["id"], @@ -209,28 +224,23 @@ def GetIslandPosts(island_url: str, start_sort_id: int = None, count: int = 10, # # 有个 group_role 不知道干什么用的,没解析 # } } - try: + with suppress(KeyError): # 没有图片则跳过 image_urls = [] for image in item["images"]: image_urls.append(image["url"]) - except KeyError: - pass # 没有图片则跳过 - try: + with suppress(KeyError): # 没有徽章则跳过 item_data["user"]["badge"] = item["user"]["badge"]["text"] - except KeyError: - pass # 没有徽章则跳过 - try: + with suppress(KeyError): # 没有话题则跳过 item_data["topic"] = { "tid": item["topic"]["id"], "tslug": item["topic"]["slug"], "topic_name": item["topic"]["name"] # 有个 group_role 不知道干什么用的,没解析 } - except KeyError: - pass # 没有话题则跳过 if get_full_content and "..." in item_data["content"]: # 获取到的帖子内容不全 - item_data["content"] = GetIslandPostFullContent(IslandPostSlugToIslandPostUrl(item_data["pslug"]), - disable_check=True) + item_data["content"] = GetIslandPostFullContent( + IslandPostSlugToIslandPostUrl(item_data["pslug"]), disable_check=True + ) result.append(item_data) return result @@ -260,17 +270,22 @@ def GetIslandAllBasicData(island_url: str, disable_check: bool = False) -> Dict: return result -def GetIslandAllPostsData(island_url: str, count: int = 10, - topic_id: int = None, sorting_method: str = "time", - get_full_content: bool = False, max_count: int = None, - disable_check: bool = False) -> Generator[Dict, None, None]: +def GetIslandAllPostsData( + island_url: str, + count: int = 10, + topic_id: Optional[int] = None, + sorting_method: Literal["time", "comment_time", "hot"] = "time", + get_full_content: bool = False, + max_count: Optional[int] = None, + disable_check: bool = False, +) -> Generator[Dict, None, None]: """获取小岛的所有帖子信息 Args: island_url (str): 小岛 URL count (int, optional): 单次获取的数据数量,会影响性能. Defaults to 10. topic_id (int, optional): 话题 ID. Defaults to None. - sorting_method (str, optional): 排序方法,time 为按照发布时间排序, + sorting_method (Literal["time", "comment_time", "hot"], optional): 排序方法,time 为按照发布时间排序, comment_time 为按照最近评论时间排序,hot 为按照热度排序. Defaults to "time". get_full_content (bool, optional): 为 True 时,当检测到获取的帖子内容不全时, 自动调用 GetIslandPostFullContent 函数获取完整内容并替换. Defaults to False. @@ -286,8 +301,15 @@ def GetIslandAllPostsData(island_url: str, count: int = 10, start_sort_id = None now_count = 0 while True: - result = GetIslandPosts(island_url, start_sort_id, count, topic_id, - sorting_method, get_full_content, disable_check=True) + result = GetIslandPosts( + island_url, + start_sort_id, + count, + topic_id, + sorting_method, + get_full_content, + disable_check=True, + ) if result: start_sort_id = result[-1]["sorted_id"] else: diff --git a/JianshuResearchTools/notebook.py b/JianshuResearchTools/notebook.py index 6f72262..98b08a4 100644 --- a/JianshuResearchTools/notebook.py +++ b/JianshuResearchTools/notebook.py @@ -1,14 +1,19 @@ from datetime import datetime -from typing import Dict, Generator, List +from typing import Dict, Generator, List, Literal, Optional from .assert_funcs import AssertNotebookStatusNormal, AssertNotebookUrl from .basic_apis import GetNotebookArticlesJsonDataApi, GetNotebookJsonDataApi __all__ = [ - "GetNotebookName", "GetNotebookArticlesCount", "GetNotebookAuthorInfo", - "GetNotebookWordage", "GetNotebookSubscribersCount", - "GetNotebookUpdateTime", "GetNotebookArticlesInfo", - "GetNotebookAllBasicData", "GetNotebookAllArticlesInfo" + "GetNotebookName", + "GetNotebookArticlesCount", + "GetNotebookAuthorInfo", + "GetNotebookWordage", + "GetNotebookSubscribersCount", + "GetNotebookUpdateTime", + "GetNotebookArticlesInfo", + "GetNotebookAllBasicData", + "GetNotebookAllArticlesInfo", ] @@ -26,8 +31,7 @@ def GetNotebookName(notebook_url: str, disable_check: bool = False) -> str: AssertNotebookUrl(notebook_url) AssertNotebookStatusNormal(notebook_url) json_obj = GetNotebookJsonDataApi(notebook_url) - result = json_obj["name"] - return result + return json_obj["name"] def GetNotebookArticlesCount(notebook_url: str, disable_check: bool = False) -> int: @@ -44,8 +48,7 @@ def GetNotebookArticlesCount(notebook_url: str, disable_check: bool = False) -> AssertNotebookUrl(notebook_url) AssertNotebookStatusNormal(notebook_url) json_obj = GetNotebookJsonDataApi(notebook_url) - result = json_obj["notes_count"] - return result + return json_obj["notes_count"] def GetNotebookAuthorInfo(notebook_url: str, disable_check: bool = False) -> Dict: @@ -62,12 +65,11 @@ def GetNotebookAuthorInfo(notebook_url: str, disable_check: bool = False) -> Dic AssertNotebookUrl(notebook_url) AssertNotebookStatusNormal(notebook_url) json_obj = GetNotebookJsonDataApi(notebook_url) - result = { + return { "name": json_obj["user"]["nickname"], "uslug": json_obj["user"]["slug"], - "avatar_url": json_obj["user"]["avatar"] + "avatar_url": json_obj["user"]["avatar"], } - return result def GetNotebookWordage(notebook_url: str, disable_check: bool = False) -> int: @@ -84,8 +86,7 @@ def GetNotebookWordage(notebook_url: str, disable_check: bool = False) -> int: AssertNotebookUrl(notebook_url) AssertNotebookStatusNormal(notebook_url) json_obj = GetNotebookJsonDataApi(notebook_url) - result = json_obj["wordage"] - return result + return json_obj["wordage"] def GetNotebookSubscribersCount(notebook_url: str, disable_check: bool = False) -> int: @@ -102,8 +103,7 @@ def GetNotebookSubscribersCount(notebook_url: str, disable_check: bool = False) AssertNotebookUrl(notebook_url) AssertNotebookStatusNormal(notebook_url) json_obj = GetNotebookJsonDataApi(notebook_url) - result = json_obj["subscribers_count"] - return result + return json_obj["subscribers_count"] def GetNotebookUpdateTime(notebook_url: str, disable_check: bool = False) -> datetime: @@ -120,20 +120,23 @@ def GetNotebookUpdateTime(notebook_url: str, disable_check: bool = False) -> dat AssertNotebookUrl(notebook_url) AssertNotebookStatusNormal(notebook_url) json_obj = GetNotebookJsonDataApi(notebook_url) - result = datetime.fromtimestamp(json_obj["last_updated_at"]) - return result + return datetime.fromtimestamp(json_obj["last_updated_at"]) -def GetNotebookArticlesInfo(notebook_url: str, page: int = 1, - count: int = 10, sorting_method: str = "time", - disable_check: bool = False) -> List[Dict]: +def GetNotebookArticlesInfo( + notebook_url: str, + page: int = 1, + count: int = 10, + sorting_method: Literal["time", "comment_time", "hot"] = "time", + disable_check: bool = False, +) -> List[Dict]: """获取文集中的文章信息 Args: notebook_url (str): 文集 URL page (int, optional): 页码. Defaults to 1. count (int, optional): 每次返回的数据数量. Defaults to 10. - sorting_method (str, optional): 排序方法,"time" 为按照发布时间排序, + sorting_method (Literal["time", "comment_time", "hot"], optional): 排序方法,"time" 为按照发布时间排序, "comment_time" 为按照最近评论时间排序,"hot" 为按照热度排序. Defaults to "time". disable_check (bool): 禁用参数有效性检查. Defaults to False. @@ -146,17 +149,20 @@ def GetNotebookArticlesInfo(notebook_url: str, page: int = 1, order_by = { "time": "added_at", "comment_time": "commented_at", - "hot": "top" + "hot": "top", }[sorting_method] - json_obj = GetNotebookArticlesJsonDataApi(notebook_url=notebook_url, - page=page, count=count, order_by=order_by) + json_obj = GetNotebookArticlesJsonDataApi( + notebook_url=notebook_url, page=page, count=count, order_by=order_by + ) result = [] for item in json_obj: item_data = { "aid": item["object"]["data"]["id"], "title": item["object"]["data"]["title"], "aslug": item["object"]["data"]["slug"], - "release_time": datetime.fromisoformat(item["object"]["data"]["first_shared_at"]), + "release_time": datetime.fromisoformat( + item["object"]["data"]["first_shared_at"] + ), "first_image_url": item["object"]["data"]["list_image_url"], "summary": item["object"]["data"]["public_abbr"], "views_count": item["object"]["data"]["views_count"], @@ -168,11 +174,11 @@ def GetNotebookArticlesInfo(notebook_url: str, page: int = 1, "uid": item["object"]["data"]["user"]["id"], "name": item["object"]["data"]["user"]["nickname"], "uslug": item["object"]["data"]["user"]["slug"], - "avatar_url": item["object"]["data"]["user"]["avatar"] + "avatar_url": item["object"]["data"]["user"]["avatar"], }, "total_fp_amount": item["object"]["data"]["total_fp_amount"] / 1000, "comments_count": item["object"]["data"]["public_comments_count"], - "rewards_count": item["object"]["data"]["total_rewards_count"] + "rewards_count": item["object"]["data"]["total_rewards_count"], } result.append(item_data) return result @@ -198,7 +204,7 @@ def GetNotebookAllBasicData(notebook_url: str, disable_check: bool = False) -> D result["author_info"] = { "name": json_obj["user"]["nickname"], "uslug": json_obj["user"]["slug"], - "avatar_url": json_obj["user"]["avatar"] + "avatar_url": json_obj["user"]["avatar"], } result["articles_count"] = json_obj["notes_count"] result["wordage"] = json_obj["wordage"] @@ -207,14 +213,19 @@ def GetNotebookAllBasicData(notebook_url: str, disable_check: bool = False) -> D return result -def GetNotebookAllArticlesInfo(notebook_url: str, count: int = 10, sorting_method: str = "time", - max_count: int = None, disable_check: bool = False) -> Generator[Dict, None, None]: +def GetNotebookAllArticlesInfo( + notebook_url: str, + count: int = 10, + sorting_method: Literal["time", "comment_time", "hot"] = "time", + max_count: Optional[int] = None, + disable_check: bool = False, +) -> Generator[Dict, None, None]: """获取文集中的全部文章信息 Args: notebook_url (str): 文集 URL count (int, optional): 单次获取的数据数量,会影响性能. Defaults to 10. - sorting_method (str, optional): 排序方法,time 为按照发布时间排序, + sorting_method (Literal["time", "comment_time", "hot"], optional): 排序方法,time 为按照发布时间排序, comment_time 为按照最近评论时间排序,hot 为按照热度排序. Defaults to "time". max_count (int, optional): 获取的文集文章信息数量上限,Defaults to None. disable_check (bool): 禁用参数有效性检查. Defaults to False. @@ -228,7 +239,9 @@ def GetNotebookAllArticlesInfo(notebook_url: str, count: int = 10, sorting_metho page = 1 now_count = 0 while True: - result = GetNotebookArticlesInfo(notebook_url, page, count, sorting_method, disable_check=True) + result = GetNotebookArticlesInfo( + notebook_url, page, count, sorting_method, disable_check=True + ) if result: page += 1 else: diff --git a/JianshuResearchTools/objects.py b/JianshuResearchTools/objects.py index 74e6cd3..464e721 100644 --- a/JianshuResearchTools/objects.py +++ b/JianshuResearchTools/objects.py @@ -1,52 +1,80 @@ from datetime import datetime -from typing import Any, Callable, Dict, List +from typing import Any, Callable, Dict, List, Optional from . import article, collection, island, notebook, user -from .assert_funcs import (AssertArticleStatusNormal, AssertArticleUrl, - AssertCollectionStatusNormal, AssertCollectionUrl, - AssertIslandStatusNormal, AssertIslandUrl, - AssertNotebookStatusNormal, AssertNotebookUrl, - AssertType, AssertUserStatusNormal, AssertUserUrl) -from .convert import (ArticleSlugToArticleUrl, CollectionSlugToCollectionUrl, - IslandSlugToIslandUrl, IslandUrlToIslandSlug, - NotebookSlugToNotebookUrl, UserSlugToUserUrl, - UserUrlToUserSlug, ArticleUrlToArticleSlug, - NotebookUrlToNotebookId, NotebookUrlToNotebookSlug, CollectionUrlToCollectionSlug) +from .assert_funcs import ( + AssertArticleStatusNormal, + AssertArticleUrl, + AssertCollectionStatusNormal, + AssertCollectionUrl, + AssertIslandStatusNormal, + AssertIslandUrl, + AssertNotebookStatusNormal, + AssertNotebookUrl, + AssertType, + AssertUserStatusNormal, + AssertUserUrl, +) +from .convert import ( + ArticleSlugToArticleUrl, + ArticleUrlToArticleSlug, + CollectionSlugToCollectionUrl, + CollectionUrlToCollectionSlug, + IslandSlugToIslandUrl, + IslandUrlToIslandSlug, + NotebookSlugToNotebookUrl, + NotebookUrlToNotebookId, + NotebookUrlToNotebookSlug, + UserSlugToUserUrl, + UserUrlToUserSlug, +) from .exceptions import InputError from .utils import CallWithoutCheck, NameValueMappingToString, OnlyOne __all__ = [ - "User", "Article", "Notebook", "Collection", "Island", - "get_cache_items_count", "get_cache_status", "set_cache_status", - "clear_cache" + "User", + "Article", + "Notebook", + "Collection", + "Island", + "get_cache_items_count", + "get_cache_status", + "set_cache_status", + "clear_cache", ] _cache_dict: Dict[int, Any] = {} _DISABLE_CACHE = False # 禁用缓存 -def cache_result_wrapper(func: Callable): +def cache_result_wrapper(func: Callable) -> Callable: """该函数是一个装饰器,用于缓存函数的返回值 Args: func (Callable): 被装饰的函数 """ - def inner(*args, **kwargs): + def inner(*args: Any, **kwargs: Any) -> Any: if _DISABLE_CACHE: # 缓存已禁用,直接执行函数并返回结果 return func(*args, **kwargs) # 生成哈希值 - args_hash = hash((hash(func.__qualname__),) + (hash(args[0]),) + tuple(args[1:]) + tuple(kwargs.items())) + args_hash = hash( + (hash(func.__qualname__),) + + (hash(args[0]),) + + tuple(args[1:]) + + tuple(kwargs.items()) + ) cache_result = _cache_dict.get(args_hash) if cache_result: # 如果缓存中有值,则直接返回缓存值 return cache_result - else: - result = func(*args, **kwargs) # 运行函数,获取返回值 - _cache_dict[args_hash] = result # 将返回值存入缓存 - return result + + result = func(*args, **kwargs) # 运行函数,获取返回值 + _cache_dict[args_hash] = result # 将返回值存入缓存 + return result + return inner @@ -68,7 +96,7 @@ def get_cache_status() -> bool: return not _DISABLE_CACHE -def set_cache_status(status: bool): +def set_cache_status(status: bool) -> None: """设置缓存状态 Args: @@ -80,16 +108,17 @@ def set_cache_status(status: bool): _DISABLE_CACHE = not status -def clear_cache(): - """该函数用于清空已缓存的所有值 - """ +def clear_cache(): # noqa: ANN201 + """该函数用于清空已缓存的所有值""" _cache_dict.clear() class User: - """用户类 - """ - def __init__(self, user_url: str = None, *, user_slug: str = None): + """用户类""" + + def __init__( + self, user_url: Optional[str] = None, *, user_slug: Optional[str] = None + ) -> None: """构建新的用户对象 Args: @@ -98,12 +127,14 @@ def __init__(self, user_url: str = None, *, user_slug: str = None): """ # TODO: 支持使用用户 ID 初始化用户对象 if not OnlyOne(user_url, user_slug): - raise("只能使用 URL 或 Slug 中的一个实例化用户对象") + raise ValueError("只能使用 URL 或 Slug 中的一个实例化用户对象") if user_url: AssertUserUrl(user_url) elif user_slug: user_url = UserSlugToUserUrl(user_slug) + else: + raise ValueError("user_url 和 user_slug 至少需要传入一个") AssertUserStatusNormal(user_url) self._url = user_url @@ -379,10 +410,7 @@ def __eq__(self, other: object) -> bool: """ if not isinstance(other, User): return False # 不是由用户类构建的必定不相等 - if self._url == other._url: - return True - else: - return False + return self._url == other._url def __hash__(self) -> int: """返回基于用户 URL 的哈希值 @@ -398,29 +426,34 @@ def __str__(self) -> str: Returns: str: 用户信息摘要 """ - return NameValueMappingToString({ - "昵称": (self.name, False), - "URL": (self.url, False), - "性别": (self.gender, False), - "关注者数": (self.followers_count, False), - "粉丝数": (self.fans_count, False), - "文章数": (self.articles_count, False), - "总字数": (self.wordage, False), - "简书钻": (self.FP_count, False), - "简书贝": (self.FTN_count, False), - "总资产": (self.assets_count, False), - "徽章": (' '.join(self.badges), False), - "最后更新时间": (self.last_update_time, False), - "会员等级": (self.VIP_info["vip_type"], False), - "会员过期时间": (self.VIP_info["expire_date"], False), - "个人简介": (self.introduction_text, True) - }, title="用户信息摘要") + return NameValueMappingToString( + { + "昵称": (self.name, False), + "URL": (self.url, False), + "性别": (self.gender, False), + "关注者数": (self.followers_count, False), + "粉丝数": (self.fans_count, False), + "文章数": (self.articles_count, False), + "总字数": (self.wordage, False), + "简书钻": (self.FP_count, False), + "简书贝": (self.FTN_count, False), + "总资产": (self.assets_count, False), + "徽章": (" ".join(self.badges), False), + "最后更新时间": (self.last_update_time, False), + "会员等级": (self.VIP_info["vip_type"], False), + "会员过期时间": (self.VIP_info["expire_date"], False), + "个人简介": (self.introduction_text, True), + }, + title="用户信息摘要", + ) class Article: - """文章类 - """ - def __init__(self, article_url: str = None, article_slug: str = None): + """文章类""" + + def __init__( + self, article_url: Optional[str] = None, article_slug: Optional[str] = None + ) -> None: """构建新的文章对象 Args: @@ -429,12 +462,14 @@ def __init__(self, article_url: str = None, article_slug: str = None): """ # TODO: 支持使用文章 ID 初始化文章对象 if not OnlyOne(article_url, article_slug): - raise("只能使用 URL 或 Slug 中的一个实例化文章对象") + raise ValueError("只能使用 URL 或 Slug 中的一个实例化文章对象") if article_url: AssertArticleUrl(article_url) elif article_slug: article_url = ArticleSlugToArticleUrl(article_slug) + else: + raise ValueError("article_url 和 article_slug 至少需要传入一个") AssertArticleStatusNormal(article_url) self._url = article_url @@ -672,10 +707,7 @@ def __eq__(self, other: object) -> bool: """ if not isinstance(other, Article): return False # 不是由文章类构建的必定不相等 - if self._url == other._url: - return True - else: - return False + return self._url == other._url def __hash__(self) -> int: """返回基于文章 URL 的哈希值 @@ -691,29 +723,34 @@ def __str__(self) -> str: Returns: str: 文章信息摘要 """ - return NameValueMappingToString({ - "标题": (self.title, False), - "URL": (self.url, False), - "作者名": (self.author_name, False), - "字数": (self.wordage, False), - "阅读量": (self.reads_count, False), - "点赞数": (self.likes_count, False), - "评论数": (self.comments_count, False), - "精选评论数": (self.most_valuable_comments_count, False), - "总获钻量": (self.total_FP_count, False), - "发布时间": (self.publish_time, False), - "更新时间": (self.update_time, False), - "需付费": (self.paid_status, False), - "可转载": (self.reprint_status, False), - "可评论": (self.comment_status, False), - "摘要": (self.description, True) - }, title="文章信息摘要") + return NameValueMappingToString( + { + "标题": (self.title, False), + "URL": (self.url, False), + "作者名": (self.author_name, False), + "字数": (self.wordage, False), + "阅读量": (self.reads_count, False), + "点赞数": (self.likes_count, False), + "评论数": (self.comments_count, False), + "精选评论数": (self.most_valuable_comments_count, False), + "总获钻量": (self.total_FP_count, False), + "发布时间": (self.publish_time, False), + "更新时间": (self.update_time, False), + "需付费": (self.paid_status, False), + "可转载": (self.reprint_status, False), + "可评论": (self.comment_status, False), + "摘要": (self.description, True), + }, + title="文章信息摘要", + ) class Notebook: - """文集类 - """ - def __init__(self, notebook_url: str = None, notebook_slug: str = None): + """文集类""" + + def __init__( + self, notebook_url: Optional[str] = None, notebook_slug: Optional[str] = None + ) -> None: """构建新的文集对象 Args: @@ -722,12 +759,14 @@ def __init__(self, notebook_url: str = None, notebook_slug: str = None): """ # TODO: 支持使用用户 ID 初始化用户对象 if not OnlyOne(notebook_url, notebook_slug): - raise("只能使用 URL 或 Slug 中的一个实例化文集对象") + raise ValueError("只能使用 URL 或 Slug 中的一个实例化文集对象") if notebook_url: AssertNotebookUrl(notebook_url) elif notebook_slug: notebook_url = NotebookSlugToNotebookUrl(notebook_slug) + else: + raise ValueError("notebook_url 和 notebook_slug 至少需要传入一个") AssertNotebookStatusNormal(notebook_url) self._url = notebook_url @@ -767,7 +806,7 @@ def url(self) -> str: @property @cache_result_wrapper - def id(self) -> int: + def id(self) -> int: # noqa: A003 """获取文集 ID Returns: @@ -856,7 +895,9 @@ def update_time(self) -> datetime: return CallWithoutCheck(notebook.GetNotebookUpdateTime, self._url) @cache_result_wrapper - def articles_info(self, page: int = 1, count: int = 10, sorting_method: str = "time") -> List[Dict]: + def articles_info( + self, page: int = 1, count: int = 10, sorting_method: str = "time" + ) -> List[Dict]: """获取文集中的文章信息 Args: @@ -868,7 +909,9 @@ def articles_info(self, page: int = 1, count: int = 10, sorting_method: str = "t Returns: List[Dict]: 文章信息 """ - return CallWithoutCheck(notebook.GetNotebookArticlesInfo, self._url, page, count, sorting_method) + return CallWithoutCheck( + notebook.GetNotebookArticlesInfo, self._url, page, count, sorting_method + ) def __eq__(self, other: object) -> bool: """判断是否是同一个文集 @@ -881,10 +924,7 @@ def __eq__(self, other: object) -> bool: """ if not isinstance(other, Notebook): return False # 不是由文集类构建的必定不相等 - if self._url == other._url: - return True - else: - return False + return self._url == other._url def __hash__(self) -> int: """返回基于文集 URL 的哈希值 @@ -900,22 +940,29 @@ def __str__(self) -> str: Returns: str: 文集信息摘要 """ - return NameValueMappingToString({ - "名称": (self.name, False), - "URL": (self.url, False), - "作者名": (self.author_name, False), - "文章数": (self.articles_count, False), - "总字数": (self.wordage, False), - "关注者数": (self.subscribers_count, False), - "更新时间": (self.update_time, False) - }, title="文集信息摘要") + return NameValueMappingToString( + { + "名称": (self.name, False), + "URL": (self.url, False), + "作者名": (self.author_name, False), + "文章数": (self.articles_count, False), + "总字数": (self.wordage, False), + "关注者数": (self.subscribers_count, False), + "更新时间": (self.update_time, False), + }, + title="文集信息摘要", + ) class Collection: - """专题类 - """ - def __init__(self, collection_url: str = None, collection_slug: str = None, - collection_id: int = None): + """专题类""" + + def __init__( + self, + collection_url: Optional[str] = None, + collection_slug: Optional[str] = None, + collection_id: Optional[int] = None, + ) -> None: """初始化专题类 Args: @@ -925,12 +972,14 @@ def __init__(self, collection_url: str = None, collection_slug: str = None, """ # TODO: 支持通过 collection_url 获取 collection_id if not OnlyOne(collection_url, collection_slug): - raise("只能使用 URL 或 Slug 中的一个实例化专题对象") + raise ValueError("只能使用 URL 或 Slug 中的一个实例化专题对象") if collection_url: AssertCollectionUrl(collection_url) elif collection_slug: collection_url = CollectionSlugToCollectionUrl(collection_slug) + else: + raise ValueError("collection_url 和 collection_slug 至少需要传入一个") AssertCollectionStatusNormal(collection_url) self._url = collection_url @@ -938,7 +987,9 @@ def __init__(self, collection_url: str = None, collection_slug: str = None, self._id = collection_id if collection_id else None @classmethod - def from_url(cls, collection_url: str, collection_id: int = None) -> "Collection": + def from_url( + cls, collection_url: str, collection_id: Optional[int] = None + ) -> "Collection": """从专题 URL 构建专题对象 Args: @@ -951,7 +1002,9 @@ def from_url(cls, collection_url: str, collection_id: int = None) -> "Collection return cls(collection_url=collection_url, collection_id=collection_id) @classmethod - def from_slug(cls, collection_slug: str, collection_id: int = None) -> "Collection": + def from_slug( + cls, collection_slug: str, collection_id: Optional[int] = None + ) -> "Collection": """从专题 Slug 构建专题对象 Args: @@ -1040,7 +1093,9 @@ def info_update_time(self) -> datetime: Returns: datetime: 专题信息更新时间 """ - return CallWithoutCheck(collection.GetCollectionInformationUpdateTime, self._url) + return CallWithoutCheck( + collection.GetCollectionInformationUpdateTime, self._url + ) @property @cache_result_wrapper @@ -1124,8 +1179,9 @@ def subscribers_info(self, start_sort_id: int) -> List: return collection.GetCollectionSubscribersInfo(self._id, start_sort_id) @cache_result_wrapper - def articles_info(self, page: int = 1, count: int = 10, - sorting_method: str = "time") -> List[Dict]: + def articles_info( + self, page: int = 1, count: int = 10, sorting_method: str = "time" + ) -> List[Dict]: """获取专题文章信息 Args: @@ -1137,7 +1193,9 @@ def articles_info(self, page: int = 1, count: int = 10, Returns: List[Dict]: 专题中的文章信息 """ - return CallWithoutCheck(collection.GetCollectionArticlesInfo, self._url, page, count, sorting_method) + return CallWithoutCheck( + collection.GetCollectionArticlesInfo, self._url, page, count, sorting_method + ) def __eq__(self, other: object) -> bool: """判断是否是同一个专题 @@ -1150,10 +1208,7 @@ def __eq__(self, other: object) -> bool: """ if not isinstance(other, Collection): return False # 不是由专题类构建的必定不相等 - if self._url == other._url: - return True - else: - return False + return self._url == other._url def __hash__(self) -> int: """返回基于专题 URL 的哈希值 @@ -1169,23 +1224,28 @@ def __str__(self) -> str: Returns: str: 专题信息摘要 """ - return NameValueMappingToString({ - "专题名": (self.name, False), - "URL": (self.url, False), - "主编名": (self.owner_info["name"], False), - "图片链接": (self.avatar_url, False), - "文章数": (self.articles_count, False), - "关注者数": (self.subscribers_count, False), - "文章更新时间": (self.articles_update_time, False), - "信息更新时间": (self.info_update_time, False), - "简介": (self.introduction_text, True), - }, title="专题信息摘要") + return NameValueMappingToString( + { + "专题名": (self.name, False), + "URL": (self.url, False), + "主编名": (self.owner_info["name"], False), + "图片链接": (self.avatar_url, False), + "文章数": (self.articles_count, False), + "关注者数": (self.subscribers_count, False), + "文章更新时间": (self.articles_update_time, False), + "信息更新时间": (self.info_update_time, False), + "简介": (self.introduction_text, True), + }, + title="专题信息摘要", + ) class Island: - """小岛类 - """ - def __init__(self, island_url: str = None, island_slug: str = None): + """小岛类""" + + def __init__( + self, island_url: Optional[str] = None, island_slug: Optional[str] = None + ) -> None: """构建新的小岛对象 Args: @@ -1193,12 +1253,14 @@ def __init__(self, island_url: str = None, island_slug: str = None): island_slug (str, optional): 小岛 Slug. Defaults to None. """ if not OnlyOne(island_url, island_slug): - raise("只能使用 URL 或 Slug 中的一个实例化小岛对象") + raise ValueError("只能使用 URL 或 Slug 中的一个实例化小岛对象") if island_url: AssertIslandUrl(island_url) elif island_slug: island_url = IslandSlugToIslandUrl(island_slug) + else: + raise ValueError("island_url 和 island_slug 至少需要传入一个") AssertIslandStatusNormal(island_url) self._url = island_url @@ -1307,8 +1369,13 @@ def category(self) -> str: return CallWithoutCheck(island.GetIslandCategory, self._url) @cache_result_wrapper - def posts(self, start_sort_id: int = None, count: int = 10, - topic_id: int = None, sorting_method: str = "time") -> List[Dict]: + def posts( + self, + start_sort_id: Optional[int] = None, + count: int = 10, + topic_id: Optional[int] = None, + sorting_method: str = "time", + ) -> List[Dict]: """获取小岛帖子信息 Args: @@ -1321,7 +1388,14 @@ def posts(self, start_sort_id: int = None, count: int = 10, Returns: List[Dict]: 帖子信息 """ - return CallWithoutCheck(island.GetIslandPosts, self._url, start_sort_id, count, topic_id, sorting_method) + return CallWithoutCheck( + island.GetIslandPosts, + self._url, + start_sort_id, + count, + topic_id, + sorting_method, + ) def __eq__(self, other: object) -> bool: """判断是否是同一个小岛 @@ -1334,10 +1408,7 @@ def __eq__(self, other: object) -> bool: """ if not isinstance(other, Collection): return False # 不是由小岛类构建的必定不相等 - if self._url == other._url: - return True - else: - return False + return self._url == other._url def __hash__(self) -> int: """返回基于小岛 URL 的哈希值 @@ -1353,11 +1424,14 @@ def __str__(self) -> str: Returns: str: 小岛信息摘要 """ - return NameValueMappingToString({ - "小岛名": (self.name, False), - "URL": (self.url, False), - "分类": (self.category, False), - "成员数": (self.members_count, False), - "帖子数": (self.posts_count, False), - "简介": (self.introduction, True) - }, title="小岛信息摘要") + return NameValueMappingToString( + { + "小岛名": (self.name, False), + "URL": (self.url, False), + "分类": (self.category, False), + "成员数": (self.members_count, False), + "帖子数": (self.posts_count, False), + "简介": (self.introduction, True), + }, + title="小岛信息摘要", + ) diff --git a/JianshuResearchTools/rank.py b/JianshuResearchTools/rank.py index 7c91130..f9596fe 100644 --- a/JianshuResearchTools/rank.py +++ b/JianshuResearchTools/rank.py @@ -1,22 +1,31 @@ -from datetime import datetime, timedelta, date +from datetime import date, datetime, timedelta from typing import Dict, List -from .basic_apis import (GetArticlesFPRankListJsonDataApi, - GetAssetsRankJsonDataApi, - GetDailyArticleRankListJsonDataApi) +from .basic_apis import ( + GetArticlesFPRankListJsonDataApi, + GetAssetsRankJsonDataApi, + GetDailyArticleRankListJsonDataApi, +) from .convert import UserSlugToUserUrl from .exceptions import APIError, ResourceError -from .user import GetUserAssetsCount +from .user import GetUserFPCount __all__ = [ - "GetAssetsRankData", "GetDailyArticleRankData", "GetUserFPRankData", - "GetArticleFPRankBasicInfo", "GetUserFPRankData" + "GetAssetsRankData", + "GetDailyArticleRankData", + "GetUserFPRankData", + "GetArticleFPRankBasicInfo", + "GetUserFPRankData", ] def GetAssetsRankData(start_id: int = 1, get_full: bool = False) -> List[Dict]: """获取资产排行榜信息 + ! 2.10 之前的版本中存在数据错误,总资产(assets)以简书钻(FP)字段返回, + ! 为保证向后兼容,FP 字段在 v3 中暂不移除,其值与 assets 字段相同。 + ! 若 get_full = True,将获取真实的简书钻数据,并替换兼容用途的 FP 字段,简书贝(FTN)字段也将正确计算。 + Args: start_id (int, optional): 起始位置. Defaults to 1. get_full (bool, optional): 为 True 时获取简书贝和总资产数据. Defaults to False. @@ -34,13 +43,16 @@ def GetAssetsRankData(start_id: int = 1, get_full: bool = False) -> List[Dict]: "uslug": item["user"]["slug"], "name": item["user"]["nickname"], "avatar_url": item["user"]["avatar"], - "FP": item["amount"] / 1000 + "FP": item["amount"] / 1000, + "assets": item["amount"] / 1000, } if get_full: user_url = UserSlugToUserUrl(item_data["uslug"]) try: - item_data["Assets"] = GetUserAssetsCount(user_url, disable_check=True) - item_data["FTN"] = round(item_data["Assets"] - item_data["FP"], 3) # 处理浮点数精度问题 + item_data["FP"] = GetUserFPCount(user_url, disable_check=True) + item_data["FTN"] = round( + item_data["assets"] - item_data["FP"], 3 + ) # 处理浮点数精度问题 except APIError: pass result.append(item_data) @@ -61,7 +73,7 @@ def GetDailyArticleRankData() -> List[Dict]: "uslug": item["slug"], "name": item["nickname"], "avatar_url": item["avatar"], - "check_in_count": item["checkin_count"] + "check_in_count": item["checkin_count"], } result.append(item_data) return result @@ -96,7 +108,7 @@ def GetArticleFPRankData(target_date: str = "latest") -> List[Dict]: "author_avatar_url": item["author_avatar"], "fp_to_author": item["author_fp"] / 1000, "fp_to_voter": item["voter_fp"] / 1000, - "total_fp": item["fp"] / 1000 + "total_fp": item["fp"] / 1000, } result.append(item_data) return result @@ -121,15 +133,16 @@ def GetArticleFPRankBasicInfo(target_date: str = "latest") -> Dict: json_obj = GetArticlesFPRankListJsonDataApi(date=target_date, type_=None) if json_obj["notes"] == []: raise ResourceError(f"对应日期 {target_date} 的排行榜数据为空") - result = { + return { "total_fp": json_obj["fp"], "fp_to_author": json_obj["author_fp"], - "fp_to_voter": json_obj["voter_fp"] + "fp_to_voter": json_obj["voter_fp"], } - return result -def GetUserFPRankData(target_date: str = "latest", rank_type: str = "all") -> List[Dict]: +def GetUserFPRankData( + target_date: str = "latest", rank_type: str = "all" +) -> List[Dict]: """获取用户收益排行榜信息 目前只能获取 2020 年 6 月 20 日之后的数据。 @@ -144,11 +157,7 @@ def GetUserFPRankData(target_date: str = "latest", rank_type: str = "all") -> Li Returns: List[Dict]: 用户收益排行榜信息 """ - type_ = { - "all": None, - "write": "note", - "vote": "like" - }[rank_type] + type_ = {"all": None, "write": "note", "vote": "like"}[rank_type] json_obj = GetArticlesFPRankListJsonDataApi(date=target_date, type_=type_) if json_obj["users"] == []: raise ResourceError(f"对应日期 {target_date} 的排行榜数据为空") @@ -160,7 +169,7 @@ def GetUserFPRankData(target_date: str = "latest", rank_type: str = "all") -> Li "name": item["nickname"], "avatar_url": item["avatar"], "fp_from_write": item["author_fp"], - "fp_from_vote": item["voter_fp"] + "fp_from_vote": item["voter_fp"], } result.append(item_data) return result diff --git a/JianshuResearchTools/user.py b/JianshuResearchTools/user.py index 76bb72b..fc4d84d 100644 --- a/JianshuResearchTools/user.py +++ b/JianshuResearchTools/user.py @@ -1,32 +1,58 @@ from datetime import datetime from re import findall -from typing import Dict, Generator, List +from typing import Dict, Generator, List, Literal, Optional from lxml import etree from .assert_funcs import AssertUserStatusNormal, AssertUserUrl -from .basic_apis import (GetUserArticlesListJsonDataApi, - GetUserCollectionsAndNotebooksJsonDataApi, - GetUserFollowersListHtmlDataApi, - GetUserFollowingListHtmlDataApi, GetUserJsonDataApi, - GetUserNextAnniversaryDayHtmlDataApi, - GetUserPCHtmlDataApi, GetUserTimelineHtmlDataApi) -from .convert import (ArticleSlugToArticleUrl, CollectionSlugToCollectionUrl, - NotebookSlugToNotebookUrl, UserSlugToUserUrl, - UserUrlToUserSlug) +from .basic_apis import ( + GetUserArticlesListJsonDataApi, + GetUserCollectionsAndNotebooksJsonDataApi, + GetUserFollowersListHtmlDataApi, + GetUserFollowingListHtmlDataApi, + GetUserJsonDataApi, + GetUserNextAnniversaryDayHtmlDataApi, + GetUserPCHtmlDataApi, + GetUserTimelineHtmlDataApi, +) +from .convert import ( + ArticleSlugToArticleUrl, + CollectionSlugToCollectionUrl, + NotebookSlugToNotebookUrl, + UserSlugToUserUrl, + UserUrlToUserSlug, +) from .exceptions import APIError __all__ = [ - "GetUserName", "GetUserGender", "GetUserFollowersCount", - "GetUserFansCount", "GetUserArticlesCount", "GetUserWordage", - "GetUserLikesCount", "GetUserAssetsCount", "GetUserFPCount", - "GetUserFTNCount", "GetUserBadgesList", "GetUserLastUpdateTime", - "GetUserVIPInfo", "GetUserIntroductionHtml", "GetUserIntroductionText", - "GetUserNextAnniversaryDay", "GetUserNotebooksInfo", - "GetUserOwnCollectionsInfo", "GetUserManageableCollectionsInfo", - "GetUserArticlesInfo", "GetUserFollowingInfo", "GetUserFansInfo", - "GetUserAllBasicData", "GetUserTimelineInfo", "GetUserAllArticlesInfo", - "GetUserAllFollowingInfo", "GetUserAllFansInfo", "GetUserAllTimelineInfo" + "GetUserName", + "GetUserGender", + "GetUserFollowersCount", + "GetUserFansCount", + "GetUserArticlesCount", + "GetUserWordage", + "GetUserLikesCount", + "GetUserAssetsCount", + "GetUserFPCount", + "GetUserFTNCount", + "GetUserBadgesList", + "GetUserLastUpdateTime", + "GetUserVIPInfo", + "GetUserIntroductionHtml", + "GetUserIntroductionText", + "GetUserNextAnniversaryDay", + "GetUserNotebooksInfo", + "GetUserOwnCollectionsInfo", + "GetUserManageableCollectionsInfo", + "GetUserArticlesInfo", + "GetUserFollowingInfo", + "GetUserFansInfo", + "GetUserAllBasicData", + "GetUserTimelineInfo", + "GetUserAllArticlesInfo", + "GetUserAllFollowingInfo", + "GetUserAllFansInfo", + "GetUserAllTimelineInfo", ] @@ -44,8 +70,7 @@ def GetUserName(user_url: str, disable_check: bool = False) -> str: AssertUserUrl(user_url) AssertUserStatusNormal(user_url) json_obj = GetUserJsonDataApi(user_url) - result = json_obj["nickname"] - return result + return json_obj["nickname"] def GetUserGender(user_url: str, disable_check: bool = False) -> int: @@ -82,8 +107,7 @@ def GetUserFollowersCount(user_url: str, disable_check: bool = False) -> int: AssertUserUrl(user_url) AssertUserStatusNormal(user_url) json_obj = GetUserJsonDataApi(user_url) - result = json_obj["following_users_count"] - return result + return json_obj["following_users_count"] def GetUserFansCount(user_url: str, disable_check: bool = False) -> int: @@ -100,8 +124,7 @@ def GetUserFansCount(user_url: str, disable_check: bool = False) -> int: AssertUserUrl(user_url) AssertUserStatusNormal(user_url) json_obj = GetUserJsonDataApi(user_url) - result = json_obj["followers_count"] - return result + return json_obj["followers_count"] def GetUserArticlesCount(user_url: str, disable_check: bool = False) -> int: @@ -118,7 +141,9 @@ def GetUserArticlesCount(user_url: str, disable_check: bool = False) -> int: AssertUserUrl(user_url) AssertUserStatusNormal(user_url) html_obj = GetUserPCHtmlDataApi(user_url) - result = html_obj.xpath("//div[@class='info']/ul/li[3]/div[@class='meta-block']/a/p")[0].text + result = html_obj.xpath( + "//div[@class='info']/ul/li[3]/div[@class='meta-block']/a/p" + )[0].text result = int(result) return result @@ -137,8 +162,7 @@ def GetUserWordage(user_url: str, disable_check: bool = False) -> int: AssertUserUrl(user_url) AssertUserStatusNormal(user_url) json_obj = GetUserJsonDataApi(user_url) - result = json_obj["total_wordage"] - return result + return json_obj["total_wordage"] def GetUserLikesCount(user_url: str, disable_check: bool = False) -> int: @@ -155,8 +179,7 @@ def GetUserLikesCount(user_url: str, disable_check: bool = False) -> int: AssertUserUrl(user_url) AssertUserStatusNormal(user_url) json_obj = GetUserJsonDataApi(user_url) - result = json_obj["total_likes_count"] - return result + return json_obj["total_likes_count"] def GetUserAssetsCount(user_url: str, disable_check: bool = False) -> float: @@ -180,9 +203,11 @@ def GetUserAssetsCount(user_url: str, disable_check: bool = False) -> float: AssertUserStatusNormal(user_url) html_obj = GetUserPCHtmlDataApi(user_url) try: - result = html_obj.xpath("//div[@class='info']/ul/li[6]/div[@class='meta-block']/p")[0].text + result = html_obj.xpath( + "//div[@class='info']/ul/li[6]/div[@class='meta-block']/p" + )[0].text except IndexError: - raise APIError("受简书 API 限制,用户无文章时无法获取其总资产数据") + raise APIError("受简书 API 限制,用户无文章时无法获取其总资产数据") from None result = float(result.replace(".", "").replace("w", "000")) return result @@ -269,8 +294,7 @@ def GetUserLastUpdateTime(user_url: str, disable_check: bool = False) -> datetim AssertUserUrl(user_url) AssertUserStatusNormal(user_url) json_obj = GetUserJsonDataApi(user_url) - result = datetime.fromtimestamp(json_obj["last_updated_at"]) - return result + return datetime.fromtimestamp(json_obj["last_updated_at"]) def GetUserVIPInfo(user_url: str, disable_check: bool = False) -> Dict: @@ -289,19 +313,13 @@ def GetUserVIPInfo(user_url: str, disable_check: bool = False) -> Dict: json_obj = GetUserJsonDataApi(user_url) try: result = { - "vip_type": { - "bronze": "铜牌", - "silver": "银牌", - "gold": "黄金", - "platina": "白金" - }[json_obj["member"]["type"]], - "expire_date": datetime.fromtimestamp(json_obj["member"]["expires_at"]) + "vip_type": {"bronze": "铜牌", "silver": "银牌", "gold": "黄金", "platina": "白金"}[ + json_obj["member"]["type"] + ], + "expire_date": datetime.fromtimestamp(json_obj["member"]["expires_at"]), } except KeyError: - result = { - "vip_type": None, - "expire_date": None - } + result = {"vip_type": None, "expire_date": None} return result @@ -319,8 +337,7 @@ def GetUserIntroductionHtml(user_url: str, disable_check: bool = False) -> str: AssertUserUrl(user_url) AssertUserStatusNormal(user_url) json_obj = GetUserJsonDataApi(user_url) - result = json_obj["intro"] - return result + return json_obj["intro"] def GetUserIntroductionText(user_url: str, disable_check: bool = False) -> str: @@ -337,9 +354,9 @@ def GetUserIntroductionText(user_url: str, disable_check: bool = False) -> str: AssertUserUrl(user_url) AssertUserStatusNormal(user_url) json_obj = GetUserJsonDataApi(user_url) - if json_obj["intro"] == "": # 简介为空 + if json_obj["intro"] == "": # 简介为空 return "" - html_obj = etree.HTML(json_obj["intro"]) + html_obj = etree.HTML(json_obj["intro"]) # type: ignore result = html_obj.xpath("//*/text()") result = "\n".join(result) return result @@ -379,14 +396,12 @@ def GetUserNotebooksInfo(user_url: str, disable_check: bool = False) -> List[Dic if not disable_check: AssertUserUrl(user_url) AssertUserStatusNormal(user_url) - json_obj = GetUserCollectionsAndNotebooksJsonDataApi(user_url=user_url, user_slug=UserUrlToUserSlug(user_url)) + json_obj = GetUserCollectionsAndNotebooksJsonDataApi( + user_url=user_url, user_slug=UserUrlToUserSlug(user_url) + ) result = [] for item in json_obj["notebooks"]: - item_data = { - "nid": item["id"], - "name": item["name"], - "is_book": item["book"] - } + item_data = {"nid": item["id"], "name": item["name"], "is_book": item["book"]} if item["book"]: item_data["is_paid_book"] = item["paid_book"] # 如果是连载,则判断是否是付费连载 result.append(item_data) @@ -406,20 +421,24 @@ def GetUserOwnCollectionsInfo(user_url: str, disable_check: bool = False) -> Lis if not disable_check: AssertUserUrl(user_url) AssertUserStatusNormal(user_url) - json_obj = GetUserCollectionsAndNotebooksJsonDataApi(user_url=user_url, user_slug=UserUrlToUserSlug(user_url)) + json_obj = GetUserCollectionsAndNotebooksJsonDataApi( + user_url=user_url, user_slug=UserUrlToUserSlug(user_url) + ) result = [] for item in json_obj["own_collections"]: item_data = { "cid": item["id"], "cslug": item["slug"], "name": item["title"], - "avatar_url": item["avatar"] + "avatar_url": item["avatar"], } result.append(item_data) return result -def GetUserManageableCollectionsInfo(user_url: str, disable_check: bool = False) -> List[Dict]: +def GetUserManageableCollectionsInfo( + user_url: str, disable_check: bool = False +) -> List[Dict]: """获取用户管理的专题信息 Args: @@ -432,28 +451,35 @@ def GetUserManageableCollectionsInfo(user_url: str, disable_check: bool = False) if not disable_check: AssertUserUrl(user_url) AssertUserStatusNormal(user_url) - json_obj = GetUserCollectionsAndNotebooksJsonDataApi(user_url=user_url, user_slug=UserUrlToUserSlug(user_url)) + json_obj = GetUserCollectionsAndNotebooksJsonDataApi( + user_url=user_url, user_slug=UserUrlToUserSlug(user_url) + ) result = [] for item in json_obj["manageable_collections"]: item_data = { "cid": item["id"], "cslug": item["slug"], "name": item["title"], - "avatar_url": item["avatar"] + "avatar_url": item["avatar"], } result.append(item_data) return result -def GetUserArticlesInfo(user_url: str, page: int = 1, count: int = 10, - sorting_method: str = "time", disable_check: bool = False) -> List[Dict]: +def GetUserArticlesInfo( + user_url: str, + page: int = 1, + count: int = 10, + sorting_method: Literal["time", "comment_time", "hot"] = "time", + disable_check: bool = False, +) -> List[Dict]: """获取用户文章信息 Args: user_url (str): 用户个人主页 URL page (int, optional): 页码,与网页端文章顺序相同. Defaults to 1. count (int, optional): 获取的文章数量. Defaults to 10. - sorting_method (str, optional): 排序方法,time 为按照发布时间排序, + sorting_method (Literal["time", "comment_time", "hot"], optional): 排序方法,time 为按照发布时间排序, comment_time 为按照最近评论时间排序,hot 为按照热度排序. Defaults to "time". disable_check (bool): 禁用参数有效性检查. Defaults to False. @@ -466,17 +492,20 @@ def GetUserArticlesInfo(user_url: str, page: int = 1, count: int = 10, order_by = { "time": "added_at", "comment_time": "commented_at", - "hot": "top" + "hot": "top", }[sorting_method] - json_obj = GetUserArticlesListJsonDataApi(user_url=user_url, page=page, - count=count, order_by=order_by) + json_obj = GetUserArticlesListJsonDataApi( + user_url=user_url, page=page, count=count, order_by=order_by + ) result = [] for item in json_obj: item_data = { "aid": item["object"]["data"]["id"], "title": item["object"]["data"]["title"], "aslug": item["object"]["data"]["slug"], - "release_time": datetime.fromisoformat(item["object"]["data"]["first_shared_at"]), + "release_time": datetime.fromisoformat( + item["object"]["data"]["first_shared_at"] + ).replace(tzinfo=None), "first_image_url": item["object"]["data"]["list_image_url"], "summary": item["object"]["data"]["public_abbr"], "views_count": item["object"]["data"]["views_count"], @@ -488,17 +517,19 @@ def GetUserArticlesInfo(user_url: str, page: int = 1, count: int = 10, "uid": item["object"]["data"]["user"]["id"], "name": item["object"]["data"]["user"]["nickname"], "uslug": item["object"]["data"]["user"]["slug"], - "avatar_url": item["object"]["data"]["user"]["avatar"] + "avatar_url": item["object"]["data"]["user"]["avatar"], }, "total_fp_amount": item["object"]["data"]["total_fp_amount"] / 1000, "comments_count": item["object"]["data"]["public_comments_count"], - "rewards_count": item["object"]["data"]["total_rewards_count"] + "rewards_count": item["object"]["data"]["total_rewards_count"], } result.append(item_data) return result -def GetUserFollowingInfo(user_url: str, page: int = 1, disable_check: bool = False) -> List[Dict]: +def GetUserFollowingInfo( + user_url: str, page: int = 1, disable_check: bool = False +) -> List[Dict]: """获取用户关注者信息 Args: @@ -527,14 +558,20 @@ def GetUserFollowingInfo(user_url: str, page: int = 1, disable_check: bool = Fal "followers_count": int(followers_raw_data[index].text.replace("关注 ", "")), "fans_count": int(fans_raw_data[index].text.replace("粉丝", "")), "articles_count": int(articles_raw_data[index].text.replace("文章 ", "")), - "words_count": int(findall(r"\d+", words_and_likes_raw_data[index].text)[0]), - "likes_count": int(findall(r"\d+", words_and_likes_raw_data[index].text)[1]) + "words_count": int( + findall(r"\d+", words_and_likes_raw_data[index].text)[0] + ), + "likes_count": int( + findall(r"\d+", words_and_likes_raw_data[index].text)[1] + ), } result.append(item_data) return result -def GetUserFansInfo(user_url: str, page: int = 1, disable_check: bool = False) -> List[Dict]: +def GetUserFansInfo( + user_url: str, page: int = 1, disable_check: bool = False +) -> List[Dict]: """获取用户粉丝信息 Args: @@ -563,8 +600,12 @@ def GetUserFansInfo(user_url: str, page: int = 1, disable_check: bool = False) - "followers_count": int(followers_raw_data[index].text.replace("关注 ", "")), "fans_count": int(fans_raw_data[index].text.replace("粉丝", "")), "articles_count": int(articles_raw_data[index].text.replace("文章 ", "")), - "words_count": int(findall(r"\d+", words_and_likes_raw_data[index].text)[0]), - "likes_count": int(findall(r"\d+", words_and_likes_raw_data[index].text)[1]) + "words_count": int( + findall(r"\d+", words_and_likes_raw_data[index].text)[0] + ), + "likes_count": int( + findall(r"\d+", words_and_likes_raw_data[index].text)[1] + ), } result.append(item_data) return result @@ -586,7 +627,9 @@ def GetUserAllBasicData(user_url: str, disable_check: bool = False) -> Dict: result = {} json_obj = GetUserJsonDataApi(user_url) html_obj = GetUserPCHtmlDataApi(user_url) - anniversary_day_html_obj = GetUserNextAnniversaryDayHtmlDataApi(UserUrlToUserSlug(user_url)) + anniversary_day_html_obj = GetUserNextAnniversaryDayHtmlDataApi( + UserUrlToUserSlug(user_url) + ) result["name"] = json_obj["nickname"] result["url"] = user_url @@ -598,8 +641,12 @@ def GetUserAllBasicData(user_url: str, disable_check: bool = False) -> Dict: result["wordage"] = json_obj["total_wordage"] result["likes_count"] = json_obj["total_likes_count"] try: - result["assets_count"] = html_obj.xpath("//div[@class='info']/ul/li[6]/div[@class='meta-block']/p")[0].text - result["assets_count"] = float(result["assets_count"].replace(".", "").replace("w", "000")) + result["assets_count"] = html_obj.xpath( + "//div[@class='info']/ul/li[6]/div[@class='meta-block']/p" + )[0].text + result["assets_count"] = float( + result["assets_count"].replace(".", "").replace("w", "000") + ) except IndexError: result["assets_count"] = None if json_obj["total_wordage"] == 0 and json_obj["jsd_balance"] == 0: @@ -612,35 +659,41 @@ def GetUserAllBasicData(user_url: str, disable_check: bool = False) -> Dict: else: result["FTN_count"] = None result["badges_list"] = html_obj.xpath("//li[@class='badge-icon']/a/text()") - result["badges_list"] = [item.replace(" ", "").replace("\n", "") for item in result["badges_list"]] # 移除空格和换行符 - result["badges_list"] = [item for item in result["badges_list"] if item != ""] # 去除空值 + result["badges_list"] = [ + item.replace(" ", "").replace("\n", "") for item in result["badges_list"] + ] # 移除空格和换行符 + result["badges_list"] = [ + item for item in result["badges_list"] if item != "" + ] # 去除空值 result["last_update_time"] = datetime.fromtimestamp(json_obj["last_updated_at"]) try: result["vip_info"] = { - "vip_type": { - "bronze": "铜牌", - "silver": "银牌", - "gold": "黄金", - "platina": "白金" - }[json_obj["member"]["type"]], - "expire_date": datetime.fromtimestamp(json_obj["member"]["expires_at"]) + "vip_type": {"bronze": "铜牌", "silver": "银牌", "gold": "黄金", "platina": "白金"}[ + json_obj["member"]["type"] + ], + "expire_date": datetime.fromtimestamp(json_obj["member"]["expires_at"]), } except KeyError: - result["vip_info"] = { - "vip_type": None, - "expire_date": None - } + result["vip_info"] = {"vip_type": None, "expire_date": None} result["introduction_html"] = json_obj["intro"] if not result["introduction_html"]: result["introduction_text"] = "" else: - result["introduction_text"] = "\n".join(etree.HTML(result["introduction_html"]).xpath("//*/text()")) - result["next_anniversary_day"] = anniversary_day_html_obj.xpath('//*[@id="app"]/div[1]/div/text()')[0] - result["next_anniversary_day"] = datetime.fromisoformat("-".join(findall(r"\d+", result["next_anniversary_day"]))) + result["introduction_text"] = "\n".join( + etree.HTML(result["introduction_html"]).xpath("//*/text()") # type: ignore + ) + result["next_anniversary_day"] = anniversary_day_html_obj.xpath( + '//*[@id="app"]/div[1]/div/text()' + )[0] + result["next_anniversary_day"] = datetime.fromisoformat( + "-".join(findall(r"\d+", result["next_anniversary_day"])) + ) return result -def GetUserTimelineInfo(user_url: str, max_id: int = 1000000000, disable_check: bool = False) -> List[Dict]: +def GetUserTimelineInfo( + user_url: str, max_id: Optional[int] = 1000000000, disable_check: bool = False +) -> List[Dict]: """获取用户动态信息 !在极少数情况下可能会遇到不在可解析列表中的动态类型,此时程序会跳过这条动态,不会抛出异常 @@ -664,158 +717,341 @@ def GetUserTimelineInfo(user_url: str, max_id: int = 1000000000, disable_check: for block in blocks: item_data = { "operation_id": int(block.xpath("//li/@id")[0][5:]), - "operation_type": block.xpath("//span[starts-with(@data-datetime, '20')]/@data-type")[0], - "operation_time": datetime.fromisoformat(block.xpath("//span[starts-with(@data-datetime, '20')]/@data-datetime")[0]) + "operation_type": block.xpath( + "//span[starts-with(@data-datetime, '20')]/@data-type" + )[0], + "operation_time": datetime.fromisoformat( + block.xpath("//span[starts-with(@data-datetime, '20')]/@data-datetime")[ + 0 + ] + ).replace(tzinfo=None), } if item_data["operation_type"] == "like_note": # 对文章点赞 item_data["operation_type"] = "like_article" # 鬼知道谁把对文章点赞写成 like_note 的 item_data["operator_name"] = block.xpath("//a[@class='nickname']/text()")[0] - item_data["operator_url"] = UserSlugToUserUrl(block.xpath("//a[@class='nickname']/@href")[0][3:]) - item_data["operator_avatar_url"] = block.xpath("//a[@class='avatar']/img/@src")[0] - item_data["target_article_title"] = block.xpath("//a[@class='title']/text()")[0] - item_data["target_article_url"] = ArticleSlugToArticleUrl(block.xpath("//a[@class='title']/@href")[0][3:]) - item_data["target_user_name"] = block.xpath("//div[@class='origin-author']/a/text()")[0] - item_data["target_user_url"] = UserSlugToUserUrl(block.xpath("//div[@class='meta']/a/@href")[0][3:]) - item_data["target_article_reads_count"] = int(block.xpath("//div[@class='meta']/a/text()")[1]) - item_data["target_article_likes_count"] = int(block.xpath("//div[@class='meta']/span/text()")[0]) + item_data["operator_url"] = UserSlugToUserUrl( + block.xpath("//a[@class='nickname']/@href")[0][3:] + ) + item_data["operator_avatar_url"] = block.xpath( + "//a[@class='avatar']/img/@src" + )[0] + item_data["target_article_title"] = block.xpath( + "//a[@class='title']/text()" + )[0] + item_data["target_article_url"] = ArticleSlugToArticleUrl( + block.xpath("//a[@class='title']/@href")[0][3:] + ) + item_data["target_user_name"] = block.xpath( + "//div[@class='origin-author']/a/text()" + )[0] + item_data["target_user_url"] = UserSlugToUserUrl( + block.xpath("//div[@class='origin-author']/a/@href")[0].split("/")[-1] + ) + item_data["target_article_reads_count"] = int( + block.xpath("//div[@class='meta']/a/text()")[1] + ) + item_data["target_article_likes_count"] = int( + block.xpath("//div[@class='meta']/span/text()")[0] + ) try: - item_data["target_article_comments_count"] = int(block.xpath("//div[@class='meta']/a/text()")[3]) + item_data["target_article_comments_count"] = int( + block.xpath("//div[@class='meta']/a/text()")[3] + ) except IndexError: # 文章没有评论或评论区关闭 item_data["target_article_comments_count"] = 0 try: - item_data["target_article_description"] = block.xpath("//p[@class='abstract']/text()")[0] + item_data["target_article_description"] = block.xpath( + "//p[@class='abstract']/text()" + )[0] except IndexError: # 文章没有摘要 item_data["target_article_description"] = "" elif item_data["operation_type"] == "like_comment": # 对评论点赞 item_data["operator_name"] = block.xpath("//a[@class='nickname']/text()")[0] - item_data["operator_url"] = UserSlugToUserUrl(block.xpath("//a[@class='nickname']/@href")[0][3:]) - item_data["operator_avatar_url"] = block.xpath("//a[@class='avatar']/img/@src")[0] - item_data["comment_content"] = "\n".join(block.xpath("//p[@class='comment']/text()")) - item_data["target_article_title"] = block.xpath("//blockquote/div/span/a/text()")[0] - item_data["target_article_url"] = ArticleSlugToArticleUrl(block.xpath("//blockquote/div/span/a/@href")[0][3:]) + item_data["operator_url"] = UserSlugToUserUrl( + block.xpath("//a[@class='nickname']/@href")[0][3:] + ) + item_data["operator_avatar_url"] = block.xpath( + "//a[@class='avatar']/img/@src" + )[0] + item_data["comment_content"] = "\n".join( + block.xpath("//p[@class='comment']/text()") + ) + item_data["target_article_title"] = block.xpath( + "//blockquote/div/span/a/text()" + )[0] + item_data["target_article_url"] = ArticleSlugToArticleUrl( + block.xpath("//blockquote/div/span/a/@href")[0][3:] + ) item_data["target_user_name"] = block.xpath("//blockquote/div/a/text()")[0] - item_data["target_user_url"] = UserSlugToUserUrl(block.xpath("//blockquote/div/a/@href")[0][3:]) + item_data["target_user_url"] = UserSlugToUserUrl( + block.xpath("//blockquote/div/a/@href")[0][3:] + ) elif item_data["operation_type"] == "share_note": # 发表文章 item_data["operation_type"] = "publish_article" # 鬼知道谁把发表文章写成 share_note 的 item_data["operator_name"] = block.xpath("//a[@class='nickname']/text()")[0] - item_data["operator_url"] = UserSlugToUserUrl(block.xpath("//a[@class='nickname']/@href")[0][3:]) - item_data["operator_avatar_url"] = block.xpath("//a[@class='avatar']/img/@src")[0] - item_data["target_article_title"] = block.xpath("//a[@class='title']/text()")[0] - item_data["target_article_url"] = ArticleSlugToArticleUrl(block.xpath("//a[@class='title']/@href")[0][3:]) - item_data["target_article_reads_count"] = int(block.xpath("//div[@class='meta']/a/text()")[1]) - item_data["target_article_likes_count"] = int(block.xpath("//div[@class='meta']/span/text()")[0]) - item_data["target_article_description"] = "\n".join(block.xpath("//p[@class='abstract']/text()")) + item_data["operator_url"] = UserSlugToUserUrl( + block.xpath("//a[@class='nickname']/@href")[0][3:] + ) + item_data["operator_avatar_url"] = block.xpath( + "//a[@class='avatar']/img/@src" + )[0] + item_data["target_article_title"] = block.xpath( + "//a[@class='title']/text()" + )[0] + item_data["target_article_url"] = ArticleSlugToArticleUrl( + block.xpath("//a[@class='title']/@href")[0][3:] + ) + item_data["target_article_reads_count"] = int( + block.xpath("//div[@class='meta']/a/text()")[1] + ) + item_data["target_article_likes_count"] = int( + block.xpath("//div[@class='meta']/span/text()")[0] + ) + item_data["target_article_description"] = "\n".join( + block.xpath("//p[@class='abstract']/text()") + ) try: - item_data["target_article_comments_count"] = int(block.xpath("//div[@class='meta']/a/text()")[3]) + item_data["target_article_comments_count"] = int( + block.xpath("//div[@class='meta']/a/text()")[3] + ) except IndexError: item_data["target_article_comments_count"] = 0 elif item_data["operation_type"] == "comment_note": # 发表评论 + item_data[ + "operation_type" + ] = "comment_article" # 鬼知道谁把评论文章写成 comment_note 的 item_data["operator_name"] = block.xpath("//a[@class='nickname']/text()")[0] - item_data["operator_url"] = UserSlugToUserUrl(block.xpath("//a[@class='nickname']/@href")[0][3:]) - item_data["operator_avatar_url"] = block.xpath("//a[@class='avatar']/img/@src")[0] - item_data["comment_content"] = "\n".join(block.xpath("//p[@class='comment']/text()")) - item_data["target_article_title"] = block.xpath("//a[@class='title']/text()")[0] - item_data["target_article_url"] = ArticleSlugToArticleUrl(block.xpath("//a[@class='title']/@href")[0][3:]) - item_data["target_user_name"] = block.xpath("//div[@class='origin-author']/a/text()")[0] - item_data["target_user_url"] = UserSlugToUserUrl(block.xpath("//div[@class='meta']/a/@href")[0][3:]) - item_data["target_article_reads_count"] = int(block.xpath("//div[@class='meta']/a/text()")[1]) - item_data["target_article_likes_count"] = int(block.xpath("//div[@class='meta']/span/text()")[0]) + item_data["operator_url"] = UserSlugToUserUrl( + block.xpath("//a[@class='nickname']/@href")[0][3:] + ) + item_data["operator_avatar_url"] = block.xpath( + "//a[@class='avatar']/img/@src" + )[0] + item_data["comment_content"] = "\n".join( + block.xpath("//p[@class='comment']/text()") + ) + item_data["target_article_title"] = block.xpath( + "//a[@class='title']/text()" + )[0] + item_data["target_article_url"] = ArticleSlugToArticleUrl( + block.xpath("//a[@class='title']/@href")[0][3:] + ) + item_data["target_user_name"] = block.xpath( + "//div[@class='origin-author']/a/text()" + )[0] + item_data["target_user_url"] = UserSlugToUserUrl( + block.xpath("//div[@class='origin-author']/a/@href")[0].split("/")[-1] + ) + item_data["target_article_reads_count"] = int( + block.xpath("//div[@class='meta']/a/text()")[1] + ) + item_data["target_article_likes_count"] = int( + block.xpath("//div[@class='meta']/span/text()")[0] + ) try: - item_data["target_article_comments_count"] = int(block.xpath("//div[@class='meta']/a/text()")[3]) + item_data["target_article_comments_count"] = int( + block.xpath("//div[@class='meta']/a/text()")[3] + ) except IndexError: # 文章没有评论或评论区关闭 item_data["target_article_comments_count"] = 0 try: - item_data["target_article_description"] = block.xpath("//p[@class='abstract']/text()")[0] + item_data["target_article_description"] = block.xpath( + "//p[@class='abstract']/text()" + )[0] except IndexError: # 文章没有描述 item_data["target_article_description"] = "" try: - item_data["target_article_rewards_count"] = int(block.xpath("//div[@class='meta']/span/text()")[1]) + item_data["target_article_rewards_count"] = int( + block.xpath("//div[@class='meta']/span/text()")[1] + ) except IndexError: # 没有赞赏数据 item_data["target_article_rewards_count"] = 0 elif item_data["operation_type"] == "like_notebook": # 关注文集 - item_data["operation_type"] = "follow_notebook" # 鬼知道谁把关注文集写成 like_notebook 的 + item_data[ + "operation_type" + ] = "follow_notebook" # 鬼知道谁把关注文集写成 like_notebook 的 item_data["operator_name"] = block.xpath("//a[@class='nickname']/text()")[0] - item_data["operator_url"] = UserSlugToUserUrl(block.xpath("//a[@class='nickname']/@href")[0][4:]) - item_data["operator_avatar_url"] = block.xpath("//a[@class='avatar']/img/@src")[0] - item_data["target_notebook_title"] = block.xpath("//a[@class='title']/text()")[0] - item_data["target_notebook_url"] = NotebookSlugToNotebookUrl(block.xpath("//a[@class='title']/@href")[0][3:]) - item_data["target_notebook_avatar_url"] = block.xpath("//div[@class='follow-detail']/div/a/img/@src")[0] - item_data["target_user_name"] = block.xpath("//a[@class='creater']/text()")[0] - item_data["target_user_url"] = UserSlugToUserUrl(block.xpath("//a[@class='creater']/@href")[0][3:]) - item_data["target_notebook_articles_count"] = int(findall(r"\d+", block.xpath("//div[@class='info'][1]/p/text()")[1])[0]) - item_data["target_notebook_subscribers_count"] = int(findall(r"\d+", block.xpath("//div[@class='info'][1]/p/text()")[1])[1]) + item_data["operator_url"] = UserSlugToUserUrl( + block.xpath("//a[@class='nickname']/@href")[0][4:] + ) + item_data["operator_avatar_url"] = block.xpath( + "//a[@class='avatar']/img/@src" + )[0] + item_data["target_notebook_title"] = block.xpath( + "//a[@class='title']/text()" + )[0] + item_data["target_notebook_url"] = NotebookSlugToNotebookUrl( + block.xpath("//a[@class='title']/@href")[0][4:] + ) + item_data["target_notebook_avatar_url"] = block.xpath( + "//div[@class='follow-detail']/div/a/img/@src" + )[0] + item_data["target_user_name"] = block.xpath("//a[@class='creater']/text()")[ + 0 + ] + item_data["target_user_url"] = UserSlugToUserUrl( + block.xpath("//a[@class='creater']/@href")[0][3:] + ) + item_data["target_notebook_articles_count"] = int( + findall(r"\d+", block.xpath("//div[@class='info'][1]/p/text()")[1])[0] + ) + item_data["target_notebook_subscribers_count"] = int( + findall(r"\d+", block.xpath("//div[@class='info'][1]/p/text()")[1])[1] + ) elif item_data["operation_type"] == "like_collection": # 关注专题 - item_data["operator_type"] = "follow_collection" # 鬼知道谁把关注专题写成 like_collection 的 + item_data[ + "operation_type" + ] = "follow_collection" # 鬼知道谁把关注专题写成 like_collection 的 item_data["operator_name"] = block.xpath("//a[@class='nickname']/text()")[0] - item_data["operator_url"] = UserSlugToUserUrl(block.xpath("//a[@class='nickname']/@href")[0][4:]) - item_data["operator_avatar_url"] = block.xpath("//a[@class='avatar']/img/@src")[0] - item_data["target_collection_title"] = block.xpath("//a[@class='title']/text()")[0] - item_data["target_collection_url"] = CollectionSlugToCollectionUrl(block.xpath("//a[@class='title']/@href")[0][3:]) - item_data["target_collection_avatar_url"] = block.xpath("//div[@class='follow-detail']/div/a/img/@src")[0] - item_data["target_user_name"] = block.xpath("//a[@class='creater']/text()")[0] - item_data["target_user_url"] = UserSlugToUserUrl(block.xpath("//a[@class='creater']/@href")[0][3:]) - item_data["target_collection_articles_count"] = int(findall(r"\d+", block.xpath("//div[@class='info'][1]/p/text()")[1])[0]) - item_data["target_collection_subscribers_count"] = int(findall(r"\d+", block.xpath("//div[@class='info'][1]/p/text()")[1])[1]) + item_data["operator_url"] = UserSlugToUserUrl( + block.xpath("//a[@class='nickname']/@href")[0][4:] + ) + item_data["operator_avatar_url"] = block.xpath( + "//a[@class='avatar']/img/@src" + )[0] + item_data["target_collection_title"] = block.xpath( + "//a[@class='title']/text()" + )[0] + item_data["target_collection_url"] = CollectionSlugToCollectionUrl( + block.xpath("//a[@class='title']/@href")[0][3:] + ) + item_data["target_collection_avatar_url"] = block.xpath( + "//div[@class='follow-detail']/div/a/img/@src" + )[0] + item_data["target_user_name"] = block.xpath("//a[@class='creater']/text()")[ + 0 + ] + item_data["target_user_url"] = UserSlugToUserUrl( + block.xpath("//a[@class='creater']/@href")[0][3:] + ) + item_data["target_collection_articles_count"] = int( + findall(r"\d+", block.xpath("//div[@class='info'][1]/p/text()")[1])[0] + ) + item_data["target_collection_subscribers_count"] = int( + findall(r"\d+", block.xpath("//div[@class='info'][1]/p/text()")[1])[1] + ) elif item_data["operation_type"] == "like_user": # 关注用户 item_data["operation_type"] = "follow_user" # 鬼知道谁把关注用户写成 like_user 的 item_data["operator_name"] = block.xpath("//a[@class='nickname']/text()")[0] - item_data["operator_url"] = UserSlugToUserUrl(block.xpath("//a[@class='nickname']/@href")[0][4:]) - item_data["operator_avatar_url"] = block.xpath("//a[@class='avatar']/img/@src")[0] - item_data["target_user_name"] = block.xpath("//div[@class='info']/a[@class='title']/text()")[0] - item_data["target_user_url"] = UserSlugToUserUrl(block.xpath("//div[@class='info']/a[@class='title']/@href")[0][3:]) - item_data["target_user_wordage"] = int(findall(r"\d+", block.xpath("//div[@class='follow-detail']/div[@class='info']/p/text()")[0])[0]) - item_data["target_user_fans_count"] = int(findall(r"\d+", block.xpath("//div[@class='follow-detail']/div[@class='info']/p/text()")[0])[1]) - item_data["target_user_likes_count"] = int(findall(r"\d+", block.xpath("//div[@class='follow-detail']/div[@class='info']/p/text()")[0])[2]) - item_data["target_user_description"] = "\n".join(block.xpath("//div[@class='signature']/text()")) + item_data["operator_url"] = UserSlugToUserUrl( + block.xpath("//a[@class='nickname']/@href")[0][4:] + ) + item_data["operator_avatar_url"] = block.xpath( + "//a[@class='avatar']/img/@src" + )[0] + item_data["target_user_name"] = block.xpath( + "//div[@class='info']/a[@class='title']/text()" + )[0] + item_data["target_user_url"] = UserSlugToUserUrl( + block.xpath("//div[@class='info']/a[@class='title']/@href")[0][3:] + ) + item_data["target_user_wordage"] = int( + findall( + r"\d+", + block.xpath( + "//div[@class='follow-detail']/div[@class='info']/p/text()" + )[0], + )[0] + ) + item_data["target_user_fans_count"] = int( + findall( + r"\d+", + block.xpath( + "//div[@class='follow-detail']/div[@class='info']/p/text()" + )[0], + )[1] + ) + item_data["target_user_likes_count"] = int( + findall( + r"\d+", + block.xpath( + "//div[@class='follow-detail']/div[@class='info']/p/text()" + )[0], + )[2] + ) + item_data["target_user_description"] = "\n".join( + block.xpath("//div[@class='signature']/text()") + ) elif item_data["operation_type"] == "reward_note": # 赞赏文章 item_data["operation_type"] = "reward_article" # 鬼知道谁把赞赏文章写成 reward_note 的 item_data["operator_name"] = block.xpath("//a[@class='nickname']/text()")[0] - item_data["operator_url"] = UserSlugToUserUrl(block.xpath("//a[@class='nickname']/@href")[0][4:]) - item_data["operator_avatar_url"] = block.xpath("//a[@class='avatar']/img/@src")[0] - item_data["target_article_title"] = block.xpath("//a[@class='title']/text()")[0] - item_data["target_article_url"] = ArticleSlugToArticleUrl(block.xpath("//a[@class='title']/@href")[0][3:]) - item_data["target_user_name"] = block.xpath("//div[@class='origin-author']/a/text()")[0] - item_data["target_user_url"] = UserSlugToUserUrl(block.xpath("//div[@class='meta']/a/@href")[0][3:]) - item_data["target_article_reads_count"] = int(block.xpath("//div[@class='meta']/a/text()")[1]) - item_data["target_article_likes_count"] = int(block.xpath("//div[@class='meta']/span/text()")[0]) + item_data["operator_url"] = UserSlugToUserUrl( + block.xpath("//a[@class='nickname']/@href")[0][4:] + ) + item_data["operator_avatar_url"] = block.xpath( + "//a[@class='avatar']/img/@src" + )[0] + item_data["target_article_title"] = block.xpath( + "//a[@class='title']/text()" + )[0] + item_data["target_article_url"] = ArticleSlugToArticleUrl( + block.xpath("//a[@class='title']/@href")[0][3:] + ) + item_data["target_user_name"] = block.xpath( + "//div[@class='origin-author']/a/text()" + )[0] + item_data["target_user_url"] = UserSlugToUserUrl( + block.xpath("//div[@class='meta']/a/@href")[0][3:] + ) + item_data["target_article_reads_count"] = int( + block.xpath("//div[@class='meta']/a/text()")[1] + ) + item_data["target_article_likes_count"] = int( + block.xpath("//div[@class='meta']/span/text()")[0] + ) try: - item_data["target_article_comments_count"] = int(block.xpath("//div[@class='meta']/a/text()")[3]) + item_data["target_article_comments_count"] = int( + block.xpath("//div[@class='meta']/a/text()")[3] + ) except IndexError: # 文章没有评论或评论区关闭 item_data["target_article_comments_count"] = 0 try: - item_data["target_article_description"] = block.xpath("//p[@class='abstract']/text()")[0] + item_data["target_article_description"] = block.xpath( + "//p[@class='abstract']/text()" + )[0] except IndexError: # 文章没有描述 item_data["target_article_description"] = "" try: - item_data["target_article_rewards_count"] = int(block.xpath("//div[@class='meta']/span/text()")[1]) + item_data["target_article_rewards_count"] = int( + block.xpath("//div[@class='meta']/span/text()")[1] + ) except IndexError: # 没有赞赏数据 item_data["target_article_rewards_count"] = 0 elif item_data["operation_type"] == "join_jianshu": # 加入简书 item_data["operator_name"] = block.xpath("//a[@class='nickname']/text()")[0] - item_data["operator_url"] = UserSlugToUserUrl(block.xpath("//a[@class='nickname']/@href")[0][4:]) - item_data["operator_avatar_url"] = block.xpath("//a[@class='avatar']/img/@src")[0] + item_data["operator_url"] = UserSlugToUserUrl( + block.xpath("//a[@class='nickname']/@href")[0][4:] + ) + item_data["operator_avatar_url"] = block.xpath( + "//a[@class='avatar']/img/@src" + )[0] result.append(item_data) return result -def GetUserAllArticlesInfo(user_url: str, count: int = 10, sorting_method: str = "time", - max_count: int = None, disable_check: bool = False) -> Generator[Dict, None, None]: +def GetUserAllArticlesInfo( + user_url: str, + count: int = 10, + sorting_method: Literal["time", "comment_time", "hot"] = "time", + max_count: Optional[int] = None, + disable_check: bool = False, +) -> Generator[Dict, None, None]: """获取用户的所有文章信息 Args: user_url (str): 用户个人主页 URL count (int, optional): 单次获取的数据数量,会影响性能. Defaults to 10. - sorting_method (str, optional): 排序方法,time 为按照发布时间排序, + sorting_method (Literal["time", "comment_time", "hot"], optional): 排序方法,time 为按照发布时间排序, comment_time 为按照最近评论时间排序,hot 为按照热度排序. Defaults to "time". max_count (int, optional): 获取的文章信息数量上限,Defaults to None. disable_check (bool): 禁用参数有效性检查. Defaults to False. @@ -829,7 +1065,9 @@ def GetUserAllArticlesInfo(user_url: str, count: int = 10, sorting_method: str = page = 1 now_count = 0 while True: - result = GetUserArticlesInfo(user_url, page, count, sorting_method, disable_check=True) + result = GetUserArticlesInfo( + user_url, page, count, sorting_method, disable_check=True + ) if result: page += 1 else: # 没有新的数据 @@ -842,7 +1080,9 @@ def GetUserAllArticlesInfo(user_url: str, count: int = 10, sorting_method: str = return -def GetUserAllFollowingInfo(user_url: str, max_count: int = None, disable_check: bool = False) -> Generator[Dict, None, None]: +def GetUserAllFollowingInfo( + user_url: str, max_count: Optional[int] = None, disable_check: bool = False +) -> Generator[Dict, None, None]: """获取用户的所有关注者信息 Args: @@ -872,7 +1112,9 @@ def GetUserAllFollowingInfo(user_url: str, max_count: int = None, disable_check: return -def GetUserAllFansInfo(user_url: str, max_count: int = None, disable_check: bool = False) -> Generator[Dict, None, None]: +def GetUserAllFansInfo( + user_url: str, max_count: Optional[int] = None, disable_check: bool = False +) -> Generator[Dict, None, None]: """获取用户的所有粉丝信息 Args: @@ -902,7 +1144,9 @@ def GetUserAllFansInfo(user_url: str, max_count: int = None, disable_check: bool return -def GetUserAllTimelineInfo(user_url: str, max_count: int = None, disable_check: bool = False) -> Generator[Dict, None, None]: +def GetUserAllTimelineInfo( + user_url: str, max_count: Optional[int] = None, disable_check: bool = False +) -> Generator[Dict, None, None]: """获取用户的所有动态信息 Args: diff --git a/JianshuResearchTools/utils.py b/JianshuResearchTools/utils.py index a97ac71..bbb0e21 100644 --- a/JianshuResearchTools/utils.py +++ b/JianshuResearchTools/utils.py @@ -1,9 +1,11 @@ -from typing import Any, Dict, Tuple, Callable +from typing import Any, Callable, Dict, Tuple __all__ = ["NameValueMappingToString", "CallWithoutCheck"] -def NameValueMappingToString(mapping: Dict[str, Tuple[Any, bool]], title: str = "") -> str: +def NameValueMappingToString( + mapping: Dict[str, Tuple[Any, bool]], title: str = "" +) -> str: """将字典转换成特定格式的字符串 Args: diff --git a/LICENSE b/LICENSE index 2b4b1a8..a582cef 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2021 FHU-yezi +Copyright (c) 2023 FHU-yezi Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Pipfile b/Pipfile deleted file mode 100644 index 0df05e9..0000000 --- a/Pipfile +++ /dev/null @@ -1,22 +0,0 @@ -[[source]] -url = "https://mirrors.aliyun.com/pypi/simple" -verify_ssl = true -name = "pypi" - -[packages] -httpx = "==0.22.0" -lxml = "==4.8.0" -tomd = "==0.1.3" -ujson = "==5.3.0" - -[dev-packages] -pytest = "*" -pytest-xdist = "*" -pytest-cov = "*" -flake8 = "*" -mypy = "*" -pyyaml = "*" -yapf = "*" - -[requires] -python_version = "3.8" diff --git a/Pipfile.lock b/Pipfile.lock deleted file mode 100644 index 0598248..0000000 --- a/Pipfile.lock +++ /dev/null @@ -1,509 +0,0 @@ -{ - "_meta": { - "hash": { - "sha256": "101db41b6c1888faa2942f0072d5871901a5c42b39df05c669cd26bbff67a9d2" - }, - "pipfile-spec": 6, - "requires": { - "python_version": "3.8" - }, - "sources": [ - { - "name": "pypi", - "url": "https://mirrors.aliyun.com/pypi/simple", - "verify_ssl": true - } - ] - }, - "default": { - "anyio": { - "hashes": [ - "sha256:413adf95f93886e442aea925f3ee43baa5a765a64a0f52c6081894f9992fdd0b", - "sha256:cb29b9c70620506a9a8f87a309591713446953302d7d995344d0d7c6c0c9a7be" - ], - "markers": "python_full_version >= '3.6.2'", - "version": "==3.6.1" - }, - "certifi": { - "hashes": [ - "sha256:9c5705e395cd70084351dd8ad5c41e65655e08ce46f2ec9cf6c2c08390f71eb7", - "sha256:f1d53542ee8cbedbe2118b5686372fb33c297fcd6379b050cca0ef13a597382a" - ], - "markers": "python_version >= '3.6'", - "version": "==2022.5.18.1" - }, - "charset-normalizer": { - "hashes": [ - "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597", - "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df" - ], - "markers": "python_version >= '3.5'", - "version": "==2.0.12" - }, - "h11": { - "hashes": [ - "sha256:36a3cb8c0a032f56e2da7084577878a035d3b61d104230d4bd49c0c6b555a9c6", - "sha256:47222cb6067e4a307d535814917cd98fd0a57b6788ce715755fa2b6c28b56042" - ], - "markers": "python_version >= '3.6'", - "version": "==0.12.0" - }, - "httpcore": { - "hashes": [ - "sha256:47d772f754359e56dd9d892d9593b6f9870a37aeb8ba51e9a88b09b3d68cfade", - "sha256:7503ec1c0f559066e7e39bc4003fd2ce023d01cf51793e3c173b864eb456ead1" - ], - "markers": "python_version >= '3.6'", - "version": "==0.14.7" - }, - "httpx": { - "hashes": [ - "sha256:d8e778f76d9bbd46af49e7f062467e3157a5a3d2ae4876a4bbfd8a51ed9c9cb4", - "sha256:e35e83d1d2b9b2a609ef367cc4c1e66fd80b750348b20cc9e19d1952fc2ca3f6" - ], - "index": "pypi", - "version": "==0.22.0" - }, - "idna": { - "hashes": [ - "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff", - "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d" - ], - "markers": "python_version >= '3.5'", - "version": "==3.3" - }, - "lxml": { - "hashes": [ - "sha256:078306d19a33920004addeb5f4630781aaeabb6a8d01398045fcde085091a169", - "sha256:0c1978ff1fd81ed9dcbba4f91cf09faf1f8082c9d72eb122e92294716c605428", - "sha256:1010042bfcac2b2dc6098260a2ed022968dbdfaf285fc65a3acf8e4eb1ffd1bc", - "sha256:1d650812b52d98679ed6c6b3b55cbb8fe5a5460a0aef29aeb08dc0b44577df85", - "sha256:20b8a746a026017acf07da39fdb10aa80ad9877046c9182442bf80c84a1c4696", - "sha256:2403a6d6fb61c285969b71f4a3527873fe93fd0abe0832d858a17fe68c8fa507", - "sha256:24f5c5ae618395ed871b3d8ebfcbb36e3f1091fd847bf54c4de623f9107942f3", - "sha256:28d1af847786f68bec57961f31221125c29d6f52d9187c01cd34dc14e2b29430", - "sha256:31499847fc5f73ee17dbe1b8e24c6dafc4e8d5b48803d17d22988976b0171f03", - "sha256:31ba2cbc64516dcdd6c24418daa7abff989ddf3ba6d3ea6f6ce6f2ed6e754ec9", - "sha256:330bff92c26d4aee79c5bc4d9967858bdbe73fdbdbacb5daf623a03a914fe05b", - "sha256:5045ee1ccd45a89c4daec1160217d363fcd23811e26734688007c26f28c9e9e7", - "sha256:52cbf2ff155b19dc4d4100f7442f6a697938bf4493f8d3b0c51d45568d5666b5", - "sha256:530f278849031b0eb12f46cca0e5db01cfe5177ab13bd6878c6e739319bae654", - "sha256:545bd39c9481f2e3f2727c78c169425efbfb3fbba6e7db4f46a80ebb249819ca", - "sha256:5804e04feb4e61babf3911c2a974a5b86f66ee227cc5006230b00ac6d285b3a9", - "sha256:5a58d0b12f5053e270510bf12f753a76aaf3d74c453c00942ed7d2c804ca845c", - "sha256:5f148b0c6133fb928503cfcdfdba395010f997aa44bcf6474fcdd0c5398d9b63", - "sha256:5f7d7d9afc7b293147e2d506a4596641d60181a35279ef3aa5778d0d9d9123fe", - "sha256:60d2f60bd5a2a979df28ab309352cdcf8181bda0cca4529769a945f09aba06f9", - "sha256:6259b511b0f2527e6d55ad87acc1c07b3cbffc3d5e050d7e7bcfa151b8202df9", - "sha256:6268e27873a3d191849204d00d03f65c0e343b3bcb518a6eaae05677c95621d1", - "sha256:627e79894770783c129cc5e89b947e52aa26e8e0557c7e205368a809da4b7939", - "sha256:62f93eac69ec0f4be98d1b96f4d6b964855b8255c345c17ff12c20b93f247b68", - "sha256:6d6483b1229470e1d8835e52e0ff3c6973b9b97b24cd1c116dca90b57a2cc613", - "sha256:6f7b82934c08e28a2d537d870293236b1000d94d0b4583825ab9649aef7ddf63", - "sha256:6fe4ef4402df0250b75ba876c3795510d782def5c1e63890bde02d622570d39e", - "sha256:719544565c2937c21a6f76d520e6e52b726d132815adb3447ccffbe9f44203c4", - "sha256:730766072fd5dcb219dd2b95c4c49752a54f00157f322bc6d71f7d2a31fecd79", - "sha256:74eb65ec61e3c7c019d7169387d1b6ffcfea1b9ec5894d116a9a903636e4a0b1", - "sha256:7993232bd4044392c47779a3c7e8889fea6883be46281d45a81451acfd704d7e", - "sha256:80bbaddf2baab7e6de4bc47405e34948e694a9efe0861c61cdc23aa774fcb141", - "sha256:86545e351e879d0b72b620db6a3b96346921fa87b3d366d6c074e5a9a0b8dadb", - "sha256:891dc8f522d7059ff0024cd3ae79fd224752676447f9c678f2a5c14b84d9a939", - "sha256:8a31f24e2a0b6317f33aafbb2f0895c0bce772980ae60c2c640d82caac49628a", - "sha256:8b99ec73073b37f9ebe8caf399001848fced9c08064effdbfc4da2b5a8d07b93", - "sha256:986b7a96228c9b4942ec420eff37556c5777bfba6758edcb95421e4a614b57f9", - "sha256:a1547ff4b8a833511eeaceacbcd17b043214fcdb385148f9c1bc5556ca9623e2", - "sha256:a2bfc7e2a0601b475477c954bf167dee6d0f55cb167e3f3e7cefad906e7759f6", - "sha256:a3c5f1a719aa11866ffc530d54ad965063a8cbbecae6515acbd5f0fae8f48eaa", - "sha256:a9f1c3489736ff8e1c7652e9dc39f80cff820f23624f23d9eab6e122ac99b150", - "sha256:aa0cf4922da7a3c905d000b35065df6184c0dc1d866dd3b86fd961905bbad2ea", - "sha256:ad4332a532e2d5acb231a2e5d33f943750091ee435daffca3fec0a53224e7e33", - "sha256:b2582b238e1658c4061ebe1b4df53c435190d22457642377fd0cb30685cdfb76", - "sha256:b6fc2e2fb6f532cf48b5fed57567ef286addcef38c28874458a41b7837a57807", - "sha256:b92d40121dcbd74831b690a75533da703750f7041b4bf951befc657c37e5695a", - "sha256:bbab6faf6568484707acc052f4dfc3802bdb0cafe079383fbaa23f1cdae9ecd4", - "sha256:c0b88ed1ae66777a798dc54f627e32d3b81c8009967c63993c450ee4cbcbec15", - "sha256:ce13d6291a5f47c1c8dbd375baa78551053bc6b5e5c0e9bb8e39c0a8359fd52f", - "sha256:db3535733f59e5605a88a706824dfcb9bd06725e709ecb017e165fc1d6e7d429", - "sha256:dd10383f1d6b7edf247d0960a3db274c07e96cf3a3fc7c41c8448f93eac3fb1c", - "sha256:e01f9531ba5420838c801c21c1b0f45dbc9607cb22ea2cf132844453bec863a5", - "sha256:e11527dc23d5ef44d76fef11213215c34f36af1608074561fcc561d983aeb870", - "sha256:e1ab2fac607842ac36864e358c42feb0960ae62c34aa4caaf12ada0a1fb5d99b", - "sha256:e1fd7d2fe11f1cb63d3336d147c852f6d07de0d0020d704c6031b46a30b02ca8", - "sha256:e9f84ed9f4d50b74fbc77298ee5c870f67cb7e91dcdc1a6915cb1ff6a317476c", - "sha256:ec4b4e75fc68da9dc0ed73dcdb431c25c57775383fec325d23a770a64e7ebc87", - "sha256:f10ce66fcdeb3543df51d423ede7e238be98412232fca5daec3e54bcd16b8da0", - "sha256:f63f62fc60e6228a4ca9abae28228f35e1bd3ce675013d1dfb828688d50c6e23", - "sha256:fa56bb08b3dd8eac3a8c5b7d075c94e74f755fd9d8a04543ae8d37b1612dd170", - "sha256:fa9b7c450be85bfc6cd39f6df8c5b8cbd76b5d6fc1f69efec80203f9894b885f" - ], - "index": "pypi", - "version": "==4.8.0" - }, - "rfc3986": { - "extras": [ - "idna2008" - ], - "hashes": [ - "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835", - "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97" - ], - "version": "==1.5.0" - }, - "sniffio": { - "hashes": [ - "sha256:471b71698eac1c2112a40ce2752bb2f4a4814c22a54a3eed3676bc0f5ca9f663", - "sha256:c4666eecec1d3f50960c6bdf61ab7bc350648da6c126e3cf6898d8cd4ddcd3de" - ], - "markers": "python_version >= '3.5'", - "version": "==1.2.0" - }, - "tomd": { - "hashes": [ - "sha256:23f21ae853157be49d163159bc7c6bc007dd5e87b69769ec5ba3e17f5de7a6d4" - ], - "index": "pypi", - "version": "==0.1.3" - }, - "ujson": { - "hashes": [ - "sha256:034c07399dff35385ecc53caf9b1f12b3e203834de27b723daeb2cbb3e02ee7f", - "sha256:089965f964d17905c48cdca88b982d525165e549b438ac86f194c6a9d852fd69", - "sha256:1358621686ddfda55171fc98c171bf5b1a80ce4d444134b70e1e449925fa014f", - "sha256:151faa9085c10351a04aea959a2bc25dfa2e21af26d9b614a221d045b7923ea4", - "sha256:285082924747958aa69e1dc2146c01db6b0921a0bb04b595beefe7fcffaffaf9", - "sha256:287dea79473ce4941598c45dc34f9f692d48d7863b451541c5ce960ab54465fb", - "sha256:2db7cbe415d7329b9bff029a83851d1077836ec728fe1c32be34c9c3a5017ab2", - "sha256:34592a3c9370745b093ebca60aee6d32f8e7abe3d5c12d54c7dba0b2f81cd863", - "sha256:47bf966e1041ae8e568d7e8eb421d72d0521c30c28306b76c256832553e316c6", - "sha256:48bed7c1f95484644a2cc658efff4d1e75b8c806f6ef2b5c815f59e1cbe0d039", - "sha256:4dc79db757b0dfa23a111a4573827a6ef57de65dbe8cdb202e45cf9ddf06aad5", - "sha256:510c3705b29bc3753ec9e6073b99000160320c1cf6e035884295401acb474dfa", - "sha256:5192505798a5734a85c763eff11e6f6072d3595c337b52f72922b4e22fe66e2e", - "sha256:522b1d60872bb6368c14ac538adb55ca9d6c39a7a962832819ef1aafb3446ff5", - "sha256:563b7ed1e789f763410c49e6fab51d61982eb94088b25338e65b89ad20b6b107", - "sha256:5700a179abacbdc8609737e595a598b7f107cd68615ded3f922f4c0d4b6009d6", - "sha256:5a87e1c05f1efc23c67bfa26be79f12c1f59f71a586b396068d5cf7eb78a2635", - "sha256:612015c6e5a9bf041b89f1eaa8ab8682469b3a745a00c7c95bbbee8080f6b346", - "sha256:66f857d8b8d7ea44e3fd5f2b7e471334f24b735423729771f5a7a7f69ab645ed", - "sha256:6aba1e39ffdd83ec14832ea25bbb18266fea46bc69b8c0acbd996495826c0e6f", - "sha256:6c5d19fbdd29d5080926c863ba89591a2d3dbf592ea35b456cb2996004433d11", - "sha256:73636001055667bbcc6a73b232da1d272f68a49a1f192efbe99e99ddf8ef1d21", - "sha256:7455fc3d69315149b95fd011c01496a5e9442c9e7c4d202bed87c5c2e449ed05", - "sha256:7d2cb50aa526032b8812975c3832058763ee50e1dc3a1302431ed9d0922c3a1b", - "sha256:865225a85e4ce48754d0036fdc0eb796b4aaf4f1e928f0efb9b4e1c081647a4c", - "sha256:8a2cbb044bc6e6764b9a089a2079432b8bd576dbff5faa808b562a8f3c97452b", - "sha256:8c734982d6560356c173817576a1f3fa074a2d2b993e63bffa69105ae9ec144b", - "sha256:8dd74570fe59c738d4dc12d44eb89538b0b01fae9dda6cfe3ff3f6934877cf35", - "sha256:972c1850cc52e57ccdea70e3c069e2da5c6090e3ee18d167dff2618a8d7dd127", - "sha256:a014531468b78c031aa04e5ca8b64385a6edb48a2e66ebf11093213c678fc383", - "sha256:a4fe193050b519ace09f7d053def30b99deadf650c18a8a874ea0f6c9a2992bc", - "sha256:a609bb1cdda9748e6a8363039926dee5ea2bcc073412279615560b967f92a524", - "sha256:a68d5a8a46712ffe86db8ae1b4311714db534725521c71fd4c9e1cd062dae9a4", - "sha256:a720b6eff73415249a3dd02e2b1b337de31bb9fa8220bd572dffba23066e538c", - "sha256:a933b3a238a48162c382e0ac338b97663d044b0485021b6670565a81e7b7ec98", - "sha256:ab938777b3ac0372231ee654a7f6a13787e587b1ca268d8aa7e6fb6846e477d0", - "sha256:b3e6431812d8008dce7b2546b1276f649f6c9aa44617762ebd3529a25092816c", - "sha256:b926f2f7a266db8f2c46498f0c2c9fcc7e53c8e0fa8bff7f08ad9c044723a2ec", - "sha256:bad1471ccfa8d100a0bc513c6db587c38de99384f2aa54eec1016a131d63d3d9", - "sha256:c1408ea1704017289c3023928065233b90953aae3e1d7d06d6d6db667e9fe159", - "sha256:c5696c99a7dd567566c18490e8e346b2657967feb1e3c2004e91dbb253db0894", - "sha256:ca5eced4ae4ba1e2c9539fca6451694d31e0243de2acfcd6965e2b6e159ba29b", - "sha256:d1fab398734634f4b412512ed230d45522fc9f3dd9ca169f579474a491f662aa", - "sha256:d45e86101a5cddd295d5870b02244fc87ecd9b8936f440acbd2bb30b4c1fe23c", - "sha256:d4830c8df958c45c16dfc43c8353403efd7f1a8e39b91a7e0e848d55b7fa8b48", - "sha256:d553f31bceda492c2bda37f48873820d28f07608ae14409c5e9d6c3aa6694840", - "sha256:decd32e8d7f934dde484e43431f60b069e87bb30a3a7e186cb6bd69caa0418f3", - "sha256:e7961c493a982c03cffc9ce4dc2b23bed1375352296f946cc36ddeb5145fa62c", - "sha256:ed9809bc36292e0d3632d50aae497b5827c1a2e07158f7d4d5c53e8e8662bf66", - "sha256:f615ee181b813c8f50a57d55354d0c0304a0be066962efdbef6f44517b26e3b2" - ], - "index": "pypi", - "version": "==5.3.0" - } - }, - "develop": { - "atomicwrites": { - "hashes": [ - "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197", - "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a" - ], - "markers": "sys_platform == 'win32'", - "version": "==1.4.0" - }, - "attrs": { - "hashes": [ - "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4", - "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==21.4.0" - }, - "colorama": { - "hashes": [ - "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b", - "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2" - ], - "markers": "sys_platform == 'win32'", - "version": "==0.4.4" - }, - "coverage": { - "extras": [ - "toml" - ], - "hashes": [ - "sha256:01c5615d13f3dd3aa8543afc069e5319cfa0c7d712f6e04b920431e5c564a749", - "sha256:106c16dfe494de3193ec55cac9640dd039b66e196e4641fa8ac396181578b982", - "sha256:129cd05ba6f0d08a766d942a9ed4b29283aff7b2cccf5b7ce279d50796860bb3", - "sha256:145f296d00441ca703a659e8f3eb48ae39fb083baba2d7ce4482fb2723e050d9", - "sha256:1480ff858b4113db2718848d7b2d1b75bc79895a9c22e76a221b9d8d62496428", - "sha256:269eaa2c20a13a5bf17558d4dc91a8d078c4fa1872f25303dddcbba3a813085e", - "sha256:26dff09fb0d82693ba9e6231248641d60ba606150d02ed45110f9ec26404ed1c", - "sha256:2bd9a6fc18aab8d2e18f89b7ff91c0f34ff4d5e0ba0b33e989b3cd4194c81fd9", - "sha256:309ce4a522ed5fca432af4ebe0f32b21d6d7ccbb0f5fcc99290e71feba67c264", - "sha256:3384f2a3652cef289e38100f2d037956194a837221edd520a7ee5b42d00cc605", - "sha256:342d4aefd1c3e7f620a13f4fe563154d808b69cccef415415aece4c786665397", - "sha256:39ee53946bf009788108b4dd2894bf1349b4e0ca18c2016ffa7d26ce46b8f10d", - "sha256:4321f075095a096e70aff1d002030ee612b65a205a0a0f5b815280d5dc58100c", - "sha256:4803e7ccf93230accb928f3a68f00ffa80a88213af98ed338a57ad021ef06815", - "sha256:4ce1b258493cbf8aec43e9b50d89982346b98e9ffdfaae8ae5793bc112fb0068", - "sha256:664a47ce62fe4bef9e2d2c430306e1428ecea207ffd68649e3b942fa8ea83b0b", - "sha256:75ab269400706fab15981fd4bd5080c56bd5cc07c3bccb86aab5e1d5a88dc8f4", - "sha256:83c4e737f60c6936460c5be330d296dd5b48b3963f48634c53b3f7deb0f34ec4", - "sha256:84631e81dd053e8a0d4967cedab6db94345f1c36107c71698f746cb2636c63e3", - "sha256:84e65ef149028516c6d64461b95a8dbcfce95cfd5b9eb634320596173332ea84", - "sha256:865d69ae811a392f4d06bde506d531f6a28a00af36f5c8649684a9e5e4a85c83", - "sha256:87f4f3df85aa39da00fd3ec4b5abeb7407e82b68c7c5ad181308b0e2526da5d4", - "sha256:8c08da0bd238f2970230c2a0d28ff0e99961598cb2e810245d7fc5afcf1254e8", - "sha256:961e2fb0680b4f5ad63234e0bf55dfb90d302740ae9c7ed0120677a94a1590cb", - "sha256:9b3e07152b4563722be523e8cd0b209e0d1a373022cfbde395ebb6575bf6790d", - "sha256:a7f3049243783df2e6cc6deafc49ea123522b59f464831476d3d1448e30d72df", - "sha256:bf5601c33213d3cb19d17a796f8a14a9eaa5e87629a53979a5981e3e3ae166f6", - "sha256:cec3a0f75c8f1031825e19cd86ee787e87cf03e4fd2865c79c057092e69e3a3b", - "sha256:d42c549a8f41dc103a8004b9f0c433e2086add8a719da00e246e17cbe4056f72", - "sha256:d67d44996140af8b84284e5e7d398e589574b376fb4de8ccd28d82ad8e3bea13", - "sha256:d9c80df769f5ec05ad21ea34be7458d1dc51ff1fb4b2219e77fe24edf462d6df", - "sha256:e57816f8ffe46b1df8f12e1b348f06d164fd5219beba7d9433ba79608ef011cc", - "sha256:ee2ddcac99b2d2aec413e36d7a429ae9ebcadf912946b13ffa88e7d4c9b712d6", - "sha256:f02cbbf8119db68455b9d763f2f8737bb7db7e43720afa07d8eb1604e5c5ae28", - "sha256:f1d5aa2703e1dab4ae6cf416eb0095304f49d004c39e9db1d86f57924f43006b", - "sha256:f5b66caa62922531059bc5ac04f836860412f7f88d38a476eda0a6f11d4724f4", - "sha256:f69718750eaae75efe506406c490d6fc5a6161d047206cc63ce25527e8a3adad", - "sha256:fb73e0011b8793c053bfa85e53129ba5f0250fdc0392c1591fd35d915ec75c46", - "sha256:fd180ed867e289964404051a958f7cccabdeed423f91a899829264bb7974d3d3", - "sha256:fdb6f7bd51c2d1714cea40718f6149ad9be6a2ee7d93b19e9f00934c0f2a74d9", - "sha256:ffa9297c3a453fba4717d06df579af42ab9a28022444cae7fa605af4df612d54" - ], - "markers": "python_version >= '3.7'", - "version": "==6.4.1" - }, - "execnet": { - "hashes": [ - "sha256:8f694f3ba9cc92cab508b152dcfe322153975c29bda272e2fd7f3f00f36e47c5", - "sha256:a295f7cc774947aac58dde7fdc85f4aa00c42adf5d8f5468fc630c1acf30a142" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==1.9.0" - }, - "flake8": { - "hashes": [ - "sha256:479b1304f72536a55948cb40a32dce8bb0ffe3501e26eaf292c7e60eb5e0428d", - "sha256:806e034dda44114815e23c16ef92f95c91e4c71100ff52813adf7132a6ad870d" - ], - "index": "pypi", - "version": "==4.0.1" - }, - "iniconfig": { - "hashes": [ - "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3", - "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32" - ], - "version": "==1.1.1" - }, - "mccabe": { - "hashes": [ - "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", - "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" - ], - "version": "==0.6.1" - }, - "mypy": { - "hashes": [ - "sha256:006be38474216b833eca29ff6b73e143386f352e10e9c2fbe76aa8549e5554f5", - "sha256:03c6cc893e7563e7b2949b969e63f02c000b32502a1b4d1314cabe391aa87d66", - "sha256:0e9f70df36405c25cc530a86eeda1e0867863d9471fe76d1273c783df3d35c2e", - "sha256:1ece702f29270ec6af25db8cf6185c04c02311c6bb21a69f423d40e527b75c56", - "sha256:3e09f1f983a71d0672bbc97ae33ee3709d10c779beb613febc36805a6e28bb4e", - "sha256:439c726a3b3da7ca84a0199a8ab444cd8896d95012c4a6c4a0d808e3147abf5d", - "sha256:5a0b53747f713f490affdceef835d8f0cb7285187a6a44c33821b6d1f46ed813", - "sha256:5f1332964963d4832a94bebc10f13d3279be3ce8f6c64da563d6ee6e2eeda932", - "sha256:63e85a03770ebf403291ec50097954cc5caf2a9205c888ce3a61bd3f82e17569", - "sha256:64759a273d590040a592e0f4186539858c948302c653c2eac840c7a3cd29e51b", - "sha256:697540876638ce349b01b6786bc6094ccdaba88af446a9abb967293ce6eaa2b0", - "sha256:9940e6916ed9371809b35b2154baf1f684acba935cd09928952310fbddaba648", - "sha256:9f5f5a74085d9a81a1f9c78081d60a0040c3efb3f28e5c9912b900adf59a16e6", - "sha256:a5ea0875a049de1b63b972456542f04643daf320d27dc592d7c3d9cd5d9bf950", - "sha256:b117650592e1782819829605a193360a08aa99f1fc23d1d71e1a75a142dc7e15", - "sha256:b24be97351084b11582fef18d79004b3e4db572219deee0212078f7cf6352723", - "sha256:b88f784e9e35dcaa075519096dc947a388319cb86811b6af621e3523980f1c8a", - "sha256:bdd5ca340beffb8c44cb9dc26697628d1b88c6bddf5c2f6eb308c46f269bb6f3", - "sha256:d5aaf1edaa7692490f72bdb9fbd941fbf2e201713523bdb3f4038be0af8846c6", - "sha256:e999229b9f3198c0c880d5e269f9f8129c8862451ce53a011326cad38b9ccd24", - "sha256:f4a21d01fc0ba4e31d82f0fff195682e29f9401a8bdb7173891070eb260aeb3b", - "sha256:f4b794db44168a4fc886e3450201365c9526a522c46ba089b55e1f11c163750d", - "sha256:f730d56cb924d371c26b8eaddeea3cc07d78ff51c521c6d04899ac6904b75492" - ], - "index": "pypi", - "version": "==0.961" - }, - "mypy-extensions": { - "hashes": [ - "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d", - "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8" - ], - "version": "==0.4.3" - }, - "packaging": { - "hashes": [ - "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb", - "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522" - ], - "markers": "python_version >= '3.6'", - "version": "==21.3" - }, - "pluggy": { - "hashes": [ - "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159", - "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3" - ], - "markers": "python_version >= '3.6'", - "version": "==1.0.0" - }, - "py": { - "hashes": [ - "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719", - "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==1.11.0" - }, - "pycodestyle": { - "hashes": [ - "sha256:720f8b39dde8b293825e7ff02c475f3077124006db4f440dcbc9a20b76548a20", - "sha256:eddd5847ef438ea1c7870ca7eb78a9d47ce0cdb4851a5523949f2601d0cbbe7f" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==2.8.0" - }, - "pyflakes": { - "hashes": [ - "sha256:05a85c2872edf37a4ed30b0cce2f6093e1d0581f8c19d7393122da7e25b2b24c", - "sha256:3bb3a3f256f4b7968c9c788781e4ff07dce46bdf12339dcda61053375426ee2e" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.4.0" - }, - "pyparsing": { - "hashes": [ - "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb", - "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc" - ], - "markers": "python_full_version >= '3.6.8'", - "version": "==3.0.9" - }, - "pytest": { - "hashes": [ - "sha256:13d0e3ccfc2b6e26be000cb6568c832ba67ba32e719443bfe725814d3c42433c", - "sha256:a06a0425453864a270bc45e71f783330a7428defb4230fb5e6a731fde06ecd45" - ], - "index": "pypi", - "version": "==7.1.2" - }, - "pytest-cov": { - "hashes": [ - "sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6", - "sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470" - ], - "index": "pypi", - "version": "==3.0.0" - }, - "pytest-forked": { - "hashes": [ - "sha256:8b67587c8f98cbbadfdd804539ed5455b6ed03802203485dd2f53c1422d7440e", - "sha256:bbbb6717efc886b9d64537b41fb1497cfaf3c9601276be8da2cccfea5a3c8ad8" - ], - "markers": "python_version >= '3.6'", - "version": "==1.4.0" - }, - "pytest-xdist": { - "hashes": [ - "sha256:4580deca3ff04ddb2ac53eba39d76cb5dd5edeac050cb6fbc768b0dd712b4edf", - "sha256:6fe5c74fec98906deb8f2d2b616b5c782022744978e7bd4695d39c8f42d0ce65" - ], - "index": "pypi", - "version": "==2.5.0" - }, - "pyyaml": { - "hashes": [ - "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293", - "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b", - "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57", - "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b", - "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4", - "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07", - "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba", - "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9", - "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287", - "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513", - "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0", - "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0", - "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92", - "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f", - "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2", - "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc", - "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c", - "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86", - "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4", - "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c", - "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34", - "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b", - "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c", - "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb", - "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737", - "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3", - "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d", - "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53", - "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78", - "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803", - "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a", - "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174", - "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5" - ], - "index": "pypi", - "version": "==6.0" - }, - "tomli": { - "hashes": [ - "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", - "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f" - ], - "markers": "python_version >= '3.7'", - "version": "==2.0.1" - }, - "typing-extensions": { - "hashes": [ - "sha256:6657594ee297170d19f67d55c05852a874e7eb634f4f753dbd667855e07c1708", - "sha256:f1c24655a0da0d1b67f07e17a5e6b2a105894e6824b92096378bb3668ef02376" - ], - "markers": "python_version >= '3.7'", - "version": "==4.2.0" - }, - "yapf": { - "hashes": [ - "sha256:8fea849025584e486fd06d6ba2bed717f396080fd3cc236ba10cb97c4c51cf32", - "sha256:a3f5085d37ef7e3e004c4ba9f9b3e40c54ff1901cd111f05145ae313a7c67d1b" - ], - "index": "pypi", - "version": "==0.32.0" - } - } -} diff --git a/readme.md b/README.md similarity index 100% rename from readme.md rename to README.md diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..2bd5062 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,900 @@ +# This file is automatically @generated by Poetry 1.4.1 and should not be changed by hand. + +[[package]] +name = "anyio" +version = "3.6.2" +description = "High level compatibility layer for multiple asynchronous event loop implementations" +category = "main" +optional = false +python-versions = ">=3.6.2" +files = [ + {file = "anyio-3.6.2-py3-none-any.whl", hash = "sha256:fbbe32bd270d2a2ef3ed1c5d45041250284e31fc0a4df4a5a6071842051a51e3"}, + {file = "anyio-3.6.2.tar.gz", hash = "sha256:25ea0d673ae30af41a0c442f81cf3b38c7e79fdc7b60335a4c14e05eb0947421"}, +] + +[package.dependencies] +idna = ">=2.8" +sniffio = ">=1.1" +typing-extensions = {version = "*", markers = "python_version < \"3.8\""} + +[package.extras] +doc = ["packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] +test = ["contextlib2", "coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "mock (>=4)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (<0.15)", "uvloop (>=0.15)"] +trio = ["trio (>=0.16,<0.22)"] + +[[package]] +name = "black" +version = "23.3.0" +description = "The uncompromising code formatter." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "black-23.3.0-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:0945e13506be58bf7db93ee5853243eb368ace1c08a24c65ce108986eac65915"}, + {file = "black-23.3.0-cp310-cp310-macosx_10_16_universal2.whl", hash = "sha256:67de8d0c209eb5b330cce2469503de11bca4085880d62f1628bd9972cc3366b9"}, + {file = "black-23.3.0-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:7c3eb7cea23904399866c55826b31c1f55bbcd3890ce22ff70466b907b6775c2"}, + {file = "black-23.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:32daa9783106c28815d05b724238e30718f34155653d4d6e125dc7daec8e260c"}, + {file = "black-23.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:35d1381d7a22cc5b2be2f72c7dfdae4072a3336060635718cc7e1ede24221d6c"}, + {file = "black-23.3.0-cp311-cp311-macosx_10_16_arm64.whl", hash = "sha256:a8a968125d0a6a404842fa1bf0b349a568634f856aa08ffaff40ae0dfa52e7c6"}, + {file = "black-23.3.0-cp311-cp311-macosx_10_16_universal2.whl", hash = "sha256:c7ab5790333c448903c4b721b59c0d80b11fe5e9803d8703e84dcb8da56fec1b"}, + {file = "black-23.3.0-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:a6f6886c9869d4daae2d1715ce34a19bbc4b95006d20ed785ca00fa03cba312d"}, + {file = "black-23.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f3c333ea1dd6771b2d3777482429864f8e258899f6ff05826c3a4fcc5ce3f70"}, + {file = "black-23.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:11c410f71b876f961d1de77b9699ad19f939094c3a677323f43d7a29855fe326"}, + {file = "black-23.3.0-cp37-cp37m-macosx_10_16_x86_64.whl", hash = "sha256:1d06691f1eb8de91cd1b322f21e3bfc9efe0c7ca1f0e1eb1db44ea367dff656b"}, + {file = "black-23.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50cb33cac881766a5cd9913e10ff75b1e8eb71babf4c7104f2e9c52da1fb7de2"}, + {file = "black-23.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:e114420bf26b90d4b9daa597351337762b63039752bdf72bf361364c1aa05925"}, + {file = "black-23.3.0-cp38-cp38-macosx_10_16_arm64.whl", hash = "sha256:48f9d345675bb7fbc3dd85821b12487e1b9a75242028adad0333ce36ed2a6d27"}, + {file = "black-23.3.0-cp38-cp38-macosx_10_16_universal2.whl", hash = "sha256:714290490c18fb0126baa0fca0a54ee795f7502b44177e1ce7624ba1c00f2331"}, + {file = "black-23.3.0-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:064101748afa12ad2291c2b91c960be28b817c0c7eaa35bec09cc63aa56493c5"}, + {file = "black-23.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:562bd3a70495facf56814293149e51aa1be9931567474993c7942ff7d3533961"}, + {file = "black-23.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:e198cf27888ad6f4ff331ca1c48ffc038848ea9f031a3b40ba36aced7e22f2c8"}, + {file = "black-23.3.0-cp39-cp39-macosx_10_16_arm64.whl", hash = "sha256:3238f2aacf827d18d26db07524e44741233ae09a584273aa059066d644ca7b30"}, + {file = "black-23.3.0-cp39-cp39-macosx_10_16_universal2.whl", hash = "sha256:f0bd2f4a58d6666500542b26354978218a9babcdc972722f4bf90779524515f3"}, + {file = "black-23.3.0-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:92c543f6854c28a3c7f39f4d9b7694f9a6eb9d3c5e2ece488c327b6e7ea9b266"}, + {file = "black-23.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a150542a204124ed00683f0db1f5cf1c2aaaa9cc3495b7a3b5976fb136090ab"}, + {file = "black-23.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:6b39abdfb402002b8a7d030ccc85cf5afff64ee90fa4c5aebc531e3ad0175ddb"}, + {file = "black-23.3.0-py3-none-any.whl", hash = "sha256:ec751418022185b0c1bb7d7736e6933d40bbb14c14a0abcf9123d1b159f98dd4"}, + {file = "black-23.3.0.tar.gz", hash = "sha256:1c7b8d606e728a41ea1ccbd7264677e494e87cf630e399262ced92d4a8dac940"}, +] + +[package.dependencies] +click = ">=8.0.0" +mypy-extensions = ">=0.4.3" +packaging = ">=22.0" +pathspec = ">=0.9.0" +platformdirs = ">=2" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typed-ast = {version = ">=1.4.2", markers = "python_version < \"3.8\" and implementation_name == \"cpython\""} +typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""} + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.7.4)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2)"] + +[[package]] +name = "certifi" +version = "2022.12.7" +description = "Python package for providing Mozilla's CA Bundle." +category = "main" +optional = false +python-versions = ">=3.6" +files = [ + {file = "certifi-2022.12.7-py3-none-any.whl", hash = "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18"}, + {file = "certifi-2022.12.7.tar.gz", hash = "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3"}, +] + +[[package]] +name = "click" +version = "8.1.3" +description = "Composable command line interface toolkit" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, + {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} +importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +category = "dev" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "coverage" +version = "7.2.3" +description = "Code coverage measurement for Python" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "coverage-7.2.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e58c0d41d336569d63d1b113bd573db8363bc4146f39444125b7f8060e4e04f5"}, + {file = "coverage-7.2.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:344e714bd0fe921fc72d97404ebbdbf9127bac0ca1ff66d7b79efc143cf7c0c4"}, + {file = "coverage-7.2.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:974bc90d6f6c1e59ceb1516ab00cf1cdfbb2e555795d49fa9571d611f449bcb2"}, + {file = "coverage-7.2.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0743b0035d4b0e32bc1df5de70fba3059662ace5b9a2a86a9f894cfe66569013"}, + {file = "coverage-7.2.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d0391fb4cfc171ce40437f67eb050a340fdbd0f9f49d6353a387f1b7f9dd4fa"}, + {file = "coverage-7.2.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4a42e1eff0ca9a7cb7dc9ecda41dfc7cbc17cb1d02117214be0561bd1134772b"}, + {file = "coverage-7.2.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:be19931a8dcbe6ab464f3339966856996b12a00f9fe53f346ab3be872d03e257"}, + {file = "coverage-7.2.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:72fcae5bcac3333a4cf3b8f34eec99cea1187acd55af723bcbd559adfdcb5535"}, + {file = "coverage-7.2.3-cp310-cp310-win32.whl", hash = "sha256:aeae2aa38395b18106e552833f2a50c27ea0000122bde421c31d11ed7e6f9c91"}, + {file = "coverage-7.2.3-cp310-cp310-win_amd64.whl", hash = "sha256:83957d349838a636e768251c7e9979e899a569794b44c3728eaebd11d848e58e"}, + {file = "coverage-7.2.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:dfd393094cd82ceb9b40df4c77976015a314b267d498268a076e940fe7be6b79"}, + {file = "coverage-7.2.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:182eb9ac3f2b4874a1f41b78b87db20b66da6b9cdc32737fbbf4fea0c35b23fc"}, + {file = "coverage-7.2.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1bb1e77a9a311346294621be905ea8a2c30d3ad371fc15bb72e98bfcfae532df"}, + {file = "coverage-7.2.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca0f34363e2634deffd390a0fef1aa99168ae9ed2af01af4a1f5865e362f8623"}, + {file = "coverage-7.2.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:55416d7385774285b6e2a5feca0af9652f7f444a4fa3d29d8ab052fafef9d00d"}, + {file = "coverage-7.2.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:06ddd9c0249a0546997fdda5a30fbcb40f23926df0a874a60a8a185bc3a87d93"}, + {file = "coverage-7.2.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:fff5aaa6becf2c6a1699ae6a39e2e6fb0672c2d42eca8eb0cafa91cf2e9bd312"}, + {file = "coverage-7.2.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ea53151d87c52e98133eb8ac78f1206498c015849662ca8dc246255265d9c3c4"}, + {file = "coverage-7.2.3-cp311-cp311-win32.whl", hash = "sha256:8f6c930fd70d91ddee53194e93029e3ef2aabe26725aa3c2753df057e296b925"}, + {file = "coverage-7.2.3-cp311-cp311-win_amd64.whl", hash = "sha256:fa546d66639d69aa967bf08156eb8c9d0cd6f6de84be9e8c9819f52ad499c910"}, + {file = "coverage-7.2.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b2317d5ed777bf5a033e83d4f1389fd4ef045763141d8f10eb09a7035cee774c"}, + {file = "coverage-7.2.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be9824c1c874b73b96288c6d3de793bf7f3a597770205068c6163ea1f326e8b9"}, + {file = "coverage-7.2.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2c3b2803e730dc2797a017335827e9da6da0e84c745ce0f552e66400abdfb9a1"}, + {file = "coverage-7.2.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f69770f5ca1994cb32c38965e95f57504d3aea96b6c024624fdd5bb1aa494a1"}, + {file = "coverage-7.2.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:1127b16220f7bfb3f1049ed4a62d26d81970a723544e8252db0efde853268e21"}, + {file = "coverage-7.2.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:aa784405f0c640940595fa0f14064d8e84aff0b0f762fa18393e2760a2cf5841"}, + {file = "coverage-7.2.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:3146b8e16fa60427e03884301bf8209221f5761ac754ee6b267642a2fd354c48"}, + {file = "coverage-7.2.3-cp37-cp37m-win32.whl", hash = "sha256:1fd78b911aea9cec3b7e1e2622c8018d51c0d2bbcf8faaf53c2497eb114911c1"}, + {file = "coverage-7.2.3-cp37-cp37m-win_amd64.whl", hash = "sha256:0f3736a5d34e091b0a611964c6262fd68ca4363df56185902528f0b75dbb9c1f"}, + {file = "coverage-7.2.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:981b4df72c93e3bc04478153df516d385317628bd9c10be699c93c26ddcca8ab"}, + {file = "coverage-7.2.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c0045f8f23a5fb30b2eb3b8a83664d8dc4fb58faddf8155d7109166adb9f2040"}, + {file = "coverage-7.2.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f760073fcf8f3d6933178d67754f4f2d4e924e321f4bb0dcef0424ca0215eba1"}, + {file = "coverage-7.2.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c86bd45d1659b1ae3d0ba1909326b03598affbc9ed71520e0ff8c31a993ad911"}, + {file = "coverage-7.2.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:172db976ae6327ed4728e2507daf8a4de73c7cc89796483e0a9198fd2e47b462"}, + {file = "coverage-7.2.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:d2a3a6146fe9319926e1d477842ca2a63fe99af5ae690b1f5c11e6af074a6b5c"}, + {file = "coverage-7.2.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:f649dd53833b495c3ebd04d6eec58479454a1784987af8afb77540d6c1767abd"}, + {file = "coverage-7.2.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:7c4ed4e9f3b123aa403ab424430b426a1992e6f4c8fd3cb56ea520446e04d152"}, + {file = "coverage-7.2.3-cp38-cp38-win32.whl", hash = "sha256:eb0edc3ce9760d2f21637766c3aa04822030e7451981ce569a1b3456b7053f22"}, + {file = "coverage-7.2.3-cp38-cp38-win_amd64.whl", hash = "sha256:63cdeaac4ae85a179a8d6bc09b77b564c096250d759eed343a89d91bce8b6367"}, + {file = "coverage-7.2.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:20d1a2a76bb4eb00e4d36b9699f9b7aba93271c9c29220ad4c6a9581a0320235"}, + {file = "coverage-7.2.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4ea748802cc0de4de92ef8244dd84ffd793bd2e7be784cd8394d557a3c751e21"}, + {file = "coverage-7.2.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21b154aba06df42e4b96fc915512ab39595105f6c483991287021ed95776d934"}, + {file = "coverage-7.2.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd214917cabdd6f673a29d708574e9fbdb892cb77eb426d0eae3490d95ca7859"}, + {file = "coverage-7.2.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c2e58e45fe53fab81f85474e5d4d226eeab0f27b45aa062856c89389da2f0d9"}, + {file = "coverage-7.2.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:87ecc7c9a1a9f912e306997ffee020297ccb5ea388421fe62a2a02747e4d5539"}, + {file = "coverage-7.2.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:387065e420aed3c71b61af7e82c7b6bc1c592f7e3c7a66e9f78dd178699da4fe"}, + {file = "coverage-7.2.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ea3f5bc91d7d457da7d48c7a732beaf79d0c8131df3ab278e6bba6297e23c6c4"}, + {file = "coverage-7.2.3-cp39-cp39-win32.whl", hash = "sha256:ae7863a1d8db6a014b6f2ff9c1582ab1aad55a6d25bac19710a8df68921b6e30"}, + {file = "coverage-7.2.3-cp39-cp39-win_amd64.whl", hash = "sha256:3f04becd4fcda03c0160d0da9c8f0c246bc78f2f7af0feea1ec0930e7c93fa4a"}, + {file = "coverage-7.2.3-pp37.pp38.pp39-none-any.whl", hash = "sha256:965ee3e782c7892befc25575fa171b521d33798132692df428a09efacaffe8d0"}, + {file = "coverage-7.2.3.tar.gz", hash = "sha256:d298c2815fa4891edd9abe5ad6e6cb4207104c7dd9fd13aea3fdebf6f9b91259"}, +] + +[package.dependencies] +tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} + +[package.extras] +toml = ["tomli"] + +[[package]] +name = "exceptiongroup" +version = "1.1.1" +description = "Backport of PEP 654 (exception groups)" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "exceptiongroup-1.1.1-py3-none-any.whl", hash = "sha256:232c37c63e4f682982c8b6459f33a8981039e5fb8756b2074364e5055c498c9e"}, + {file = "exceptiongroup-1.1.1.tar.gz", hash = "sha256:d484c3090ba2889ae2928419117447a14daf3c1231d5e30d0aae34f354f01785"}, +] + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "execnet" +version = "1.9.0" +description = "execnet: rapid multi-Python deployment" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "execnet-1.9.0-py2.py3-none-any.whl", hash = "sha256:a295f7cc774947aac58dde7fdc85f4aa00c42adf5d8f5468fc630c1acf30a142"}, + {file = "execnet-1.9.0.tar.gz", hash = "sha256:8f694f3ba9cc92cab508b152dcfe322153975c29bda272e2fd7f3f00f36e47c5"}, +] + +[package.extras] +testing = ["pre-commit"] + +[[package]] +name = "h11" +version = "0.14.0" +description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, + {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, +] + +[package.dependencies] +typing-extensions = {version = "*", markers = "python_version < \"3.8\""} + +[[package]] +name = "h2" +version = "4.1.0" +description = "HTTP/2 State-Machine based protocol implementation" +category = "main" +optional = false +python-versions = ">=3.6.1" +files = [ + {file = "h2-4.1.0-py3-none-any.whl", hash = "sha256:03a46bcf682256c95b5fd9e9a99c1323584c3eec6440d379b9903d709476bc6d"}, + {file = "h2-4.1.0.tar.gz", hash = "sha256:a83aca08fbe7aacb79fec788c9c0bac936343560ed9ec18b82a13a12c28d2abb"}, +] + +[package.dependencies] +hpack = ">=4.0,<5" +hyperframe = ">=6.0,<7" + +[[package]] +name = "hpack" +version = "4.0.0" +description = "Pure-Python HPACK header compression" +category = "main" +optional = false +python-versions = ">=3.6.1" +files = [ + {file = "hpack-4.0.0-py3-none-any.whl", hash = "sha256:84a076fad3dc9a9f8063ccb8041ef100867b1878b25ef0ee63847a5d53818a6c"}, + {file = "hpack-4.0.0.tar.gz", hash = "sha256:fc41de0c63e687ebffde81187a948221294896f6bdc0ae2312708df339430095"}, +] + +[[package]] +name = "httpcore" +version = "0.17.0" +description = "A minimal low-level HTTP client." +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "httpcore-0.17.0-py3-none-any.whl", hash = "sha256:0fdfea45e94f0c9fd96eab9286077f9ff788dd186635ae61b312693e4d943599"}, + {file = "httpcore-0.17.0.tar.gz", hash = "sha256:cc045a3241afbf60ce056202301b4d8b6af08845e3294055eb26b09913ef903c"}, +] + +[package.dependencies] +anyio = ">=3.0,<5.0" +certifi = "*" +h11 = ">=0.13,<0.15" +sniffio = ">=1.0.0,<2.0.0" + +[package.extras] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (>=1.0.0,<2.0.0)"] + +[[package]] +name = "httpx" +version = "0.24.0" +description = "The next generation HTTP client." +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "httpx-0.24.0-py3-none-any.whl", hash = "sha256:447556b50c1921c351ea54b4fe79d91b724ed2b027462ab9a329465d147d5a4e"}, + {file = "httpx-0.24.0.tar.gz", hash = "sha256:507d676fc3e26110d41df7d35ebd8b3b8585052450f4097401c9be59d928c63e"}, +] + +[package.dependencies] +certifi = "*" +h2 = {version = ">=3,<5", optional = true, markers = "extra == \"http2\""} +httpcore = ">=0.15.0,<0.18.0" +idna = "*" +sniffio = "*" + +[package.extras] +brotli = ["brotli", "brotlicffi"] +cli = ["click (>=8.0.0,<9.0.0)", "pygments (>=2.0.0,<3.0.0)", "rich (>=10,<14)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (>=1.0.0,<2.0.0)"] + +[[package]] +name = "hyperframe" +version = "6.0.1" +description = "HTTP/2 framing layer for Python" +category = "main" +optional = false +python-versions = ">=3.6.1" +files = [ + {file = "hyperframe-6.0.1-py3-none-any.whl", hash = "sha256:0ec6bafd80d8ad2195c4f03aacba3a8265e57bc4cff261e802bf39970ed02a15"}, + {file = "hyperframe-6.0.1.tar.gz", hash = "sha256:ae510046231dc8e9ecb1a6586f63d2347bf4c8905914aa84ba585ae85f28a914"}, +] + +[[package]] +name = "idna" +version = "3.4" +description = "Internationalized Domain Names in Applications (IDNA)" +category = "main" +optional = false +python-versions = ">=3.5" +files = [ + {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, + {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, +] + +[[package]] +name = "importlib-metadata" +version = "6.4.1" +description = "Read metadata from Python packages" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "importlib_metadata-6.4.1-py3-none-any.whl", hash = "sha256:63ace321e24167d12fbb176b6015f4dbe06868c54a2af4f15849586afb9027fd"}, + {file = "importlib_metadata-6.4.1.tar.gz", hash = "sha256:eb1a7933041f0f85c94cd130258df3fb0dec060ad8c1c9318892ef4192c47ce1"}, +] + +[package.dependencies] +typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} +zipp = ">=0.5" + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +perf = ["ipython"] +testing = ["flake8 (<5)", "flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)"] + +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + +[[package]] +name = "lxml" +version = "4.9.2" +description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, != 3.4.*" +files = [ + {file = "lxml-4.9.2-cp27-cp27m-macosx_10_15_x86_64.whl", hash = "sha256:76cf573e5a365e790396a5cc2b909812633409306c6531a6877c59061e42c4f2"}, + {file = "lxml-4.9.2-cp27-cp27m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b1f42b6921d0e81b1bcb5e395bc091a70f41c4d4e55ba99c6da2b31626c44892"}, + {file = "lxml-4.9.2-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:9f102706d0ca011de571de32c3247c6476b55bb6bc65a20f682f000b07a4852a"}, + {file = "lxml-4.9.2-cp27-cp27m-win32.whl", hash = "sha256:8d0b4612b66ff5d62d03bcaa043bb018f74dfea51184e53f067e6fdcba4bd8de"}, + {file = "lxml-4.9.2-cp27-cp27m-win_amd64.whl", hash = "sha256:4c8f293f14abc8fd3e8e01c5bd86e6ed0b6ef71936ded5bf10fe7a5efefbaca3"}, + {file = "lxml-4.9.2-cp27-cp27mu-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2899456259589aa38bfb018c364d6ae7b53c5c22d8e27d0ec7609c2a1ff78b50"}, + {file = "lxml-4.9.2-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6749649eecd6a9871cae297bffa4ee76f90b4504a2a2ab528d9ebe912b101975"}, + {file = "lxml-4.9.2-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:a08cff61517ee26cb56f1e949cca38caabe9ea9fbb4b1e10a805dc39844b7d5c"}, + {file = "lxml-4.9.2-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:85cabf64adec449132e55616e7ca3e1000ab449d1d0f9d7f83146ed5bdcb6d8a"}, + {file = "lxml-4.9.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:8340225bd5e7a701c0fa98284c849c9b9fc9238abf53a0ebd90900f25d39a4e4"}, + {file = "lxml-4.9.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:1ab8f1f932e8f82355e75dda5413a57612c6ea448069d4fb2e217e9a4bed13d4"}, + {file = "lxml-4.9.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:699a9af7dffaf67deeae27b2112aa06b41c370d5e7633e0ee0aea2e0b6c211f7"}, + {file = "lxml-4.9.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b9cc34af337a97d470040f99ba4282f6e6bac88407d021688a5d585e44a23184"}, + {file = "lxml-4.9.2-cp310-cp310-win32.whl", hash = "sha256:d02a5399126a53492415d4906ab0ad0375a5456cc05c3fc0fc4ca11771745cda"}, + {file = "lxml-4.9.2-cp310-cp310-win_amd64.whl", hash = "sha256:a38486985ca49cfa574a507e7a2215c0c780fd1778bb6290c21193b7211702ab"}, + {file = "lxml-4.9.2-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:c83203addf554215463b59f6399835201999b5e48019dc17f182ed5ad87205c9"}, + {file = "lxml-4.9.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:2a87fa548561d2f4643c99cd13131acb607ddabb70682dcf1dff5f71f781a4bf"}, + {file = "lxml-4.9.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:d6b430a9938a5a5d85fc107d852262ddcd48602c120e3dbb02137c83d212b380"}, + {file = "lxml-4.9.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:3efea981d956a6f7173b4659849f55081867cf897e719f57383698af6f618a92"}, + {file = "lxml-4.9.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:df0623dcf9668ad0445e0558a21211d4e9a149ea8f5666917c8eeec515f0a6d1"}, + {file = "lxml-4.9.2-cp311-cp311-win32.whl", hash = "sha256:da248f93f0418a9e9d94b0080d7ebc407a9a5e6d0b57bb30db9b5cc28de1ad33"}, + {file = "lxml-4.9.2-cp311-cp311-win_amd64.whl", hash = "sha256:3818b8e2c4b5148567e1b09ce739006acfaa44ce3156f8cbbc11062994b8e8dd"}, + {file = "lxml-4.9.2-cp35-cp35m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ca989b91cf3a3ba28930a9fc1e9aeafc2a395448641df1f387a2d394638943b0"}, + {file = "lxml-4.9.2-cp35-cp35m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:822068f85e12a6e292803e112ab876bc03ed1f03dddb80154c395f891ca6b31e"}, + {file = "lxml-4.9.2-cp35-cp35m-win32.whl", hash = "sha256:be7292c55101e22f2a3d4d8913944cbea71eea90792bf914add27454a13905df"}, + {file = "lxml-4.9.2-cp35-cp35m-win_amd64.whl", hash = "sha256:998c7c41910666d2976928c38ea96a70d1aa43be6fe502f21a651e17483a43c5"}, + {file = "lxml-4.9.2-cp36-cp36m-macosx_10_15_x86_64.whl", hash = "sha256:b26a29f0b7fc6f0897f043ca366142d2b609dc60756ee6e4e90b5f762c6adc53"}, + {file = "lxml-4.9.2-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:ab323679b8b3030000f2be63e22cdeea5b47ee0abd2d6a1dc0c8103ddaa56cd7"}, + {file = "lxml-4.9.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:689bb688a1db722485e4610a503e3e9210dcc20c520b45ac8f7533c837be76fe"}, + {file = "lxml-4.9.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:f49e52d174375a7def9915c9f06ec4e569d235ad428f70751765f48d5926678c"}, + {file = "lxml-4.9.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:36c3c175d34652a35475a73762b545f4527aec044910a651d2bf50de9c3352b1"}, + {file = "lxml-4.9.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:a35f8b7fa99f90dd2f5dc5a9fa12332642f087a7641289ca6c40d6e1a2637d8e"}, + {file = "lxml-4.9.2-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:58bfa3aa19ca4c0f28c5dde0ff56c520fbac6f0daf4fac66ed4c8d2fb7f22e74"}, + {file = "lxml-4.9.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:bc718cd47b765e790eecb74d044cc8d37d58562f6c314ee9484df26276d36a38"}, + {file = "lxml-4.9.2-cp36-cp36m-win32.whl", hash = "sha256:d5bf6545cd27aaa8a13033ce56354ed9e25ab0e4ac3b5392b763d8d04b08e0c5"}, + {file = "lxml-4.9.2-cp36-cp36m-win_amd64.whl", hash = "sha256:3ab9fa9d6dc2a7f29d7affdf3edebf6ece6fb28a6d80b14c3b2fb9d39b9322c3"}, + {file = "lxml-4.9.2-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:05ca3f6abf5cf78fe053da9b1166e062ade3fa5d4f92b4ed688127ea7d7b1d03"}, + {file = "lxml-4.9.2-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:a5da296eb617d18e497bcf0a5c528f5d3b18dadb3619fbdadf4ed2356ef8d941"}, + {file = "lxml-4.9.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:04876580c050a8c5341d706dd464ff04fd597095cc8c023252566a8826505726"}, + {file = "lxml-4.9.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:c9ec3eaf616d67db0764b3bb983962b4f385a1f08304fd30c7283954e6a7869b"}, + {file = "lxml-4.9.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2a29ba94d065945944016b6b74e538bdb1751a1db6ffb80c9d3c2e40d6fa9894"}, + {file = "lxml-4.9.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:a82d05da00a58b8e4c0008edbc8a4b6ec5a4bc1e2ee0fb6ed157cf634ed7fa45"}, + {file = "lxml-4.9.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:223f4232855ade399bd409331e6ca70fb5578efef22cf4069a6090acc0f53c0e"}, + {file = "lxml-4.9.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d17bc7c2ccf49c478c5bdd447594e82692c74222698cfc9b5daae7ae7e90743b"}, + {file = "lxml-4.9.2-cp37-cp37m-win32.whl", hash = "sha256:b64d891da92e232c36976c80ed7ebb383e3f148489796d8d31a5b6a677825efe"}, + {file = "lxml-4.9.2-cp37-cp37m-win_amd64.whl", hash = "sha256:a0a336d6d3e8b234a3aae3c674873d8f0e720b76bc1d9416866c41cd9500ffb9"}, + {file = "lxml-4.9.2-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:da4dd7c9c50c059aba52b3524f84d7de956f7fef88f0bafcf4ad7dde94a064e8"}, + {file = "lxml-4.9.2-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:821b7f59b99551c69c85a6039c65b75f5683bdc63270fec660f75da67469ca24"}, + {file = "lxml-4.9.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:e5168986b90a8d1f2f9dc1b841467c74221bd752537b99761a93d2d981e04889"}, + {file = "lxml-4.9.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:8e20cb5a47247e383cf4ff523205060991021233ebd6f924bca927fcf25cf86f"}, + {file = "lxml-4.9.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:13598ecfbd2e86ea7ae45ec28a2a54fb87ee9b9fdb0f6d343297d8e548392c03"}, + {file = "lxml-4.9.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:880bbbcbe2fca64e2f4d8e04db47bcdf504936fa2b33933efd945e1b429bea8c"}, + {file = "lxml-4.9.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:7d2278d59425777cfcb19735018d897ca8303abe67cc735f9f97177ceff8027f"}, + {file = "lxml-4.9.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5344a43228767f53a9df6e5b253f8cdca7dfc7b7aeae52551958192f56d98457"}, + {file = "lxml-4.9.2-cp38-cp38-win32.whl", hash = "sha256:925073b2fe14ab9b87e73f9a5fde6ce6392da430f3004d8b72cc86f746f5163b"}, + {file = "lxml-4.9.2-cp38-cp38-win_amd64.whl", hash = "sha256:9b22c5c66f67ae00c0199f6055705bc3eb3fcb08d03d2ec4059a2b1b25ed48d7"}, + {file = "lxml-4.9.2-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:5f50a1c177e2fa3ee0667a5ab79fdc6b23086bc8b589d90b93b4bd17eb0e64d1"}, + {file = "lxml-4.9.2-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:090c6543d3696cbe15b4ac6e175e576bcc3f1ccfbba970061b7300b0c15a2140"}, + {file = "lxml-4.9.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:63da2ccc0857c311d764e7d3d90f429c252e83b52d1f8f1d1fe55be26827d1f4"}, + {file = "lxml-4.9.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:5b4545b8a40478183ac06c073e81a5ce4cf01bf1734962577cf2bb569a5b3bbf"}, + {file = "lxml-4.9.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2e430cd2824f05f2d4f687701144556646bae8f249fd60aa1e4c768ba7018947"}, + {file = "lxml-4.9.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6804daeb7ef69e7b36f76caddb85cccd63d0c56dedb47555d2fc969e2af6a1a5"}, + {file = "lxml-4.9.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:a6e441a86553c310258aca15d1c05903aaf4965b23f3bc2d55f200804e005ee5"}, + {file = "lxml-4.9.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ca34efc80a29351897e18888c71c6aca4a359247c87e0b1c7ada14f0ab0c0fb2"}, + {file = "lxml-4.9.2-cp39-cp39-win32.whl", hash = "sha256:6b418afe5df18233fc6b6093deb82a32895b6bb0b1155c2cdb05203f583053f1"}, + {file = "lxml-4.9.2-cp39-cp39-win_amd64.whl", hash = "sha256:f1496ea22ca2c830cbcbd473de8f114a320da308438ae65abad6bab7867fe38f"}, + {file = "lxml-4.9.2-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:b264171e3143d842ded311b7dccd46ff9ef34247129ff5bf5066123c55c2431c"}, + {file = "lxml-4.9.2-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:0dc313ef231edf866912e9d8f5a042ddab56c752619e92dfd3a2c277e6a7299a"}, + {file = "lxml-4.9.2-pp38-pypy38_pp73-macosx_10_15_x86_64.whl", hash = "sha256:16efd54337136e8cd72fb9485c368d91d77a47ee2d42b057564aae201257d419"}, + {file = "lxml-4.9.2-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:0f2b1e0d79180f344ff9f321327b005ca043a50ece8713de61d1cb383fb8ac05"}, + {file = "lxml-4.9.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:7b770ed79542ed52c519119473898198761d78beb24b107acf3ad65deae61f1f"}, + {file = "lxml-4.9.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:efa29c2fe6b4fdd32e8ef81c1528506895eca86e1d8c4657fda04c9b3786ddf9"}, + {file = "lxml-4.9.2-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7e91ee82f4199af8c43d8158024cbdff3d931df350252288f0d4ce656df7f3b5"}, + {file = "lxml-4.9.2-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:b23e19989c355ca854276178a0463951a653309fb8e57ce674497f2d9f208746"}, + {file = "lxml-4.9.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:01d36c05f4afb8f7c20fd9ed5badca32a2029b93b1750f571ccc0b142531caf7"}, + {file = "lxml-4.9.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7b515674acfdcadb0eb5d00d8a709868173acece5cb0be3dd165950cbfdf5409"}, + {file = "lxml-4.9.2.tar.gz", hash = "sha256:2455cfaeb7ac70338b3257f41e21f0724f4b5b0c0e7702da67ee6c3640835b67"}, +] + +[package.extras] +cssselect = ["cssselect (>=0.7)"] +html5 = ["html5lib"] +htmlsoup = ["BeautifulSoup4"] +source = ["Cython (>=0.29.7)"] + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +description = "Type system extensions for programs checked with the mypy type checker." +category = "dev" +optional = false +python-versions = ">=3.5" +files = [ + {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, + {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, +] + +[[package]] +name = "nodeenv" +version = "1.7.0" +description = "Node.js virtual environment builder" +category = "dev" +optional = false +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" +files = [ + {file = "nodeenv-1.7.0-py2.py3-none-any.whl", hash = "sha256:27083a7b96a25f2f5e1d8cb4b6317ee8aeda3bdd121394e5ac54e498028a042e"}, + {file = "nodeenv-1.7.0.tar.gz", hash = "sha256:e0e7f7dfb85fc5394c6fe1e8fa98131a2473e04311a45afb6508f7cf1836fa2b"}, +] + +[package.dependencies] +setuptools = "*" + +[[package]] +name = "packaging" +version = "23.1" +description = "Core utilities for Python packages" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "packaging-23.1-py3-none-any.whl", hash = "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61"}, + {file = "packaging-23.1.tar.gz", hash = "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"}, +] + +[[package]] +name = "pathspec" +version = "0.11.1" +description = "Utility library for gitignore style pattern matching of file paths." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pathspec-0.11.1-py3-none-any.whl", hash = "sha256:d8af70af76652554bd134c22b3e8a1cc46ed7d91edcdd721ef1a0c51a84a5293"}, + {file = "pathspec-0.11.1.tar.gz", hash = "sha256:2798de800fa92780e33acca925945e9a19a133b715067cf165b8866c15a31687"}, +] + +[[package]] +name = "platformdirs" +version = "3.2.0" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "platformdirs-3.2.0-py3-none-any.whl", hash = "sha256:ebe11c0d7a805086e99506aa331612429a72ca7cd52a1f0d277dc4adc20cb10e"}, + {file = "platformdirs-3.2.0.tar.gz", hash = "sha256:d5b638ca397f25f979350ff789db335903d7ea010ab28903f57b27e1b16c2b08"}, +] + +[package.dependencies] +typing-extensions = {version = ">=4.5", markers = "python_version < \"3.8\""} + +[package.extras] +docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=6.1.3)", "sphinx-autodoc-typehints (>=1.22,!=1.23.4)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.2.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"] + +[[package]] +name = "pluggy" +version = "1.0.0" +description = "plugin and hook calling mechanisms for python" +category = "dev" +optional = false +python-versions = ">=3.6" +files = [ + {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, + {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, +] + +[package.dependencies] +importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "pyright" +version = "1.1.303" +description = "Command line wrapper for pyright" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pyright-1.1.303-py3-none-any.whl", hash = "sha256:8fe3d122d7e965e2df2cef64e1ceb98cff8200f458e7892d92a4c21ee85689c7"}, + {file = "pyright-1.1.303.tar.gz", hash = "sha256:7daa516424555681e8974b21a95c108c5def791bf5381522b1410026d4da62c1"}, +] + +[package.dependencies] +nodeenv = ">=1.6.0" +typing-extensions = {version = ">=3.7", markers = "python_version < \"3.8\""} + +[package.extras] +all = ["twine (>=3.4.1)"] +dev = ["twine (>=3.4.1)"] + +[[package]] +name = "pytest" +version = "7.3.1" +description = "pytest: simple powerful testing with Python" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest-7.3.1-py3-none-any.whl", hash = "sha256:3799fa815351fea3a5e96ac7e503a96fa51cc9942c3753cda7651b93c1cfa362"}, + {file = "pytest-7.3.1.tar.gz", hash = "sha256:434afafd78b1d78ed0addf160ad2b77a30d35d4bdf8af234fe621919d9ed15e3"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} +importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" +tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} + +[package.extras] +testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] + +[[package]] +name = "pytest-cov" +version = "4.0.0" +description = "Pytest plugin for measuring coverage." +category = "dev" +optional = false +python-versions = ">=3.6" +files = [ + {file = "pytest-cov-4.0.0.tar.gz", hash = "sha256:996b79efde6433cdbd0088872dbc5fb3ed7fe1578b68cdbba634f14bb8dd0470"}, + {file = "pytest_cov-4.0.0-py3-none-any.whl", hash = "sha256:2feb1b751d66a8bd934e5edfa2e961d11309dc37b73b0eabe73b5945fee20f6b"}, +] + +[package.dependencies] +coverage = {version = ">=5.2.1", extras = ["toml"]} +pytest = ">=4.6" + +[package.extras] +testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] + +[[package]] +name = "pytest-xdist" +version = "3.2.1" +description = "pytest xdist plugin for distributed testing, most importantly across multiple CPUs" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest-xdist-3.2.1.tar.gz", hash = "sha256:1849bd98d8b242b948e472db7478e090bf3361912a8fed87992ed94085f54727"}, + {file = "pytest_xdist-3.2.1-py3-none-any.whl", hash = "sha256:37290d161638a20b672401deef1cba812d110ac27e35d213f091d15b8beb40c9"}, +] + +[package.dependencies] +execnet = ">=1.1" +pytest = ">=6.2.0" + +[package.extras] +psutil = ["psutil (>=3.0)"] +setproctitle = ["setproctitle"] +testing = ["filelock"] + +[[package]] +name = "pyyaml" +version = "6.0" +description = "YAML parser and emitter for Python" +category = "dev" +optional = false +python-versions = ">=3.6" +files = [ + {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, + {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"}, + {file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"}, + {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"}, + {file = "PyYAML-6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358"}, + {file = "PyYAML-6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1"}, + {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d"}, + {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f"}, + {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782"}, + {file = "PyYAML-6.0-cp311-cp311-win32.whl", hash = "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7"}, + {file = "PyYAML-6.0-cp311-cp311-win_amd64.whl", hash = "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf"}, + {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4"}, + {file = "PyYAML-6.0-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"}, + {file = "PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"}, + {file = "PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9"}, + {file = "PyYAML-6.0-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"}, + {file = "PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"}, + {file = "PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287"}, + {file = "PyYAML-6.0-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"}, + {file = "PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"}, + {file = "PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"}, + {file = "PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0"}, + {file = "PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"}, + {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"}, + {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, +] + +[[package]] +name = "ruff" +version = "0.0.261" +description = "An extremely fast Python linter, written in Rust." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "ruff-0.0.261-py3-none-macosx_10_7_x86_64.whl", hash = "sha256:6624a966c4a21110cee6780333e2216522a831364896f3d98f13120936eff40a"}, + {file = "ruff-0.0.261-py3-none-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:2dba68a9e558ab33e6dd5d280af798a2d9d3c80c913ad9c8b8e97d7b287f1cc9"}, + {file = "ruff-0.0.261-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8dbd0cee5a81b0785dc0feeb2640c1e31abe93f0d77c5233507ac59731a626f1"}, + {file = "ruff-0.0.261-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:581e64fa1518df495ca890a605ee65065101a86db56b6858f848bade69fc6489"}, + {file = "ruff-0.0.261-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cc970f6ece0b4950e419f0252895ee42e9e8e5689c6494d18f5dc2c6ebb7f798"}, + {file = "ruff-0.0.261-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:8fa98e747e0fe185d65a40b0ea13f55c492f3b5f9a032a1097e82edaddb9e52e"}, + {file = "ruff-0.0.261-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f268d52a71bf410aa45c232870c17049df322a7d20e871cfe622c9fc784aab7b"}, + {file = "ruff-0.0.261-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d1293acc64eba16a11109678dc4743df08c207ed2edbeaf38b3e10eb2597321b"}, + {file = "ruff-0.0.261-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d95596e2f4cafead19a6d1ec0b86f8fda45ba66fe934de3956d71146a87959b3"}, + {file = "ruff-0.0.261-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4bcec45abdf65c1328a269cf6cc193f7ff85b777fa2865c64cf2c96b80148a2c"}, + {file = "ruff-0.0.261-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:6c5f397ec0af42a434ad4b6f86565027406c5d0d0ebeea0d5b3f90c4bf55bc82"}, + {file = "ruff-0.0.261-py3-none-musllinux_1_2_i686.whl", hash = "sha256:39abd02342cec0c131b2ddcaace08b2eae9700cab3ca7dba64ae5fd4f4881bd0"}, + {file = "ruff-0.0.261-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:aaa4f52a6e513f8daa450dac4859e80390d947052f592f0d8e796baab24df2fc"}, + {file = "ruff-0.0.261-py3-none-win32.whl", hash = "sha256:daff64b4e86e42ce69e6367d63aab9562fc213cd4db0e146859df8abc283dba0"}, + {file = "ruff-0.0.261-py3-none-win_amd64.whl", hash = "sha256:0fbc689c23609edda36169c8708bb91bab111d8f44cb4a88330541757770ab30"}, + {file = "ruff-0.0.261-py3-none-win_arm64.whl", hash = "sha256:d2eddc60ae75fc87f8bb8fd6e8d5339cf884cd6de81e82a50287424309c187ba"}, + {file = "ruff-0.0.261.tar.gz", hash = "sha256:c1c715b0d1e18f9c509d7c411ca61da3543a4aa459325b1b1e52b8301d65c6d2"}, +] + +[[package]] +name = "setuptools" +version = "67.6.1" +description = "Easily download, build, install, upgrade, and uninstall Python packages" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "setuptools-67.6.1-py3-none-any.whl", hash = "sha256:e728ca814a823bf7bf60162daf9db95b93d532948c4c0bea762ce62f60189078"}, + {file = "setuptools-67.6.1.tar.gz", hash = "sha256:257de92a9d50a60b8e22abfcbb771571fde0dbf3ec234463212027a4eeecbe9a"}, +] + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] + +[[package]] +name = "sniffio" +version = "1.3.0" +description = "Sniff out which async library your code is running under" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "sniffio-1.3.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"}, + {file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"}, +] + +[[package]] +name = "tomd" +version = "0.1.3" +description = "Convert HTML to Markdown." +category = "main" +optional = true +python-versions = "*" +files = [ + {file = "tomd-0.1.3.tar.gz", hash = "sha256:23f21ae853157be49d163159bc7c6bc007dd5e87b69769ec5ba3e17f5de7a6d4"}, +] + +[[package]] +name = "tomli" +version = "2.0.1" +description = "A lil' TOML parser" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] + +[[package]] +name = "typed-ast" +version = "1.5.4" +description = "a fork of Python 2 and 3 ast modules with type comment support" +category = "dev" +optional = false +python-versions = ">=3.6" +files = [ + {file = "typed_ast-1.5.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:669dd0c4167f6f2cd9f57041e03c3c2ebf9063d0757dc89f79ba1daa2bfca9d4"}, + {file = "typed_ast-1.5.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:211260621ab1cd7324e0798d6be953d00b74e0428382991adfddb352252f1d62"}, + {file = "typed_ast-1.5.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:267e3f78697a6c00c689c03db4876dd1efdfea2f251a5ad6555e82a26847b4ac"}, + {file = "typed_ast-1.5.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c542eeda69212fa10a7ada75e668876fdec5f856cd3d06829e6aa64ad17c8dfe"}, + {file = "typed_ast-1.5.4-cp310-cp310-win_amd64.whl", hash = "sha256:a9916d2bb8865f973824fb47436fa45e1ebf2efd920f2b9f99342cb7fab93f72"}, + {file = "typed_ast-1.5.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:79b1e0869db7c830ba6a981d58711c88b6677506e648496b1f64ac7d15633aec"}, + {file = "typed_ast-1.5.4-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a94d55d142c9265f4ea46fab70977a1944ecae359ae867397757d836ea5a3f47"}, + {file = "typed_ast-1.5.4-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:183afdf0ec5b1b211724dfef3d2cad2d767cbefac291f24d69b00546c1837fb6"}, + {file = "typed_ast-1.5.4-cp36-cp36m-win_amd64.whl", hash = "sha256:639c5f0b21776605dd6c9dbe592d5228f021404dafd377e2b7ac046b0349b1a1"}, + {file = "typed_ast-1.5.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cf4afcfac006ece570e32d6fa90ab74a17245b83dfd6655a6f68568098345ff6"}, + {file = "typed_ast-1.5.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed855bbe3eb3715fca349c80174cfcfd699c2f9de574d40527b8429acae23a66"}, + {file = "typed_ast-1.5.4-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6778e1b2f81dfc7bc58e4b259363b83d2e509a65198e85d5700dfae4c6c8ff1c"}, + {file = "typed_ast-1.5.4-cp37-cp37m-win_amd64.whl", hash = "sha256:0261195c2062caf107831e92a76764c81227dae162c4f75192c0d489faf751a2"}, + {file = "typed_ast-1.5.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2efae9db7a8c05ad5547d522e7dbe62c83d838d3906a3716d1478b6c1d61388d"}, + {file = "typed_ast-1.5.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7d5d014b7daa8b0bf2eaef684295acae12b036d79f54178b92a2b6a56f92278f"}, + {file = "typed_ast-1.5.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:370788a63915e82fd6f212865a596a0fefcbb7d408bbbb13dea723d971ed8bdc"}, + {file = "typed_ast-1.5.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4e964b4ff86550a7a7d56345c7864b18f403f5bd7380edf44a3c1fb4ee7ac6c6"}, + {file = "typed_ast-1.5.4-cp38-cp38-win_amd64.whl", hash = "sha256:683407d92dc953c8a7347119596f0b0e6c55eb98ebebd9b23437501b28dcbb8e"}, + {file = "typed_ast-1.5.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4879da6c9b73443f97e731b617184a596ac1235fe91f98d279a7af36c796da35"}, + {file = "typed_ast-1.5.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3e123d878ba170397916557d31c8f589951e353cc95fb7f24f6bb69adc1a8a97"}, + {file = "typed_ast-1.5.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebd9d7f80ccf7a82ac5f88c521115cc55d84e35bf8b446fcd7836eb6b98929a3"}, + {file = "typed_ast-1.5.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98f80dee3c03455e92796b58b98ff6ca0b2a6f652120c263efdba4d6c5e58f72"}, + {file = "typed_ast-1.5.4-cp39-cp39-win_amd64.whl", hash = "sha256:0fdbcf2fef0ca421a3f5912555804296f0b0960f0418c440f5d6d3abb549f3e1"}, + {file = "typed_ast-1.5.4.tar.gz", hash = "sha256:39e21ceb7388e4bb37f4c679d72707ed46c2fbf2a5609b8b8ebc4b067d977df2"}, +] + +[[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 = "ujson" +version = "5.7.0" +description = "Ultra fast JSON encoder and decoder for Python" +category = "main" +optional = true +python-versions = ">=3.7" +files = [ + {file = "ujson-5.7.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5eba5e69e4361ac3a311cf44fa71bc619361b6e0626768a494771aacd1c2f09b"}, + {file = "ujson-5.7.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:aae4d9e1b4c7b61780f0a006c897a4a1904f862fdab1abb3ea8f45bd11aa58f3"}, + {file = "ujson-5.7.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d2e43ccdba1cb5c6d3448eadf6fc0dae7be6c77e357a3abc968d1b44e265866d"}, + {file = "ujson-5.7.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54384ce4920a6d35fa9ea8e580bc6d359e3eb961fa7e43f46c78e3ed162d56ff"}, + {file = "ujson-5.7.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:24ad1aa7fc4e4caa41d3d343512ce68e41411fb92adf7f434a4d4b3749dc8f58"}, + {file = "ujson-5.7.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:afff311e9f065a8f03c3753db7011bae7beb73a66189c7ea5fcb0456b7041ea4"}, + {file = "ujson-5.7.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6e80f0d03e7e8646fc3d79ed2d875cebd4c83846e129737fdc4c2532dbd43d9e"}, + {file = "ujson-5.7.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:137831d8a0db302fb6828ee21c67ad63ac537bddc4376e1aab1c8573756ee21c"}, + {file = "ujson-5.7.0-cp310-cp310-win32.whl", hash = "sha256:7df3fd35ebc14dafeea031038a99232b32f53fa4c3ecddb8bed132a43eefb8ad"}, + {file = "ujson-5.7.0-cp310-cp310-win_amd64.whl", hash = "sha256:af4639f684f425177d09ae409c07602c4096a6287027469157bfb6f83e01448b"}, + {file = "ujson-5.7.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9b0f2680ce8a70f77f5d70aaf3f013d53e6af6d7058727a35d8ceb4a71cdd4e9"}, + {file = "ujson-5.7.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:67a19fd8e7d8cc58a169bea99fed5666023adf707a536d8f7b0a3c51dd498abf"}, + {file = "ujson-5.7.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6abb8e6d8f1ae72f0ed18287245f5b6d40094e2656d1eab6d99d666361514074"}, + {file = "ujson-5.7.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8cd622c069368d5074bd93817b31bdb02f8d818e57c29e206f10a1f9c6337dd"}, + {file = "ujson-5.7.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:14f9082669f90e18e64792b3fd0bf19f2b15e7fe467534a35ea4b53f3bf4b755"}, + {file = "ujson-5.7.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d7ff6ebb43bc81b057724e89550b13c9a30eda0f29c2f506f8b009895438f5a6"}, + {file = "ujson-5.7.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:f7f241488879d91a136b299e0c4ce091996c684a53775e63bb442d1a8e9ae22a"}, + {file = "ujson-5.7.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5593263a7fcfb934107444bcfba9dde8145b282de0ee9f61e285e59a916dda0f"}, + {file = "ujson-5.7.0-cp311-cp311-win32.whl", hash = "sha256:26c2b32b489c393106e9cb68d0a02e1a7b9d05a07429d875c46b94ee8405bdb7"}, + {file = "ujson-5.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:ed24406454bb5a31df18f0a423ae14beb27b28cdfa34f6268e7ebddf23da807e"}, + {file = "ujson-5.7.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:18679484e3bf9926342b1c43a3bd640f93a9eeeba19ef3d21993af7b0c44785d"}, + {file = "ujson-5.7.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ee295761e1c6c30400641f0a20d381633d7622633cdf83a194f3c876a0e4b7e"}, + {file = "ujson-5.7.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b738282e12a05f400b291966630a98d622da0938caa4bc93cf65adb5f4281c60"}, + {file = "ujson-5.7.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:00343501dbaa5172e78ef0e37f9ebd08040110e11c12420ff7c1f9f0332d939e"}, + {file = "ujson-5.7.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c0d1f7c3908357ee100aa64c4d1cf91edf99c40ac0069422a4fd5fd23b263263"}, + {file = "ujson-5.7.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:a5d2f44331cf04689eafac7a6596c71d6657967c07ac700b0ae1c921178645da"}, + {file = "ujson-5.7.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:16b2254a77b310f118717715259a196662baa6b1f63b1a642d12ab1ff998c3d7"}, + {file = "ujson-5.7.0-cp37-cp37m-win32.whl", hash = "sha256:6faf46fa100b2b89e4db47206cf8a1ffb41542cdd34dde615b2fc2288954f194"}, + {file = "ujson-5.7.0-cp37-cp37m-win_amd64.whl", hash = "sha256:ff0004c3f5a9a6574689a553d1b7819d1a496b4f005a7451f339dc2d9f4cf98c"}, + {file = "ujson-5.7.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:75204a1dd7ec6158c8db85a2f14a68d2143503f4bafb9a00b63fe09d35762a5e"}, + {file = "ujson-5.7.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7312731c7826e6c99cdd3ac503cd9acd300598e7a80bcf41f604fee5f49f566c"}, + {file = "ujson-5.7.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b9dc5a90e2149643df7f23634fe202fed5ebc787a2a1be95cf23632b4d90651"}, + {file = "ujson-5.7.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b6a6961fc48821d84b1198a09516e396d56551e910d489692126e90bf4887d29"}, + {file = "ujson-5.7.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b01a9af52a0d5c46b2c68e3f258fdef2eacaa0ce6ae3e9eb97983f5b1166edb6"}, + {file = "ujson-5.7.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b7316d3edeba8a403686cdcad4af737b8415493101e7462a70ff73dd0609eafc"}, + {file = "ujson-5.7.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:4ee997799a23227e2319a3f8817ce0b058923dbd31904761b788dc8f53bd3e30"}, + {file = "ujson-5.7.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:dda9aa4c33435147262cd2ea87c6b7a1ca83ba9b3933ff7df34e69fee9fced0c"}, + {file = "ujson-5.7.0-cp38-cp38-win32.whl", hash = "sha256:bea8d30e362180aafecabbdcbe0e1f0b32c9fa9e39c38e4af037b9d3ca36f50c"}, + {file = "ujson-5.7.0-cp38-cp38-win_amd64.whl", hash = "sha256:c96e3b872bf883090ddf32cc41957edf819c5336ab0007d0cf3854e61841726d"}, + {file = "ujson-5.7.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6411aea4c94a8e93c2baac096fbf697af35ba2b2ed410b8b360b3c0957a952d3"}, + {file = "ujson-5.7.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3d3b3499c55911f70d4e074c626acdb79a56f54262c3c83325ffb210fb03e44d"}, + {file = "ujson-5.7.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:341f891d45dd3814d31764626c55d7ab3fd21af61fbc99d070e9c10c1190680b"}, + {file = "ujson-5.7.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2f242eec917bafdc3f73a1021617db85f9958df80f267db69c76d766058f7b19"}, + {file = "ujson-5.7.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c3af9f9f22a67a8c9466a32115d9073c72a33ae627b11de6f592df0ee09b98b6"}, + {file = "ujson-5.7.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4a3d794afbf134df3056a813e5c8a935208cddeae975bd4bc0ef7e89c52f0ce0"}, + {file = "ujson-5.7.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:800bf998e78dae655008dd10b22ca8dc93bdcfcc82f620d754a411592da4bbf2"}, + {file = "ujson-5.7.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b5ac3d5c5825e30b438ea92845380e812a476d6c2a1872b76026f2e9d8060fc2"}, + {file = "ujson-5.7.0-cp39-cp39-win32.whl", hash = "sha256:cd90027e6d93e8982f7d0d23acf88c896d18deff1903dd96140613389b25c0dd"}, + {file = "ujson-5.7.0-cp39-cp39-win_amd64.whl", hash = "sha256:523ee146cdb2122bbd827f4dcc2a8e66607b3f665186bce9e4f78c9710b6d8ab"}, + {file = "ujson-5.7.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:e87cec407ec004cf1b04c0ed7219a68c12860123dfb8902ef880d3d87a71c172"}, + {file = "ujson-5.7.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bab10165db6a7994e67001733f7f2caf3400b3e11538409d8756bc9b1c64f7e8"}, + {file = "ujson-5.7.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b522be14a28e6ac1cf818599aeff1004a28b42df4ed4d7bc819887b9dac915fc"}, + {file = "ujson-5.7.0-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7592f40175c723c032cdbe9fe5165b3b5903604f774ab0849363386e99e1f253"}, + {file = "ujson-5.7.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:ed22f9665327a981f288a4f758a432824dc0314e4195a0eaeb0da56a477da94d"}, + {file = "ujson-5.7.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:adf445a49d9a97a5a4c9bb1d652a1528de09dd1c48b29f79f3d66cea9f826bf6"}, + {file = "ujson-5.7.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64772a53f3c4b6122ed930ae145184ebaed38534c60f3d859d8c3f00911eb122"}, + {file = "ujson-5.7.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:35209cb2c13fcb9d76d249286105b4897b75a5e7f0efb0c0f4b90f222ce48910"}, + {file = "ujson-5.7.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:90712dfc775b2c7a07d4d8e059dd58636bd6ff1776d79857776152e693bddea6"}, + {file = "ujson-5.7.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:0e4e8981c6e7e9e637e637ad8ffe948a09e5434bc5f52ecbb82b4b4cfc092bfb"}, + {file = "ujson-5.7.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:581c945b811a3d67c27566539bfcb9705ea09cb27c4be0002f7a553c8886b817"}, + {file = "ujson-5.7.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d36a807a24c7d44f71686685ae6fbc8793d784bca1adf4c89f5f780b835b6243"}, + {file = "ujson-5.7.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b4257307e3662aa65e2644a277ca68783c5d51190ed9c49efebdd3cbfd5fa44"}, + {file = "ujson-5.7.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea7423d8a2f9e160c5e011119741682414c5b8dce4ae56590a966316a07a4618"}, + {file = "ujson-5.7.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:4c592eb91a5968058a561d358d0fef59099ed152cfb3e1cd14eee51a7a93879e"}, + {file = "ujson-5.7.0.tar.gz", hash = "sha256:e788e5d5dcae8f6118ac9b45d0b891a0d55f7ac480eddcb7f07263f2bcf37b23"}, +] + +[[package]] +name = "zipp" +version = "3.15.0" +description = "Backport of pathlib-compatible object wrapper for zip files" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "zipp-3.15.0-py3-none-any.whl", hash = "sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556"}, + {file = "zipp-3.15.0.tar.gz", hash = "sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b"}, +] + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] + +[extras] +full = ["tomd", "ujson"] +high-perf = ["ujson"] +md-convert = ["tomd"] + +[metadata] +lock-version = "2.0" +python-versions = "^3.7" +content-hash = "9eb6c3266d733cd4d60b5b792bf6fa111100b4cdc948bc9a0da31235aaa1b54a" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..2c60303 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,53 @@ +[tool.poetry] +name = "JianshuResearchTools" +version = "2.11.0" +description = "科技赋能创作星辰" +authors = ["yezi "] +license = "MIT" +readme = "README.md" +repository = "https://github.com/FHU-yezi/JianshuResearchTools" +keywords = ["jianshu", "SDK"] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Typing :: Typed", + "Programming Language :: Python :: 3" +] +packages = [ + { include = "JianshuResearchTools" }, + { include = "README.md" }, +] + +[tool.poetry.dependencies] +python = "^3.7" +lxml = "^4.9.2" +httpx = { version = "^0.24.0", extras = ["http2"] } +tomd = { version = "^0.1.3", optional = true } +ujson = { version = "^5.7.0", optional = true } + +[tool.poetry.group.dev.dependencies] +ruff = "^0.0.261" +pyright = "^1.1.303" +black = "^23.3.0" +pyyaml = "^6.0" +pytest = "^7.3.1" +pytest-xdist = "^3.2.1" +pytest-cov = "^4.0.0" + +[tool.poetry.extras] +md-convert = ["tomd"] +high-perf = ["ujson"] +full = ["tomd", "ujson"] + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" + +[tool.ruff] + +select = ["A", "ANN", "B", "C", "E", "F", "I", "RET", "S", "SIM", "UP", "W"] + +ignore = ["ANN101", "ANN102", "ANN401", "C901", "E501", "S101", "S104"] + +target-version = "py38" \ No newline at end of file diff --git a/setup.py b/setup.py deleted file mode 100644 index 7d92fca..0000000 --- a/setup.py +++ /dev/null @@ -1,29 +0,0 @@ -import setuptools -from JianshuResearchTools import __version__ - -with open("README.md", "r", encoding="utf-8") as file: - long_description = file.read() - -setuptools.setup( - name="JianshuResearchTools", - version=__version__, - author="FHU-yezi", - author_email="yehaowei20060411@qq.com", - description="科技赋能创作星辰", - long_description=long_description, - long_description_content_type="text/markdown", - url="https://github.com/FHU-yezi/JianshuResearchTools", - packages=["JianshuResearchTools"], - install_requires=["lxml==4.8.0", "httpx==0.22.0"], - extras_require={ - "md-convert": ["tomd==0.1.3"], - "high-perf": ["ujson==5.3.0"], - "full": ["tomd==0.1.3", "ujson==5.3.0"] - }, - classifiers=[ - "Programming Language :: Python :: 3", - "License :: OSI Approved :: MIT Licerense", - "Operating System :: OS Independent", - ], - python_requires=">=3.6" -) diff --git a/test_all.py b/test_all.py index bf87fed..6123dea 100644 --- a/test_all.py +++ b/test_all.py @@ -2,34 +2,38 @@ from typing import Any, List, Union import pytest -from yaml import FullLoader -from yaml import load as yaml_load +from yaml import full_load as yaml_load import JianshuResearchTools as jrt -from JianshuResearchTools.convert import (ArticleSlugToArticleId, - ArticleSlugToArticleUrl, - ArticleUrlToArticleId, - ArticleUrlToArticleSlug, - CollectionSlugToCollectionUrl, - CollectionUrlToCollectionId, - CollectionUrlToCollectionSlug, - IslandSlugToIslandUrl, - IslandUrlToIslandSlug, - NotebookSlugToNotebookUrl, - NotebookUrlToNotebookSlug, - UserSlugToUserId, UserSlugToUserUrl, - UserUrlToUserId, UserUrlToUserSlug) +from JianshuResearchTools.convert import ( + ArticleSlugToArticleId, + ArticleSlugToArticleUrl, + ArticleUrlToArticleId, + ArticleUrlToArticleSlug, + CollectionSlugToCollectionUrl, + CollectionUrlToCollectionId, + CollectionUrlToCollectionSlug, + IslandSlugToIslandUrl, + IslandUrlToIslandSlug, + NotebookSlugToNotebookUrl, + NotebookUrlToNotebookSlug, + UserSlugToUserId, + UserSlugToUserUrl, + UserUrlToUserId, + UserUrlToUserSlug, +) from JianshuResearchTools.exceptions import APIError, InputError, ResourceError error_text_to_obj = { "InputError": InputError, "APIError": APIError, - "ResourceError": ResourceError + "ResourceError": ResourceError, } class NumberNotInRangeError(Exception): """内容不在数值范围内时抛出此异常""" + pass @@ -47,83 +51,83 @@ def AssertRangeCase(value: Union[int, float], case: List[Union[int, float]]) -> raise NumberNotInRangeError(f"{value} 不在范围 {case} 中") -def AssertListCase(value: List[Any], case: List[Any]): +def AssertListCase(value: List[Any], case: List[Any]) -> None: assert set(case).issubset(set(value)) -with open("test_cases.yaml", "r", encoding="utf-8") as f: - test_cases = yaml_load(f, Loader=FullLoader) +with open("test_cases.yaml", encoding="utf-8") as f: + test_cases = yaml_load(f) class TestEggs: # 测试彩蛋内容 - def TestFuture(self): + def TestFuture(self) -> None: jrt.future() class TestConvertModule: - def test_UserUrlToUserId(self): + def test_UserUrlToUserId(self) -> None: for case in test_cases["convert_cases"]["user_convert_cases"]: AssertNormalCase(UserUrlToUserId(case["url"]), case["uid"]) - def test_UserSlugToUserId(self): + def test_UserSlugToUserId(self) -> None: for case in test_cases["convert_cases"]["user_convert_cases"]: AssertNormalCase(UserSlugToUserId(case["uslug"]), case["uid"]) - def test_UserUrlToUserSlug(self): + def test_UserUrlToUserSlug(self) -> None: for case in test_cases["convert_cases"]["user_convert_cases"]: AssertNormalCase(UserUrlToUserSlug(case["url"]), case["uslug"]) - def test_UserSlugToUserUrl(self): + def test_UserSlugToUserUrl(self) -> None: for case in test_cases["convert_cases"]["user_convert_cases"]: AssertNormalCase(UserSlugToUserUrl(case["uslug"]), case["url"]) - def test_ArticleUrlToArticleSlug(self): + def test_ArticleUrlToArticleSlug(self) -> None: for case in test_cases["convert_cases"]["article_convert_cases"]: AssertNormalCase(ArticleUrlToArticleSlug(case["url"]), case["aslug"]) - def test_ArticleSlugToArticleUrl(self): + def test_ArticleSlugToArticleUrl(self) -> None: for case in test_cases["convert_cases"]["article_convert_cases"]: AssertNormalCase(ArticleSlugToArticleUrl(case["aslug"]), case["url"]) - def test_ArticleSlugToArticleId(self): + def test_ArticleSlugToArticleId(self) -> None: for case in test_cases["convert_cases"]["article_convert_cases"]: AssertNormalCase(ArticleSlugToArticleId(case["aslug"]), case["aid"]) - def test_ArticleUrlToArticleId(self): + def test_ArticleUrlToArticleId(self) -> None: for case in test_cases["article_cases"]["success_cases"]: AssertNormalCase(ArticleUrlToArticleId(case["url"]), case["aid"]) - def test_NotebookUrlToNotebookSlug(self): + def test_NotebookUrlToNotebookSlug(self) -> None: for case in test_cases["convert_cases"]["notebook_convert_cases"]: AssertNormalCase(NotebookUrlToNotebookSlug(case["url"]), case["nslug"]) - def test_NotebookSlugToNotebookUrl(self): + def test_NotebookSlugToNotebookUrl(self) -> None: for case in test_cases["convert_cases"]["notebook_convert_cases"]: AssertNormalCase(NotebookSlugToNotebookUrl(case["nslug"]), case["url"]) - def test_CollectionUrlToCollectionSlug(self): + def test_CollectionUrlToCollectionSlug(self) -> None: for case in test_cases["convert_cases"]["collection_convert_cases"]: AssertNormalCase(CollectionUrlToCollectionSlug(case["url"]), case["cslug"]) - def test_CollectionSlugToCollectionUrl(self): + def test_CollectionSlugToCollectionUrl(self) -> None: for case in test_cases["convert_cases"]["collection_convert_cases"]: AssertNormalCase(CollectionSlugToCollectionUrl(case["cslug"]), case["url"]) - def test_CollectionUrlToCollectionId(self): + def test_CollectionUrlToCollectionId(self) -> None: for case in test_cases["convert_cases"]["collection_convert_cases"]: AssertNormalCase(CollectionUrlToCollectionId(case["url"]), case["cid"]) - def test_IslandUrlToIslandSlug(self): + def test_IslandUrlToIslandSlug(self) -> None: for case in test_cases["convert_cases"]["island_convert_cases"]: AssertNormalCase(IslandUrlToIslandSlug(case["url"]), case["islug"]) - def test_IslandSlugToIslandUrl(self): + def test_IslandSlugToIslandUrl(self) -> None: for case in test_cases["convert_cases"]["island_convert_cases"]: AssertNormalCase(IslandSlugToIslandUrl(case["islug"]), case["url"]) class TestArticleModule: - def test_GetArticleTitle(self): + def test_GetArticleTitle(self) -> None: for case in test_cases["article_cases"]["success_cases"]: AssertNormalCase(jrt.article.GetArticleTitle(case["url"]), case["title"]) @@ -131,105 +135,132 @@ def test_GetArticleTitle(self): with pytest.raises(error_text_to_obj[case["exception_name"]]): jrt.article.GetArticleTitle(case["url"]) - def test_GetArticleAuthorName(self): + def test_GetArticleAuthorName(self) -> None: for case in test_cases["article_cases"]["success_cases"]: - AssertNormalCase(jrt.article.GetArticleAuthorName(case["url"]), case["author_name"]) + AssertNormalCase( + jrt.article.GetArticleAuthorName(case["url"]), case["author_name"] + ) for case in test_cases["article_cases"]["fail_cases"]: with pytest.raises(error_text_to_obj[case["exception_name"]]): jrt.article.GetArticleAuthorName(case["url"]) - def test_GetArticleReadsCount(self): + def test_GetArticleReadsCount(self) -> None: for case in test_cases["article_cases"]["success_cases"]: - AssertRangeCase(jrt.article.GetArticleReadsCount(case["url"]), case["reads_count"]) + AssertRangeCase( + jrt.article.GetArticleReadsCount(case["url"]), case["reads_count"] + ) for case in test_cases["article_cases"]["fail_cases"]: with pytest.raises(error_text_to_obj[case["exception_name"]]): jrt.article.GetArticleReadsCount(case["url"]) - def test_GetArticleWordage(self): + def test_GetArticleWordage(self) -> None: for case in test_cases["article_cases"]["success_cases"]: - AssertNormalCase(jrt.article.GetArticleWordage(case["url"]), case["wordage"]) + AssertNormalCase( + jrt.article.GetArticleWordage(case["url"]), case["wordage"] + ) for case in test_cases["article_cases"]["fail_cases"]: with pytest.raises(error_text_to_obj[case["exception_name"]]): jrt.article.GetArticleWordage(case["url"]) - def test_GetArticleLikesCount(self): + def test_GetArticleLikesCount(self) -> None: for case in test_cases["article_cases"]["success_cases"]: - AssertRangeCase(jrt.article.GetArticleLikesCount(case["url"]), case["likes_count"]) + AssertRangeCase( + jrt.article.GetArticleLikesCount(case["url"]), case["likes_count"] + ) for case in test_cases["article_cases"]["fail_cases"]: with pytest.raises(error_text_to_obj[case["exception_name"]]): jrt.article.GetArticleLikesCount(case["url"]) - def test_GetArticleCommentsCount(self): + def test_GetArticleCommentsCount(self) -> None: for case in test_cases["article_cases"]["success_cases"]: - AssertRangeCase(jrt.article.GetArticleCommentsCount(case["url"]), case["comments_count"]) + AssertRangeCase( + jrt.article.GetArticleCommentsCount(case["url"]), case["comments_count"] + ) for case in test_cases["article_cases"]["fail_cases"]: with pytest.raises(error_text_to_obj[case["exception_name"]]): jrt.article.GetArticleCommentsCount(case["url"]) - def test_GetArticleMostValuableCommentsCount(self): + def test_GetArticleMostValuableCommentsCount(self) -> None: for case in test_cases["article_cases"]["success_cases"]: - AssertRangeCase(jrt.article.GetArticleMostValuableCommentsCount(case["url"]), case["most_valuable_comments_count"]) + AssertRangeCase( + jrt.article.GetArticleMostValuableCommentsCount(case["url"]), + case["most_valuable_comments_count"], + ) for case in test_cases["article_cases"]["fail_cases"]: with pytest.raises(error_text_to_obj[case["exception_name"]]): jrt.article.GetArticleMostValuableCommentsCount(case["url"]) - def test_GetArticleTotalFPCount(self): + def test_GetArticleTotalFPCount(self) -> None: for case in test_cases["article_cases"]["success_cases"]: - AssertRangeCase(jrt.article.GetArticleTotalFPCount(case["url"]), case["total_FP_count"]) + AssertRangeCase( + jrt.article.GetArticleTotalFPCount(case["url"]), case["total_FP_count"] + ) for case in test_cases["article_cases"]["fail_cases"]: with pytest.raises(error_text_to_obj[case["exception_name"]]): jrt.article.GetArticleTotalFPCount(case["url"]) - def test_GetArticleDescription(self): + def test_GetArticleDescription(self) -> None: for case in test_cases["article_cases"]["success_cases"]: - AssertNormalCase(jrt.article.GetArticleDescription(case["url"]), case["description"]) + AssertNormalCase( + jrt.article.GetArticleDescription(case["url"]), case["description"] + ) for case in test_cases["article_cases"]["fail_cases"]: with pytest.raises(error_text_to_obj[case["exception_name"]]): jrt.article.GetArticleDescription(case["url"]) - def test_GetArticlePublishTime(self): + def test_GetArticlePublishTime(self) -> None: for case in test_cases["article_cases"]["success_cases"]: - AssertDatetimeCase(jrt.article.GetArticlePublishTime(case["url"]), case["publish_time"]) + AssertDatetimeCase( + jrt.article.GetArticlePublishTime(case["url"]), case["publish_time"] + ) for case in test_cases["article_cases"]["fail_cases"]: with pytest.raises(error_text_to_obj[case["exception_name"]]): jrt.article.GetArticlePublishTime(case["url"]) - def test_GetArticleUpdateTime(self): + def test_GetArticleUpdateTime(self) -> None: for case in test_cases["article_cases"]["success_cases"]: - AssertDatetimeCase(jrt.article.GetArticleUpdateTime(case["url"]), case["update_time"]) + AssertDatetimeCase( + jrt.article.GetArticleUpdateTime(case["url"]), case["update_time"] + ) for case in test_cases["article_cases"]["fail_cases"]: with pytest.raises(error_text_to_obj[case["exception_name"]]): jrt.article.GetArticleUpdateTime(case["url"]) - def test_GetArticlePaidStatus(self): + def test_GetArticlePaidStatus(self) -> None: for case in test_cases["article_cases"]["success_cases"]: - AssertNormalCase(jrt.article.GetArticlePaidStatus(case["url"]), case["paid_status"]) + AssertNormalCase( + jrt.article.GetArticlePaidStatus(case["url"]), case["paid_status"] + ) for case in test_cases["article_cases"]["fail_cases"]: with pytest.raises(error_text_to_obj[case["exception_name"]]): jrt.article.GetArticlePaidStatus(case["url"]) - def test_GetArticleReprintStatus(self): + def test_GetArticleReprintStatus(self) -> None: for case in test_cases["article_cases"]["success_cases"]: - AssertNormalCase(jrt.article.GetArticleReprintStatus(case["url"]), case["reprint_status"]) + AssertNormalCase( + jrt.article.GetArticleReprintStatus(case["url"]), case["reprint_status"] + ) for case in test_cases["article_cases"]["fail_cases"]: with pytest.raises(error_text_to_obj[case["exception_name"]]): jrt.article.GetArticleReprintStatus(case["url"]) - def test_GetArticleCommentStatus(self): + def test_GetArticleCommentStatus(self) -> None: for case in test_cases["article_cases"]["success_cases"]: - AssertNormalCase(jrt.article.GetArticleCommentStatus(case["url"]), case["comment_status"]) + AssertNormalCase( + jrt.article.GetArticleCommentStatus(case["url"]), case["comment_status"] + ) for case in test_cases["article_cases"]["fail_cases"]: with pytest.raises(error_text_to_obj[case["exception_name"]]): @@ -237,7 +268,7 @@ def test_GetArticleCommentStatus(self): class TestUserModule: - def test_GetUserName(self): + def test_GetUserName(self) -> None: for case in test_cases["user_cases"]["success_cases"]: AssertNormalCase(jrt.user.GetUserName(case["url"]), case["name"]) @@ -245,7 +276,7 @@ def test_GetUserName(self): with pytest.raises(error_text_to_obj[case["exception_name"]]): jrt.user.GetUserName(case["url"]) - def test_GetUserGender(self): + def test_GetUserGender(self) -> None: for case in test_cases["user_cases"]["success_cases"]: AssertNormalCase(jrt.user.GetUserGender(case["url"]), case["gender"]) @@ -253,15 +284,17 @@ def test_GetUserGender(self): with pytest.raises(error_text_to_obj[case["exception_name"]]): jrt.user.GetUserGender(case["url"]) - def test_GetUserFollowersCount(self): + def test_GetUserFollowersCount(self) -> None: for case in test_cases["user_cases"]["success_cases"]: - AssertRangeCase(jrt.user.GetUserFollowersCount(case["url"]), case["followers_count"]) + AssertRangeCase( + jrt.user.GetUserFollowersCount(case["url"]), case["followers_count"] + ) for case in test_cases["user_cases"]["fail_cases"]: with pytest.raises(error_text_to_obj[case["exception_name"]]): jrt.user.GetUserFollowersCount(case["url"]) - def test_GetUserFansCount(self): + def test_GetUserFansCount(self) -> None: for case in test_cases["user_cases"]["success_cases"]: AssertRangeCase(jrt.user.GetUserFansCount(case["url"]), case["fans_count"]) @@ -269,15 +302,17 @@ def test_GetUserFansCount(self): with pytest.raises(error_text_to_obj[case["exception_name"]]): jrt.user.GetUserFansCount(case["url"]) - def test_GetUserArticlesCount(self): + def test_GetUserArticlesCount(self) -> None: for case in test_cases["user_cases"]["success_cases"]: - AssertRangeCase(jrt.user.GetUserArticlesCount(case["url"]), case["articles_count"]) + AssertRangeCase( + jrt.user.GetUserArticlesCount(case["url"]), case["articles_count"] + ) for case in test_cases["user_cases"]["fail_cases"]: with pytest.raises(error_text_to_obj[case["exception_name"]]): jrt.user.GetUserArticlesCount(case["url"]) - def test_GetUserWordage(self): + def test_GetUserWordage(self) -> None: for case in test_cases["user_cases"]["success_cases"]: AssertRangeCase(jrt.user.GetUserWordage(case["url"]), case["wordage"]) @@ -285,23 +320,27 @@ def test_GetUserWordage(self): with pytest.raises(error_text_to_obj[case["exception_name"]]): jrt.user.GetUserWordage(case["url"]) - def test_GetUserLikesCount(self): + def test_GetUserLikesCount(self) -> None: for case in test_cases["user_cases"]["success_cases"]: - AssertRangeCase(jrt.user.GetUserLikesCount(case["url"]), case["likes_count"]) + AssertRangeCase( + jrt.user.GetUserLikesCount(case["url"]), case["likes_count"] + ) for case in test_cases["user_cases"]["fail_cases"]: with pytest.raises(error_text_to_obj[case["exception_name"]]): jrt.user.GetUserLikesCount(case["url"]) - def test_GetUserAssetsCount(self): + def test_GetUserAssetsCount(self) -> None: for case in test_cases["user_cases"]["success_cases"]: - AssertRangeCase(jrt.user.GetUserAssetsCount(case["url"]), case["assets_count"]) + AssertRangeCase( + jrt.user.GetUserAssetsCount(case["url"]), case["assets_count"] + ) for case in test_cases["user_cases"]["fail_cases"]: with pytest.raises(error_text_to_obj[case["exception_name"]]): jrt.user.GetUserAssetsCount(case["url"]) - def test_GetUserFPCount(self): + def test_GetUserFPCount(self) -> None: for case in test_cases["user_cases"]["success_cases"]: AssertRangeCase(jrt.user.GetUserFPCount(case["url"]), case["FP_count"]) @@ -309,7 +348,7 @@ def test_GetUserFPCount(self): with pytest.raises(error_text_to_obj[case["exception_name"]]): jrt.user.GetUserFPCount(case["url"]) - def test_GetUserFTNCount(self): + def test_GetUserFTNCount(self) -> None: for case in test_cases["user_cases"]["success_cases"]: AssertRangeCase(jrt.user.GetUserFTNCount(case["url"]), case["FTN_count"]) @@ -317,7 +356,7 @@ def test_GetUserFTNCount(self): with pytest.raises(error_text_to_obj[case["exception_name"]]): jrt.user.GetUserFTNCount(case["url"]) - def test_GetUserBadgesList(self): + def test_GetUserBadgesList(self) -> None: for case in test_cases["user_cases"]["success_cases"]: AssertListCase(jrt.user.GetUserBadgesList(case["url"]), case["badges_list"]) @@ -325,17 +364,22 @@ def test_GetUserBadgesList(self): with pytest.raises(error_text_to_obj[case["exception_name"]]): jrt.user.GetUserBadgesList(case["url"]) - def test_GetUserLastUpdateTime(self): + def test_GetUserLastUpdateTime(self) -> None: for case in test_cases["user_cases"]["success_cases"]: - AssertDatetimeCase(jrt.user.GetUserLastUpdateTime(case["url"]), case["last_update_time"]) + AssertDatetimeCase( + jrt.user.GetUserLastUpdateTime(case["url"]), case["last_update_time"] + ) for case in test_cases["user_cases"]["fail_cases"]: with pytest.raises(error_text_to_obj[case["exception_name"]]): jrt.user.GetUserLastUpdateTime(case["url"]) - def test_GetUserNextAnniversaryDay(self): + def test_GetUserNextAnniversaryDay(self) -> None: for case in test_cases["user_cases"]["success_cases"]: - AssertDatetimeCase(jrt.user.GetUserNextAnniversaryDay(case["url"]), case["next_anniversary_day"]) + AssertDatetimeCase( + jrt.user.GetUserNextAnniversaryDay(case["url"]), + case["next_anniversary_day"], + ) for case in test_cases["user_cases"]["fail_cases"]: with pytest.raises(error_text_to_obj[case["exception_name"]]): @@ -343,41 +387,55 @@ def test_GetUserNextAnniversaryDay(self): class TestCollectionModule: - def test_GetCollectionAvatarUrl(self): + def test_GetCollectionAvatarUrl(self) -> None: for case in test_cases["collection_cases"]["success_cases"]: - AssertNormalCase(jrt.collection.GetCollectionAvatarUrl(case["url"]), case["avatar_url"]) + AssertNormalCase( + jrt.collection.GetCollectionAvatarUrl(case["url"]), case["avatar_url"] + ) for case in test_cases["collection_cases"]["fail_cases"]: with pytest.raises(error_text_to_obj[case["exception_name"]]): jrt.collection.GetCollectionAvatarUrl(case["url"]) - def test_GetCollectionArticlesCount(self): + def test_GetCollectionArticlesCount(self) -> None: for case in test_cases["collection_cases"]["success_cases"]: - AssertRangeCase(jrt.collection.GetCollectionArticlesCount(case["url"]), case["articles_count"]) + AssertRangeCase( + jrt.collection.GetCollectionArticlesCount(case["url"]), + case["articles_count"], + ) for case in test_cases["collection_cases"]["fail_cases"]: with pytest.raises(error_text_to_obj[case["exception_name"]]): jrt.collection.GetCollectionArticlesCount(case["url"]) - def test_GetCollectionSubscribersCount(self): + def test_GetCollectionSubscribersCount(self) -> None: for case in test_cases["collection_cases"]["success_cases"]: - AssertRangeCase(jrt.collection.GetCollectionSubscribersCount(case["url"]), case["subscribers_count"]) + AssertRangeCase( + jrt.collection.GetCollectionSubscribersCount(case["url"]), + case["subscribers_count"], + ) for case in test_cases["collection_cases"]["fail_cases"]: with pytest.raises(error_text_to_obj[case["exception_name"]]): jrt.collection.GetCollectionSubscribersCount(case["url"]) - def test_GetCollectionArticlesUpdateTime(self): + def test_GetCollectionArticlesUpdateTime(self) -> None: for case in test_cases["collection_cases"]["success_cases"]: - AssertDatetimeCase(jrt.collection.GetCollectionArticlesUpdateTime(case["url"]), case["articles_update_time"]) + AssertDatetimeCase( + jrt.collection.GetCollectionArticlesUpdateTime(case["url"]), + case["articles_update_time"], + ) for case in test_cases["collection_cases"]["fail_cases"]: with pytest.raises(error_text_to_obj[case["exception_name"]]): jrt.collection.GetCollectionArticlesUpdateTime(case["url"]) - def test_GetCollectionInformationUpdateTime(self): + def test_GetCollectionInformationUpdateTime(self) -> None: for case in test_cases["collection_cases"]["success_cases"]: - AssertDatetimeCase(jrt.collection.GetCollectionInformationUpdateTime(case["url"]), case["information_update_time"]) + AssertDatetimeCase( + jrt.collection.GetCollectionInformationUpdateTime(case["url"]), + case["information_update_time"], + ) for case in test_cases["collection_cases"]["fail_cases"]: with pytest.raises(error_text_to_obj[case["exception_name"]]): @@ -385,7 +443,7 @@ def test_GetCollectionInformationUpdateTime(self): class TestIslandModule: - def test_GetArticleName(self): + def test_GetArticleName(self) -> None: for case in test_cases["island_cases"]["success_cases"]: AssertNormalCase(jrt.island.GetIslandName(case["url"]), case["name"]) @@ -393,33 +451,41 @@ def test_GetArticleName(self): with pytest.raises(error_text_to_obj[case["exception_name"]]): jrt.island.GetIslandName(case["url"]) - def test_GetIslandAvatarUrl(self): + def test_GetIslandAvatarUrl(self) -> None: for case in test_cases["island_cases"]["success_cases"]: - AssertNormalCase(jrt.island.GetIslandAvatarUrl(case["url"]), case["avatar_url"]) + AssertNormalCase( + jrt.island.GetIslandAvatarUrl(case["url"]), case["avatar_url"] + ) for case in test_cases["island_cases"]["fail_cases"]: with pytest.raises(error_text_to_obj[case["exception_name"]]): jrt.island.GetIslandAvatarUrl(case["url"]) - def test_GetIslandMembersCount(self): + def test_GetIslandMembersCount(self) -> None: for case in test_cases["island_cases"]["success_cases"]: - AssertRangeCase(jrt.island.GetIslandMembersCount(case["url"]), case["members_count"]) + AssertRangeCase( + jrt.island.GetIslandMembersCount(case["url"]), case["members_count"] + ) for case in test_cases["island_cases"]["fail_cases"]: with pytest.raises(error_text_to_obj[case["exception_name"]]): jrt.island.GetIslandMembersCount(case["url"]) - def test_GetIslandPostsCount(self): + def test_GetIslandPostsCount(self) -> None: for case in test_cases["island_cases"]["success_cases"]: - AssertRangeCase(jrt.island.GetIslandPostsCount(case["url"]), case["posts_count"]) + AssertRangeCase( + jrt.island.GetIslandPostsCount(case["url"]), case["posts_count"] + ) for case in test_cases["island_cases"]["fail_cases"]: with pytest.raises(error_text_to_obj[case["exception_name"]]): jrt.island.GetIslandPostsCount(case["url"]) - def test_GetIslandCategory(self): + def test_GetIslandCategory(self) -> None: for case in test_cases["island_cases"]["success_cases"]: - AssertNormalCase(jrt.island.GetIslandCategory(case["url"]), case["category"]) + AssertNormalCase( + jrt.island.GetIslandCategory(case["url"]), case["category"] + ) for case in test_cases["island_cases"]["fail_cases"]: with pytest.raises(error_text_to_obj[case["exception_name"]]): @@ -427,7 +493,7 @@ def test_GetIslandCategory(self): class TestNotebookModule: - def test_GetNotebookName(self): + def test_GetNotebookName(self) -> None: for case in test_cases["notebook_cases"]["success_cases"]: AssertNormalCase(jrt.notebook.GetNotebookName(case["url"]), case["name"]) @@ -435,49 +501,65 @@ def test_GetNotebookName(self): with pytest.raises(error_text_to_obj[case["exception_name"]]): jrt.notebook.GetNotebookName(case["url"]) - def test_GetNotebookArticlesCount(self): + def test_GetNotebookArticlesCount(self) -> None: for case in test_cases["notebook_cases"]["success_cases"]: - AssertRangeCase(jrt.notebook.GetNotebookArticlesCount(case["url"]), case["articles_count"]) + AssertRangeCase( + jrt.notebook.GetNotebookArticlesCount(case["url"]), + case["articles_count"], + ) for case in test_cases["notebook_cases"]["fail_cases"]: with pytest.raises(error_text_to_obj[case["exception_name"]]): jrt.notebook.GetNotebookArticlesCount(case["url"]) - def test_GetNotebookAuthorName(self): + def test_GetNotebookAuthorName(self) -> None: for case in test_cases["notebook_cases"]["success_cases"]: - AssertNormalCase(jrt.notebook.GetNotebookAuthorInfo(case["url"])["name"], case["author_name"]) + AssertNormalCase( + jrt.notebook.GetNotebookAuthorInfo(case["url"])["name"], + case["author_name"], + ) for case in test_cases["notebook_cases"]["fail_cases"]: with pytest.raises(error_text_to_obj[case["exception_name"]]): _ = jrt.notebook.GetNotebookAuthorInfo(case["url"])["name"] - def test_GetNotebookAuthorAvatarUrl(self): + def test_GetNotebookAuthorAvatarUrl(self) -> None: for case in test_cases["notebook_cases"]["success_cases"]: - AssertNormalCase(jrt.notebook.GetNotebookAuthorInfo(case["url"])["avatar_url"], case["author_avatar_url"]) + AssertNormalCase( + jrt.notebook.GetNotebookAuthorInfo(case["url"])["avatar_url"], + case["author_avatar_url"], + ) for case in test_cases["notebook_cases"]["fail_cases"]: with pytest.raises(error_text_to_obj[case["exception_name"]]): _ = jrt.notebook.GetNotebookAuthorInfo(case["url"])["author_avatar_url"] - def test_GetNotebookWordage(self): + def test_GetNotebookWordage(self) -> None: for case in test_cases["notebook_cases"]["success_cases"]: - AssertRangeCase(jrt.notebook.GetNotebookWordage(case["url"]), case["wordage"]) + AssertRangeCase( + jrt.notebook.GetNotebookWordage(case["url"]), case["wordage"] + ) for case in test_cases["notebook_cases"]["fail_cases"]: with pytest.raises(error_text_to_obj[case["exception_name"]]): jrt.notebook.GetNotebookWordage(case["url"]) - def test_GetNotebookSubscribersCount(self): + def test_GetNotebookSubscribersCount(self) -> None: for case in test_cases["notebook_cases"]["success_cases"]: - AssertRangeCase(jrt.notebook.GetNotebookSubscribersCount(case["url"]), case["subscribers_count"]) + AssertRangeCase( + jrt.notebook.GetNotebookSubscribersCount(case["url"]), + case["subscribers_count"], + ) for case in test_cases["notebook_cases"]["fail_cases"]: with pytest.raises(error_text_to_obj[case["exception_name"]]): jrt.notebook.GetNotebookSubscribersCount(case["url"]) - def test_GetNotebookUpdateTime(self): + def test_GetNotebookUpdateTime(self) -> None: for case in test_cases["notebook_cases"]["success_cases"]: - AssertDatetimeCase(jrt.notebook.GetNotebookUpdateTime(case["url"]), case["update_time"]) + AssertDatetimeCase( + jrt.notebook.GetNotebookUpdateTime(case["url"]), case["update_time"] + ) for case in test_cases["notebook_cases"]["fail_cases"]: with pytest.raises(error_text_to_obj[case["exception_name"]]): diff --git a/test_case_creater.py b/test_case_creater.py index 2bc01f5..974d6f4 100644 --- a/test_case_creater.py +++ b/test_case_creater.py @@ -1,6 +1,7 @@ -from yaml import dump as yaml_dump from datetime import datetime +from yaml import dump as yaml_dump + test_cases = { # "type_cases": [ # { @@ -60,7 +61,7 @@ "uid": 14715425, "url": "https://www.jianshu.com/u/c5a2ce84f60b", "uslug": "c5a2ce84f60b", - } + }, ], "collection_convert_cases": [ { @@ -77,7 +78,7 @@ "cid": 1938174, "url": "https://www.jianshu.com/c/a335661c66d9", "cslug": "a335661c66d9", - } + }, ], "island_convert_cases": [ { @@ -93,8 +94,8 @@ { "url": "https://www.jianshu.com/nb/36131833", "nslug": "36131833", - } - ] + }, + ], }, "article_cases": { "success_cases": [ @@ -115,7 +116,7 @@ "update_time": datetime(2021, 5, 1, 11, 34, 19).timestamp(), "paid_status": False, "reprint_status": True, - "comment_status": True + "comment_status": True, }, { "aid": 89748991, @@ -134,7 +135,7 @@ "update_time": datetime(2021, 7, 2, 14, 35, 36).timestamp(), "paid_status": False, "reprint_status": True, - "comment_status": True + "comment_status": True, }, { "aid": 78722940, @@ -142,7 +143,7 @@ "aslug": "09c5bf171574", "title": "简书社区守护者徽章奖励公告", "author_name": "简书钻首席小管家", - "reads_count": (3500, 5000), + "reads_count": (7000, 12000), "wordage": 185, "likes_count": (65, 120), "comments_count": (0, 0), @@ -153,16 +154,16 @@ "update_time": datetime(2020, 12, 24, 12, 38, 30).timestamp(), "paid_status": False, "reprint_status": True, - "comment_status": False + "comment_status": False, }, ], "fail_cases": [ { "exception_name": "ResourceError", - "url": "https://www.jianshu.com/p/1b9ad61ade73", - "aslug": "1b9ad61ade73", + "url": "https://www.jianshu.com/p/abc1234qwert", + "aslug": "abc1234qwert", } - ] + ], }, "user_cases": { "success_cases": [ @@ -173,19 +174,16 @@ "name": "初心不变_叶子", "gender": 1, "followers_count": (200, 500), - "fans_count": (800, 1200), - "articles_count": (130, 180), + "fans_count": (1000, 3000), + "articles_count": (150, 270), "wordage": (270000, 800000), - "likes_count": (4300, 7000), - "assets_count": (17000, 30000), - "FP_count": (10000, 20000), - "FTN_count": (3000, 12000), + "likes_count": (4000, 7000), + "assets_count": (50000, 120000), + "FP_count": (30000, 100000), + "FTN_count": (1000, 50000), "badges_list": ["简书创作者", "岛主", "社区守护者"], "last_update_time": datetime(2021, 7, 31, 23, 6, 16).timestamp(), - "VIP_info": { - "vip_type": None, - "expire_date": None - }, + "VIP_info": {"vip_type": None, "expire_date": None}, "next_anniversary_day": datetime(2022, 10, 21, 0, 0).timestamp(), }, { @@ -202,11 +200,11 @@ "assets_count": (500000, 700000), "FP_count": (150000, 200000), "FTN_count": (380000, 500000), - "badges_list": ['简书员工', '鼠年大吉', '锦鲤', '幸运四叶草', '怦然心动', '岛主'], + "badges_list": ["简书员工", "鼠年大吉", "锦鲤", "幸运四叶草", "怦然心动", "岛主"], "last_update_time": datetime(2021, 3, 19, 11, 56, 14).timestamp(), "VIP_info": { "vip_type": "铜牌", - "expire_date": datetime(2022, 3, 26, 11, 56, 14).timestamp() + "expire_date": datetime(2022, 3, 26, 11, 56, 14).timestamp(), }, "next_anniversary_day": datetime(2022, 8, 19, 0, 0).timestamp(), }, @@ -221,15 +219,12 @@ "articles_count": (450, 600), "wordage": (450000, 600000), "likes_count": (110000, 150000), - "assets_count": (400000, 600000), - "FP_count": (700, 1500), - "FTN_count": (500000, 600000), - "badges_list": ['简书创作者', '鼠年大吉', '二〇一九新春快乐~', '简书员工'], + "assets_count": (0, 5000), + "FP_count": (0, 3000), + "FTN_count": (0, 3000), + "badges_list": ["简书创作者", "鼠年大吉", "二〇一九新春快乐~", "简书员工"], "last_update_time": datetime(2020, 10, 9, 18, 38, 36).timestamp(), - "VIP_info": { - "vip_type": None, - "expire_date": None - }, + "VIP_info": {"vip_type": None, "expire_date": None}, "next_anniversary_day": datetime(2022, 10, 29, 0, 0).timestamp(), }, ], @@ -239,7 +234,7 @@ "url": "https://www.jianshu.com/u/ea36c8d8aa31", "uslug": "ea36c8d8aa31", } - ] + ], }, "collection_cases": { "success_cases": [ @@ -258,10 +253,12 @@ "url": "https://www.jianshu.com/c/V2CqjW", "cslug": "V2CqjW", "avatar_url": "https://upload.jianshu.io/collections/images/14/6249340_194140034135_2.jpg", - "articles_count": (40000, 45000), + "articles_count": (35000, 45000), "subscribers_count": (2500000, 3000000), "articles_update_time": datetime(2021, 12, 1, 22, 40, 15).timestamp(), - "information_update_time": datetime(2021, 3, 12, 17, 46, 57).timestamp(), + "information_update_time": datetime( + 2021, 3, 12, 17, 46, 57 + ).timestamp(), }, { "cid": 1938174, @@ -271,16 +268,18 @@ "articles_count": (2000, 5000), "subscribers_count": (1000, 3000), "articles_update_time": datetime(2021, 12, 3, 6, 2, 22).timestamp(), - "information_update_time": datetime(2021, 1, 28, 15, 40, 42).timestamp(), + "information_update_time": datetime( + 2021, 1, 28, 15, 40, 42 + ).timestamp(), }, ], "fail_cases": [ { "exception_name": "ResourceError", "url": "https://www.jianshu.com/c/a335661c66d0", - "uslug": "a335661c66d0" + "uslug": "a335661c66d0", } - ] + ], }, "island_cases": { "success_cases": [ @@ -289,18 +288,18 @@ "slug": "6187f99def472f5e", "name": "简友动态广场", "avatar_url": "https://upload.jianshu.io/group_image/18454410/6b6138a6-685a-4f12-80a7-5e731b5fc935", - "members_count": (60000, 80000), - "posts_count": (140000, 160000), - "category": "生活" + "members_count": (80000, 120000), + "posts_count": (160000, 240000), + "category": "生活", } ], "fail_cases": [ { "exception_name": "ResourceError", "url": "https://www.jianshu.com/g/6187f99def472f5f", - "uslug": "6187f99def472f5e" + "uslug": "6187f99def472f5e", } - ] + ], }, "notebook_cases": { "success_cases": [ @@ -321,10 +320,10 @@ { "exception_name": "ResourceError", "url": "https://www.jianshu.com/nb/12345678", - "uslug": "12345678" + "uslug": "12345678", } - ] - } + ], + }, } with open("test_cases.yaml", "w", encoding="utf-8") as f: diff --git a/test_cases.yaml b/test_cases.yaml index 7a08f1b..b046600 100644 --- a/test_cases.yaml +++ b/test_cases.yaml @@ -1,8 +1,8 @@ article_cases: fail_cases: - - aslug: 1b9ad61ade73 + - aslug: abc1234qwert exception_name: ResourceError - url: https://www.jianshu.com/p/1b9ad61ade73 + url: https://www.jianshu.com/p/abc1234qwert success_cases: - aid: 87256893 aslug: 52698676395f @@ -73,8 +73,8 @@ article_cases: paid_status: false publish_time: 1603099326.0 reads_count: !!python/tuple - - 3500 - - 5000 + - 7000 + - 12000 reprint_status: true title: 简书社区守护者徽章奖励公告 total_FP_count: !!python/tuple @@ -102,7 +102,7 @@ collection_cases: - 2800000 url: https://www.jianshu.com/c/fcd7a62be697 - articles_count: !!python/tuple - - 40000 + - 35000 - 45000 articles_update_time: 1638369615.0 avatar_url: https://upload.jianshu.io/collections/images/14/6249340_194140034135_2.jpg @@ -168,12 +168,12 @@ island_cases: - avatar_url: https://upload.jianshu.io/group_image/18454410/6b6138a6-685a-4f12-80a7-5e731b5fc935 category: 生活 members_count: !!python/tuple - - 60000 - 80000 + - 120000 name: 简友动态广场 posts_count: !!python/tuple - - 140000 - 160000 + - 240000 slug: 6187f99def472f5e url: https://www.jianshu.com/g/6187f99def472f5e notebook_cases: @@ -205,34 +205,32 @@ user_cases: uslug: ea36c8d8aa31 success_cases: - FP_count: !!python/tuple - - 10000 - - 20000 + - 30000 + - 100000 FTN_count: !!python/tuple - - 3000 - - 12000 + - 1000 + - 50000 VIP_info: expire_date: null vip_type: null articles_count: !!python/tuple - - 130 - - 180 + - 150 + - 270 assets_count: !!python/tuple - - 17000 - - 30000 + - 50000 + - 120000 badges_list: - 简书创作者 - 岛主 - 社区守护者 - fans_count: !!python/tuple - - 800 - - 1200 + fans_count: *id003 followers_count: !!python/tuple - 200 - 500 gender: 1 last_update_time: 1627743976.0 likes_count: !!python/tuple - - 4300 + - 4000 - 7000 name: 初心不变_叶子 next_anniversary_day: 1666281600.0 @@ -281,12 +279,10 @@ user_cases: wordage: !!python/tuple - 18000 - 30000 - - FP_count: !!python/tuple - - 700 - - 1500 - FTN_count: !!python/tuple - - 500000 - - 600000 + - FP_count: &id005 !!python/tuple + - 0 + - 3000 + FTN_count: *id005 VIP_info: expire_date: null vip_type: null @@ -294,8 +290,8 @@ user_cases: - 450 - 600 assets_count: !!python/tuple - - 400000 - - 600000 + - 0 + - 5000 badges_list: - 简书创作者 - 鼠年大吉