From 767df6a7560c6dd33c5c17f8cac2d5d2b39bbaa6 Mon Sep 17 00:00:00 2001 From: Akirami <66513481+A-kirami@users.noreply.github.com> Date: Sat, 12 Aug 2023 18:02:03 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=96=20v0.0.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .devcontainer/devcontainer.json | 47 + .devcontainer/docker-compose.yml | 17 + .github/CODE_OF_CONDUCT.md | 87 + .github/CONTRIBUTING.md | 96 + .github/FUNDING.yml | 1 + .github/ISSUE_TEMPLATE/bug_report.yml | 93 + .github/ISSUE_TEMPLATE/config.yml | 5 + .github/ISSUE_TEMPLATE/feature_request.yml | 75 + .github/PULL_REQUEST_TEMPLATE.md | 61 + .github/SUPPORT.md | 39 + .github/TYPES_OF_CONTRIBUTIONS.md | 51 + .github/release.yml | 18 + .gitignore | 186 ++ .pre-commit-config.yaml | 19 + .vscode/extensions.json | 3 + .vscode/settings.json | 17 + bot.py | 10 - kirami/__init__.py | 276 +++ kirami/config/__init__.py | 32 + kirami/config/config.py | 225 ++ kirami/config/path.py | 43 + kirami/config/utils.py | 31 + kirami/database/__init__.py | 31 + kirami/database/models/__init__.py | 0 kirami/database/models/argot.py | 25 + kirami/database/models/group.py | 21 + kirami/database/models/user.py | 21 + kirami/depends.py | 464 ++++ kirami/event.py | 45 + kirami/exception.py | 92 + kirami/hook.py | 177 ++ kirami/log.py | 253 +++ kirami/matcher.py | 1407 ++++++++++++ kirami/matcher.pyi | 980 ++++++++ kirami/message.py | 223 ++ kirami/message.pyi | 147 ++ kirami/patch.py | 195 ++ kirami/permission.py | 37 + kirami/py.typed | 0 kirami/rule.py | 188 ++ kirami/server/__init__.py | 4 + kirami/server/api.py | 17 + kirami/server/server.py | 76 + kirami/service/__init__.py | 18 + kirami/service/access.py | 245 ++ kirami/service/controller.py | 218 ++ kirami/service/limiter.py | 379 ++++ kirami/service/manager.py | 259 +++ kirami/service/service.py | 334 +++ kirami/service/subject.py | 113 + kirami/state.py | 121 + kirami/typing.py | 20 + kirami/utils/__init__.py | 24 + kirami/utils/downloader.py | 130 ++ kirami/utils/helpers.py | 74 + kirami/utils/jsondata.py | 176 ++ kirami/utils/memcache.py | 6 + kirami/utils/renderer/__init__.py | 115 + .../renderer/template/github-markdown.css | 969 ++++++++ .../renderer/template/highlight-dark.css | 85 + .../renderer/template/highlight-light.css | 68 + kirami/utils/renderer/template/markdown.html | 39 + kirami/utils/request/__init__.py | 665 ++++++ kirami/utils/request/fake_user_agent.json | 1251 +++++++++++ kirami/utils/request/utils.py | 41 + kirami/utils/resource.py | 329 +++ kirami/utils/scheduler.py | 33 + kirami/utils/utils.py | 477 ++++ kirami/utils/webwright.py | 130 ++ kirami/version.py | 19 + pdm.lock | 1994 +++++++++++++++++ pyproject.toml | 124 + 72 files changed, 14281 insertions(+), 10 deletions(-) create mode 100644 .devcontainer/devcontainer.json create mode 100644 .devcontainer/docker-compose.yml create mode 100644 .github/CODE_OF_CONDUCT.md create mode 100644 .github/CONTRIBUTING.md create mode 100644 .github/FUNDING.yml create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/feature_request.yml create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/SUPPORT.md create mode 100644 .github/TYPES_OF_CONTRIBUTIONS.md create mode 100644 .github/release.yml create mode 100644 .gitignore create mode 100644 .pre-commit-config.yaml create mode 100644 .vscode/extensions.json create mode 100644 .vscode/settings.json delete mode 100644 bot.py create mode 100644 kirami/__init__.py create mode 100644 kirami/config/__init__.py create mode 100644 kirami/config/config.py create mode 100644 kirami/config/path.py create mode 100644 kirami/config/utils.py create mode 100644 kirami/database/__init__.py create mode 100644 kirami/database/models/__init__.py create mode 100644 kirami/database/models/argot.py create mode 100644 kirami/database/models/group.py create mode 100644 kirami/database/models/user.py create mode 100644 kirami/depends.py create mode 100644 kirami/event.py create mode 100644 kirami/exception.py create mode 100644 kirami/hook.py create mode 100644 kirami/log.py create mode 100644 kirami/matcher.py create mode 100644 kirami/matcher.pyi create mode 100644 kirami/message.py create mode 100644 kirami/message.pyi create mode 100644 kirami/patch.py create mode 100644 kirami/permission.py create mode 100644 kirami/py.typed create mode 100644 kirami/rule.py create mode 100644 kirami/server/__init__.py create mode 100644 kirami/server/api.py create mode 100644 kirami/server/server.py create mode 100644 kirami/service/__init__.py create mode 100644 kirami/service/access.py create mode 100644 kirami/service/controller.py create mode 100644 kirami/service/limiter.py create mode 100644 kirami/service/manager.py create mode 100644 kirami/service/service.py create mode 100644 kirami/service/subject.py create mode 100644 kirami/state.py create mode 100644 kirami/typing.py create mode 100644 kirami/utils/__init__.py create mode 100644 kirami/utils/downloader.py create mode 100644 kirami/utils/helpers.py create mode 100644 kirami/utils/jsondata.py create mode 100644 kirami/utils/memcache.py create mode 100644 kirami/utils/renderer/__init__.py create mode 100644 kirami/utils/renderer/template/github-markdown.css create mode 100644 kirami/utils/renderer/template/highlight-dark.css create mode 100644 kirami/utils/renderer/template/highlight-light.css create mode 100644 kirami/utils/renderer/template/markdown.html create mode 100644 kirami/utils/request/__init__.py create mode 100644 kirami/utils/request/fake_user_agent.json create mode 100644 kirami/utils/request/utils.py create mode 100644 kirami/utils/resource.py create mode 100644 kirami/utils/scheduler.py create mode 100644 kirami/utils/utils.py create mode 100644 kirami/utils/webwright.py create mode 100644 kirami/version.py create mode 100644 pdm.lock create mode 100644 pyproject.toml diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..f55b02d --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,47 @@ +{ + "name": "KiramiBot", + "dockerComposeFile": "docker-compose.yml", + "service": "kiramibot", + "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", + "features": { + "ghcr.io/devcontainers-contrib/features/pdm:2": {} + }, + "forwardPorts": [ + 8120, + 27017 + ], + "postCreateCommand": "pdm install", + "customizations": { + "codespaces": { + "openFiles": [ + "README.md" + ] + }, + "vscode": { + "settings": { + "python.analysis.diagnosticMode": "workspace", + "python.analysis.typeCheckingMode": "basic", + "[python]": { + "editor.defaultFormatter": "ms-python.black-formatter", + "editor.codeActionsOnSave": { + "source.fixAll": true, + "source.organizeImports": true + } + }, + "files.exclude": { + "**/__pycache__": true + }, + "files.watcherExclude": { + "**/__pycache__": true + } + }, + "extensions": [ + "ms-python.python", + "ms-python.vscode-pylance", + "ms-python.black-formatter", + "charliermarsh.ruff", + "tamasfe.even-better-toml" + ] + } + } +} diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml new file mode 100644 index 0000000..3620ff1 --- /dev/null +++ b/.devcontainer/docker-compose.yml @@ -0,0 +1,17 @@ +version: '3.8' +services: + kiramibot: + image: mcr.microsoft.com/devcontainers/python:1-3.10-bullseye + volumes: + - ../..:/workspaces:cached + network_mode: service:kirami-db + command: sleep infinity + + kirami-db: + image: mongo:latest + restart: unless-stopped + volumes: + - kirami-data:/data/db + +volumes: + kirami-data: diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..38a70b9 --- /dev/null +++ b/.github/CODE_OF_CONDUCT.md @@ -0,0 +1,87 @@ +# KiramiBot 贡献者公约 + +## 我们的承诺 + +身为项目成员、贡献者、负责人,我们保证参与此社区的每个人都不受骚扰,不论其年龄、体型、身体条件、民族、性征、性别认同与表现、经验水平、教育程度、社会地位、国籍、相貌、种族、宗教信仰及性取向如何。 + +我们承诺致力于建设开放、友善、多元、包容、健康的社区环境。 + +## 我们的准则 + +有助于促进本社区积极环境的行为包括但不限于: + +- 与人为善、推己及人 +- 尊重不同的主张、观点和经历 +- 积极提出、耐心接受有益批评 +- 面对过失,承担责任、认真道歉、从中学习 +- 关注社区共同诉求,而非一己私利 + +不当行为包括但不限于: + +- 发布与性有关的言论或图像,以及任何形式的献殷勤或勾引 +- 挑衅行为、侮辱或贬损的言论、人身及政治攻击 +- 公开或私下骚扰 +- 未获明确授权擅自发布他人的资料,如地址、电子邮箱等 +- 其他有理由认定为违反职业操守的不当行为 + +## 落实之义务 + +社区负责人有责任诠释什么是“妥当行为”,并据此准则,妥善公正地认定与处置不当、威胁、冒犯及有害的行为。 + +社区负责人有权利和义务删除、编辑、拒绝违背本公约的评论(comment)、提交(commit)、代码、维基(wiki)编辑、议题(issue)等贡献。如有必要,需告知采取措施的理由。 + +## 适用范围 + +此行为标准适用于本社区全部场合,以及在其他场合代表本社区的个人。 + +代表本社区的情形包括但不限于:使用官方电子邮件与社交平台、作为指定代表参与在线或线下活动。 + +## 贯彻落实 + +如遇滥用、骚扰等不当行为,请通过 向我们举报。我们将迅速审议并调查全部投诉。 + +社区全体负责人有义务保密举报者信息。 + +## 指导方针 + +社区负责人将依据下列方案判断并处置违纪行为: + +### 一、督促 + +**社区影响**:用语不当、举止不符合道德或不受社区欢迎。 + +**处理意见**:由社区负责人予以非公开的书面警告,阐明违纪事由、解释举止如何不妥。或要求公开道歉。 + +### 二、警告 + +**社区影响**:一起或多起事件中的违纪行为。 + +**处理意见**:警告继续违纪的后果、违纪者在特定时间内禁止与当事人往来、不得擅自与社区执法者往来,禁令涵盖社区内外、社交网络在内的一切联络。如有违反,可致封禁乃至开除。 + +### 三、封禁 + +**社区影响**:严重违纪行为,包括屡教不改。 + +**处理意见**:违纪者在特定时间内禁止与社区的任何往来或公开联络,禁止任何与当事人公开或私下往来,不得擅自与社区管理者往来。如有违反,可导致开除。 + +### 四、开除 + +**社区影响**:典型违纪行为,例如屡教不改、骚扰某个人、敌对或贬低某个群体。 + +**处理意见**:无限期禁止违纪者与项目社区的一切公开往来。 + +## 来源 + +本行为标准改编自[贡献者公约][homepage]2.1 版,可在此查阅:[https://www.contributor-covenant.org/zh-cn/version/2/1/code_of_conduct.html][v2.1] + +指导方针借鉴自[Mozilla 纪检分级][mozilla coc]。 + +此行为标准常见问题请洽:[https://www.contributor-covenant.org/faq][faq]。 + +另有诸译本:[https://www.contributor-covenant.org/translations][translations]。 + +[homepage]: https://www.contributor-covenant.org +[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html +[mozilla coc]: https://github.com/mozilla/diversity +[faq]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 0000000..5a89a77 --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,96 @@ +# 欢迎来到 KiramiBot 贡献指南 + +首先,感谢您抽出宝贵时间为 KiramiBot 做出贡献! + +我们鼓励并重视所有类型的贡献,您可以通过多种不同的方式为 KiramiBot 做出贡献,请参阅我们支持的[贡献类型](./TYPES_OF_CONTRIBUTIONS.md)。 + +在做出贡献之前,请务必阅读相关章节。这将大大方便我们的维护人员,并使所有相关人员都能顺利完成工作。 + +**我们欢迎一切贡献!并对每个愿意贡献的人表示衷心的感谢!💖** + +> 如果您喜欢这个项目,可以为本项目点亮⭐️,这是对我们最大的鼓励。 + +## 新贡献者指南 + +要了解项目概况,请阅读 [README](../README.md)。以下是一些帮助您开始开源贡献的资源: + +- [如何为开源做贡献](https://opensource.guide/zh-hans/how-to-contribute/) +- [寻找在 GitHub 上贡献开源的方法](https://docs.github.com/zh/get-started/exploring-projects-on-github/finding-ways-to-contribute-to-open-source-on-github) +- [设置 Git](https://docs.github.com/zh/get-started/quickstart/set-up-git) +- [GitHub 流](https://docs.github.com/zh/get-started/quickstart/github-flow) +- [协作处理拉取请求](https://docs.github.com/zh/pull-requests/collaborating-with-pull-requests) + +## 让我们开始吧 + +### 议题 + +在创建议题之前,请确保您已经搜索了[现有的议题](https://github.com/A-kirami/KiramiBot/issues),以确保您的议题没有重复。如果您发现议题已经存在,请在现有的议题下添加评论,而不是创建新的议题。 + +#### 报告问题 + +如果您发现了一个问题,但是没有时间解决它,或者您不知道如何解决它,您可以创建一个议题。 + +#### 特性请求 + +如果您想要添加新的功能,您可以创建一个议题,并描述您想要的特性。 + +#### 帮助解决 + +浏览我们现有的议题,找到您感兴趣的议题。您可以使用标签作为过滤器缩小搜索范围。有关更多信息,请参见[标签]()。 + +如果您发现了需要解决的议题,请在议题下添加评论,以便我们知道您正在解决该议题。 + +### 进行更改 + +在修复 bug 之前,我们建议您检查是否存在描述 bug 的问题,因为这可能是一个文档问题,或者是否存在一些有助于了解的上下文。 + +如果您正在开发一个特性,那么我们要求您首先打开一个特性请求议题,以便与维护人员讨论是否需要该特性以及这些特性的设计。这有助于为维护人员和贡献者节省时间,并意味着可以更快地提供特性。 + +#### 在代码空间中进行更改 + +[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://github.com/codespaces/new?hide_repo_select=true&ref=master&repo=) + +有关代码空间的更多信息,请参阅 [GitHub Codespaces 概述](https://docs.github.com/zh/codespaces/overview)。 + +#### 在开发容器中进行更改 + +[![Open in Dev Containers](https://img.shields.io/static/v1?label=Dev%20Containers&message=Open&color=blue&logo=visualstudiocode)](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/A-kirami/KiramiBot) + +如果您已经安装了 VS Code 和 Docker,可以点击上面的徽标或 [这里](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/A-kirami/KiramiBot) 开始使用。点击这些链接后,VS Code 将根据需要自动安装 Dev Containers 扩展,将源代码克隆到容器卷中,并启动一个开发容器以供使用。 + +#### 在本地环境中进行更改 + +如果您不想使用代码空间或者开发容器,您可以在本地环境中开发。 + +KiramiBot 使用 [pdm](https://pdm.fming.dev/) 管理项目依赖。请确保您已经安装了 pdm,然后在项目根目录下运行 `pdm install` 安装依赖。 + +### 提交修改 + +一旦对修改满意,就可以将其提交。别忘了进行自我审核和本地测试,以加快审核过程⚡。 + +请确保您的每一个 commit 都能清晰地描述其意图,一个 commit 尽量只有一个意图。 + +KiramiBot 的 commit message 格式遵循 [gitmoji](https://gitmoji.dev/) 规范,在创建 commit 时请牢记这一点。 + +### 创建拉取请求 + +完成修改后,创建一个拉取请求,也称为 PR。 + +- 拉取请求标题应尽量使用中文,以便自动生成更新日志。 +- 填写拉取请求模板,以便我们审核您的 PR。该模板可帮助审核员了解您的更改以及您的拉取请求的目的。 +- 如果您的 PR 修复或解决了现有问题,请[将拉取请求链接到议题](https://docs.github.com/zh/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue)。 +- 在一个 PR 中有多个提交是可以的。您不需要重新变基或强制推送您的更改,因为我们将使用 Squash Merge 在合并时将您的提交压缩成一个提交。 +- 最好提交多个涉及少量文件的拉取请求,而不是提交涉及多个文件的大型拉取请求。这样做可以使审核员更容易理解您的更改,并且可以更快地合并您的更改。 +- 我们可能会要求在合并 PR 之前进行更改,或者使用[建议的更改](https://docs.github.com/zh/pull-requests/collaborating-with-pull-requests/reviewing-changes-in-pull-requests/incorporating-feedback-in-your-pull-request),或者拉请求注释。您可以通过 UI 直接应用建议的更改,也可以在 fork 中进行任何其他更改,然后将它们提交到分支。 +- 当您更新 PR 并应用更改时,将每个对话标记为[已解决](https://docs.github.com/zh/pull-requests/collaborating-with-pull-requests/reviewing-changes-in-pull-requests/commenting-on-a-pull-request#%E8%A7%A3%E5%86%B3%E5%AF%B9%E8%AF%9D)。 +- 如果遇到任何合并问题,请查看[解决合并冲突](https://github.com/skills/resolve-merge-conflicts),以帮助您解决合并冲突和其他问题。 + +## 风格指南 + +KiramiBot 的代码风格遵循 [PEP 8](https://www.python.org/dev/peps/pep-0008/) 与 [PEP 484](https://www.python.org/dev/peps/pep-0484/) 规范,请确保您的代码风格和项目已有的代码保持一致,变量命名清晰,代码类型完整,有适当的注释与测试代码。 + +## 版权声明 + +在为此项目做贡献时,请确保您的贡献内容不会侵犯他人的知识产权,否则您的贡献将被视为无效。 + +通过贡献您的代码、问题或建议,即表示您同意将您的贡献内容以开源的形式提供,并遵守项目所采用的开源许可证。 diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..b14a7d8 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +custom: ["https://afdian.net/a/kiramibot"] diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..4420cb6 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,93 @@ +name: 🐛 错误报告 +title: "BUG: " +description: 创建一个错误报告来帮助我们改进 +labels: ["bug"] +body: + - type: markdown + attributes: + value: | + ## 注意事项 + [GitHub Issues](https://github.com/A-kirami/KiramiBot/issues) 专门用于错误报告和功能需求,这意味着我们不接受使用问题。如果您打开的问题不符合要求,它将会被无条件关闭。 + + 有关使用问题,请通过以下途径: + - 阅读文档以解决 + - 在社区内寻求他人解答 + - 在 [GitHub Discussions](https://github.com/A-kirami/KiramiBot/discussions) 上提问 + - 在网络中搜索是否有人遇到过类似的问题 + + 如果您不知道如何有效、精准地提出一个问题,我们建议您先阅读[《提问的智慧》](https://github.com/ryanhanwu/How-To-Ask-Questions-The-Smart-Way/blob/main/README-zh_CN.md)。 + + 最后,请记得遵守我们的社区准则,友好交流。 + + - type: checkboxes + id: terms + attributes: + label: 确认事项 + description: 请确认您已遵守所有必选项。 + options: + - label: 我已仔细阅读并了解上述注意事项。 + required: true + - label: 我已使用最新版本测试过,确认问题依旧存在。 + required: true + - label: 我确定在 [GitHub Issues](https://github.com/A-kirami/KiramiBot/issues) 中没有相同或相似的问题。 + required: true + + - type: input + id: version + attributes: + label: 影响版本 + description: 这个问题出现在哪个版本上? + placeholder: 发布版本号或 Commit ID + validations: + required: true + + - type: textarea + id: describe + attributes: + label: 问题描述 + description: 请清晰简洁地说明问题是什么,并解释您是如何遇到此问题的,以及您为此做出的尝试。 + placeholder: 我遇到的问题是…… + validations: + required: true + + - type: textarea + id: reproduction + attributes: + label: 复现步骤 + description: | + 提供能复现此问题的详细操作步骤。如果可能,请尝试提供一个可复现的测试用例,该测试用例是发生问题所需的最低限度。 + 推荐阅读:[《如何创建一个最小的、可复现的示例》](https://stackoverflow.com/help/minimal-reproducible-example) + placeholder: | + 1. 首先…… + 2. 然后…… + 3. 发生…… + validations: + required: true + + - type: textarea + id: expected + attributes: + label: 预期行为 + description: 您的预期中会发生什么? + placeholder: 正常情况下它应该…… + + - type: textarea + id: logs + attributes: + label: 日志信息 + description: 提供有助于诊断问题的任何日志和完整的错误信息。 + placeholder: 请注意将您的敏感信息从日志中过滤或替换。 + + - type: textarea + id: context + attributes: + label: 额外补充 + description: 在此处添加相关的任何其他上下文或截图,或者您觉得有帮助的信息。 + + - type: checkboxes + id: contribute + attributes: + label: 参与贡献 + description: 欢迎加入我们的贡献者行列! + options: + - label: 我有足够的时间和能力,愿意为此提交 PR 来修复问题。 diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..ea3e107 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: 💬 交流论坛 + url: https://github.com/A-kirami/KiramiBot/discussions + about: 在这里交流想法、提出问题或者讨论其他事情 diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..2667c52 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,75 @@ +name: ✨ 功能需求 +title: "Feature: " +description: 为项目提出一个新的想法或建议 +labels: ["enhancement"] +body: + - type: markdown + attributes: + value: | + ## 注意事项 + [GitHub Issues](https://github.com/A-kirami/KiramiBot/issues) 专门用于错误报告和功能需求,这意味着我们不接受使用问题。如果您打开的问题不符合要求,它将会被无条件关闭。 + + 有关使用问题,请通过以下途径: + - 阅读文档以解决 + - 在社区内寻求他人解答 + - 在 [GitHub Discussions](https://github.com/A-kirami/KiramiBot/discussions) 上提问 + - 在网络中搜索是否有人遇到过类似的问题 + + 最后,请记得遵守我们的社区准则,友好交流。 + + - type: checkboxes + id: terms + attributes: + label: 确认事项 + description: 请确认您已遵守所有必选项。 + options: + - label: 我已仔细阅读并了解上述注意事项。 + required: true + - label: 我已使用最新版本测试过,确认功能并未实现。 + required: true + - label: 我确定在 [GitHub Issues](https://github.com/A-kirami/KiramiBot/issues) 中没有相同或相似的需求。 + required: true + + - type: textarea + id: problem + attributes: + label: 您希望能解决什么样的问题? + description: 请简要地说明是什么问题导致您想要一个新功能。也许我们可以提出一种现有的解决办法。 + validations: + required: true + + - type: textarea + id: solution + attributes: + label: 您想要的解决方案 + description: 请说明您希望使用什么样的方法解决上述问题。 + validations: + required: true + + - type: textarea + id: alternatives + attributes: + label: 您考虑过的替代方案 + description: 除了上述方法以外,您还考虑过哪些其他的实现方式? + + - type: textarea + id: usecase + attributes: + label: 实现的功能是什么样的? + description: | + 提供功能在实现后如何使用的代码示例。请注意,您可以使用 Markdown 来设置代码块的格式。 + 尽可能多地提供细节。您希望它如何使用的示例代码会有所帮助。 + + - type: textarea + id: context + attributes: + label: 还有什么要补充的吗? + description: 在此处添加相关的任何其他上下文或截图,或者您觉得有帮助的信息。 + + - type: checkboxes + id: contribute + attributes: + label: 参与贡献 + description: 欢迎加入我们的贡献者行列! + options: + - label: 我有足够的时间和能力,愿意为此提交 PR 来实现功能。 diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..6d53b12 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,61 @@ + + +### 这个 PR 带来了什么样的更改? + + +- [ ] 错误修复 +- [ ] 新功能 +- [ ] 文档/注释 +- [ ] 代码格式 +- [ ] 代码重构 +- [ ] 测试用例 +- [ ] 性能优化 +- [ ] 外观样式 +- [ ] 项目构建 +- [ ] 依赖环境 +- [ ] 持续集成/部署 +- [ ] 其他,请描述: + +### 这个 PR 是否存在破坏性变更? + + +- [ ] 是的,并已在 issue #___ 号中获得批准 +- [ ] 没有 + +### 描述 + + + + +### 动机和背景 + + + + +### 其他信息 + + + + +### 检查工作 + +- [ ] 我对我的代码进行了注释,特别是在难以理解的部分 +- [ ] 我的更改需要更新文档,并且已对文档进行了相应的更改 +- [ ] 我添加了测试并且已经在本地通过,以证明我的修复补丁或新功能有效 +- [ ] 我已检查并确保更改没有与其他打开的 [Pull Requests](https://github.com/A-kirami/KiramiBot/pulls) 重复 diff --git a/.github/SUPPORT.md b/.github/SUPPORT.md new file mode 100644 index 0000000..0880b63 --- /dev/null +++ b/.github/SUPPORT.md @@ -0,0 +1,39 @@ +# KiramiBot 帮助支持 + +嗨!👋很高兴您正在使用 KiramiBot,我们很乐意为您提供帮助。为了让我们更好地帮助您,请阅读本文内容,感谢您的理解。 + +请理解参与此项目的人员通常是在业余时间从事此项工作,他们并不承担向您提供免费客户服务的义务。 + +> 👉 **注意**:在加入我们的社区之前,请阅读我们的[行为准则](./CODE_OF_CONDUCT)。通过与此存储库、组织或社区进行交互,即表示您同意遵守其条款。 + +## 如何提出一个优质的问题 + +花点时间来好好构思一下问题。从长远来看,预先花费额外的时间可以帮助每个人节省时间。下面是一些建议: + +- [和小黄鸭说话!](https://rubberduckdebugging.com/) +- 不要掉进 [XY 问题](https://meta.stackexchange.com/questions/66377/what-is-the-xy-problem/66378#66378) 的陷阱 +- 掌握[提问的智慧](https://github.com/ryanhanwu/How-To-Ask-Questions-The-Smart-Way/blob/main/README-zh_CN.md) +- 搜索看看是否有人问过类似的问题 +- 尝试定义您需要帮助的内容: + - 您有什么特别想做的事情吗? + - 您遇到了什么问题,您采取了哪些步骤来尝试修复它? + - 有没有您不明白的概念? +- 如果可能,请提供最小重现实例 +- 屏幕截图可能会有所帮助,但如果其中有重要的文本,例如代码或错误日志,也请以文本形式提供这些内容 +- 您花在提问上的时间越多,我们就越能为您提供更好的帮助 + +## 在哪里可以得到帮助 + +### 文档 + +- [官方文档](https://kiramibot.dev/docs) + +### 聊天 + +- [官方用户交流群](https://qm.qq.com/q/7OD95ZDCMM) +- [官方开发者交流群](https://qm.qq.com/q/fQvd478kz8) + +### GitHub + +- [Github Issues](https://github.com/A-kirami/KiramiBot/issues) +- [Github Discussions](https://github.com/A-kirami/KiramiBot/discussions) diff --git a/.github/TYPES_OF_CONTRIBUTIONS.md b/.github/TYPES_OF_CONTRIBUTIONS.md new file mode 100644 index 0000000..6a9313c --- /dev/null +++ b/.github/TYPES_OF_CONTRIBUTIONS.md @@ -0,0 +1,51 @@ +# KiramiBot 贡献类型 + +您可以通过多种方式为 KiramiBot 做出贡献,其中一些甚至不需要编写一行代码✨。 + +## 报告错误 + +报告您在使用 KiramiBot 时遇到的错误。这有助于开发人员和用户识别和解决问题。 + +如果已经确定了问题,请首先搜索存储库上的现有问题列表。如果找不到类似的问题,请创建一个新问题。 + +## 想法建议 + +在 [Github Issues](https://github.com/A-kirami/KiramiBot/issues) 或者 [Github Discussions](https://github.com/A-kirami/KiramiBot/discussions) 中讨论高级主题或想法(例如:社区、愿景或政策),或是提出新功能或其他项目创意。 + +如果想要提出新功能,请首先搜索存储库上的现有问题列表。如果找不到类似的问题,请创建一个新问题。 + +## 编写代码 + +修复现有问题或实现新功能,帮助改进 KiramiBot。 + +我们非常欢迎代码贡献。在创建拉取请求之前,最好先发布问题或打开问题以报告错误或建议新功能。 + +## 改进文档 + +我们的目标是制作高质量的文档和教程。您可以帮助我们改进文档。如拼写错误更正、错误修复、更好的解释和新示例等。 + +## 参与讨论 + +您可以在[官方用户交流群](https://qm.qq.com/q/7OD95ZDCMM)、[官方开发者交流群](https://qm.qq.com/q/fQvd478kz8)或者 [Github Discussions](https://github.com/A-kirami/KiramiBot/discussions) 中参与社区的讨论,为人们回答项目相关的问题,引导或帮助他们解决遇到的问题。 + +## 构建生态 + +你可以在 KiramiBot 的插件商店发布自己开发的插件,构建社区的开源生态。 + +我们仅对插件的可用性进行简单测试,并会在每一个版本发布后对所有插件进行可用性检查。 + +如果你参考或基于他人发行的代码进行开发,请注意遵守各代码所使用的开源许可协议。 + +## 经济支持 + +您可以通过赞助和捐赠来支持项目的发展,帮助项目维持运行。 + +## 还有更多 + +如果您喜欢策划,您可以组织项目的会议(如果有),帮助社区成员找到合适的会议并提交演讲建议。 + +如果您喜欢设计,您可以重构布局以提高项目的可用性,也可以整理风格指南,帮助项目进行一致的视觉设计。 + +如果您喜欢写作,您可以整理一个包含示例的文件夹,展示项目的使用方式,或者撰写社区文档和教程。 + +如果您喜欢组织,您可以链接重复的问题,并建议新的问题标签,以保持事情井井有条。 diff --git a/.github/release.yml b/.github/release.yml new file mode 100644 index 0000000..09adc0a --- /dev/null +++ b/.github/release.yml @@ -0,0 +1,18 @@ +changelog: + categories: + - title: 💥 破坏性更新 + labels: + - Semver-Major + - breaking-change + - title: ✨ 新特性 + labels: + - enhancement + - title: 🐛 错误修复 + labels: + - bug + - title: 💫 杂项 + labels: + - "*" + exclude: + labels: + - dependencies diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4af20d4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,186 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ +results.xml +allure_report/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock +# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration +poetry.toml + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm-python + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# ruff +.ruff_cache/ + +# LSP config files +pyrightconfig.json + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +# VisualStudioCode +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# VS Code Counter +.VSCodeCounter/ + +# KiramiBot +kirami.* +/logs/ +/data/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..871d9db --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,19 @@ +ci: + autofix_commit_msg: "🚨 通过预提交挂钩自动修复" + autofix_prs: true + autoupdate_branch: main + autoupdate_schedule: monthly + autoupdate_commit_msg: "⬆️ 自动更新预提交挂钩" +repos: + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.0.282 + hooks: + - id: ruff + args: [--fix, --exit-non-zero-on-fix] + stages: [commit] + + - repo: https://github.com/psf/black + rev: 23.7.0 + hooks: + - id: black + stages: [commit] diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..fdf7a70 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": ["ms-python.python", "ms-python.vscode-pylance", "ms-python.black-formatter", "charliermarsh.ruff", "tamasfe.even-better-toml"] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..98c5b1e --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,17 @@ +{ + "python.languageServer": "Pylance", + "python.analysis.typeCheckingMode": "basic", + "python.analysis.inlayHints.callArgumentNames": true, + "python.analysis.inlayHints.functionReturnTypes": true, + "python.analysis.inlayHints.pytestParameters": true, + "python.analysis.inlayHints.variableTypes": true, + "[python]": { + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.fixAll": true, + "source.organizeImports": true + }, + "editor.defaultFormatter": "ms-python.black-formatter" + }, + "editor.rulers": [88], +} diff --git a/bot.py b/bot.py deleted file mode 100644 index 47cd2ec..0000000 --- a/bot.py +++ /dev/null @@ -1,10 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -from kirami import KiramiBot - -bot = KiramiBot() -app = bot.asgi - -if __name__ == "__main__": - bot.run(app="__mp_main__:app") diff --git a/kirami/__init__.py b/kirami/__init__.py new file mode 100644 index 0000000..99450eb --- /dev/null +++ b/kirami/__init__.py @@ -0,0 +1,276 @@ +"""本模块主要定义了 KiramiBot 启动所需类和函数,供 bot 入口文件调用 + +## 快捷导入 + +为方便使用,本模块从子模块导入了部分内容,以下内容可以直接通过本模块导入: + +- `on` +- `on_type` +- `on_metaevent` +- `on_message` +- `on_notice` +- `on_request` +- `on_command` +- `on_shell_command` +- `on_startswith` +- `on_endswith` +- `on_fullmatch` +- `on_keyword` +- `on_regex` +- `on_prefix` +- `on_suffix` +- `on_time` +- `CommandGroup` +- `Matchergroup` +""" + +from typing import Any, ClassVar + +import nonebot +from fastapi import FastAPI +from nonebot.adapters.onebot.v11 import Adapter as OneBotAdapter +from nonebot.adapters.onebot.v11 import Bot +from nonebot.drivers.fastapi import Driver +from nonebot.plugin.manager import PluginManager, _managers +from nonebot.plugin.plugin import Plugin + +from kirami import hook +from kirami.config import bot_config, kirami_config, plugin_config +from kirami.log import Columns, Panel, Text, console, logger +from kirami.server import Server +from kirami.version import __metadata__, __version__ + + +def get_driver() -> Driver: + """获取全局`Driver` 实例。 + + ### 异常 + ValueError: 全局 `nonebot.drivers.Driver` 对象尚未初始化(`kirami.KiramiBot` 尚未实例化) + """ + if KiramiBot.driver is None: + raise ValueError("KiramiBot has not been initialized.") + return KiramiBot.driver + + +def get_app() -> FastAPI: + """获取全局`ReverseDriver`对应的 Server App 对象。 + + ### 异常 + AssertionError: 全局 Driver 对象不是 `nonebot.drivers.ReverseDriver` 类型 + ValueError: 全局 `nonebot.drivers.Driver` 对象尚未初始化(`kirami.KiramiBot` 尚未实例化) + """ + driver = get_driver() + return driver.server_app + + +def get_asgi() -> FastAPI: + """获取全局`ReverseDriver`对应 [ASGI](https://asgi.readthedocs.io/) 对象。 + + ### 异常 + AssertionError: 全局 Driver 对象不是 `nonebot.drivers.ReverseDriver` 类型 + ValueError: 全局 `nonebot.drivers.Driver` 对象尚未初始化(`kirami.KiramiBot` 尚未实例化) + """ + driver = get_driver() + return driver.asgi + + +def get_bot(self_id: str | None = None) -> Bot: + """获取一个连接到 KiramiBot 的`Bot` 对象。 + + 当提供 `self_id` 时,此函数是 `get_bots()[self_id]` 的简写; + 当不提供时,返回一个 `nonebot.adapters.Bot`。 + + ### 参数 + self_id: 用来识别 `nonebot.adapters.Bot` 的 `nonebot.adapters.Bot.self_id` 属性 + + ### 异常 + KeyError: 对应 self_id 的 Bot 不存在 + ValueError: 没有传入 self_id 且没有 Bot 可用 + ValueError: 全局 `nonebot.drivers.Driver` 对象尚未初始化(`kirami.KiramiBot` 尚未实例化) + """ + bots = get_bots() + if self_id: + return bots[self_id] + + try: + return next(iter(bots.values())) + except StopIteration as e: + raise ValueError("There are no bots to get.") from e + + +def get_bots() -> dict[str, Bot]: + """获取所有连接到 KiramiBot 的 `nonebot.adapters.Bot` 对象。 + + ### 返回 + 一个以 `nonebot.adapters.Bot.self_id` 为键,`nonebot.adapters.Bot` 对象为值的字典 + + ### 异常 + ValueError: 全局 `nonebot.drivers.Driver` 对象尚未初始化(`kirami.KiramiBot` 尚未实例化) + """ + driver = get_driver() + return driver.bots # type: ignore + + +def get_bot_ids() -> list[str]: + """获取所有连接到 KiramiBot 的机器人的 self_id。 + + ### 返回 + 一个包含所有连接到 KiramiBot 的机器人的 self_id 的列表 + + ### 异常 + ValueError: 全局 `nonebot.drivers.Driver` 对象尚未初始化(`kirami.KiramiBot` 尚未实例化) + """ + return list(get_bots().keys()) + + +class KiramiBot: + driver: ClassVar[Driver] + asgi: ClassVar[FastAPI] + + def __init__(self) -> None: + from kirami import patch as patch + + self.show_logo() + + if bot_config.debug: + self.print_environment() + console.rule() + + nonebot.init(**_mixin_config(bot_config.dict())) + + self.__class__.driver = nonebot.get_driver() # type: ignore + self.__class__.driver.register_adapter(OneBotAdapter) + self.__class__.asgi = nonebot.get_asgi() + + logger.success("🌟 KiramiBot is initializing...") + logger.opt(colors=True).debug( + f"Loaded Config: {kirami_config.dict()}" + ) + + self.load_plugins() + + Server.init() + hook.install_hook() + + logger.opt(colors=True).success("🌟 KiramiBot is Running...") + + if bot_config.debug: + console.rule("[blink][yellow]当前处于调试模式中, 请勿在生产环境打开[/][/]") + + def run(self, *args, **kwargs) -> None: + """启动 KiramiBot""" + self.driver.run(*args, **kwargs) + + def load_plugins(self) -> None: + """加载插件""" + manager = PluginManager(plugin_config.plugins, plugin_config.plugin_dirs) + plugins = manager.available_plugins + _managers.append(manager) + + if plugin_config.whitelist: + plugins &= plugin_config.whitelist + + if plugin_config.blacklist: + plugins -= plugin_config.blacklist + + loaded_plugins = set( + filter(None, (manager.load_plugin(name) for name in plugins)) + ) + + self.loading_state(loaded_plugins) + + def loading_state(self, plugins: set[Plugin]) -> None: + """打印插件加载状态""" + logger.opt(colors=True).info( + f"🌟 [magenta]Total {len(nonebot.get_loaded_plugins())} plugin are successfully loaded.[/]" + ) + + failed_plugins = plugins - nonebot.get_loaded_plugins() + + if failed_total := len(failed_plugins): + logger.opt(colors=True).error( + f"🌠 [magenta]Total {failed_total} plugin are failed loaded.[/]: {', '.join(plugin.name for plugin in failed_plugins)}" + ) + + def show_logo(self) -> None: + """打印 LOGO""" + console.print( + Columns( + [Text(LOGO.lstrip("\n"), style="bold blue")], + align="center", + expand=True, + ) + ) + + def print_environment(self) -> None: + """打印环境信息""" + import platform + + environment_info = { + "OS": platform.system(), + "Arch": platform.machine(), + "Python": platform.python_version(), + "KiramiBot": __version__, + "NoneBot": nonebot.__version__, + } + + renderables = [ + Panel( + Text(justify="center") + .append(k, style="bold") + .append(f"\n{v}", style="yellow"), + expand=True, + width=console.size.width // 6, + ) + for k, v in environment_info.items() + ] + console.print( + Columns( + renderables, + align="center", + title="Environment Info", + expand=True, + equal=True, + ) + ) + + +def _mixin_config(config: dict[str, Any]) -> dict[str, Any]: + config["fastapi_openapi_url"] = ( + config["fastapi_openapi_url"] if config["debug"] else None + ) + config["fastapi_extra"] = { + "title": __metadata__.name, + "version": __metadata__.version, + "description": __metadata__.summary, + } + + return config + + +LOGO = r""" + _ _ _ _ ______ +| | / )(_) (_)(____ \ _ +| | / / _ ____ ____ ____ _ ____) ) ___ | |_ +| |< < | | / ___)/ _ || \ | || __ ( / _ \ | _) +| | \ \ | || | ( ( | || | | || || |__) )| |_| || |__ +|_| \_)|_||_| \_||_||_|_|_||_||______/ \___/ \___) +""" + +from kirami.matcher import CommandGroup as CommandGroup +from kirami.matcher import MatcherGroup as MatcherGroup +from kirami.matcher import on_command as on_command +from kirami.matcher import on_endswith as on_endswith +from kirami.matcher import on_fullmatch as on_fullmatch +from kirami.matcher import on_keyword as on_keyword +from kirami.matcher import on_message as on_message +from kirami.matcher import on_metaevent as on_metaevent +from kirami.matcher import on_notice as on_notice +from kirami.matcher import on_prefix as on_prefix +from kirami.matcher import on_regex as on_regex +from kirami.matcher import on_request as on_request +from kirami.matcher import on_shell_command as on_shell_command +from kirami.matcher import on_startswith as on_startswith +from kirami.matcher import on_suffix as on_suffix +from kirami.matcher import on_time as on_time +from kirami.matcher import on_type as on_type diff --git a/kirami/config/__init__.py b/kirami/config/__init__.py new file mode 100644 index 0000000..70e2065 --- /dev/null +++ b/kirami/config/__init__.py @@ -0,0 +1,32 @@ +"""本模块提供了 KiramiBot 运行所需的配置及目录。""" + +from .config import BaseConfig as BaseConfig +from .config import KiramiConfig +from .path import AUDIO_DIR as AUDIO_DIR +from .path import BOT_DIR as BOT_DIR +from .path import DATA_DIR as DATA_DIR +from .path import FONT_DIR as FONT_DIR +from .path import IMAGE_DIR as IMAGE_DIR +from .path import LOG_DIR as LOG_DIR +from .path import PAGE_DIR as PAGE_DIR +from .path import RES_DIR as RES_DIR +from .path import VIDEO_DIR as VIDEO_DIR +from .utils import load_config + +kirami_config = KiramiConfig(**load_config()) +"""KiramiBot 配置""" + +bot_config = kirami_config.bot +"""本体主要配置""" + +plugin_config = kirami_config.plugin +"""插件加载相关配置""" + +server_config = kirami_config.server +"""服务器相关配置""" + +log_config = kirami_config.log +"""日志相关配置""" + +database_config = kirami_config.database +"""数据库相关配置""" diff --git a/kirami/config/config.py b/kirami/config/config.py new file mode 100644 index 0000000..d7f1092 --- /dev/null +++ b/kirami/config/config.py @@ -0,0 +1,225 @@ +"""本模块定义了 KiramiBot 运行所需的配置项""" + +from collections.abc import KeysView, Mapping +from datetime import timedelta +from ipaddress import IPv4Address +from typing import TYPE_CHECKING, Any, Literal, NoReturn, TypeAlias + +from mango.drive import DEFAULT_CONNECT_URI +from nonebot.config import Config, Env +from pydantic import BaseModel, Field, IPvAnyAddress, root_validator + +LevelName: TypeAlias = Literal[ + "TRACE", "DEBUG", "INFO", "SUCCESS", "WARNING", "ERROR", "CRITICAL" +] + + +class BaseConfig(BaseModel, Mapping): + def __getitem__(self, key: str) -> Any: + try: + return getattr(self, key) + except AttributeError as e: + raise RuntimeError( + f"{self.__class__.__name__} 不存在 {key} 配置, 请检查拼写是否正确" + ) from e + + def __setitem__(self, *_) -> NoReturn: + raise RuntimeError("无法在运行时修改配置") + + def __delitem__(self, _) -> NoReturn: + raise RuntimeError("无法在运行时修改配置") + + def __setattr__(self, *_) -> NoReturn: + raise RuntimeError("无法在运行时修改配置") + + def __delattr__(self, _) -> NoReturn: + raise RuntimeError("无法在运行时修改配置") + + def __len__(self) -> int: + return len(self.__dict__) + + def __repr__(self) -> str: + return f"<{self.__class__.__name__} {self.__dict__}>" + + def keys(self) -> KeysView[str]: + return self.__dict__.keys() + + +class LogConfig(BaseConfig): + log_expire_timeout: int = 7 + """日志文件过期时间""" + + +class DatabaseConfig(BaseConfig): + """ + MongoDB 数据库配置 + """ + + uri: str = DEFAULT_CONNECT_URI + """MongoDB 连接 URI""" + + host: IPvAnyAddress = IPv4Address("127.0.0.1") # type: ignore + """MongoDB 服务器地址""" + + port: int = Field(default=27017, ge=1, le=65535) + """MongoDB 服务器端口""" + + username: str = "" + """MongoDB 连接用户名""" + + password: str = "" + """MongoDB 连接密码""" + + database: str = "kirami" + """MongoDB 数据库名称""" + + +class PluginConfig(BaseConfig): + """ + 插件加载配置 + """ + + plugins: set[str] = set() + """加载的插件""" + + plugin_dirs: set[str] = set() + """插件目录列表""" + + whitelist: set[str] | None = None + """插件白名单,只加载指定插件""" + + blacklist: set[str] | None = None + """插件黑名单,不加载指定插件""" + + +class ServerConfig(BaseConfig): + """ + APP 服务器配置 + """ + + allow_cors: bool = True + """是否允许跨域请求。默认为 True""" + + allow_origins: list[str] = ["*"] + """允许跨域请求的源列表。默认允许所有""" + + allow_origin_regex: str | None = None + """正则表达式字符串,匹配的源允许跨域请求""" + + allow_methods: list[str] = ["*"] + """允许跨域请求的 HTTP 方法列表。默认允许所有标准方法""" + + allow_headers: list[str] = ["*"] + """允许跨域请求的 HTTP 请求头列表。默认允许所有的请求头。`Accept`、`Accept-Language`、`Content-Language` 以及 `Content-Type` 请求头总是允许 CORS 请求""" + + allow_credentials: bool = False + """指示跨域请求支持 cookies。默认为 False。另外,允许凭证时 allow_origins 不能设定为 ['*'],必须指定源""" + + expose_headers: list[str] = [] + """指示可以被浏览器访问的响应头。默认为 []""" + + max_age: int = 600 + """设定浏览器缓存 CORS 响应的最长时间,单位是秒。默认为 600""" + + +class BotConfig(BaseConfig): + """ + Bot 主要配置。 + """ + + driver: str = "nonebot.drivers.fastapi" + """KiramiBot 运行所使用的 ``Driver``""" + + host: IPvAnyAddress = IPv4Address("127.0.0.1") # type: ignore + """KiramiBot 的 HTTP 和 WebSocket 服务端监听的 IP/主机名""" + + port: int = Field(default=8120, ge=1, le=65535) + """KiramiBot 的 HTTP 和 WebSocket 服务端监听的端口""" + + debug: bool = False + """是否以调试模式运行 KiramiBot""" + + log_level: LevelName | int = "INFO" + """配置 KiramiBot 日志输出等级,可以为 `int` 类型等级或等级名称,参考 [loguru 日志等级](https://loguru.readthedocs.io/en/stable/api/logger.html#levels)""" + + log_file: LevelName | tuple[LevelName] = "ERROR" + """KiramiBot 的日志保存等级,必须为等级名称""" + + api_root: dict[str, str] = {} + """以机器人 ID 为键,上报地址为值的字典""" + + api_timeout: float = 30.0 + """API 请求超时时间,单位: 秒""" + + onebot_access_token: str = Field(default=None, alias="access_token") + """API 请求以及上报所需密钥,在请求头中携带""" + + secret: str | None = None + """HTTP POST 形式上报所需签名,在请求头中携带""" + + superusers: set[str] = set() + """机器人超级用户""" + + nickname: set[str] = {"kirami", "星见"} + """机器人昵称""" + + command_start: set[str] = {"/", ""} + """命令的起始标记,用于判断一条消息是不是命令""" + + command_sep: set[str] = {"."} + """命令的分隔标记,用于将文本形式的命令切分为元组(实际的命令名)""" + + session_expire_timeout: timedelta = timedelta(minutes=2) + """等待用户回复的超时时间""" + + proxy_url: str | dict[str, str] | None = None + """HTTP 代理地址""" + + http_timeout: float = 10.0 + """HTTP 请求超时时间,单位: 秒""" + + browser: Literal["chromium", "firefox", "webkit"] = "chromium" + """浏览器类型""" + + _env_file: str | None = Field(default=None, alias="env_file") + """配置文件名默认从 `.env.{env_name}` 中读取配置""" + + @root_validator(pre=True) + def mixin_config(cls, values: dict[str, Any]) -> dict[str, Any]: + config = Config(**values, _env_file=(".env", f".env.{Env().environment}")) + return config.dict() + + class Config: + extra = "allow" + + if TYPE_CHECKING: + + def __getattr__(self, name: str) -> Any: + ... + + +class KiramiConfig(BaseConfig): + """ + KiramiBot 主要配置。 + """ + + bot: BotConfig + """本体主要配置""" + + plugin: PluginConfig + """插件加载相关配置""" + + server: ServerConfig + """服务器相关配置""" + + log: LogConfig + """日志相关配置""" + + database: DatabaseConfig + """数据库相关配置""" + + @root_validator(pre=True) + def set_default_config(cls, values: dict[str, Any]) -> dict[str, Any]: + for name, config in cls.__annotations__.items(): + values.setdefault(name, config()) + return values diff --git a/kirami/config/path.py b/kirami/config/path.py new file mode 100644 index 0000000..a9e2997 --- /dev/null +++ b/kirami/config/path.py @@ -0,0 +1,43 @@ +"""本模块定义了 KiramiBot 运行所需的文件目录""" + +import sys +from pathlib import Path + +# ========== 根目录 ========== + +BOT_DIR = Path(sys.path[0]) +"""Bot 根目录""" + +# ========== 文件目录 ========== + +DATA_DIR = BOT_DIR / "data" +"""数据保存目录""" + +LOG_DIR = BOT_DIR / "logs" +"""日志保存目录""" + +RES_DIR = BOT_DIR / "resources" +"""资源文件目录""" + +# ========== 资源目录 ========== + +IMAGE_DIR = RES_DIR / "image" +"""图片文件目录""" + +VIDEO_DIR = RES_DIR / "video" +"""视频文件目录""" + +AUDIO_DIR = RES_DIR / "audio" +"""音频文件目录""" + +FONT_DIR = RES_DIR / "font" +"""字体文件目录""" + +PAGE_DIR = RES_DIR / "page" +"""网页文件目录""" + +# ========== 创建目录 ========== + +for var in locals().copy().values(): + if isinstance(var, Path): + var.mkdir(parents=True, exist_ok=True) diff --git a/kirami/config/utils.py b/kirami/config/utils.py new file mode 100644 index 0000000..db375f0 --- /dev/null +++ b/kirami/config/utils.py @@ -0,0 +1,31 @@ +import itertools +from pathlib import Path +from typing import Any + +try: # pragma: py-gte-311 + import tomllib # pyright: ignore[reportMissingImports] +except ModuleNotFoundError: # pragma: py-lt-311 + import tomli as tomllib + + +def load_config() -> dict[str, Any]: + """加载 KiramiBot 配置。 + + ### 说明 + 配置文件优先级: `kirami.toml`,`kirami.config.toml`,`kirami.yaml`,`kirami.config.yaml`,`kirami.yml`,`kirami.config.yml`,`kirami.json`,`kirami.config.json` + + 当以上文件均不存在时,会尝试读取 `pyproject.toml` 中的 `tool.kirami` 配置 + """ + + def load_file(path: str | Path) -> dict[str, Any]: + return tomllib.loads(Path(path).read_text(encoding="utf-8")) + + file_name = ("kirami", "kirami.config") + file_type = ("toml", "yaml", "yml", "json") + config_files = itertools.product(file_name, file_type) + for name, type in config_files: + file = Path(f"{name}.{type}") + if file.is_file(): + return load_file(file) + pyproject = load_file("pyproject.toml") + return pyproject.get("tool", {}).get("kirami", {}) diff --git a/kirami/database/__init__.py b/kirami/database/__init__.py new file mode 100644 index 0000000..aa521bc --- /dev/null +++ b/kirami/database/__init__.py @@ -0,0 +1,31 @@ +"""本模块包含了数据库连接和数据库模型的定义""" + +from mango import Mango +from pymongo.errors import ServerSelectionTimeoutError + +from kirami.exception import DatabaseError +from kirami.hook import on_shutdown, on_startup + +from .models.argot import Argot as Argot +from .models.group import Group as Group +from .models.user import User as User + + +@on_startup(pre=True) +async def connect_database() -> None: + from kirami.config import database_config + + try: + await Mango.init( + database_config.database, + uri=database_config.uri, + tz_aware=True, + serverSelectionTimeoutMS=1000 * 10, + ) + except ServerSelectionTimeoutError as e: + raise DatabaseError("无法连接到数据库, 请确保 MangoDB 已启动且配置正确") from e + + +@on_shutdown +def disconnect_database() -> None: + Mango.disconnect() diff --git a/kirami/database/models/__init__.py b/kirami/database/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kirami/database/models/argot.py b/kirami/database/models/argot.py new file mode 100644 index 0000000..dce7d17 --- /dev/null +++ b/kirami/database/models/argot.py @@ -0,0 +1,25 @@ +from datetime import datetime, timedelta +from typing import Any + +from mango import Document, Field + +from kirami.event import MessageEvent + + +class Argot(Document): + msg_id: int = Field(primary_key=True) + """消息 ID""" + content: dict[str, Any] + """内容""" + time: datetime = Field( + default_factory=lambda: datetime.now().astimezone(), + expire=int(timedelta(days=1).total_seconds()), + init=False, + ) + """创建时间""" + + @classmethod + async def mark(cls, event: MessageEvent, content: dict[str, Any]) -> None: + """标记一条消息为暗语""" + mid = event.message_id + await Argot(msg_id=mid, content=content).save() diff --git a/kirami/database/models/group.py b/kirami/database/models/group.py new file mode 100644 index 0000000..5efba7c --- /dev/null +++ b/kirami/database/models/group.py @@ -0,0 +1,21 @@ +from typing import Any, TypeVar + +from mango import Document, Field + +T = TypeVar("T") + + +class Group(Document): + """群组文档""" + + id: int = Field(primary_key=True) + """群组 ID""" + data: dict[str, Any] = Field(default_factory=dict, init=False) + """群组数据""" + + def get_data(self, name: str, default: T = None) -> T: + return self.data.get(name, default) + + async def set_data(self, name: str, value: Any = None) -> None: + self.data[name] = value + await self.save() diff --git a/kirami/database/models/user.py b/kirami/database/models/user.py new file mode 100644 index 0000000..62b9ba6 --- /dev/null +++ b/kirami/database/models/user.py @@ -0,0 +1,21 @@ +from typing import Any, TypeVar + +from mango import Document, Field + +T = TypeVar("T") + + +class User(Document): + """用户文档""" + + id: int = Field(primary_key=True) + """用户 ID""" + data: dict[str, Any] = Field(default_factory=dict, init=False) + """用户数据""" + + def get_data(self, name: str, default: T = None) -> T: + return self.data.get(name, default) + + async def set_data(self, name: str, value: Any = None) -> None: + self.data[name] = value + await self.save() diff --git a/kirami/depends.py b/kirami/depends.py new file mode 100644 index 0000000..9e41421 --- /dev/null +++ b/kirami/depends.py @@ -0,0 +1,464 @@ +"""本模块定义了各类常用的依赖注入参数""" + +from collections.abc import AsyncGenerator +from datetime import time +from re import Match +from typing import Annotated, Any, Literal, TypeAlias + +from httpx._types import ProxiesTypes, VerifyTypes +from nonebot.exception import ParserExit +from nonebot.internal.params import Arg as useArg +from nonebot.internal.params import ArgPlainText as useArgPlainText +from nonebot.internal.params import ArgStr as useArgStr +from nonebot.internal.params import DependsInner as BaseDependsInner +from nonebot.params import Command as useCommand +from nonebot.params import CommandArg as useCommandArg +from nonebot.params import CommandWhitespace as useCommandWhitespace +from nonebot.params import EventMessage as useEventMessage +from nonebot.params import EventPlainText as useEventPlainText +from nonebot.params import EventType as useEventType +from nonebot.params import LastReceived as originalLastReceived +from nonebot.params import RawCommand as useRawCommand +from nonebot.params import Received as originalReceived +from nonebot.params import RegexDict as useRegexDict +from nonebot.params import RegexGroup as useRegexGroup +from nonebot.params import RegexMatched as useRegexMatched +from nonebot.params import RegexStr as useRegexStr +from nonebot.params import ShellCommandArgs as useShellCommandArgs +from nonebot.params import ShellCommandArgv as useShellCommandArgv +from nonebot.rule import Namespace +from playwright.async_api import ( + BrowserContext, + Page, +) + +from kirami.database import Group, User +from kirami.matcher import Matcher +from kirami.service.limiter import Cooldown, LimitScope, Lock, Quota, get_scope_key +from kirami.service.service import Ability +from kirami.service.subject import Subjects as Subjects +from kirami.typing import ( + AsyncClient, + Bot, + Event, + Message, + MessageEvent, + MessageSegment, + State, + T_Handler, +) +from kirami.utils import ( + Request, + WebWright, + extract_at_users, + extract_image_urls, + extract_match, + extract_plain_text, +) + + +class DependsInner(BaseDependsInner): + def __call__( + self, + dependency: T_Handler | None = None, + *, + use_cache: bool | None = None, + ) -> Any: + dependency = dependency or self.dependency + use_cache = use_cache or self.use_cache + return self.__class__(dependency, use_cache=use_cache) + + +def depends(dependency: T_Handler | None = None, *, use_cache: bool = True) -> Any: + """子依赖装饰器。 + + ### 参数 + dependency: 依赖函数。默认为参数的类型注释 + + use_cache: 是否使用缓存。默认为 `True` + + ### 用例 + + ```python + @depends + def depend_func() -> Any: + return ... + + @depends(use_cache=False) + def depend_gen_func(): + try: + yield ... + finally: + ... + + async def handler(param_name: Any = depend_func, gen: Any = depend_gen_func): + ... + ``` + """ + return DependsInner(dependency, use_cache=use_cache) + + +Arg = Annotated[Message, useArg()] +"""`got` 的 Arg 参数消息""" + +ArgPlainText = Annotated[str, useArgPlainText()] +"""`got` 的 Arg 参数消息文本""" + +ArgStr = Annotated[str, useArgStr()] +"""`got` 的 Arg 参数消息纯文本""" + +Command = Annotated[tuple[str, ...], useCommand()] +"""消息命令元组""" + +CommandArg = Annotated[Message, useCommandArg()] +"""消息命令参数""" + +CommandWhitespace = Annotated[str, useCommandWhitespace()] +"""消息命令与参数之间的空白""" + +EventMessage = Annotated[Message, useEventMessage()] +"""事件消息参数""" + +EventPlainText = Annotated[str, useEventPlainText()] +"""事件纯文本消息参数""" + +EventType = Annotated[str, useEventType()] +"""事件类型参数""" + +RawCommand = Annotated[str, useRawCommand()] +"""消息命令文本""" + +RegexDict = Annotated[dict[str, Any], useRegexDict()] +"""正则匹配结果 group 字典""" + +RegexGroup = Annotated[tuple[Any, ...], useRegexGroup()] +"""正则匹配结果 group 元组""" + +RegexMatched = Annotated[Match[str], useRegexMatched()] +"""正则匹配结果""" + +RegexStr = Annotated[str, useRegexStr()] +"""正则匹配结果文本""" + +ShellCommandArgs = Annotated[Namespace, useShellCommandArgs()] +"""shell 命令解析后的参数字典""" + +ShellCommandExit = Annotated[ParserExit, useShellCommandArgs()] +"""shell 命令解析失败的异常""" + +ShellCommandArgv = Annotated[list[str | MessageSegment], useShellCommandArgv()] +"""shell 命令原始参数列表""" + + +def useReceived(id: str | None = None, default: Any = None) -> Any: + """`receive` 事件参数""" + return originalReceived(id, default) + + +def useLastReceived(default: Any = None) -> Any: + """`last_receive` 事件参数""" + return originalLastReceived(default) + + +LastReceived = Annotated[Event, useLastReceived()] +"""`last_receive` 事件参数""" + + +@depends +def useToMe(event: Event) -> bool: + """事件是否与机器人有关""" + return event.is_tome() + + +ToMe: TypeAlias = Annotated[bool, useToMe()] +"""事件是否与机器人有关""" + + +@depends +def useReplyMe(event: MessageEvent) -> bool: + """是否回复了机器人的消息""" + return bool(event.reply) + + +ReplyMe: TypeAlias = Annotated[bool, useReplyMe()] +"""是否回复了机器人的消息""" + + +@depends +async def useUserData(event: Event) -> User | None: + """获取用户数据文档模型""" + if uid := getattr(event, "user_id"): + return await User.get_or_create(User.id == uid) + return None + + +UserData: TypeAlias = Annotated[User, useUserData()] +"""用户数据文档模型""" + + +@depends +async def useGroupData(event: Event) -> Group | None: + """获取群数据文档模型""" + if gid := getattr(event, "group_id"): + return await Group.get_or_create(Group.id == gid) + return None + + +GroupData: TypeAlias = Annotated[User, useGroupData()] +"""群数据文档模型""" + + +def useImageURLs(prompt: str | None = None, from_reply: bool = False) -> list[str]: + """提取消息中图片链接。 + + ### 参数 + prompt: 当不存在图片链接时发送给用户的错误消息,默认不发送 + + from_reply: 是否从回复中提取,默认为 `False` + """ + + @depends + async def image_urls(event: MessageEvent, matcher: Matcher) -> list[str]: + return await extract_match( + extract_image_urls, event, matcher, prompt, from_reply + ) + + return image_urls + + +ImageURLs = Annotated[list[str], useImageURLs()] +"""消息中图片链接""" + + +def useAtUsers(prompt: str | None = None, from_reply: bool = False) -> list[str]: + """获取消息中提及的用户。 + + ### 参数 + prompt: 当不存在提及用户时发送给用户的错误消息,默认不发送 + + from_reply: 是否从回复中提取,默认为 `False` + """ + + @depends + async def at_users(event: MessageEvent, matcher: Matcher) -> list[str]: + return await extract_match(extract_at_users, event, matcher, prompt, from_reply) + + return at_users + + +AtUsers = Annotated[list[str], useAtUsers()] +"""消息中提及的用户""" + + +def usePlainText(prompt: str | None = None, from_reply: bool = False) -> str: + """提取消息内纯文本消息。 + + ### 参数 + prompt: 当不存在纯文本消息时发送给用户的错误消息,默认不发送 + + from_reply: 是否从回复中提取,默认为 `False` + """ + + @depends + async def plain_text(event: MessageEvent, matcher: Matcher) -> str: + return await extract_match( + extract_plain_text, event, matcher, prompt, from_reply + ) + + return plain_text + + +PlainText = Annotated[str, usePlainText()] +"""消息内纯文本消息""" + + +def useClientSession( + verify: VerifyTypes = True, + http2: bool = False, + proxies: ProxiesTypes | None = None, + **kwargs, +) -> AsyncClient: + """获取网络连接会话对象""" + + @depends + async def client_session() -> AsyncGenerator[AsyncClient, None]: + async with Request.client_session( + verify=verify, http2=http2, proxies=proxies, **kwargs + ) as session: + yield session + + return client_session + + +ClientSession = Annotated[AsyncClient, useClientSession()] +"""网络连接会话对象""" + + +def useArgotArg(key: str | None = None) -> Any: + """提取暗语中的字段""" + + @depends + def argot_arg(state: State) -> Any: + return state.argot[key] if key else state.argot + + return argot_arg + + +def useWebContext(**kwargs) -> BrowserContext: + """创建浏览器上下文""" + + @depends + async def web_context() -> AsyncGenerator[BrowserContext, None]: + async with WebWright.new_context(**kwargs) as context: + yield context + + return web_context + + +WebContext: TypeAlias = Annotated[BrowserContext, useWebContext()] +"""浏览器上下文""" + + +def useWebPage(**kwargs) -> Page: + """创建浏览器页面""" + + @depends + async def web_page() -> AsyncGenerator[Page, None]: + async with WebWright.new_page(**kwargs) as page: + yield page + + return web_page + + +WebPage: TypeAlias = Annotated[Page, useWebPage()] +"""浏览器页面""" + + +def useUserName() -> str: + """获取用户名,如果为群事件则获取群名片""" + + @depends + async def user_name(bot: Bot, event: Event) -> str: + if isinstance(event, MessageEvent): + return event.sender.card or event.sender.nickname or "" + if not (uid := getattr(event, "user_id", None)): + return "" + if gid := getattr(event, "group_id", None): + info = await bot.get_group_member_info(group_id=gid, user_id=uid) + return info["card"] or info["nickname"] + info = await bot.get_stranger_info(user_id=uid) + return info["nickname"] + + return user_name + + +UserName = Annotated[str, useUserName()] +"""用户名、群名片或用户昵称""" + + +avatar_spec = {"small": 40, "medium": 140, "large": 640} + + +def useUserAvatar(size: Literal["small", "medium", "large"] = "large") -> str: + """获取用户头像链接""" + + @depends + def user_avatar(event: Event) -> str | None: + if uid := getattr(event, "user_id", None): + return f"https://q1.qlogo.cn/g?b=qq&nk={uid}&s={avatar_spec[size]}" + return None + + return user_avatar + + +UserAvatar = Annotated[str, useUserAvatar()] +"""用户头像链接""" + + +def useGroupAvatar(size: Literal["small", "medium", "large"] = "large") -> str: + """获取群头像链接""" + + @depends + def group_avatar(event: Event) -> str | None: + if gid := getattr(event, "group_id", None): + return f"https://p.qlogo.cn/gh/{gid}/{gid}/{avatar_spec[size]}" + return None + + return group_avatar + + +GroupAvatar = Annotated[str, useGroupAvatar()] +"""获取头像链接""" + + +def useCooldown( + cd_time: int, + *, + prompt: str | None = None, + scope: LimitScope = LimitScope.LOCAL, + **kwargs: Any, +) -> Cooldown: + """使用冷却时间限制""" + + @depends + async def check_cooldown( + matcher: Matcher, event: Event + ) -> AsyncGenerator[Cooldown, None]: + name = f"depends:{Ability.got(matcher).id}" + cooldown = await Cooldown.get(name) or Cooldown( + name=name, scope=scope, prompt=prompt, duration=cd_time + ) + + if not (key := get_scope_key(event, scope)): + return + + if not cooldown.check(key): + await matcher.finish( + prompt.format(**cooldown.get_info(key)) if prompt else None, + **kwargs, + ) + + yield cooldown + + await cooldown.start(key) + + return check_cooldown + + +def useQuota( + limit: int, + *, + prompt: str | None = None, + scope: LimitScope = LimitScope.LOCAL, + reset_time: time | None = None, + **kwargs: Any, +) -> Quota: + """使用配额次数限制""" + + @depends + async def check_quota( + matcher: Matcher, event: Event + ) -> AsyncGenerator[Quota, None]: + name = f"depends:{Ability.got(matcher).id}" + quota = await Quota.get(name) or Quota( + name=name, + scope=scope, + prompt=prompt, + limit=limit, + reset_time=reset_time or time(), + ) + + if not (key := get_scope_key(event, scope)): + return + + if not quota.check(key): + await matcher.finish( + prompt.format(**quota.get_info(key)) if prompt else None, + **kwargs, + ) + + yield quota + + await quota.consume(key) + + return check_quota diff --git a/kirami/event.py b/kirami/event.py new file mode 100644 index 0000000..cb8dcd2 --- /dev/null +++ b/kirami/event.py @@ -0,0 +1,45 @@ +"""本模块包含了 KiramiBot 运行中接收的事件类型""" + +from typing import Literal, NoReturn + +from nonebot.adapters.onebot.v11 import Adapter +from nonebot.adapters.onebot.v11 import Event as Event +from nonebot.adapters.onebot.v11 import FriendAddNoticeEvent as FriendAddNoticeEvent +from nonebot.adapters.onebot.v11 import ( + FriendRecallNoticeEvent as FriendRecallNoticeEvent, +) +from nonebot.adapters.onebot.v11 import FriendRequestEvent as FriendRequestEvent +from nonebot.adapters.onebot.v11 import GroupAdminNoticeEvent as GroupAdminNoticeEvent +from nonebot.adapters.onebot.v11 import GroupBanNoticeEvent as GroupBanNoticeEvent +from nonebot.adapters.onebot.v11 import ( + GroupDecreaseNoticeEvent as GroupDecreaseNoticeEvent, +) +from nonebot.adapters.onebot.v11 import ( + GroupIncreaseNoticeEvent as GroupIncreaseNoticeEvent, +) +from nonebot.adapters.onebot.v11 import GroupMessageEvent as GroupMessageEvent +from nonebot.adapters.onebot.v11 import GroupRecallNoticeEvent as GroupRecallNoticeEvent +from nonebot.adapters.onebot.v11 import GroupRequestEvent as GroupRequestEvent +from nonebot.adapters.onebot.v11 import GroupUploadNoticeEvent as GroupUploadNoticeEvent +from nonebot.adapters.onebot.v11 import HeartbeatMetaEvent as HeartbeatMetaEvent +from nonebot.adapters.onebot.v11 import HonorNotifyEvent as HonorNotifyEvent +from nonebot.adapters.onebot.v11 import LifecycleMetaEvent as LifecycleMetaEvent +from nonebot.adapters.onebot.v11 import LuckyKingNotifyEvent as LuckyKingNotifyEvent +from nonebot.adapters.onebot.v11 import MessageEvent as MessageEvent +from nonebot.adapters.onebot.v11 import MetaEvent as MetaEvent +from nonebot.adapters.onebot.v11 import NoticeEvent as NoticeEvent +from nonebot.adapters.onebot.v11 import NotifyEvent as NotifyEvent +from nonebot.adapters.onebot.v11 import PokeNotifyEvent as PokeNotifyEvent +from nonebot.adapters.onebot.v11 import PrivateMessageEvent as PrivateMessageEvent +from nonebot.adapters.onebot.v11 import RequestEvent as RequestEvent +from nonebot.exception import NoLogException + + +class TimerNoticeEvent(NoticeEvent): + """定时器事件""" + + notice_type: Literal["timer"] + timer_id: str + + def get_log_string(self) -> NoReturn: + raise NoLogException(Adapter.get_name()) diff --git a/kirami/exception.py b/kirami/exception.py new file mode 100644 index 0000000..72734b8 --- /dev/null +++ b/kirami/exception.py @@ -0,0 +1,92 @@ +"""本模块包含了所有 KiramiBot 运行时可能会抛出的异常""" + +from nonebot.adapters.onebot.v11.exception import ActionFailed as ActionFailed +from nonebot.adapters.onebot.v11.exception import ApiNotAvailable as ApiNotAvailable +from nonebot.exception import FinishedException as FinishedException +from nonebot.exception import IgnoredException as IgnoredException +from nonebot.exception import MockApiException as MockApiException +from nonebot.exception import NoLogException as NoLogException +from nonebot.exception import NoneBotException +from nonebot.exception import ParserExit as ParserExit +from nonebot.exception import PausedException as PausedException +from nonebot.exception import RejectedException as RejectedException +from nonebot.exception import SkippedException as SkippedException +from nonebot.exception import StopPropagation as StopPropagation + + +class KiramiBotError(NoneBotException): + """所有 KiramiBot 发生的异常基类""" + + +# ============================================================================== + + +class NetworkError(KiramiBotError): + """网络错误""" + + +class HttpRequestError(NetworkError): + """HTTP 请求异常""" + + +# ============================================================================== + + +class ServerError(KiramiBotError): + """服务器错误""" + + +# ============================================================================== + + +class ResourceError(KiramiBotError): + """资源操作异常""" + + +class FileNotExistError(ResourceError, FileNotFoundError): + """文件不存在""" + + +class ReadFileError(ResourceError): + """读取文件错误""" + + +class WriteFileError(ResourceError): + """写入文件错误""" + + +class FileTypeError(ResourceError): + """文件类型错误""" + + +# ============================================================================== + + +class ServiceError(KiramiBotError): + """插件服务异常""" + + +class ServitizationError(ServiceError): + """插件服务化异常""" + + +# ============================================================================== + + +class StoreError(KiramiBotError): + """存储异常""" + + +class DatabaseError(StoreError): + """数据库异常""" + + +# ============================================================================== + + +class PermissionError(KiramiBotError): + """权限异常""" + + +class UnauthorizedError(PermissionError): + """无权访问""" diff --git a/kirami/hook.py b/kirami/hook.py new file mode 100644 index 0000000..32d7378 --- /dev/null +++ b/kirami/hook.py @@ -0,0 +1,177 @@ +"""本模块提供了生命周期钩子,用于在特定时机执行函数""" + +from collections import defaultdict +from collections.abc import Callable +from functools import wraps +from typing import Any, ParamSpec, TypeVar, cast + +from nonebot import get_driver +from nonebot.adapters import Bot +from nonebot.message import ( + event_postprocessor, + event_preprocessor, + run_postprocessor, + run_preprocessor, +) +from nonebot.typing import ( + T_BotConnectionHook, + T_BotDisconnectionHook, + T_CalledAPIHook, + T_CallingAPIHook, + T_EventPostProcessor, + T_EventPreProcessor, + T_RunPostProcessor, + T_RunPreProcessor, +) + +R = TypeVar("R") + +P = ParamSpec("P") + +AnyCallable = Callable[..., Any] + +_backlog_hooks = defaultdict(list) + +_hook_installed = False + + +def backlog_hook(hook: Callable[P, R]) -> Callable[P, R]: + """在初始化之前暂存钩子""" + + @wraps(hook) + def wrapper(*args: P.args, **kwargs: P.kwargs) -> R: + if _hook_installed: + return hook(*args, **kwargs) + try: + _backlog_hooks[hook].append(args[0]) + return cast(R, args[0]) + except IndexError: + return hook(*args, **kwargs) + + return wrapper + + +def install_hook() -> None: + """将暂存的钩子安装""" + for hook, funcs in _backlog_hooks.items(): + while funcs: + func = funcs.pop(0) + hook(func) + global _hook_installed # noqa: PLW0603 + _hook_installed = True + + +@backlog_hook +def on_startup(func: AnyCallable | None = None, pre: bool = False) -> AnyCallable: + """在 `KramiBot` 启动时有序执行""" + if func is None: + return lambda f: on_startup(f, pre=pre) + if pre: + _backlog_hooks[on_startup.__wrapped__].insert(0, func) + return func + return get_driver().on_startup(func) + + +@backlog_hook +def on_shutdown(func: AnyCallable) -> AnyCallable: + """在 `KramiBot` 停止时有序执行""" + return get_driver().on_shutdown(func) + + +@backlog_hook +def on_connect(func: T_BotConnectionHook) -> T_BotConnectionHook: + """在 bot 成功连接到 `KramiBot` 时执行。 + + 钩子函数参数: + + - bot: (依赖注入) 当前连接上的 Bot 对象 + """ + return get_driver().on_bot_connect(func) + + +@backlog_hook +def on_disconnect(func: T_BotDisconnectionHook) -> T_BotDisconnectionHook: + """在 bot 与 `KramiBot` 连接断开时执行。 + + 钩子函数参数: + + - bot: (依赖注入) 当前连接上的 Bot 对象 + """ + return get_driver().on_bot_disconnect(func) + + +def before_api(func: T_CallingAPIHook) -> T_CallingAPIHook: + """在调用 API 之前执行。 + + 钩子函数参数: + + - bot: 当前 bot 对象 + - api: 调用的 api 名称 + - data: api 调用的参数字典 + """ + return Bot.on_calling_api(func) + + +def after_api(func: T_CalledAPIHook) -> T_CalledAPIHook: + """在调用 API 之后执行。 + + 钩子函数参数: + + - bot: 当前 bot 对象 + - exception: 调用 api 时发生的错误 + - api: 调用的 api 名称 + - data: api 调用的参数字典 + - result: api 调用的返回 + """ + return Bot.on_called_api(func) + + +def before_event(func: T_EventPreProcessor) -> T_EventPreProcessor: + """在分发事件之前执行。 + + 钩子函数参数: + + - bot: (依赖注入) 当前连接上的 Bot 对象 + - event: (依赖注入) 事件对象 + - state: (依赖注入) 会话状态 + """ + return event_preprocessor(func) + + +def after_event(func: T_EventPostProcessor) -> T_EventPostProcessor: + """在分发事件之后执行。 + + 钩子函数参数: + + - bot: (依赖注入) 当前连接上的 Bot 对象 + - event: (依赖注入) 事件对象 + - state: (依赖注入) 会话状态 + """ + return event_postprocessor(func) + + +def before_run(func: T_RunPreProcessor) -> T_RunPreProcessor: + """在事件响应器运行之前执行。 + + 钩子函数参数: + + - bot: (依赖注入) 当前连接上的 Bot 对象 + - event: (依赖注入) 事件对象 + - matcher: (依赖注入) 匹配到的事件响应器 + - state: (依赖注入) 会话状态 + """ + return run_preprocessor(func) + + +def after_run(func: T_RunPostProcessor) -> T_RunPostProcessor: + """在事件响应器运行之后执行。 + + 钩子函数参数: + + - bot: (依赖注入) 当前连接上的 Bot 对象 + - event: (依赖注入) 事件对象 + - matcher: (依赖注入) 匹配到的事件响应器 + - state: (依赖注入) 会话状态 + - exception: (依赖注入) 事件响应器运行中产生的异常 + """ + return run_postprocessor(func) diff --git a/kirami/log.py b/kirami/log.py new file mode 100644 index 0000000..50a18ad --- /dev/null +++ b/kirami/log.py @@ -0,0 +1,253 @@ +"""本模块调整了日志的显示效果,并将其保存到本地文件""" + +import builtins +import logging +import re +from collections.abc import Callable +from datetime import datetime +from functools import wraps +from typing import TYPE_CHECKING, Any, ClassVar, Literal, TypeAlias, get_args + +import nonebot +import rich +from loguru import logger +from loguru._file_sink import FileSink +from loguru._handler import Message +from loguru._logger import Core, Logger +from rich.columns import Columns as Columns +from rich.console import Console +from rich.logging import RichHandler +from rich.markdown import Markdown as Markdown +from rich.markup import escape +from rich.panel import Panel as Panel +from rich.progress import Progress as Progress +from rich.table import Table as Table +from rich.text import Text as Text +from rich.theme import Theme +from rich.traceback import install + +from kirami.config import LOG_DIR, bot_config, log_config + +if TYPE_CHECKING: + from loguru import Record + +# ruff: noqa: ANN001 + + +LevelName: TypeAlias = Literal[ + "TRACE", "DEBUG", "INFO", "SUCCESS", "WARNING", "ERROR", "CRITICAL" +] + +suppress = () if bot_config.debug else (nonebot,) + +install(width=None, suppress=suppress, show_locals=bot_config.debug) + +builtins.print = rich.print + +for lv in Core().levels.values(): + logging.addLevelName(lv.no, lv.name) + + +color_dict = { + "k": "black", + "e": "blue", + "c": "cyan", + "g": "green", + "m": "magenta", + "r": "red", + "w": "white", + "y": "yellow", +} + +style_dict = { + "b": "bold", + "d": "dim", + "n": "normal", + "i": "italic", + "u": "underline", + "s": "strike", + "v": "reverse", + "l": "blink", + "h": "hide", +} + + +def tag_convert(match: re.Match[str]) -> str: + """loguru 标签转写 rich 标签""" + + def get_color(color: str) -> str: + if color.isnumeric(): + return f"color({color})" + return f"rgb({color})" if "," in color else color + + markup: str = match[0] + tag: str = match[1] + is_forecolor = tag.islower() + tag = tag.lower() + is_brightcolor = tag.startswith("light") + tag = tag.removeprefix("light-") + + if len(tag) == 2: # noqa: PLR2004 + tag = tag[:-1] + + match tag.split(" "): + case [abbr] if abbr in color_dict: + color = color_dict.get(abbr, "") + if is_brightcolor: + color = f"bright_{color}" + _type = color if is_forecolor else f"on {color}" + case [abbr] if abbr in style_dict: + _type = style_dict.get(abbr, "") + case ["fg", color]: + _type = get_color(color) + case ["bg", color]: + _type = f"on {get_color(color)}" + case _: + _type = tag + + return markup.replace("<", "[").replace(">", "]").replace(match[1], _type) + + +def handle_log(func) -> Callable[..., None]: + """将 loguru 的样式标记转换为 rich 的样式标记""" + + @wraps(func) + def wrapper( + self, + level, + from_decorator, + options, + message, + args, + kwargs, + ) -> None: + (exception, depth, record, lazy, colors, *_, extra) = options + if colors: + extra["colorize"] = True + message = re.compile(r"(?\s]*)>").sub( + tag_convert, message + ) + else: + message = escape(message) + options = (exception, depth + 1, record, lazy, False, *_, extra) + return func( + self, + level, + from_decorator, + options, + message, + args, + kwargs, + ) + + return wrapper + + +Logger._log = handle_log(Logger._log) + + +def handle_write(func) -> Callable[..., None]: + """清洗日志中的 rich 样式标记""" + + @wraps(func) + def wrapper(self, message) -> None: + record = message.record + extra = record.get("extra", {}) + if extra.get("colorize"): + message = Message(re.sub(r"(\\*)(\[[a-z#/@][^[]*?])", "", message)) + message.record = record + return func(self, message) + + return wrapper + + +FileSink.write = handle_write(FileSink.write) + + +custom_theme = Theme( + { + "log.time": "cyan", + "logging.level.debug": "blue", + "logging.level.info": "", + "logging.level.warning": "yellow", + "logging.level.success": "bright_green", + "logging.level.trace": "bright_black", + }, +) + +console = Console(theme=custom_theme) + +handler = RichHandler( + console=console, + show_path=False, + omit_repeated_times=False, + markup=True, + rich_tracebacks=True, + tracebacks_show_locals=bot_config.debug, + tracebacks_suppress=suppress, + log_time_format="%m-%d %H:%M:%S", +) + + +class LogFilter: + level: ClassVar[LevelName | int] = ( + "DEBUG" if bot_config.debug else bot_config.log_level + ) + + def __call__(self, record: "Record") -> bool: + level = record["extra"].get("filter_level") or self.level + levelno = level if isinstance(level, int) else logger.level(level).no + return record["level"].no >= levelno + + +LOG_CONFIG = { + "rotation": "00:00", + "enqueue": True, + "encoding": "utf-8", + "retention": f"{log_config.log_expire_timeout} days", +} + + +def file_handler(levels: LevelName | tuple[LevelName]) -> list[dict[str, Any]]: + if not isinstance(levels, tuple): + level_names = get_args(LevelName) + minimum = level_names.index(levels) + levels = level_names[minimum:] + return [ + { + "sink": LOG_DIR + / level.lower() + / f"{level.lower()}-{datetime.now().date()}.log", + "level": level, + **LOG_CONFIG, + } + for level in levels + ] + + +logger.remove() +logger.configure( + handlers=[ + { + "sink": handler, + "level": 0, + "colorize": False, + "diagnose": False, + "backtrace": True, + "filter": LogFilter(), + "format": lambda _: "[light_slate_blue bold][link={file.path}]{name}[/][/] [dim]|[/] {message}", + }, + *file_handler(bot_config.log_file), + ] +) + + +def new_logger(name: str, *, filter_level: LevelName | int | None = None) -> Logger: + """创建新的日志记录器。 + + ### 参数 + name: 日志名称 + + filter_level: 过滤等级,当日志等级大于过滤等级时才会显示 + """ + return logger.patch(lambda record: record.update(name=name)).bind(filter_level=filter_level) # type: ignore diff --git a/kirami/matcher.py b/kirami/matcher.py new file mode 100644 index 0000000..e6bb54f --- /dev/null +++ b/kirami/matcher.py @@ -0,0 +1,1407 @@ +"""本模块对原生事件响应器进行定制,并提供一些常用事件响应器的注册工具""" + +import asyncio +import re +import time +from datetime import datetime, timedelta +from typing import TYPE_CHECKING, Any, Literal, NoReturn + +from nonebot import get_bots +from nonebot.adapters import Bot, Event +from nonebot.dependencies import Dependent +from nonebot.matcher import Matcher as BaseMatcher +from nonebot.matcher import current_bot +from nonebot.message import handle_event +from nonebot.permission import Permission +from nonebot.plugin import CommandGroup as BaseCommandGroup +from nonebot.plugin import MatcherGroup as BaseMatcherGroup +from nonebot.plugin.on import get_matcher_module, get_matcher_plugin, store_matcher +from nonebot.rule import ( + ArgumentParser, + Rule, + ToMeRule, + command, + endswith, + fullmatch, + is_type, + keyword, + regex, + shell_command, + startswith, +) +from nonebot.typing import ( + T_Handler, + T_PermissionChecker, + T_RuleChecker, +) + +from kirami.database import Argot +from kirami.event import TimerNoticeEvent +from kirami.message import ( + Message, + MessageSegment, + MessageTemplate, +) +from kirami.rule import ArgotRule, ReplyRule, TimerRule, prefix, suffix +from kirami.state import State +from kirami.utils import scheduler + +_param_rules = { + "to_me": ToMeRule, + "reply": ReplyRule, + "argot": ArgotRule, +} + + +def _extend_rule(rule: Rule | T_RuleChecker | None, **kwargs) -> Rule: + rule &= Rule() + for key in _param_rules: + if kwargs.get(key, None): + rule &= Rule(_param_rules[key]()) + return rule + + +class Matcher(BaseMatcher): + @classmethod + def destroy(cls) -> None: + for checker in cls.rule.checkers: + if isinstance(checker.call, TimerRule): + scheduler.remove_job(checker.call.timer_id) + super().destroy() + + @classmethod + async def send( + cls, + message: str | Message | MessageSegment | MessageTemplate, + **kwargs: Any, + ) -> Any: + """发送一条消息给当前交互用户。 + + ### 参数 + message: 消息内容 + + at_sender: at 发送者 + + reply_message: 回复原消息 + + recall_time: 指定时间后撤回消息 + + argot_content: 消息暗语内容 + + **kwargs: 用于传递给 API 的参数 + """ + info = await super().send(message, **kwargs) + if argot := kwargs.pop("argot_content", None): + await Argot(msg_id=info["message_id"], content=argot).save() + if recall_time := kwargs.pop("recall_time", None): + bot = current_bot.get() + loop = asyncio.get_running_loop() + loop.call_later( + recall_time, + lambda: loop.create_task(bot.delete_msg(message_id=info["message_id"])), + ) + return info + + @classmethod + async def finish( + cls, + message: str | Message | MessageSegment | MessageTemplate | None = None, + **kwargs, + ) -> NoReturn: + """发送一条消息给当前交互用户并结束当前事件响应器。 + + ### 参数 + message: 消息内容 + + at_sender: at 发送者 + + reply_message: 回复原消息 + + recall_time: 指定时间后撤回消息 + + argot_content: 消息暗语内容 + + **kwargs: 用于传递给 API 的参数 + """ + await super().finish(message, **kwargs) + + +class MatcherCase: + """事件响应器容器类,用于包装事件响应器,以使其支持简写形式""" + + def __init__(self, matcher: type[Matcher]) -> None: + """将 `Matcher` 包装为 `MatcherCase`""" + self.matcher = matcher + """被包装的事件响应器""" + + def __call__(self, func: T_Handler | None = None) -> T_Handler | Matcher: + if func: + self.matcher.append_handler(func) + return func + return self.matcher() + + def __getattr__(self, name: str) -> Any: + return getattr(self.matcher, name) + + +def on( + type: str = "", + /, + *, + rule: Rule | T_RuleChecker | None = None, + permission: Permission | T_PermissionChecker | None = None, + handlers: list[T_Handler | Dependent] | None = None, + temp: bool = False, + expire_time: datetime | timedelta | None = None, + priority: int = 1, + block: bool = False, + state: State | None = None, + _depth: int = 0, +) -> MatcherCase: + """注册一个基础事件响应器,可自定义类型。 + + ### 参数 + type: 事件响应器类型 + + rule: 事件响应规则 + + permission: 事件响应权限 + + handlers: 事件处理函数列表 + + temp: 是否为临时事件响应器(仅执行一次) + + expire_time: 事件响应器最终有效时间点,过期即被删除 + + priority: 事件响应器优先级 + + block: 是否阻止事件向更低优先级传递 + + state: 默认会话状态 + """ + matcher = Matcher.new( + type, + Rule() & rule, + Permission() | permission, + temp=temp, + expire_time=expire_time, + priority=priority, + block=block, + handlers=handlers, + plugin=get_matcher_plugin(_depth + 1), + module=get_matcher_module(_depth + 1), + default_state=state, + ) + if not state: + matcher._default_state = State() + store_matcher(matcher) + return MatcherCase(matcher) + + +def on_type( + *types: type[Event], + rule: Rule | T_RuleChecker | None = None, + _depth: int = 0, + **kwargs, +) -> MatcherCase: + """注册一个事件响应器,并且当事件为指定类型时响应。 + + ### 参数 + *types: 事件类型 + + rule: 事件响应规则 + + permission: 事件响应权限 + + handlers: 事件处理函数列表 + + temp: 是否为临时事件响应器(仅执行一次) + + expire_time: 事件响应器最终有效时间点,过期即被删除 + + priority: 事件响应器优先级 + + block: 是否阻止事件向更低优先级传递 + + state: 默认会话状态 + """ + return on(rule=is_type(*types) & rule, **kwargs, _depth=_depth + 1) + + +def on_metaevent(*, _depth: int = 0, **kwargs) -> MatcherCase: + """注册一个元事件响应器。 + + ### 参数 + rule: 事件响应规则 + + handlers: 事件处理函数列表 + + temp: 是否为临时事件响应器(仅执行一次) + + expire_time: 事件响应器最终有效时间点,过期即被删除 + + priority: 事件响应器优先级 + + block: 是否阻止事件向更低优先级传递 + + state: 默认会话状态 + """ + return on("meta_event", **kwargs, _depth=_depth + 1) + + +def on_message( + *, rule: Rule | T_RuleChecker | None = None, _depth: int = 0, **kwargs +) -> MatcherCase: + """注册一个消息事件响应器。 + + ### 参数 + rule: 事件响应规则 + + permission: 事件响应权限 + + handlers: 事件处理函数列表 + + temp: 是否为临时事件响应器(仅执行一次) + + expire_time: 事件响应器最终有效时间点,过期即被删除 + + priority: 事件响应器优先级 + + block: 是否阻止事件向更低优先级传递 + + state: 默认会话状态 + + to_me: 是否仅响应与自身有关的消息 + + reply: 是否仅响应回复消息 + + argot: 是否仅响应暗语消息,当 `argot` 为 `True` 时,`priority` 会被强制设置为 `0` + """ + if "argot" in kwargs: + kwargs["priority"] = 0 + return on( + "message", + rule=_extend_rule(rule, **kwargs), + **{k: v for k, v in kwargs.items() if k not in _param_rules}, + _depth=_depth + 1, + ) + + +def on_notice(*, _depth: int = 0, **kwargs) -> MatcherCase: + """注册一个通知事件响应器。 + + ### 参数 + rule: 事件响应规则 + + handlers: 事件处理函数列表 + + temp: 是否为临时事件响应器(仅执行一次) + + expire_time: 事件响应器最终有效时间点,过期即被删除 + + priority: 事件响应器优先级 + + block: 是否阻止事件向更低优先级传递 + + state: 默认会话状态 + """ + return on("notice", **kwargs, _depth=_depth + 1) + + +def on_request(*, _depth: int = 0, **kwargs) -> MatcherCase: + """注册一个请求事件响应器。 + + ### 参数 + rule: 事件响应规则 + + handlers: 事件处理函数列表 + + temp: 是否为临时事件响应器(仅执行一次) + + expire_time: 事件响应器最终有效时间点,过期即被删除 + + priority: 事件响应器优先级 + + block: 是否阻止事件向更低优先级传递 + + state: 默认会话状态 + """ + return on("request", **kwargs, _depth=_depth + 1) + + +def on_command( + *cmds: str, + force_whitespace: str | bool | None = None, + rule: Rule | T_RuleChecker | None = None, + _depth: int = 0, + **kwargs, +) -> MatcherCase: + """注册一个消息事件响应器,并且当消息以指定命令开头时响应。 + + ### 参数 + *cmds: 指定命令内容 + + force_whitespace: 是否强制命令后必须有指定空白符 + + rule: 事件响应规则 + + permission: 事件响应权限 + + handlers: 事件处理函数列表 + + temp: 是否为临时事件响应器(仅执行一次) + + expire_time: 事件响应器最终有效时间点,过期即被删除 + + priority: 事件响应器优先级 + + block: 是否阻止事件向更低优先级传递 + + state: 默认会话状态 + + to_me: 是否仅响应与自身有关的消息 + + reply: 是否仅响应回复消息 + + argot: 是否仅响应暗语消息,当 `argot` 为 `True` 时,`priority` 会被强制设置为 `0` + """ + kwargs.setdefault("block", True) + return on_message( + rule=command(*cmds, force_whitespace=force_whitespace) & rule, + **kwargs, + _depth=_depth + 1, + ) + + +def on_shell_command( + *cmds: str, + parser: ArgumentParser | None = None, + rule: Rule | T_RuleChecker | None = None, + _depth: int = 0, + **kwargs, +) -> MatcherCase: + """注册一个支持 `shell_like` 解析参数的命令消息事件响应器。 + + 与普通的 `on_command` 不同的是,在添加 `parser` 参数时, 响应器会自动处理消息。 + + 并将用户输入的原始参数列表保存在 `state.shell_argv`, `parser` 处理的参数保存在 `state.shell_args` 中 + + ### 参数 + *cmds: 指定命令内容 + + parser: `kirami.rule.ArgumentParser` 对象 + + rule: 事件响应规则 + + permission: 事件响应权限 + + handlers: 事件处理函数列表 + + temp: 是否为临时事件响应器(仅执行一次) + + expire_time: 事件响应器最终有效时间点,过期即被删除 + + priority: 事件响应器优先级 + + block: 是否阻止事件向更低优先级传递 + + state: 默认会话状态 + + to_me: 是否仅响应与自身有关的消息 + + reply: 是否仅响应回复消息 + + argot: 是否仅响应暗语消息,当 `argot` 为 `True` 时,`priority` 会被强制设置为 `0` + """ + kwargs.setdefault("block", True) + return on_message( + rule=shell_command(*cmds, parser=parser) & rule, + **kwargs, + _depth=_depth + 1, + ) + + +def on_startswith( + *msgs: str, + ignorecase: bool = False, + rule: Rule | T_RuleChecker | None = None, + _depth: int = 0, + **kwargs, +) -> MatcherCase: + """注册一个消息事件响应器,并且当消息的**文本部分**以指定内容开头时响应。 + + ### 参数 + *msgs: 指定消息开头内容 + + ignorecase: 是否忽略大小写 + + rule: 事件响应规则 + + permission: 事件响应权限 + + handlers: 事件处理函数列表 + + temp: 是否为临时事件响应器(仅执行一次) + + expire_time: 事件响应器最终有效时间点,过期即被删除 + + priority: 事件响应器优先级 + + block: 是否阻止事件向更低优先级传递 + + state: 默认会话状态 + + to_me: 是否仅响应与自身有关的消息 + + reply: 是否仅响应回复消息 + + argot: 是否仅响应暗语消息,当 `argot` 为 `True` 时,`priority` 会被强制设置为 `0` + """ + kwargs.setdefault("block", True) + return on_message( + rule=startswith(msgs, ignorecase) & rule, **kwargs, _depth=_depth + 1 + ) + + +def on_endswith( + *msgs: str, + ignorecase: bool = False, + rule: Rule | T_RuleChecker | None = None, + _depth: int = 0, + **kwargs, +) -> MatcherCase: + """注册一个消息事件响应器,并且当消息的**文本部分**以指定内容结尾时响应。 + + ### 参数 + *msgs: 指定消息结尾内容 + + ignorecase: 是否忽略大小写 + + rule: 事件响应规则 + + permission: 事件响应权限 + + handlers: 事件处理函数列表 + + temp: 是否为临时事件响应器(仅执行一次) + + expire_time: 事件响应器最终有效时间点,过期即被删除 + + priority: 事件响应器优先级 + + block: 是否阻止事件向更低优先级传递 + + state: 默认会话状态 + + to_me: 是否仅响应与自身有关的消息 + + reply: 是否仅响应回复消息 + + argot: 是否仅响应暗语消息,当 `argot` 为 `True` 时,`priority` 会被强制设置为 `0` + """ + kwargs.setdefault("block", True) + return on_message( + rule=endswith(msgs, ignorecase) & rule, **kwargs, _depth=_depth + 1 + ) + + +def on_fullmatch( + *msgs: str, + ignorecase: bool = False, + rule: Rule | T_RuleChecker | None = None, + _depth: int = 0, + **kwargs, +) -> MatcherCase: + """注册一个消息事件响应器,并且当消息的**文本部分**与指定内容完全一致时响应。 + + ### 参数 + *msgs: 指定消息全匹配内容 + + ignorecase: 是否忽略大小写 + + rule: 事件响应规则 + + permission: 事件响应权限 + + handlers: 事件处理函数列表 + + temp: 是否为临时事件响应器(仅执行一次) + + expire_time: 事件响应器最终有效时间点,过期即被删除 + + priority: 事件响应器优先级 + + block: 是否阻止事件向更低优先级传递 + + state: 默认会话状态 + + to_me: 是否仅响应与自身有关的消息 + + reply: 是否仅响应回复消息 + + argot: 是否仅响应暗语消息,当 `argot` 为 `True` 时,`priority` 会被强制设置为 `0` + """ + kwargs.setdefault("block", True) + return on_message( + rule=fullmatch(msgs, ignorecase) & rule, **kwargs, _depth=_depth + 1 + ) + + +def on_keyword( + *keywords: str, + rule: Rule | T_RuleChecker | None = None, + _depth: int = 0, + **kwargs, +) -> MatcherCase: + """注册一个消息事件响应器,并且当消息纯文本部分包含关键词时响应。 + + ### 参数 + *keywords: 包含关键词 + + rule: 事件响应规则 + + permission: 事件响应权限 + + handlers: 事件处理函数列表 + + temp: 是否为临时事件响应器(仅执行一次) + + expire_time: 事件响应器最终有效时间点,过期即被删除 + + priority: 事件响应器优先级 + + block: 是否阻止事件向更低优先级传递 + + state: 默认会话状态 + + to_me: 是否仅响应与自身有关的消息 + + reply: 是否仅响应回复消息 + + argot: 是否仅响应暗语消息,当 `argot` 为 `True` 时,`priority` 会被强制设置为 `0` + """ + kwargs.setdefault("block", True) + return on_message(rule=keyword(*keywords) & rule, **kwargs, _depth=_depth + 1) + + +def on_regex( + pattern: str, + /, + *, + flags: int | re.RegexFlag = 0, + rule: Rule | T_RuleChecker | None = None, + _depth: int = 0, + **kwargs, +) -> MatcherCase: + """注册一个消息事件响应器,并且当消息匹配正则表达式时响应。 + + ### 参数 + pattern: 正则表达式 + + flags: 正则匹配标志 + + rule: 事件响应规则 + + permission: 事件响应权限 + + handlers: 事件处理函数列表 + + temp: 是否为临时事件响应器(仅执行一次) + + expire_time: 事件响应器最终有效时间点,过期即被删除 + + priority: 事件响应器优先级 + + block: 是否阻止事件向更低优先级传递 + + state: 默认会话状态 + + to_me: 是否仅响应与自身有关的消息 + + reply: 是否仅响应回复消息 + + argot: 是否仅响应暗语消息,当 `argot` 为 `True` 时,`priority` 会被强制设置为 `0` + """ + kwargs.setdefault("block", True) + return on_message(rule=regex(pattern, flags) & rule, **kwargs, _depth=_depth + 1) + + +def on_prefix( + *msgs: str, + ignorecase: bool = False, + rule: Rule | T_RuleChecker | None = None, + _depth: int = 0, + **kwargs, +) -> MatcherCase: + """注册一个消息事件响应器,并且当消息的**文本部分**以指定内容开头时响应。 + + 与普通的 `on_startswith` 不同的是,`on_prefix` 会将前缀从消息中去除。 + + ### 参数 + *msgs: 指定消息开头内容 + + ignorecase: 是否忽略大小写 + + rule: 事件响应规则 + + permission: 事件响应权限 + + handlers: 事件处理函数列表 + + temp: 是否为临时事件响应器(仅执行一次) + + expire_time: 事件响应器最终有效时间点,过期即被删除 + + priority: 事件响应器优先级 + + block: 是否阻止事件向更低优先级传递 + + state: 默认会话状态 + + to_me: 是否仅响应与自身有关的消息 + + reply: 是否仅响应回复消息 + + argot: 是否仅响应暗语消息,当 `argot` 为 `True` 时,`priority` 会被强制设置为 `0` + """ + kwargs.setdefault("block", True) + return on_message(rule=prefix(msgs, ignorecase) & rule, **kwargs, _depth=_depth + 1) + + +def on_suffix( + *msgs: str, + ignorecase: bool = False, + rule: Rule | T_RuleChecker | None = None, + _depth: int = 0, + **kwargs, +) -> MatcherCase: + """注册一个消息事件响应器,并且当消息的**文本部分**以指定内容结尾时响应。 + + 与普通的 `on_endswith` 不同的是,`on_suffix` 会将后缀从消息中去除。 + + ### 参数 + *msgs: 指定消息结尾内容 + + ignorecase: 是否忽略大小写 + + rule: 事件响应规则 + + permission: 事件响应权限 + + handlers: 事件处理函数列表 + + temp: 是否为临时事件响应器(仅执行一次) + + expire_time: 事件响应器最终有效时间点,过期即被删除 + + priority: 事件响应器优先级 + + block: 是否阻止事件向更低优先级传递 + + state: 默认会话状态 + + to_me: 是否仅响应与自身有关的消息 + + reply: 是否仅响应回复消息 + + argot: 是否仅响应暗语消息,当 `argot` 为 `True` 时,`priority` 会被强制设置为 `0` + """ + kwargs.setdefault("block", True) + return on_message(rule=suffix(msgs, ignorecase) & rule, **kwargs, _depth=_depth + 1) + + +def on_time( + trigger: Literal["cron", "interval", "date"], + /, + *, + rule: Rule | T_RuleChecker | None = None, + handlers: list[T_Handler | Dependent] | None = None, + temp: bool = False, + expire_time: datetime | timedelta | None = None, + priority: int = 1, + block: bool = False, + state: State | None = None, + _depth: int = 0, + **kwargs: Any, +) -> MatcherCase: + """注册一个通知事件响应器,并且当时间到达指定时间时响应。 + + ### 参数 + trigger: 触发器类型,可选值为 `cron`、`interval`、`date` + + rule: 事件响应规则 + + handlers: 事件处理函数列表 + + temp: 是否为临时事件响应器(仅执行一次) + + expire_time: 事件响应器最终有效时间点,过期即被删除 + + priority: 事件响应器优先级 + + block: 是否阻止事件向更低优先级传递 + + state: 默认会话状态 + + **kwargs: 定时触发器参数 + """ + matcher = on_notice( + rule=rule, + handlers=handlers, + temp=temp, + expire_time=expire_time, + priority=priority, + block=block, + state=state, + _depth=_depth + 1, + ) + + timer_id = str(hash(matcher)) + + matcher.rule &= Rule(TimerRule(timer_id, {"trigger": trigger} | kwargs)) # type: ignore + + async def timer_notice() -> None: + async def circular(bot: Bot) -> None: + timer_event = TimerNoticeEvent( + time=int(time.time()), + self_id=int(bot.self_id), + post_type="notice", + notice_type="timer", + timer_id=timer_id, + ) + await handle_event(bot, timer_event) + + tasks = [circular(bot) for bot in get_bots().values()] + await asyncio.gather(*tasks) + + scheduler.add_job(timer_notice, trigger=trigger, id=timer_id, **kwargs) + + return matcher + + +class CommandGroup(BaseCommandGroup): + """命令组,用于声明一组有相同名称前缀的命令。 + + ### 参数 + *cmds: 指定命令内容 + + prefix_aliases: 是否影响命令别名,给命令别名加前缀 + + rule: 事件响应规则 + + permission: 事件响应权限 + + handlers: 事件处理函数列表 + + temp: 是否为临时事件响应器(仅执行一次) + + expire_time: 事件响应器最终有效时间点,过期即被删除 + + priority: 事件响应器优先级 + + block: 是否阻止事件向更低优先级传递 + + state: 默认会话状态 + + to_me: 是否仅响应与自身有关的消息 + + reply: 是否仅响应回复消息 + + argot: 是否仅响应暗语消息,当 `argot` 为 `True` 时,`priority` 会被强制设置为 `0` + """ + + if TYPE_CHECKING: + matchers: list[MatcherCase] + + def __init__(self, *cmds: str, **kwargs) -> None: + super().__init__(cmds, **kwargs) + + def command(self, *cmds: str, **kwargs) -> MatcherCase: + """注册一个新的命令。 + + 新参数将会覆盖命令组默认值。 + + ### 参数 + *cmds: 指定命令内容 + + force_whitespace: 是否强制命令后必须有指定空白符 + + rule: 事件响应规则 + + permission: 事件响应权限 + + handlers: 事件处理函数列表 + + temp: 是否为临时事件响应器(仅执行一次) + + expire_time: 事件响应器最终有效时间点,过期即被删除 + + priority: 事件响应器优先级 + + block: 是否阻止事件向更低优先级传递 + + state: 默认会话状态 + + to_me: 是否仅响应与自身有关的消息 + + reply: 是否仅响应回复消息 + + argot: 是否仅响应暗语消息,当 `argot` 为 `True` 时,`priority` 会被强制设置为 `0` + """ + commands = self.basecmd + cmds + if self.prefix_aliases and (aliases := kwargs.get("aliases")): + kwargs["aliases"] = { + self.basecmd + ((alias,) if isinstance(alias, str) else alias) + for alias in aliases + } + matcher = on_command(*commands, **self._get_final_kwargs(kwargs)) + self.matchers.append(matcher) + return matcher + + def shell_command(self, *cmds: str, **kwargs) -> MatcherCase: + """注册一个新的 `shell_like` 命令。新参数将会覆盖命令组默认值。 + + ### 参数 + *cmds: 指定命令内容 + + parser: `kirami.rule.ArgumentParser` 对象 + + rule: 事件响应规则 + + permission: 事件响应权限 + + handlers: 事件处理函数列表 + + temp: 是否为临时事件响应器(仅执行一次) + + expire_time: 事件响应器最终有效时间点,过期即被删除 + + priority: 事件响应器优先级 + + block: 是否阻止事件向更低优先级传递 + + state: 默认会话状态 + + to_me: 是否仅响应与自身有关的消息 + + reply: 是否仅响应回复消息 + + argot: 是否仅响应暗语消息,当 `argot` 为 `True` 时,`priority` 会被强制设置为 `0` + """ + commands = self.basecmd + cmds + if self.prefix_aliases and (aliases := kwargs.get("aliases")): + kwargs["aliases"] = { + self.basecmd + ((alias,) if isinstance(alias, str) else alias) + for alias in aliases + } + matcher = on_shell_command(*commands, **self._get_final_kwargs(kwargs)) + self.matchers.append(matcher) + return matcher + + +class MatcherGroup(BaseMatcherGroup): + if TYPE_CHECKING: + matchers: list[MatcherCase] + + def on(self, **kwargs) -> MatcherCase: + """注册一个基础事件响应器,可自定义类型。 + + ### 参数 + type: 事件响应器类型 + + rule: 事件响应规则 + + permission: 事件响应权限 + + handlers: 事件处理函数列表 + + temp: 是否为临时事件响应器(仅执行一次) + + expire_time: 事件响应器最终有效时间点,过期即被删除 + + priority: 事件响应器优先级 + + block: 是否阻止事件向更低优先级传递 + + state: 默认会话状态 + """ + matcher = on(**self._get_final_kwargs(kwargs)) + self.matchers.append(matcher) + return matcher + + def on_type(self, *types: type[Event], **kwargs) -> MatcherCase: + """注册一个事件响应器,并且当事件为指定类型时响应。 + + ### 参数 + *types: 事件类型 + + rule: 事件响应规则 + + permission: 事件响应权限 + + handlers: 事件处理函数列表 + + temp: 是否为临时事件响应器(仅执行一次) + + expire_time: 事件响应器最终有效时间点,过期即被删除 + + priority: 事件响应器优先级 + + block: 是否阻止事件向更低优先级传递 + + state: 默认会话状态 + """ + final_kwargs = self._get_final_kwargs(kwargs, exclude={"type"}) + matcher = on_type(*types, **final_kwargs) + self.matchers.append(matcher) + return matcher + + def on_metaevent(self, **kwargs) -> MatcherCase: + """注册一个元事件响应器。 + + ### 参数 + rule: 事件响应规则 + + handlers: 事件处理函数列表 + + temp: 是否为临时事件响应器(仅执行一次) + + expire_time: 事件响应器最终有效时间点,过期即被删除 + + priority: 事件响应器优先级 + + block: 是否阻止事件向更低优先级传递 + + state: 默认会话状态 + """ + final_kwargs = self._get_final_kwargs(kwargs, exclude={"type", "permission"}) + matcher = on_metaevent(**final_kwargs) + self.matchers.append(matcher) + return matcher + + def on_message(self, **kwargs) -> MatcherCase: + """注册一个消息事件响应器。 + + ### 参数 + rule: 事件响应规则 + + permission: 事件响应权限 + + handlers: 事件处理函数列表 + + temp: 是否为临时事件响应器(仅执行一次) + + expire_time: 事件响应器最终有效时间点,过期即被删除 + + priority: 事件响应器优先级 + + block: 是否阻止事件向更低优先级传递 + + state: 默认会话状态 + + to_me: 是否仅响应与自身有关的消息 + + reply: 是否仅响应回复消息 + + argot: 是否仅响应暗语消息,当 `argot` 为 `True` 时,`priority` 会被强制设置为 `0` + """ + final_kwargs = self._get_final_kwargs(kwargs, exclude={"type"}) + matcher = on_message(**final_kwargs) + self.matchers.append(matcher) + return matcher + + def on_notice(self, **kwargs) -> MatcherCase: + """注册一个通知事件响应器。 + + ### 参数 + rule: 事件响应规则 + + handlers: 事件处理函数列表 + + temp: 是否为临时事件响应器(仅执行一次) + + expire_time: 事件响应器最终有效时间点,过期即被删除 + + priority: 事件响应器优先级 + + block: 是否阻止事件向更低优先级传递 + + state: 默认会话状态 + """ + final_kwargs = self._get_final_kwargs(kwargs, exclude={"type", "permission"}) + matcher = on_notice(**final_kwargs) + self.matchers.append(matcher) + return matcher + + def on_request(self, **kwargs) -> MatcherCase: + """注册一个请求事件响应器。 + + ### 参数 + rule: 事件响应规则 + + handlers: 事件处理函数列表 + + temp: 是否为临时事件响应器(仅执行一次) + + expire_time: 事件响应器最终有效时间点,过期即被删除 + + priority: 事件响应器优先级 + + block: 是否阻止事件向更低优先级传递 + + state: 默认会话状态 + """ + final_kwargs = self._get_final_kwargs(kwargs, exclude={"type", "permission"}) + matcher = on_request(**final_kwargs) + self.matchers.append(matcher) + return matcher + + def on_command(self, *cmds: str, **kwargs) -> MatcherCase: + """注册一个消息事件响应器,并且当消息以指定命令开头时响应。 + + ### 参数 + *cmds: 指定命令内容 + + force_whitespace: 是否强制命令后必须有指定空白符 + + rule: 事件响应规则 + + permission: 事件响应权限 + + handlers: 事件处理函数列表 + + temp: 是否为临时事件响应器(仅执行一次) + + expire_time: 事件响应器最终有效时间点,过期即被删除 + + priority: 事件响应器优先级 + + block: 是否阻止事件向更低优先级传递 + + state: 默认会话状态 + + to_me: 是否仅响应与自身有关的消息 + + reply: 是否仅响应回复消息 + + argot: 是否仅响应暗语消息,当 `argot` 为 `True` 时,`priority` 会被强制设置为 `0` + """ + final_kwargs = self._get_final_kwargs(kwargs, exclude={"type"}) + matcher = on_command(*cmds, **final_kwargs) + self.matchers.append(matcher) + return matcher + + def on_shell_command(self, *cmds: str, **kwargs) -> MatcherCase: + """注册一个支持 `shell_like` 解析参数的命令消息事件响应器。 + + 与普通的 `on_command` 不同的是,在添加 `parser` 参数时, 响应器会自动处理消息。 + + 并将用户输入的原始参数列表保存在 `state.shell_argv`, `parser` 处理的参数保存在 `state.shell_args` 中 + + ### 参数 + *cmds: 指定命令内容 + + parser: `kirami.rule.ArgumentParser` 对象 + + rule: 事件响应规则 + + permission: 事件响应权限 + + handlers: 事件处理函数列表 + + temp: 是否为临时事件响应器(仅执行一次) + + expire_time: 事件响应器最终有效时间点,过期即被删除 + + priority: 事件响应器优先级 + + block: 是否阻止事件向更低优先级传递 + + state: 默认会话状态 + + to_me: 是否仅响应与自身有关的消息 + + reply: 是否仅响应回复消息 + + argot: 是否仅响应暗语消息,当 `argot` 为 `True` 时,`priority` 会被强制设置为 `0` + """ + final_kwargs = self._get_final_kwargs(kwargs, exclude={"type"}) + matcher = on_shell_command(*cmds, **final_kwargs) + self.matchers.append(matcher) + return matcher + + def on_startswith(self, *msgs: str, **kwargs) -> MatcherCase: + """注册一个消息事件响应器,并且当消息的**文本部分**以指定内容开头时响应。 + + ### 参数 + *msgs: 指定消息开头内容 + + ignorecase: 是否忽略大小写 + + rule: 事件响应规则 + + permission: 事件响应权限 + + handlers: 事件处理函数列表 + + temp: 是否为临时事件响应器(仅执行一次) + + expire_time: 事件响应器最终有效时间点,过期即被删除 + + priority: 事件响应器优先级 + + block: 是否阻止事件向更低优先级传递 + + state: 默认会话状态 + + to_me: 是否仅响应与自身有关的消息 + + reply: 是否仅响应回复消息 + + argot: 是否仅响应暗语消息,当 `argot` 为 `True` 时,`priority` 会被强制设置为 `0` + """ + final_kwargs = self._get_final_kwargs(kwargs, exclude={"type"}) + matcher = on_startswith(*msgs, **final_kwargs) + self.matchers.append(matcher) + return matcher + + def on_endswith(self, *msgs: str, **kwargs) -> MatcherCase: + """注册一个消息事件响应器,并且当消息的**文本部分**以指定内容结尾时响应。 + + ### 参数 + *msgs: 指定消息结尾内容 + + ignorecase: 是否忽略大小写 + + rule: 事件响应规则 + + permission: 事件响应权限 + + handlers: 事件处理函数列表 + + temp: 是否为临时事件响应器(仅执行一次) + + expire_time: 事件响应器最终有效时间点,过期即被删除 + + priority: 事件响应器优先级 + + block: 是否阻止事件向更低优先级传递 + + state: 默认会话状态 + + to_me: 是否仅响应与自身有关的消息 + + reply: 是否仅响应回复消息 + + argot: 是否仅响应暗语消息,当 `argot` 为 `True` 时,`priority` 会被强制设置为 `0` + """ + final_kwargs = self._get_final_kwargs(kwargs, exclude={"type"}) + matcher = on_endswith(*msgs, **final_kwargs) + self.matchers.append(matcher) + return matcher + + def on_fullmatch(self, *msgs: str, **kwargs) -> MatcherCase: + """注册一个消息事件响应器,并且当消息的**文本部分**与指定内容完全一致时响应。 + + ### 参数 + *msgs: 指定消息全匹配内容 + + ignorecase: 是否忽略大小写 + + rule: 事件响应规则 + + permission: 事件响应权限 + + handlers: 事件处理函数列表 + + temp: 是否为临时事件响应器(仅执行一次) + + expire_time: 事件响应器最终有效时间点,过期即被删除 + + priority: 事件响应器优先级 + + block: 是否阻止事件向更低优先级传递 + + state: 默认会话状态 + + to_me: 是否仅响应与自身有关的消息 + + reply: 是否仅响应回复消息 + + argot: 是否仅响应暗语消息,当 `argot` 为 `True` 时,`priority` 会被强制设置为 `0` + """ + final_kwargs = self._get_final_kwargs(kwargs, exclude={"type"}) + matcher = on_fullmatch(*msgs, **final_kwargs) + self.matchers.append(matcher) + return matcher + + def on_keyword(self, *keywords: str, **kwargs) -> MatcherCase: + """注册一个消息事件响应器,并且当消息纯文本部分包含关键词时响应。 + + ### 参数 + *keywords: 包含关键词 + + rule: 事件响应规则 + + permission: 事件响应权限 + + handlers: 事件处理函数列表 + + temp: 是否为临时事件响应器(仅执行一次) + + expire_time: 事件响应器最终有效时间点,过期即被删除 + + priority: 事件响应器优先级 + + block: 是否阻止事件向更低优先级传递 + + state: 默认会话状态 + + to_me: 是否仅响应与自身有关的消息 + + reply: 是否仅响应回复消息 + + argot: 是否仅响应暗语消息,当 `argot` 为 `True` 时,`priority` 会被强制设置为 `0` + """ + final_kwargs = self._get_final_kwargs(kwargs, exclude={"type"}) + matcher = on_keyword(*keywords, **final_kwargs) + self.matchers.append(matcher) + return matcher + + def on_regex(self, pattern: str, /, **kwargs) -> MatcherCase: + """注册一个消息事件响应器,并且当消息匹配正则表达式时响应。 + + ### 参数 + pattern: 正则表达式 + + flags: 正则匹配标志 + + rule: 事件响应规则 + + permission: 事件响应权限 + + handlers: 事件处理函数列表 + + temp: 是否为临时事件响应器(仅执行一次) + + expire_time: 事件响应器最终有效时间点,过期即被删除 + + priority: 事件响应器优先级 + + block: 是否阻止事件向更低优先级传递 + + state: 默认会话状态 + + to_me: 是否仅响应与自身有关的消息 + + reply: 是否仅响应回复消息 + + argot: 是否仅响应暗语消息,当 `argot` 为 `True` 时,`priority` 会被强制设置为 `0` + """ + final_kwargs = self._get_final_kwargs(kwargs, exclude={"type"}) + matcher = on_regex(pattern, **final_kwargs) + self.matchers.append(matcher) + return matcher + + def on_prefix(self, *msgs: str, **kwargs) -> MatcherCase: + """注册一个消息事件响应器,并且当消息的**文本部分**以指定内容开头时响应。 + + 与普通的 `on_startswith` 不同的是,`on_prefix` 会将前缀从消息中去除。 + + ### 参数 + *msgs: 指定消息开头内容 + + ignorecase: 是否忽略大小写 + + rule: 事件响应规则 + + permission: 事件响应权限 + + handlers: 事件处理函数列表 + + temp: 是否为临时事件响应器(仅执行一次) + + expire_time: 事件响应器最终有效时间点,过期即被删除 + + priority: 事件响应器优先级 + + block: 是否阻止事件向更低优先级传递 + + state: 默认会话状态 + + to_me: 是否仅响应与自身有关的消息 + + reply: 是否仅响应回复消息 + + argot: 是否仅响应暗语消息,当 `argot` 为 `True` 时,`priority` 会被强制设置为 `0` + """ + final_kwargs = self._get_final_kwargs(kwargs, exclude={"type"}) + matcher = on_prefix(*msgs, **final_kwargs) + self.matchers.append(matcher) + return matcher + + def on_suffix(self, *msgs: str, **kwargs) -> MatcherCase: + """注册一个消息事件响应器,并且当消息的**文本部分**以指定内容结尾时响应。 + + 与普通的 `on_endswith` 不同的是,`on_suffix` 会将后缀从消息中去除。 + + ### 参数 + *msgs: 指定消息结尾内容 + + ignorecase: 是否忽略大小写 + + rule: 事件响应规则 + + permission: 事件响应权限 + + handlers: 事件处理函数列表 + + temp: 是否为临时事件响应器(仅执行一次) + + expire_time: 事件响应器最终有效时间点,过期即被删除 + + priority: 事件响应器优先级 + + block: 是否阻止事件向更低优先级传递 + + state: 默认会话状态 + + to_me: 是否仅响应与自身有关的消息 + + reply: 是否仅响应回复消息 + + argot: 是否仅响应暗语消息,当 `argot` 为 `True` 时,`priority` 会被强制设置为 `0` + """ + final_kwargs = self._get_final_kwargs(kwargs, exclude={"type"}) + matcher = on_suffix(*msgs, **final_kwargs) + self.matchers.append(matcher) + return matcher + + def on_time( + self, trigger: Literal["cron", "interval", "date"], **kwargs + ) -> MatcherCase: + """注册一个通知事件响应器,并且当时间到达指定时间时响应。 + + ### 参数 + trigger: 触发器类型,可选值为 `cron`、`interval`、`date` + + rule: 事件响应规则 + + handlers: 事件处理函数列表 + + temp: 是否为临时事件响应器(仅执行一次) + + expire_time: 事件响应器最终有效时间点,过期即被删除 + + priority: 事件响应器优先级 + + block: 是否阻止事件向更低优先级传递 + + state: 默认会话状态 + + **kwargs: 定时触发器参数 + """ + final_kwargs = self._get_final_kwargs(kwargs, exclude={"type"}) + matcher = on_time(trigger, **final_kwargs) + self.matchers.append(matcher) + return matcher diff --git a/kirami/matcher.pyi b/kirami/matcher.pyi new file mode 100644 index 0000000..98bdf4f --- /dev/null +++ b/kirami/matcher.pyi @@ -0,0 +1,980 @@ +import re +from datetime import datetime, timedelta, tzinfo +from typing import Any, Literal, NoReturn, overload + +from nonebot.dependencies import Dependent +from nonebot.matcher import Matcher as BaseMatcher +from nonebot.permission import Permission +from nonebot.plugin import CommandGroup as BaseCommandGroup +from nonebot.plugin import MatcherGroup as BaseMatcherGroup +from nonebot.rule import ArgumentParser, Rule +from nonebot.typing import ( + T_Handler, + T_PermissionChecker, + T_RuleChecker, +) + +from kirami.typing import ( + Event, + Message, + MessageSegment, + MessageTemplate, + State, +) + +# ruff: noqa: PYI021 + +class Matcher(BaseMatcher): + @classmethod + async def send( + cls, + message: str | Message | MessageSegment | MessageTemplate, + *, + at_sender: bool = False, + reply_message: bool = False, + recall_time: int = 0, + argot_content: dict[str, Any] | None = None, + **kwargs, + ) -> Any: ... + @classmethod + async def finish( + cls, + message: str | Message | MessageSegment | MessageTemplate | None = None, + *, + at_sender: bool = False, + reply_message: bool = False, + recall_time: int = 0, + argot_content: dict[str, Any] | None = None, + **kwargs, + ) -> NoReturn: ... + +class MatcherCase(Matcher): + matcher: type[Matcher] = ... + + def __init__(self, matcher: type[Matcher]) -> None: ... + @overload + def __call__(self, func: T_Handler) -> T_Handler: ... + @overload + def __call__(self) -> Matcher: ... + def __call__(self, func: T_Handler | None = ...) -> T_Handler | Matcher: ... + +def on( + type: str = "", + /, + *, + rule: Rule | T_RuleChecker | None = None, + permission: Permission | T_PermissionChecker | None = None, + handlers: list[T_Handler | Dependent] | None = None, + temp: bool = False, + expire_time: datetime | timedelta | None = None, + priority: int = 1, + block: bool = False, + state: State | None = None, +) -> MatcherCase: ... +def on_type( + *types: type[Event], + rule: Rule | T_RuleChecker | None = None, + permission: Permission | T_PermissionChecker | None = None, + handlers: list[T_Handler | Dependent] | None = None, + temp: bool = False, + expire_time: datetime | timedelta | None = None, + priority: int = 1, + block: bool = False, + state: State | None = None, +) -> MatcherCase: ... +def on_metaevent( + *, + rule: Rule | T_RuleChecker | None = None, + handlers: list[T_Handler | Dependent] | None = None, + temp: bool = False, + expire_time: datetime | timedelta | None = None, + priority: int = 1, + block: bool = False, + state: State | None = None, +) -> MatcherCase: ... +def on_message( + *, + rule: Rule | T_RuleChecker | None = None, + permission: Permission | T_PermissionChecker | None = None, + handlers: list[T_Handler | Dependent] | None = None, + temp: bool = False, + expire_time: datetime | timedelta | None = None, + priority: int = 1, + block: bool = False, + state: State | None = None, + to_me: bool = False, + reply: bool = False, + argot: bool = False, +) -> MatcherCase: ... +def on_notice( + *, + rule: Rule | T_RuleChecker | None = None, + handlers: list[T_Handler | Dependent] | None = None, + temp: bool = False, + expire_time: datetime | timedelta | None = None, + priority: int = 1, + block: bool = False, + state: State | None = None, +) -> MatcherCase: ... +def on_request( + *, + rule: Rule | T_RuleChecker | None = None, + handlers: list[T_Handler | Dependent] | None = None, + temp: bool = False, + expire_time: datetime | timedelta | None = None, + priority: int = 1, + block: bool = False, + state: State | None = None, +) -> MatcherCase: ... +def on_command( + *cmds: str, + force_whitespace: str | bool | None = None, + rule: Rule | T_RuleChecker | None = None, + permission: Permission | T_PermissionChecker | None = None, + handlers: list[T_Handler | Dependent] | None = None, + temp: bool = False, + expire_time: datetime | timedelta | None = None, + priority: int = 1, + block: bool = True, + state: State | None = None, + to_me: bool = False, + reply: bool = False, + argot: bool = False, +) -> MatcherCase: ... +def on_shell_command( + *cmds: str, + parser: ArgumentParser | None = None, + rule: Rule | T_RuleChecker | None = None, + permission: Permission | T_PermissionChecker | None = None, + handlers: list[T_Handler | Dependent] | None = None, + temp: bool = False, + expire_time: datetime | timedelta | None = None, + priority: int = 1, + block: bool = True, + state: State | None = None, + to_me: bool = False, + reply: bool = False, + argot: bool = False, +) -> MatcherCase: ... +def on_startswith( + *msgs: str, + ignorecase: bool = False, + rule: Rule | T_RuleChecker | None = None, + permission: Permission | T_PermissionChecker | None = None, + handlers: list[T_Handler | Dependent] | None = None, + temp: bool = False, + expire_time: datetime | timedelta | None = None, + priority: int = 1, + block: bool = True, + state: State | None = None, + to_me: bool = False, + reply: bool = False, + argot: bool = False, +) -> MatcherCase: ... +def on_endswith( + *msgs: str, + ignorecase: bool = False, + rule: Rule | T_RuleChecker | None = None, + permission: Permission | T_PermissionChecker | None = None, + handlers: list[T_Handler | Dependent] | None = None, + temp: bool = False, + expire_time: datetime | timedelta | None = None, + priority: int = 1, + block: bool = True, + state: State | None = None, + to_me: bool = False, + reply: bool = False, + argot: bool = False, +) -> MatcherCase: ... +def on_fullmatch( + *msgs: str, + ignorecase: bool = False, + rule: Rule | T_RuleChecker | None = None, + permission: Permission | T_PermissionChecker | None = None, + handlers: list[T_Handler | Dependent] | None = None, + temp: bool = False, + expire_time: datetime | timedelta | None = None, + priority: int = 1, + block: bool = True, + state: State | None = None, + to_me: bool = False, + reply: bool = False, + argot: bool = False, +) -> MatcherCase: ... +def on_keyword( + *keywords: str, + rule: Rule | T_RuleChecker | None = None, + permission: Permission | T_PermissionChecker | None = None, + handlers: list[T_Handler | Dependent] | None = None, + temp: bool = False, + expire_time: datetime | timedelta | None = None, + priority: int = 1, + block: bool = True, + state: State | None = None, + to_me: bool = False, + reply: bool = False, + argot: bool = False, +) -> MatcherCase: ... +def on_regex( + pattern: str, + /, + *, + flags: int | re.RegexFlag = 0, + rule: Rule | T_RuleChecker | None = None, + permission: Permission | T_PermissionChecker | None = None, + handlers: list[T_Handler | Dependent] | None = None, + temp: bool = False, + expire_time: datetime | timedelta | None = None, + priority: int = 1, + block: bool = True, + state: State | None = None, + to_me: bool = False, + reply: bool = False, + argot: bool = False, +) -> MatcherCase: ... +def on_prefix( + *msgs: str, + ignorecase: bool = False, + rule: Rule | T_RuleChecker | None = None, + permission: Permission | T_PermissionChecker | None = None, + handlers: list[T_Handler | Dependent] | None = None, + temp: bool = False, + expire_time: datetime | timedelta | None = None, + priority: int = 1, + block: bool = True, + state: State | None = None, + to_me: bool = False, + reply: bool = False, + argot: bool = False, +) -> MatcherCase: ... +def on_suffix( + *msgs: str, + ignorecase: bool = False, + rule: Rule | T_RuleChecker | None = None, + permission: Permission | T_PermissionChecker | None = None, + handlers: list[T_Handler | Dependent] | None = None, + temp: bool = False, + expire_time: datetime | timedelta | None = None, + priority: int = 1, + block: bool = True, + state: State | None = None, + to_me: bool = False, + reply: bool = False, + argot: bool = False, +) -> MatcherCase: ... +@overload +def on_time( + trigger: Literal["cron"], + /, + *, + year: int | str | None = None, + month: int | str | None = None, + day: int | str | None = None, + week: int | str | None = None, + day_of_week: int | str | None = None, + hour: int | str | None = None, + minute: int | str | None = None, + second: int | str | None = None, + jitter: int | None = None, + start_date: datetime | str | None = None, + end_date: datetime | str | None = None, + timezone: tzinfo | str | None = None, + misfire_grace_time: int = 30, + rule: Rule | T_RuleChecker | None = None, + handlers: list[T_Handler | Dependent] | None = None, + temp: bool = False, + expire_time: datetime | timedelta | None = None, + priority: int = 1, + block: bool = False, + state: State | None = None, + **kwargs: Any, +) -> MatcherCase: + """注册一个 cron 定时事件响应器。 + + 在当前时间与所有指定的时间约束匹配时触发,与 UNIX cron 调度程序的工作方式类似。 + + 与 crontab 表达式不同,可以省略不需要的字段。 + + 大于显式定义的最低有效字段的字段默认为*,而较小字段默认为其最小值,除了默认为*的 `week` 和 `day_of_week`。 + + 例如,`day=1, minute=20`等于`year='*', month='*', day=1, week='*', day_of_week='*', hour='*'', minute=20, second=0`。 + + 然后,此定时事件响应器将在每年每月的第一天执行,每小时20分钟。 + + ### 表达式类型 + + | 表达式 | 时间段 | 描述 | + |:-----:|:----:|:----:| + | * | 任何 | 每个时间段触发 | + | */a | 任何 | 从最小值开始, 每隔 a 个时间段触发 | + | a-b | 任何 | 在 a-b 范围内的每个时间段触发(a 必须小于 b) | + | a-b/c | 任何 | 在 a-b 范围内每隔 c 个时间段触发 | + | xth y | day | 第 x 个星期 y 触发 | + | last x | day | 月份最后一个星期 x 触发 | + | last | day | 月份最后一天触发 | + | x,y,z | 任何 | 可以将以上表达式任意组合 | + + ### 参数 + year: 4位数字的年份 + + month: 1-12月 + + day: 1-31日 + + week: 1-53周 + + day_of_week: 一周中的第几天,工作日的数字或名称(0-6或 mon、 tue、 ws、 thu、 fri、 sat、 sun),第一个工作日总是星期一 + + hour: 0-23小时 + + minute: 0-59分钟 + + second: 0-59秒 + + start_date: 定时任务开始时间 + + end_date: 定时任务结束时间 + + timezone: 用于日期/时间计算的时区 + + jitter: 定时器执行时间的抖动范围(-x ~ x),单位秒 + + misfire_grace_time: 容错时间,单位秒 + + rule: 事件响应规则 + + handlers: 事件处理函数列表 + + temp: 是否为临时事件响应器(仅执行一次) + + expire_time: 事件响应器最终有效时间点,过期即被删除 + + priority: 事件响应器优先级 + + block: 是否阻止事件向更低优先级传递 + + state: 默认会话状态 + + **kwargs: 用于传递给定时任务的参数 + + [apscheduler.triggers.cron](https://apscheduler.readthedocs.io/en/3.x/modules/triggers/cron.html#module-apscheduler.triggers.cron) + """ + ... + +@overload +def on_time( + trigger: Literal["interval"], + /, + *, + weeks: int = 0, + days: int = 0, + hours: int = 0, + minutes: int = 0, + seconds: int = 0, + jitter: int | None = None, + start_date: datetime | str | None = None, + end_date: datetime | str | None = None, + timezone: tzinfo | str | None = None, + misfire_grace_time: int = 30, + rule: Rule | T_RuleChecker | None = None, + handlers: list[T_Handler | Dependent] | None = None, + temp: bool = False, + expire_time: datetime | timedelta | None = None, + priority: int = 1, + block: bool = False, + state: State | None = None, + **kwargs: Any, +) -> MatcherCase: + """注册一个间隔定时事件响应器。 + + 在指定的间隔时间上触发,如果指定了 `start_date`, 则从 `start_date` 开始, 否则为 `datetime.now()` + 间隔时间。 + + 此定时事件响应器按选定的间隔安排作业周期性运行。 + + ### 参数 + weeks: 执行间隔的周数 + + days: 执行间隔的天数 + + hours: 执行间隔的小时数 + + minutes: 执行间隔的分钟数 + + seconds: 执行间隔的秒数 + + start_date: 定时任务开始时间 + + end_date: 定时任务结束时间 + + timezone: 用于日期/时间计算的时区 + + jitter: 定时器执行时间的抖动范围(-x ~ x),单位秒 + + misfire_grace_time: 容错时间,单位秒 + + rule: 事件响应规则 + + handlers: 事件处理函数列表 + + temp: 是否为临时事件响应器(仅执行一次) + + expire_time: 事件响应器最终有效时间点,过期即被删除 + + priority: 事件响应器优先级 + + block: 是否阻止事件向更低优先级传递 + + state: 默认会话状态 + + [apscheduler.triggers.interval](https://apscheduler.readthedocs.io/en/3.x/modules/triggers/interval.html#module-apscheduler.triggers.interval) + """ + ... + +@overload +def on_time( + trigger: Literal["date"], + /, + *, + run_date: str | datetime | None = None, + timezone: tzinfo | str | None = None, + misfire_grace_time: int = 30, + rule: Rule | T_RuleChecker | None = None, + handlers: list[T_Handler | Dependent] | None = None, + temp: bool = False, + expire_time: datetime | timedelta | None = None, + priority: int = 1, + block: bool = False, + state: State | None = None, + **kwargs: Any, +) -> MatcherCase: + """注册一个日期定时事件响应器。 + + 在指定日期时间触发一次。如果 `run_date` 留空,则使用当前时间。 + + ### 参数 + run_date: 运行作业的日期/时间 + + timezone: 用于日期/时间计算的时区 + + misfire_grace_time: 容错时间,单位秒 + + rule: 事件响应规则 + + handlers: 事件处理函数列表 + + temp: 是否为临时事件响应器(仅执行一次) + + expire_time: 事件响应器最终有效时间点,过期即被删除 + + priority: 事件响应器优先级 + + block: 是否阻止事件向更低优先级传递 + + state: 默认会话状态 + + [apscheduler.triggers.date](https://apscheduler.readthedocs.io/en/3.x/modules/triggers/date.html#module-apscheduler.triggers.date) + """ + ... + +class CommandGroup(BaseCommandGroup): + basecmd: tuple[str, ...] = ... + prefix_aliases: bool = ... + def __init__( + self, + *cmds: str, + prefix_aliases: bool = False, + rule: Rule | T_RuleChecker | None = None, + permission: Permission | T_PermissionChecker | None = None, + handlers: list[T_Handler | Dependent] | None = None, + temp: bool = False, + expire_time: datetime | timedelta | None = None, + priority: int = 1, + block: bool = False, + state: State | None = None, + to_me: bool = False, + reply: bool = False, + argot: bool = False, + ) -> None: ... + def command( + self, + *cmds: str, + force_whitespace: str | bool | None = None, + rule: Rule | T_RuleChecker | None = None, + permission: Permission | T_PermissionChecker | None = None, + handlers: list[T_Handler | Dependent] | None = None, + temp: bool = False, + expire_time: datetime | timedelta | None = None, + priority: int = 1, + block: bool = True, + state: State | None = None, + to_me: bool = False, + reply: bool = False, + argot: bool = False, + ) -> MatcherCase: ... + def shell_command( + self, + *cmds: str, + parser: ArgumentParser | None = None, + rule: Rule | T_RuleChecker | None = None, + permission: Permission | T_PermissionChecker | None = None, + handlers: list[T_Handler | Dependent] | None = None, + temp: bool = False, + expire_time: datetime | timedelta | None = None, + priority: int = 1, + block: bool = True, + state: State | None = None, + to_me: bool = False, + reply: bool = False, + argot: bool = False, + ) -> MatcherCase: ... + +class MatcherGroup(BaseMatcherGroup): + def __init__( + self, + *, + type: str = "", + rule: Rule | T_RuleChecker | None = None, + permission: Permission | T_PermissionChecker | None = None, + handlers: list[T_Handler | Dependent] | None = None, + temp: bool = False, + expire_time: datetime | timedelta | None = None, + priority: int = 1, + block: bool = False, + state: State | None = None, + to_me: bool = False, + reply: bool = False, + argot: bool = False, + ) -> None: ... + def on( + self, + type: str = "", + /, + *, + rule: Rule | T_RuleChecker | None = None, + permission: Permission | T_PermissionChecker | None = None, + handlers: list[T_Handler | Dependent] | None = None, + temp: bool = False, + expire_time: datetime | timedelta | None = None, + priority: int = 1, + block: bool = False, + state: State | None = None, + ) -> MatcherCase: ... + def on_type( + self, + *types: type[Event], + rule: Rule | T_RuleChecker | None = None, + permission: Permission | T_PermissionChecker | None = None, + handlers: list[T_Handler | Dependent] | None = None, + temp: bool = False, + expire_time: datetime | timedelta | None = None, + priority: int = 1, + block: bool = True, + state: State | None = None, + ) -> MatcherCase: ... + def on_metaevent( + self, + *, + rule: Rule | T_RuleChecker | None = None, + handlers: list[T_Handler | Dependent] | None = None, + temp: bool = False, + expire_time: datetime | timedelta | None = None, + priority: int = 1, + block: bool = False, + state: State | None = None, + ) -> MatcherCase: ... + def on_message( + self, + *, + rule: Rule | T_RuleChecker | None = None, + permission: Permission | T_PermissionChecker | None = None, + handlers: list[T_Handler | Dependent] | None = None, + temp: bool = False, + expire_time: datetime | timedelta | None = None, + priority: int = 1, + block: bool = False, + state: State | None = None, + to_me: bool = False, + reply: bool = False, + argot: bool = False, + ) -> MatcherCase: ... + def on_notice( + self, + *, + rule: Rule | T_RuleChecker | None = None, + handlers: list[T_Handler | Dependent] | None = None, + temp: bool = False, + expire_time: datetime | timedelta | None = None, + priority: int = 1, + block: bool = False, + state: State | None = None, + ) -> MatcherCase: ... + def on_request( + self, + *, + rule: Rule | T_RuleChecker | None = None, + handlers: list[T_Handler | Dependent] | None = None, + temp: bool = False, + expire_time: datetime | timedelta | None = None, + priority: int = 1, + block: bool = False, + state: State | None = None, + ) -> MatcherCase: ... + def on_command( + self, + *cmds: str, + force_whitespace: str | bool | None = None, + rule: Rule | T_RuleChecker | None = None, + permission: Permission | T_PermissionChecker | None = None, + handlers: list[T_Handler | Dependent] | None = None, + temp: bool = False, + expire_time: datetime | timedelta | None = None, + priority: int = 1, + block: bool = True, + state: State | None = None, + to_me: bool = False, + reply: bool = False, + argot: bool = False, + ) -> MatcherCase: ... + def on_shell_command( + self, + *cmds: str, + parser: ArgumentParser | None = None, + rule: Rule | T_RuleChecker | None = None, + permission: Permission | T_PermissionChecker | None = None, + handlers: list[T_Handler | Dependent] | None = None, + temp: bool = False, + expire_time: datetime | timedelta | None = None, + priority: int = 1, + block: bool = True, + state: State | None = None, + to_me: bool = False, + reply: bool = False, + argot: bool = False, + ) -> MatcherCase: ... + def on_startswith( + self, + *msgs: str, + ignorecase: bool = False, + rule: Rule | T_RuleChecker | None = None, + permission: Permission | T_PermissionChecker | None = None, + handlers: list[T_Handler | Dependent] | None = None, + temp: bool = False, + expire_time: datetime | timedelta | None = None, + priority: int = 1, + block: bool = True, + state: State | None = None, + to_me: bool = False, + reply: bool = False, + argot: bool = False, + ) -> MatcherCase: ... + def on_endswith( + self, + *msgs: str, + ignorecase: bool = False, + rule: Rule | T_RuleChecker | None = None, + permission: Permission | T_PermissionChecker | None = None, + handlers: list[T_Handler | Dependent] | None = None, + temp: bool = False, + expire_time: datetime | timedelta | None = None, + priority: int = 1, + block: bool = True, + state: State | None = None, + to_me: bool = False, + reply: bool = False, + argot: bool = False, + ) -> MatcherCase: ... + def on_fullmatch( + self, + *msgs: str, + ignorecase: bool = False, + rule: Rule | T_RuleChecker | None = None, + permission: Permission | T_PermissionChecker | None = None, + handlers: list[T_Handler | Dependent] | None = None, + temp: bool = False, + expire_time: datetime | timedelta | None = None, + priority: int = 1, + block: bool = True, + state: State | None = None, + to_me: bool = False, + reply: bool = False, + argot: bool = False, + ) -> MatcherCase: ... + def on_keyword( + self, + *keywords: str, + rule: Rule | T_RuleChecker | None = None, + permission: Permission | T_PermissionChecker | None = None, + handlers: list[T_Handler | Dependent] | None = None, + temp: bool = False, + expire_time: datetime | timedelta | None = None, + priority: int = 1, + block: bool = True, + state: State | None = None, + to_me: bool = False, + reply: bool = False, + argot: bool = False, + ) -> MatcherCase: ... + def on_regex( + self, + pattern: str, + /, + *, + flags: int | re.RegexFlag = 0, + rule: Rule | T_RuleChecker | None = None, + permission: Permission | T_PermissionChecker | None = None, + handlers: list[T_Handler | Dependent] | None = None, + temp: bool = False, + expire_time: datetime | timedelta | None = None, + priority: int = 1, + block: bool = True, + state: State | None = None, + to_me: bool = False, + reply: bool = False, + argot: bool = False, + ) -> MatcherCase: ... + def on_prefix( + self, + *msgs: str, + ignorecase: bool = False, + rule: Rule | T_RuleChecker | None = None, + permission: Permission | T_PermissionChecker | None = None, + handlers: list[T_Handler | Dependent] | None = None, + temp: bool = False, + expire_time: datetime | timedelta | None = None, + priority: int = 1, + block: bool = True, + state: State | None = None, + to_me: bool = False, + reply: bool = False, + argot: bool = False, + ) -> MatcherCase: ... + def on_suffix( + self, + *msgs: str, + ignorecase: bool = False, + rule: Rule | T_RuleChecker | None = None, + permission: Permission | T_PermissionChecker | None = None, + handlers: list[T_Handler | Dependent] | None = None, + temp: bool = False, + expire_time: datetime | timedelta | None = None, + priority: int = 1, + block: bool = True, + state: State | None = None, + to_me: bool = False, + reply: bool = False, + argot: bool = False, + ) -> MatcherCase: ... + @overload + def on_time( + self, + trigger: Literal["cron"], + /, + *, + year: int | str | None = None, + month: int | str | None = None, + day: int | str | None = None, + week: int | str | None = None, + day_of_week: int | str | None = None, + hour: int | str | None = None, + minute: int | str | None = None, + second: int | str | None = None, + jitter: int | None = None, + start_date: datetime | str | None = None, + end_date: datetime | str | None = None, + timezone: tzinfo | str | None = None, + misfire_grace_time: int = 30, + rule: Rule | T_RuleChecker | None = None, + handlers: list[T_Handler | Dependent] | None = None, + temp: bool = False, + expire_time: datetime | timedelta | None = None, + priority: int = 1, + block: bool = False, + state: State | None = None, + **kwargs: Any, + ) -> MatcherCase: + """注册一个 cron 定时事件响应器。 + + 在当前时间与所有指定的时间约束匹配时触发,与 UNIX cron 调度程序的工作方式类似。 + + 与 crontab 表达式不同,可以省略不需要的字段。 + + 大于显式定义的最低有效字段的字段默认为*,而较小字段默认为其最小值,除了默认为*的 `week` 和 `day_of_week`。 + + 例如,`day=1, minute=20`等于`year='*', month='*', day=1, week='*', day_of_week='*', hour='*'', minute=20, second=0`。 + + 然后,此定时事件响应器将在每年每月的第一天执行,每小时20分钟。 + + ### 表达式类型 + + | 表达式 | 时间段 | 描述 | + |:-----:|:----:|:----:| + | * | 任何 | 每个时间段触发 | + | */a | 任何 | 从最小值开始, 每隔 a 个时间段触发 | + | a-b | 任何 | 在 a-b 范围内的每个时间段触发(a 必须小于 b) | + | a-b/c | 任何 | 在 a-b 范围内每隔 c 个时间段触发 | + | xth y | day | 第 x 个星期 y 触发 | + | last x | day | 月份最后一个星期 x 触发 | + | last | day | 月份最后一天触发 | + | x,y,z | 任何 | 可以将以上表达式任意组合 | + + ### 参数 + year: 4位数字的年份 + + month: 1-12月 + + day: 1-31日 + + week: 1-53周 + + day_of_week: 一周中的第几天,工作日的数字或名称(0-6或 mon、 tue、 ws、 thu、 fri、 sat、 sun),第一个工作日总是星期一 + + hour: 0-23小时 + + minute: 0-59分钟 + + second: 0-59秒 + + start_date: 定时任务开始时间 + + end_date: 定时任务结束时间 + + timezone: 用于日期/时间计算的时区 + + jitter: 定时器执行时间的抖动范围(-x ~ x),单位秒 + + misfire_grace_time: 容错时间,单位秒 + + rule: 事件响应规则 + + handlers: 事件处理函数列表 + + temp: 是否为临时事件响应器(仅执行一次) + + expire_time: 事件响应器最终有效时间点,过期即被删除 + + priority: 事件响应器优先级 + + block: 是否阻止事件向更低优先级传递 + + state: 默认会话状态 + + **kwargs: 用于传递给定时任务的参数 + + [apscheduler.triggers.cron](https://apscheduler.readthedocs.io/en/3.x/modules/triggers/cron.html#module-apscheduler.triggers.cron) + """ + ... + @overload + def on_time( + self, + trigger: Literal["interval"], + /, + *, + weeks: int = 0, + days: int = 0, + hours: int = 0, + minutes: int = 0, + seconds: int = 0, + jitter: int | None = None, + start_date: datetime | str | None = None, + end_date: datetime | str | None = None, + timezone: tzinfo | str | None = None, + misfire_grace_time: int = 30, + rule: Rule | T_RuleChecker | None = None, + handlers: list[T_Handler | Dependent] | None = None, + temp: bool = False, + expire_time: datetime | timedelta | None = None, + priority: int = 1, + block: bool = False, + state: State | None = None, + **kwargs: Any, + ) -> MatcherCase: + """注册一个间隔定时事件响应器。 + + 在指定的间隔时间上触发,如果指定了 `start_date`, 则从 `start_date` 开始, 否则为 `datetime.now()` + 间隔时间。 + + 此定时事件响应器按选定的间隔安排作业周期性运行。 + + ### 参数 + weeks: 执行间隔的周数 + + days: 执行间隔的天数 + + hours: 执行间隔的小时数 + + minutes: 执行间隔的分钟数 + + seconds: 执行间隔的秒数 + + start_date: 定时任务开始时间 + + end_date: 定时任务结束时间 + + timezone: 用于日期/时间计算的时区 + + jitter: 定时器执行时间的抖动范围(-x ~ x),单位秒 + + misfire_grace_time: 容错时间,单位秒 + + rule: 事件响应规则 + + handlers: 事件处理函数列表 + + temp: 是否为临时事件响应器(仅执行一次) + + expire_time: 事件响应器最终有效时间点,过期即被删除 + + priority: 事件响应器优先级 + + block: 是否阻止事件向更低优先级传递 + + state: 默认会话状态 + + [apscheduler.triggers.interval](https://apscheduler.readthedocs.io/en/3.x/modules/triggers/interval.html#module-apscheduler.triggers.interval) + """ + ... + @overload + def on_time( + self, + trigger: Literal["date"], + /, + *, + run_date: str | datetime | None = None, + timezone: tzinfo | str | None = None, + misfire_grace_time: int = 30, + rule: Rule | T_RuleChecker | None = None, + handlers: list[T_Handler | Dependent] | None = None, + temp: bool = False, + expire_time: datetime | timedelta | None = None, + priority: int = 1, + block: bool = False, + state: State | None = None, + **kwargs: Any, + ) -> MatcherCase: + """注册一个日期定时事件响应器。 + + 在指定日期时间触发一次。如果 `run_date` 留空,则使用当前时间。 + + ### 参数 + run_date: 运行作业的日期/时间 + + timezone: 用于日期/时间计算的时区 + + misfire_grace_time: 容错时间,单位秒 + + rule: 事件响应规则 + + handlers: 事件处理函数列表 + + temp: 是否为临时事件响应器(仅执行一次) + + expire_time: 事件响应器最终有效时间点,过期即被删除 + + priority: 事件响应器优先级 + + block: 是否阻止事件向更低优先级传递 + + state: 默认会话状态 + + [apscheduler.triggers.date](https://apscheduler.readthedocs.io/en/3.x/modules/triggers/date.html#module-apscheduler.triggers.date) + """ + ... diff --git a/kirami/message.py b/kirami/message.py new file mode 100644 index 0000000..7812480 --- /dev/null +++ b/kirami/message.py @@ -0,0 +1,223 @@ +"""本模块定义了消息构造器,可用于快速创建混合消息""" + +from collections.abc import Callable +from io import BytesIO +from pathlib import Path +from typing import ClassVar, Protocol, TypeVar, runtime_checkable + +from flowery import Imager +from flowery.typing import PILImage +from nonebot.adapters import MessageTemplate as MessageTemplate +from nonebot.adapters.onebot.v11 import Message as BaseMessage +from nonebot.adapters.onebot.v11 import MessageSegment as BaseMessageSegment +from typing_extensions import Self + +from kirami.exception import ReadFileError + + +@runtime_checkable +class MessageResource(Protocol): + def message(self, *args, **kwargs) -> "MessageSegment": + ... + + +class MessageSegment(BaseMessageSegment): + """消息段构造器""" + + @classmethod + def text(cls, text: str) -> Self: + """返回一个纯文本消息段。 + + ### 参数 + text: 文本内容 + """ + return super().text(text) + + @classmethod + def face(cls, id_: int) -> Self: + """返回一个表情消息段。 + + ### 参数 + id_: 表情 ID + """ + return super().face(id_) + + @classmethod + def at(cls, user_id: int | str) -> Self: + """返回一个 @ 某人消息段。 + + ### 参数 + user_id: 要 @ 的用户 ID + """ + return super().at(user_id) + + @classmethod + def reply(cls, id_: int) -> Self: + """返回一个回复消息段。 + + ### 参数 + id_: 回复的消息 ID + """ + return super().reply(id_) + + @classmethod + def image( + cls, + file: str | bytes | BytesIO | Path | MessageResource | PILImage | Imager, + *args, + **kwargs, + ) -> Self: + """返回一个图片消息段。 + + ### 参数 + file: 图像文件,可传入本地路径、网络链接或者其他支持的对象 + + type_: 图片类型,无此参数表示普通图片。默认为 None + + cache: 是否使用缓存。默认为 True + + proxy: 是否使用代理。默认为 True + + timeout: 超时时间。默认为 None + + ### 异常 + ReadFileError: 读取文件错误,不是一个有效的文件 + """ + if isinstance(file, PILImage): + file = Imager(file) + if isinstance(file, Imager): + file = f"base64://{file.to_base64()}" + if isinstance(file, str) and "://" not in file: + file = Path(file) + if not file.is_file(): + raise ReadFileError(f"不是一个有效的文件: {file}") + if isinstance(file, MessageResource): + return file.message(*args, **kwargs) + return super().image(file, *args, **kwargs) + + @classmethod + def record( + cls, + file: str | bytes | BytesIO | Path | MessageResource, + *args, + **kwargs, + ) -> Self: + """返回一个语音消息段。 + + ### 参数 + file: 语音文件,可传入本地路径、网络链接或者其他支持的对象 + + magic: 是否是变声文件。默认为 False + + cache: 是否使用缓存。默认为 True + + proxy: 是否使用代理。默认为 True + + timeout: 超时时间。默认为 None + + ### 异常 + ReadFileError: 读取文件错误,不是一个有效的文件 + """ + if isinstance(file, str) and "://" not in file: + file = Path(file) + if not file.is_file(): + raise ReadFileError(f"不是一个有效的文件: {file}") + if isinstance(file, MessageResource): + return file.message(*args, **kwargs) + return super().record(file, *args, **kwargs) + + @classmethod + def video( + cls, + file: str | bytes | BytesIO | Path | MessageResource, + *args, + **kwargs, + ) -> Self: + """返回一个视频消息段。 + + ### 参数 + file: 视频文件,可传入本地路径、网络链接或者其他支持的对象 + + cache: 是否使用缓存。默认为 True + + proxy: 是否使用代理。默认为 True + + timeout: 超时时间。默认为 None + + ### 异常 + ReadFileError: 读取文件错误,不是一个有效的文件 + """ + if isinstance(file, str) and "://" not in file: + file = Path(file) + if not file.is_file(): + raise ReadFileError(f"不是一个有效的文件: {file}") + if isinstance(file, MessageResource): + return file.message(*args, **kwargs) + return super().video(file, *args, **kwargs) + + @classmethod + def poke(cls, id_: int | str) -> Self: + """返回一个戳一戳消息段。 + + ### 参数 + id_: 目标用户 ID + """ + return cls("poke", {"qq": str(id_)}) + + @classmethod + def refer(cls, id_: int) -> Self: + """返回一个合并转发引用消息段。 + + ### 参数 + id_: 引用的消息 ID + """ + return super().node(id_) + + @classmethod + def node(cls, user_id: int | str, nickname: str, content: "str | Message") -> Self: + """返回一个合并转发节点消息段。 + + ### 参数 + user_id: 用户 ID + + nickname: 用户昵称 + + content: 节点内容 + """ + return super().node_custom(int(user_id), nickname, content) + + +M = TypeVar("M", bound=BaseMessage) + + +class MessageBuilder: + def __init__(self, ms: type[BaseMessageSegment]) -> None: + self.ms = ms + + def __set_name__(self, _owner: type[M], name: str) -> None: + self.name = name + + def __get__(self, instance: M, owner: type[M]) -> Callable[..., M]: + def chain(*args, **kwargs) -> M: + msg = owner() if instance is None else instance + return msg + getattr(self.ms, self.name)(*args, **kwargs) + + return chain + + +class Message(BaseMessage): + """链式消息构造器""" + + __segments: ClassVar[tuple[str, ...]] = ( + "text", + "face", + "at", + "reply", + "image", + "anonymous", + "refer", + "node", + ) + + for segment in __segments: + locals()[segment] = MessageBuilder(MessageSegment) diff --git a/kirami/message.pyi b/kirami/message.pyi new file mode 100644 index 0000000..e8ab3b2 --- /dev/null +++ b/kirami/message.pyi @@ -0,0 +1,147 @@ +from io import BytesIO +from pathlib import Path +from typing import Protocol + +from flowery import Imager +from flowery.typing import PILImage +from nonebot.adapters import MessageTemplate as MessageTemplate +from nonebot.adapters.onebot.v11 import Message as BaseMessage +from nonebot.adapters.onebot.v11 import MessageSegment as BaseMessageSegment +from typing_extensions import Self + +# ruff: noqa: PYI021 + +class MessageResource(Protocol): + def message(self, *args, **kwargs) -> MessageSegment: ... + +class MessageSegment(BaseMessageSegment): + @classmethod + def text(cls, text: str) -> Self: ... + @classmethod + def face(cls, id_: int) -> Self: ... + @classmethod + def at(cls, user_id: int | str) -> Self: ... + @classmethod + def reply(cls, id_: int) -> Self: ... + @classmethod + def image( + cls, + file: str | bytes | BytesIO | Path | MessageResource | PILImage | Imager, + type_: str | None = None, + cache: bool = True, + proxy: bool = True, + timeout: int | None = None, + ) -> Self: ... + @classmethod + def record( + cls, + file: str | bytes | BytesIO | Path | MessageResource, + magic: bool | None = None, + cache: bool = True, + proxy: bool = True, + timeout: int | None = None, + ) -> Self: ... + @classmethod + def video( + cls, + file: str | bytes | BytesIO | Path | MessageResource, + cache: bool = True, + proxy: bool = True, + timeout: int | None = None, + ) -> Self: ... + @classmethod + def poke(cls, id_: int | str) -> Self: ... + @classmethod + def refer(cls, id_: int) -> Self: ... + @classmethod + def node( + cls, user_id: int | str, nickname: str, content: str | Message + ) -> Self: ... + +class Message(BaseMessage): + @classmethod + def text(cls, text: str) -> Self: + """纯文本消息。 + + ### 参数 + text: 文本内容 + """ + ... + @classmethod + def face(cls, id_: int) -> Self: + """表情消息。 + + ### 参数 + id_: 表情 ID + """ + ... + @classmethod + def at(cls, user_id: int | str) -> Self: + """@ 某人消息。 + + ### 参数 + user_id: 要 @ 的用户 ID + """ + ... + @classmethod + def reply(cls, id_: int) -> Self: + """回复消息。 + + ### 参数 + id_: 回复的消息 ID + """ + ... + @classmethod + def image( + cls, + file: str | bytes | BytesIO | Path | MessageResource | PILImage | Imager, + type_: str | None = None, + cache: bool = True, + proxy: bool = True, + timeout: int | None = None, + ) -> Self: + """图片消息。 + + ### 参数 + file: 图像文件,可传入本地路径、网络链接或者其他支持的对象 + + type_: 图片类型,无此参数表示普通图片。默认为 None + + cache: 是否使用缓存。默认为 True + + proxy: 是否使用代理。默认为 True + + timeout: 超时时间。默认为 None + + ### 异常 + ReadFileError: 读取文件错误,不是一个有效的文件 + """ + ... + @classmethod + def anonymous(cls, ignore_failure: bool | None = None) -> Self: + """匿名消息。 + + ### 参数 + ignore_failure: 无法匿名时是否继续发送。默认为 None + """ + ... + @classmethod + def refer(cls, id_: int) -> Self: + """合并转发引用消息。 + + ### 参数 + id_: 引用的消息 ID + """ + ... + @classmethod + def node(cls, user_id: int | str, nickname: str, content: str | Self) -> Self: + """合并转发节点消息。 + + ### 参数 + user_id: 用户 ID + + nickname: 用户昵称 + + content: 节点内容 + """ + ... diff --git a/kirami/patch.py b/kirami/patch.py new file mode 100644 index 0000000..2eaed45 --- /dev/null +++ b/kirami/patch.py @@ -0,0 +1,195 @@ +"""本模块用于修改 NoneBot 以满足定制需求""" + +# ruff: noqa: F811, PLR0913 + +# ============================================================================== + +import inspect +from types import ModuleType +from typing import Any + +from nonebot.dependencies import CustomConfig, Param +from nonebot.dependencies.utils import check_field_type +from nonebot.internal.params import StateParam +from nonebot.log import logger +from nonebot.plugin import _current_plugin_chain +from nonebot.plugin.plugin import Plugin +from nonebot.typing import T_State +from nonebot.utils import generic_check_issubclass +from pydantic.fields import ModelField, Required + +from kirami.typing import State + + +@classmethod +def check_param( + cls: type[StateParam], + param: inspect.Parameter, + _allow_types: tuple[type[Param], ...], +) -> StateParam | None: + """允许自定义类型 `State` 通过依赖注入检查""" + # param type is T_State + if param.annotation is T_State: + return cls(Required) + # param type is State or subclass of State or None + if generic_check_issubclass(param.annotation, State): + checker: ModelField | None = None + if param.annotation is not State: + checker = ModelField( + name=param.name, + type_=param.annotation, + class_validators=None, + model_config=CustomConfig, + default=None, + required=True, + ) + return cls(Required, checker=checker) + # legacy: param is named "state" and has no type annotation + if param.annotation == param.empty and param.name == "state": + return cls(Required) + return None + + +async def check(self: StateParam, state: State, **_kwargs: Any) -> Any: + if checker := self.extra.get("checker", None): + check_field_type(checker, state) + + +StateParam._check_param = check_param # type: ignore +StateParam._check = check + +# ============================================================================== + +import importlib +import time + +from nonebot.log import logger +from nonebot.plugin.manager import PluginManager +from nonebot.plugin.plugin import Plugin +from nonebot.utils import path_to_module_name + + +def load_plugin(self: PluginManager, name: str) -> Plugin | None: + """在 NoneBot 加载插件后进行服务化处理""" + try: + start_time = time.time() + + if name in self.plugins: + module = importlib.import_module(name) + elif name in self._third_party_plugin_names: + module = importlib.import_module(self._third_party_plugin_names[name]) + elif name in self._searched_plugin_names: + module = importlib.import_module( + path_to_module_name(self._searched_plugin_names[name]) + ) + else: + raise RuntimeError( # noqa: TRY301 + f"Plugin not found: {name}! Check your plugin name" + ) + + if (plugin := getattr(module, "__plugin__", None)) is None or not isinstance( + plugin, Plugin + ): + raise RuntimeError( # noqa: TRY301 + f"Module {module.__name__} is not loaded as a plugin! " + "Make sure not to import it before loading." + ) + full_name = getattr(plugin, "full_name") + logger.opt(colors=True).success(f'✨ Loading "{full_name}" complete.') + + end_time = time.time() + load_time = (end_time - start_time) * 1000 + logger.opt(colors=True).debug( + f'💠 Service "{full_name}" ready. Load time: {load_time:.2f} ms.' + ) + except Exception as e: + logger.opt(colors=True, exception=e).error( + f'🌠 Loading "{name}" failed: {e}' + ) + else: + return plugin + + +PluginManager.load_plugin = load_plugin + +# ============================================================================== + +from nonebot.plugin.plugin import Plugin + + +@property +def full_name(self: Plugin) -> str: + """插件完整索引标识,包含所有父插件的标识符""" + if parent := self.parent_plugin: + return getattr(parent, "full_name") + "." + self.name + return self.name + + +setattr(Plugin, "full_name", full_name) + +# ============================================================================== + +from importlib.machinery import SourceFileLoader +from types import ModuleType + +from nonebot.plugin import _current_plugin_chain, _managers, _new_plugin, _revert_plugin +from nonebot.plugin.manager import PluginLoader +from nonebot.plugin.plugin import PluginMetadata + +from kirami.service.manager import ServiceManager, load_subplugin + + +def exec_module(self: PluginLoader, module: ModuleType) -> None: + """支持从配置中加载子插件""" + if self.loaded: + return + + # create plugin before executing + plugin = _new_plugin(self.name, module, self.manager) + setattr(module, "__plugin__", plugin) + + # detect parent plugin before entering current plugin context + parent_plugins = _current_plugin_chain.get() + for pre_plugin in reversed(parent_plugins): + if _managers.index(pre_plugin.manager) < _managers.index(self.manager): + plugin.parent_plugin = pre_plugin + pre_plugin.sub_plugins.add(plugin) + break + + # enter plugin context + _plugin_token = _current_plugin_chain.set((*parent_plugins, plugin)) + + try: + try: + super(SourceFileLoader, self).exec_module(module) + except Exception: + _revert_plugin(plugin) + raise + + # Move: get plugin metadata + metadata: PluginMetadata | None = getattr(module, "__plugin_meta__", None) + plugin.metadata = metadata + + # Add: create service + ServiceManager.load_service(plugin) + + # Add: load subplugin + load_subplugin(plugin) + finally: + # leave plugin context + _current_plugin_chain.reset(_plugin_token) + + +PluginLoader.exec_module = exec_module + +# ============================================================================== + +from nonebot.internal.adapter.event import Event + + +def get_plaintext(self: Event) -> str: + """去除纯文本首尾空白字符""" + return self.get_message().extract_plain_text().strip() + + +Event.get_plaintext = get_plaintext diff --git a/kirami/permission.py b/kirami/permission.py new file mode 100644 index 0000000..9da6f50 --- /dev/null +++ b/kirami/permission.py @@ -0,0 +1,37 @@ +"""本模块包含了权限类和权限检查辅助。""" + +from nonebot.adapters.onebot.v11.permission import GROUP as GROUP +from nonebot.adapters.onebot.v11.permission import GROUP_ADMIN as GROUP_ADMIN +from nonebot.adapters.onebot.v11.permission import GROUP_MEMBER as GROUP_MEMBER +from nonebot.adapters.onebot.v11.permission import GROUP_OWNER as GROUP_OWNER +from nonebot.adapters.onebot.v11.permission import PRIVATE as PRIVATE +from nonebot.adapters.onebot.v11.permission import PRIVATE_FRIEND as PRIVATE_FRIEND +from nonebot.adapters.onebot.v11.permission import PRIVATE_GROUP as PRIVATE_GROUP +from nonebot.adapters.onebot.v11.permission import PRIVATE_OTHER as PRIVATE_OTHER +from nonebot.permission import MESSAGE as MESSAGE +from nonebot.permission import METAEVENT as METAEVENT +from nonebot.permission import NOTICE as NOTICE +from nonebot.permission import REQUEST as REQUEST +from nonebot.permission import SUPERUSER as SUPERUSER +from nonebot.permission import USER as USER +from nonebot.permission import Permission as Permission +from nonebot.permission import User as User + +from kirami.event import Event, GroupMessageEvent +from kirami.service import Role, Subjects + + +async def role_permission(role: Role) -> Permission: + """检查用户是否满足角色要求""" + + def _role(event: Event, subjects: Subjects) -> bool: + user_role = Role.roles["normal"] + if isinstance(event, GroupMessageEvent): + sender_role = event.sender.role + sender_role = "normal" if sender_role in ("member", None) else sender_role + user_role = Role.roles[sender_role] + if uid := getattr(event, "user_id", None): + user_role = Role.get_user_role(uid, *subjects) or user_role + return user_role >= role + + return Permission(_role) diff --git a/kirami/py.typed b/kirami/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/kirami/rule.py b/kirami/rule.py new file mode 100644 index 0000000..71927e4 --- /dev/null +++ b/kirami/rule.py @@ -0,0 +1,188 @@ +"""本模块定义了内置响应器的各类规则""" + +import re +from typing import Any + +from nonebot.consts import ENDSWITH_KEY, STARTSWITH_KEY +from nonebot.rule import ArgumentParser as ArgumentParser +from nonebot.rule import EndswithRule, StartswithRule +from nonebot.rule import Rule as Rule + +from kirami.database import Argot +from kirami.event import Event, MessageEvent, TimerNoticeEvent +from kirami.state import State + + +class PrefixRule(StartswithRule): + """检查消息纯文本是否以指定字符串开头并移除前缀。 + + ### 参数 + msg: 指定消息开头字符串元组 + + ignorecase: 是否忽略大小写 + """ + + __slots__ = ("msg", "ignorecase") + + def __repr__(self) -> str: + return f"Prefix(msg={self.msg}, ignorecase={self.ignorecase})" + + async def __call__(self, event: Event, state: State) -> bool: + try: + text = event.get_plaintext() + except Exception: + return False + if match := re.match( + f"^(?:{'|'.join(re.escape(prefix) for prefix in self.msg)})", + text, + re.IGNORECASE if self.ignorecase else 0, + ): + prefix = match.group() + state[STARTSWITH_KEY] = prefix + + # 从消息中移除前缀 + message = event.get_message() + message_seg = message.pop(0) + if message_seg.is_text() and ( + mseg_str := str(message_seg).removeprefix(prefix).lstrip() + ): + new_message = message.__class__(mseg_str) + for new_segment in reversed(new_message): + message.insert(0, new_segment) + return True + return False + + +def prefix(msg: tuple[str, ...], ignorecase: bool = False) -> Rule: + """匹配消息纯文本开头。在匹配成功时,会将前缀从消息中移除。 + + ### 参数 + msg: 指定消息开头字符串元组 + + ignorecase: 是否忽略大小写 + """ + return Rule(PrefixRule(msg, ignorecase)) + + +class SuffixRule(EndswithRule): + """检查消息纯文本是否以指定字符串结尾并移除后缀。 + + ### 参数 + msg: 指定消息结尾字符串元组 + + ignorecase: 是否忽略大小写 + """ + + __slots__ = ("msg", "ignorecase") + + def __repr__(self) -> str: + return f"Suffix(msg={self.msg}, ignorecase={self.ignorecase})" + + async def __call__(self, event: Event, state: State) -> bool: + try: + text = event.get_plaintext() + except Exception: + return False + if match := re.search( + f"(?:{'|'.join(re.escape(suffix) for suffix in self.msg)})$", + text, + re.IGNORECASE if self.ignorecase else 0, + ): + suffix = match.group() + state[ENDSWITH_KEY] = suffix + + # 从消息中移除后缀 + message = event.get_message() + message_seg = message.pop(0) + if message_seg.is_text() and ( + mseg_str := str(message_seg).removesuffix(suffix).lstrip() + ): + new_message = message.__class__(mseg_str) + for new_segment in reversed(new_message): + message.insert(0, new_segment) + return True + return False + + +def suffix(msg: tuple[str, ...], ignorecase: bool = False) -> Rule: + """匹配消息纯文本结尾。在匹配成功时,会将后缀从消息中移除。 + + ### 参数 + msg: 指定消息开头字符串元组 + + ignorecase: 是否忽略大小写 + """ + return Rule(SuffixRule(msg, ignorecase)) + + +class ReplyRule: + """检查是否是回复消息""" + + __slots__ = () + + def __repr__(self) -> str: + return "ReplyMe()" + + def __eq__(self, other: object) -> bool: + return isinstance(other, self.__class__) + + def __hash__(self) -> int: + return hash((self.__class__,)) + + async def __call__(self, event: MessageEvent) -> bool: + return bool(event.reply) + + +class ArgotRule: + """检查是否存在暗语""" + + __slots__ = () + + def __repr__(self) -> str: + return "Argot()" + + def __eq__(self, other: object) -> bool: + return isinstance(other, self.__class__) + + def __hash__(self) -> int: + return hash((self.__class__,)) + + async def __call__(self, event: MessageEvent, state: State) -> bool: + if not event.reply: + return False + if argot := await Argot.get(event.reply.message_id): + state["_argot"] = argot.content + return True + return False + + +class TimerRule: + """响应指定的定时器事件 + + ### 参数 + timer_id: 定时器 ID + + timer_params: 定时器参数 + """ + + __slots__ = ("timer_id", "timer_params") + + def __init__(self, timer_id: str, timer_params: dict[str, Any]) -> None: + self.timer_id = timer_id + self.timer_params = timer_params + + def __repr__(self) -> str: + return f"Timer(timer_id={self.timer_id}), timer_params={self.timer_params}" + + def __eq__(self, other: object) -> bool: + return ( + isinstance(other, self.__class__) + and self.timer_id == other.timer_id + and self.timer_params == other.timer_params + ) + + def __hash__(self) -> int: + return hash((self.__class__,)) + + async def __call__(self, event: TimerNoticeEvent) -> bool: + return event.timer_id == self.timer_id diff --git a/kirami/server/__init__.py b/kirami/server/__init__.py new file mode 100644 index 0000000..678bd5e --- /dev/null +++ b/kirami/server/__init__.py @@ -0,0 +1,4 @@ +"""本模块包含了网络服务器以及 API 路由""" + +from .api import api as api +from .server import Server as Server diff --git a/kirami/server/api.py b/kirami/server/api.py new file mode 100644 index 0000000..1872274 --- /dev/null +++ b/kirami/server/api.py @@ -0,0 +1,17 @@ +"""本模块用于定义 API 路由""" + +from kirami.version import __version__ + +from .server import Server + +api = Server.get_router("api", tags=["API"]) + + +@api.get("/version", summary="获取版本信息") +def get_version() -> dict: + """ + 获取版本信息 + """ + return { + "version": __version__, + } diff --git a/kirami/server/server.py b/kirami/server/server.py new file mode 100644 index 0000000..ec48a02 --- /dev/null +++ b/kirami/server/server.py @@ -0,0 +1,76 @@ +"""本模块定义了网络服务器""" + +import inspect +from enum import Enum +from typing import ClassVar + +import nonebot +from fastapi import APIRouter, FastAPI +from fastapi.middleware.cors import CORSMiddleware + +from kirami.config import bot_config, server_config +from kirami.log import logger + + +class Server: + _routers: ClassVar[dict[str, APIRouter]] = {} + + @classmethod + def init(cls) -> None: + """初始化服务器""" + app = cls.get_app() + if cls._routers: + # 添加 API 路由对象 + for prefix, router in cls._routers.items(): + app.include_router(router) + logger.opt(colors=True).debug( + f'API Router "{prefix}" registered.' + ) + + # 添加 CORS 跨域中间件 + if server_config.allow_cors: + app.add_middleware( + CORSMiddleware, **server_config.dict(exclude={"allow_cors"}) + ) + + cls._routers.clear() + + logger.debug( + f"API Docs Url is http://{bot_config.host}:{bot_config.port}{bot_config.fastapi_docs_url}" + ) + + @classmethod + def get_app(cls) -> FastAPI: + """获取全局 Server App 对象""" + return nonebot.get_app() + + @classmethod + def get_router( + cls, route: str | None = None, tags: list[str | Enum] | None = None, **kwargs + ) -> APIRouter: + """获取 API 路由对象。 + + ### 参数 + route: 路由前缀,不填写则自动获取模块名。默认为 None + + tags: 路由标签,不填写则与前缀相同。默认为 None + + **kwargs: APIRouter 参数 + + ### 返回 + API 路由对象 + """ + + if route is None: + prefix = inspect.currentframe().f_back.f_globals["__name__"].split(".")[-1] # type: ignore + else: + prefix = route.strip("/") + + if tags is None: + tags = [prefix] + + router = APIRouter(prefix=f"/{prefix}", tags=tags, **kwargs) + + cls._routers[router.prefix] = router + + return router diff --git a/kirami/service/__init__.py b/kirami/service/__init__.py new file mode 100644 index 0000000..397fce0 --- /dev/null +++ b/kirami/service/__init__.py @@ -0,0 +1,18 @@ +""" +服务控制 +==== + +将一组功能组合成一个服务,并对服务进行控制。 + +本模块定义了服务的基本概念,包括服务、功能、主体、角色、策略等。 +""" + +from .access import Policy as Policy +from .access import Role as Role +from .controller import register_checker as register_checker +from .manager import ServiceManager as ServiceManager +from .service import Ability as Ability +from .service import Service as Service +from .subject import Subject as Subject +from .subject import Subjects as Subjects +from .subject import register_extractor as register_extractor diff --git a/kirami/service/access.py b/kirami/service/access.py new file mode 100644 index 0000000..bd6446b --- /dev/null +++ b/kirami/service/access.py @@ -0,0 +1,245 @@ +"""本模块定义了权限系统的基本数据结构, 包括角色和策略""" + +import asyncio +from collections import defaultdict +from typing import ClassVar + +from mango import Document, Field +from typing_extensions import Self + +from kirami.config import bot_config +from kirami.hook import on_startup + +from .subject import Subject + + +class BaseAccess(Document): + name: str = Field(primary_key=True) + """权限名称""" + remark: str = "" + """权限备注""" + + def __hash__(self) -> int: + return hash(self.name) + + async def sync(self) -> None: + """从数据库同步""" + if access := await self.get(self.name): + await self.update(**access.dict()) + + +class Role(BaseAccess): + weight: int + """角色权重""" + assigned: defaultdict[Subject, set[str]] = Field( + default_factory=lambda: defaultdict(set), init=False + ) + """分配此角色的用户, 键表示范围, 值表示用户id""" + + roles: ClassVar[dict[str, Self]] = {} + """角色列表""" + + def __hash__(self) -> int: + return super().__hash__() + + def __eq__(self, other: Self) -> bool: + return self.name == other.name + + def __ne__(self, other: Self) -> bool: + return self.name != other.name + + def __lt__(self, other: Self) -> bool: + return self.weight < other.weight + + def __gt__(self, other: Self) -> bool: + return self.weight > other.weight + + def __le__(self, other: Self) -> bool: + return self.weight <= other.weight + + def __ge__(self, other: Self) -> bool: + return self.weight >= other.weight + + @classmethod + def create(cls, name: str, weight: int, remark: str = "") -> Self: + """创建角色。 + + ### 参数 + name: 角色名称 + + weight: 角色权重 + + remark: 角色备注 + """ + cls.roles[name] = cls(name=name, weight=weight, remark=remark) + return cls.roles[name] + + async def delete(self) -> None: + """删除角色""" + self.roles.pop(self.name, None) + await super().delete() + + async def assign_user( + self, user_id: int | str, subject: Subject | None = None + ) -> None: + """分配用户角色。 + + ### 参数 + user_id: 用户 ID + + subject: 范围主体,为 None 时表示全局范围 + """ + subject = subject or Subject("global", "*") + self.assigned[subject].add(str(user_id)) + await self.save() + + async def revoke_user( + self, user_id: int | str, subject: Subject | None = None + ) -> None: + """撤销用户角色。 + + ### 参数 + user_id: 用户 ID + + subject: 范围主体,为 None 时撤销所有范围的角色 + """ + if subject and user_id in self.assigned[subject]: + self.assigned[subject].remove(str(user_id)) + else: + for subject in self.assigned: + self.assigned[subject].discard(str(user_id)) + await self.save() + + @classmethod + def query_user(cls, user_id: int | str, *subjects: Subject) -> set[Self]: + """查询用户角色。 + + ### 参数 + user_id: 用户 ID + + *subjects: 范围主体 + + ### 返回 + 用户角色集合 + """ + user_roles = { + role + for role in cls.roles.values() + if any(user_id in role.assigned[subject] for subject in subjects) + } + if user_id in bot_config.superusers: + user_roles.add(cls.roles["superuser"]) + return user_roles + + @classmethod + def get_user_role(cls, user_id: int | str, *subjects: Subject) -> Self | None: + """获取用户最大权限角色。 + + ### 参数 + user_id: 用户 ID + + *subjects: 范围主体 + + ### 返回 + 用户最大权限角色,若无则返回 None + """ + return max(cls.query_user(user_id, *subjects), default=None) + + +class Policy(BaseAccess): + allow: set[str] = Field(default_factory=set) + """授权访问""" + applied: set[Subject] = Field(default_factory=set, init=False) + """应用此策略的主体""" + + policies: ClassVar[dict[str, Self]] = {} + """策略列表""" + + def __hash__(self) -> int: + return super().__hash__() + + @classmethod + def create_policy(cls, name: str, allow: set[str] | None, remark: str = "") -> Self: + """创建策略。 + + ### 参数 + name: 策略名称 + + allow: 授权访问 + + remark: 策略备注 + """ + cls.policies[name] = cls(name=name, allow=allow or set(), remark=remark) + return cls.policies[name] + + async def delete(self) -> None: + """删除策略""" + self.policies.pop(self.name, None) + await super().delete() + + async def apply_policy(self, subject: Subject) -> None: + """应用策略。 + + ### 参数 + subject: 应用主体 + """ + self.applied.add(subject) + + async def unapply_policy(self, subject: Subject) -> None: + """取消策略。 + + ### 参数 + subject: 应用主体 + """ + self.applied.remove(subject) + + async def add_allow(self, *allow: str) -> None: + """添加授权访问。 + + ### 参数 + *allow: 授权访问 + """ + self.allow |= set(allow) + + async def remove_allow(self, *allow: str) -> None: + """移除授权访问。 + + ### 参数 + *allow: 授权访问 + """ + self.allow -= set(allow) + + @classmethod + def get_policies(cls, *subjects: Subject) -> set[Self]: + """获取主体应用的策略。 + + ### 参数 + *subjects: 范围主体 + + ### 返回 + 主体策略集合 + """ + return { + policy + for policy in cls.policies.values() + if any(subject in policy.applied for subject in subjects) + } + + +Role.roles |= { + "normal": Role(name="normal", weight=1, remark="普通用户"), + "admin": Role(name="admin", weight=9, remark="管理员"), + "owner": Role(name="owner", weight=99, remark="群主"), + "superuser": Role(name="superuser", weight=999, remark="超级用户"), +} + +Policy.policies |= { + "whitelist": Policy(name="whitelist", allow={"*"}, remark="白名单"), + "blacklist": Policy(name="blacklist", allow=set(), remark="黑名单"), +} + + +@on_startup +async def init_access_control() -> None: + tasks = [access.sync() for access in (Role.roles | Policy.policies).values()] + await asyncio.gather(*tasks) diff --git a/kirami/service/controller.py b/kirami/service/controller.py new file mode 100644 index 0000000..df9f556 --- /dev/null +++ b/kirami/service/controller.py @@ -0,0 +1,218 @@ +"""本模块定义了服务控制器, 用于控制服务的运行""" + +import asyncio +from contextlib import AsyncExitStack +from typing import Annotated, Any + +from nonebot.dependencies import Dependent +from nonebot.exception import SkippedException +from nonebot.message import RUN_PREPCS_PARAMS +from nonebot.typing import _DependentCallable +from nonebot.utils import run_coro_with_catch + +from kirami.depends import depends +from kirami.exception import IgnoredException +from kirami.hook import before_run +from kirami.log import logger +from kirami.matcher import Matcher +from kirami.typing import Bot, Event, GroupMessageEvent, MessageEvent, State + +from .access import Policy, Role +from .limiter import Cooldown, Quota, get_scope_key +from .manager import ServiceManager +from .service import Ability, Service +from .subject import Subjects + +_checkers: set[Dependent[Any]] = set() + + +@before_run +async def service_controller( + matcher: Matcher, + bot: Bot, + event: Event, + state: State, +) -> None: + async with AsyncExitStack() as stack: + coros = [ + run_coro_with_catch( + checker( + matcher=matcher, + bot=bot, + event=event, + state=state, + stack=stack, + dependency_cache={}, + ), + (SkippedException,), + ) + for checker in _checkers + ] + with matcher.ensure_context(bot, event): + try: + await asyncio.gather(*coros) + except IgnoredException as e: + logger.opt(colors=True).debug( + f"{matcher} 服务检查未通过: {e.reason}", + ) + raise + except Exception as e: + logger.opt(colors=True, exception=e).error( + "服务控制器检查时出现意外错误, 运行已取消", + ) + raise + + +T_ServiceChecker = _DependentCallable[Any] + + +def register_checker(checker: T_ServiceChecker) -> None: + """注册服务检查器""" + _checkers.add( + Dependent[bool].parse( + call=checker, + allow_types=RUN_PREPCS_PARAMS, + ) + ) + + +@depends +def useService(matcher: Matcher) -> Service: + return ServiceManager.get_service(matcher) + + +D_Service = Annotated[Service, useService()] + + +@depends +def useAbility(matcher: Matcher) -> Ability: + return Ability.got(matcher) + + +D_Ability = Annotated[Ability, useAbility()] + + +@register_checker +async def event_scope_checker( + event: MessageEvent, service: D_Service, ability: D_Ability +) -> None: + """事件作用域检查""" + + async def check_scope(source: Service | Ability, message_type: str) -> None: + if source.scope != "all" and message_type != source.scope: + raise IgnoredException("事件作用域不一致") + + sources = [service, ability] + tasks = [check_scope(source, event.message_type) for source in sources] + await asyncio.gather(*tasks) + + +@register_checker +async def enabled_checker( + service: D_Service, ability: D_Ability, subjects: Subjects +) -> None: + """服务开关检查""" + if dissbj := ability.get_disabled_subjects(*subjects): + dissbj_str = ", ".join(repr(s) for s in dissbj) + raise IgnoredException(f'功能"{service.name}#{ability.name}"未启用: {dissbj_str}') + if dissbj := service.get_disabled_subjects(*subjects): + dissbj_str = ", ".join(repr(s) for s in dissbj) + raise IgnoredException(f'服务"{service.name}"未启用: {dissbj_str}') + + +@register_checker +async def role_checker( + event: Event, service: D_Service, ability: D_Ability, subjects: Subjects +) -> None: + """角色检查""" + role = Role.roles["normal"] + if isinstance(event, GroupMessageEvent): + sender_role = event.sender.role + sender_role = "normal" if sender_role in ("member", None) else sender_role + role = Role.roles[sender_role] + if uid := getattr(event, "user_id", None): + role = Role.get_user_role(uid, *subjects) or role + if role >= Role.roles[ability.role.user]: + return + if role >= Role.roles[service.role.user]: + return + raise IgnoredException(f"用户角色权限不足, 服务或功能至少需要{role.name}, 当前为{role.name}") + + +@register_checker +async def policy_checker( + service: D_Service, ability: D_Ability, subjects: Subjects +) -> None: + """策略检查""" + policies = Policy.get_policies(*subjects) + if not policies: + return + accesses = {allow for policy in policies for allow in policy.allow} + if ability.name in accesses: + return + if service.name in accesses: + return + raise IgnoredException(f"主体策略没有访问服务或功能的许可: {', '.join(repr(s) for s in subjects)}") + + +@register_checker +async def cooldown_checker( + matcher: Matcher, event: Event, service: D_Service, ability: D_Ability +) -> None: + """冷却时间检查""" + + async def check_cooldown(source: Service | Ability) -> None: + """检查冷却时间是否已过""" + cd_cfg = source.cooldown + if not cd_cfg: + return + cooldown = await Cooldown.get(source.id) or Cooldown( + name=source.id, + scope=cd_cfg.type, + prompt=cd_cfg.prompt, + duration=cd_cfg.time, + ) + if not (key := get_scope_key(event, cd_cfg.type)): + return + if cooldown.check(key): + await cooldown.start(key) + return + if prompt := cooldown.get_prompt(key): + await matcher.send(prompt) + raise IgnoredException("服务或功能正在冷却中") + + sources = [service, ability] + tasks = [check_cooldown(source) for source in sources] + await asyncio.gather(*tasks) + + +@register_checker +async def quota_checker( + matcher: Matcher, event: Event, service: D_Service, ability: D_Ability +) -> None: + """使用次数检查""" + + async def check_quota(source: Service | Ability) -> None: + """检查使用次数是否已达上限""" + qt_cfg = source.quota + if not qt_cfg: + return + quota = await Quota.get(source.id) or Quota( + name=source.id, + scope=qt_cfg.type, + prompt=qt_cfg.prompt, + limit=qt_cfg.limit, + reset_time=Quota.time(**qt_cfg.reset.dict()), + ) + if not (key := get_scope_key(event, qt_cfg.type)): + return + if quota.check(key): + await quota.consume(key) + return + if prompt := quota.get_prompt(key): + await matcher.send(prompt) + raise IgnoredException("服务或功能的配额已达上限") + + sources = [service, ability] + tasks = [check_quota(source) for source in sources] + await asyncio.gather(*tasks) diff --git a/kirami/service/limiter.py b/kirami/service/limiter.py new file mode 100644 index 0000000..3225931 --- /dev/null +++ b/kirami/service/limiter.py @@ -0,0 +1,379 @@ +"""本模块定义了限制器,用于限制用户的行为""" + +import asyncio +import math +import time +from abc import ABC, abstractmethod +from collections import defaultdict +from datetime import datetime, tzinfo +from datetime import time as Time +from enum import Enum +from typing import ClassVar, TypedDict + +from mango import Document, Field +from nonebot.adapters import MessageTemplate as MessageTemplate +from pydantic import BaseModel +from pydantic import Field as PField +from typing_extensions import Self + +from kirami.event import Event +from kirami.hook import on_startup +from kirami.matcher import Matcher +from kirami.utils import get_daily_datetime, human_readable_time + + +class LimitScope(str, Enum): + """限制隔离范围""" + + GLOBAL = "global" + """全局共用""" + GROUP = "group" + """群组内共用""" + USER = "user" + """用户独立""" + LOCAL = "local" + """群组内每个用户独立""" + + +TARGET = { + LimitScope.GLOBAL: "全局", + LimitScope.GROUP: "本群", + LimitScope.USER: "你", + LimitScope.LOCAL: "你", +} + + +def get_scope_key(event: Event, scope: LimitScope = LimitScope.LOCAL) -> str | None: + """获取限制隔离范围键值。 + + ### 参数 + event: 事件对象 + + scope: 限制隔离范围 + """ + if group_id := getattr(event, "group_id", None): + group_id = str(group_id) + if user_id := getattr(event, "user_id", None): + user_id = str(user_id) + + match scope: + case LimitScope.GLOBAL: + return scope.name + case LimitScope.GROUP: + return group_id or user_id + case LimitScope.USER: + return user_id + case _: + return f"{group_id}_{user_id}" if group_id else user_id + + +class Limiter(BaseModel, ABC): + scope: LimitScope = PField(default=LimitScope.LOCAL) + """限制隔离范围""" + prompt: str | None = PField(default=None) + """限制状态提示""" + + @abstractmethod + def get_info(self, key: str) -> TypedDict: + raise NotImplementedError + + def get_prompt(self, key: str, **kwargs) -> str | None: + """获取限制提示。 + + ### 参数 + key: 限制器键值 + + **kwargs: prompt 格式化参数 + """ + if self.prompt is None: + return None + return self.prompt.format(**self.get_info(key), **kwargs) + + +class PersistLimiter(Document, Limiter, ABC): + name: str = Field(primary_key=True) + """限制组名""" + + _instances: ClassVar[set[Self]] = set() + _synced: ClassVar[bool] = False + + def __init__(self, **data) -> None: + super().__init__(**data) + if not self._synced: + self._instances.add(self) + + def __hash__(self) -> int: + return hash(self.name) + + @abstractmethod + async def sync(self) -> None: + """从数据库中同步配置""" + raise NotImplementedError + + +class CooldownInfo(TypedDict): + target: str + """冷却对象""" + duration: int | float + """默认冷却时间""" + remain_time: int + """剩余冷却时间""" + human_remain_time: str + """剩余冷却时间(人类可读)""" + + +class Cooldown(PersistLimiter): + duration: int | float = Field(default=15) + """默认冷却时间""" + expire: defaultdict[str, float] = Field( + default_factory=lambda: defaultdict(float), init=False + ) + """冷却到期时间""" + + async def start(self, key: str, duration: int | float = 0) -> None: + """进入冷却时间。 + + ### 参数 + key: 冷却键值 + + duration: 冷却时间,若为 0 则使用默认冷却时间 + """ + self.expire[key] = time.time() + (duration or self.duration) + await self.save() + + def check(self, key: str) -> bool: + """检查是否冷却中。 + + ### 参数 + key: 冷却键值 + """ + return time.time() >= self.expire[key] + + def get_remain_time(self, key: str) -> int: + """获取剩余冷却时间。 + + ### 参数 + key: 冷却键值 + """ + return math.ceil(self.expire[key] - time.time()) + + def get_info(self, key: str) -> CooldownInfo: + """获取冷却信息。 + + ### 参数 + key: 冷却键值 + """ + remain_time = self.get_remain_time(key) + return { + "target": TARGET[self.scope], + "duration": self.duration, + "remain_time": remain_time, + "human_remain_time": human_readable_time(remain_time), + } + + async def sync(self) -> None: + if cooldown := await self.get(self.name): + self.expire = cooldown.expire + await self.save() + + +class QuotaInfo(TypedDict): + target: str + """配额对象""" + limit: int + """每日配额数量""" + accum: int + """累计消耗配额""" + remain_amount: int + """剩余配额""" + + +class Quota(PersistLimiter): + limit: int = Field(default=3) + """每日配额数量""" + accum: defaultdict[str, int] = Field( + default_factory=lambda: defaultdict(int), init=False + ) + """累计消耗配额""" + reset_time: Time = Field(default_factory=Time, exclude=True) + """重置时间""" + reset_at: datetime | None = Field(default=None, expire=0, init=False) + """下次重置时间""" + + @classmethod + def time( + cls, + hour: int = 0, + minute: int = 0, + second: int = 0, + tzinfo: tzinfo | None = None, + ) -> Time: + """创建重置时间。 + + ### 参数 + hour: 时 + + minute: 分 + + second: 秒 + + tzinfo: 时区 + """ + return Time(hour, minute, second, tzinfo=tzinfo) + + async def consume(self, key: str, amount: int = 1) -> None: + """消耗配额。 + + ### 参数 + key: 配额键值 + + amount: 消耗数量,默认为 1 + """ + self.accum[key] += amount + if self.reset_at is None: + self.reset_at = get_daily_datetime(self.reset_time) + await self.save() + + def check(self, key: str) -> bool: + """检查是否有剩余配额。 + + ### 参数 + key: 配额键值 + """ + return self.accum[key] < self.limit + + def get_accum(self, key: str) -> int: + """获取已消耗的配额。 + + ### 参数 + key: 配额键值 + """ + return self.accum[key] + + def get_info(self, key: str) -> QuotaInfo: + """获取配额信息。 + + ### 参数 + key: 配额键值 + """ + return { + "target": TARGET[self.scope], + "limit": self.limit, + "accum": self.get_accum(key), + "remain_amount": self.limit - self.get_accum(key), + } + + async def reset(self, key: str) -> None: + """重置配额。 + + ### 参数 + key: 配额键值 + """ + self.accum[key] = 0 + await self.save() + + async def reset_all(self) -> None: + """重置所有配额""" + self.accum = defaultdict(int) + await self.save() + + async def sync(self) -> None: + if quota := await self.get(self.name): + self.accum = quota.accum + await self.save() + + +class LockInfo(TypedDict): + target: str + """锁定对象""" + max_count: int + """最大锁定数量""" + count: int + """已锁定数量""" + remain_count: int + """剩余锁定数量""" + + +class Lock(Limiter): + matcher: type[Matcher] + """事件响应器""" + max_count: int = PField(default=1) + """最大锁定数量""" + tasks: defaultdict[str, int] = PField( + default_factory=lambda: defaultdict(int), init=False + ) + """运行中的任务""" + _locked_matchers: ClassVar[dict[str, Self]] = {} + """事件锁定器""" + + @classmethod + def get(cls, id: str) -> Self | None: + """获取事件锁定器。 + + ### 参数 + id: 事件响应器 ID + """ + return cls._locked_matchers.get(id) + + @classmethod + def set(cls, id: str, lock: Self) -> None: + """保存事件锁定器。 + + ### 参数 + id: 事件响应器 ID + + lock: 事件锁定器 + """ + cls._locked_matchers[id] = lock + + def claim(self, key: str) -> None: + """锁定事件。 + + ### 参数 + key: 锁定键值 + """ + self.tasks[key] += 1 + + def unclaim(self, key: str) -> None: + """释放事件。 + + ### 参数 + key: 锁定键值 + """ + self.tasks[key] -= 1 + + def check(self, key: str) -> bool: + """检查是否已到锁定上限。 + + ### 参数 + key: 锁定键值 + """ + return self.get_count(key) < self.max_count + + def get_count(self, key: str) -> int: + """获取已锁定数量。 + + ### 参数 + key: 锁定键值 + """ + return self.tasks[key] + + def get_info(self, key: str) -> LockInfo: + """获取锁定信息。 + + ### 参数 + key: 锁定键值 + """ + return { + "target": TARGET[self.scope], + "max_count": self.max_count, + "count": self.get_count(key), + "remain_count": self.max_count - self.get_count(key), + } + + +@on_startup +async def sync_limiter() -> None: + tasks = [limiter.sync() for limiter in PersistLimiter._instances] + await asyncio.gather(*tasks) + PersistLimiter._synced = True diff --git a/kirami/service/manager.py b/kirami/service/manager.py new file mode 100644 index 0000000..c464a87 --- /dev/null +++ b/kirami/service/manager.py @@ -0,0 +1,259 @@ +"""本模块定义了插件服务化逻辑与流程""" + +import asyncio +import contextlib +import inspect +import locale +from collections.abc import Generator +from dataclasses import asdict +from pathlib import Path +from typing import Any, overload + +import nonebot +from bidict import bidict +from nonebot.matcher import Matcher +from nonebot.plugin import Plugin + +from kirami.exception import ServiceError +from kirami.hook import on_startup +from kirami.log import logger +from kirami.utils import load_data + +from .service import Ability, Service + + +@on_startup +async def init_service() -> None: + order_service = sorted( + Service.sp_map.items(), key=lambda item: _sort_by_position(item[0]) + ) + Service.sp_map = bidict(order_service) + tasks = [service.init() for service in ServiceManager.get_services()] + await asyncio.gather(*tasks) + + +def load_subplugin(parent_plugin: Plugin) -> None: + """加载子服务。 + + ### 参数 + parent_plugin: 父插件 + """ + service = ServiceManager.get_correspond(parent_plugin) + if not (plugin_file := parent_plugin.module.__file__): + return + parent_path = Path(plugin_file).parent + if subplugins := service.extra.get("subplugins"): + if isinstance(subplugins, str): + subplugins = [subplugins] + for subplugin in subplugins: + nonebot.load_plugin(parent_path / subplugin) + if subplugin_dirs := service.extra.get("subplugin_dirs"): + if isinstance(subplugin_dirs, str): + subplugin_dirs = [subplugin_dirs] + nonebot.load_plugins( + *(str((parent_path / spd).resolve()) for spd in subplugin_dirs) + ) + + +def _from_metadata(plugin: Plugin) -> dict[str, Any]: + """从 `module.__plugin_meta__` 中获取服务配置。 + + ### 参数 + plugin: 插件对象 + """ + metadata = plugin.metadata + config = asdict(metadata) if metadata else {} + config |= config.get("extra", {}) + config.setdefault("name", getattr(plugin, "full_name")) + config.setdefault("author", "unknown") + return config + + +def _from_file(plugin: Plugin) -> dict[str, Any]: + """从 `service.toml` 中获取服务配置。 + + ### 参数 + plugin: 插件对象 + """ + file_path = Path(plugin.module.__path__[0]) / "service.toml" + return load_data(file_path)["plugin"] + + +def _sort_by_position(item: Service | Ability) -> tuple[int | float, str]: + """对服务或功能进行排序。 + + ### 参数 + item: 服务或功能对象 + """ + locale.setlocale(locale.LC_ALL, "") + position = item.position if item.position is not None else float("inf") + name_key = locale.strxfrm(item.name) + return (position, name_key) + + +class ServiceManager: + @overload + @classmethod + def get_correspond(cls, item: Plugin) -> Service: + ... + + @overload + @classmethod + def get_correspond(cls, item: Service) -> Plugin: + ... + + @overload + @classmethod + def get_correspond(cls, item: type[Matcher]) -> Ability: + ... + + @overload + @classmethod + def get_correspond(cls, item: Ability) -> type[Matcher]: + ... + + @classmethod + def get_correspond( + cls, item: Plugin | Service | type[Matcher] | Ability + ) -> Plugin | Service | type[Matcher] | Ability: + """获取对应的映射对象。 + + ### 参数 + item: 插件、服务、功能或事件响应器 + """ + if isinstance(item, Plugin): + return Service.got(item) + if isinstance(item, Service): + return item.plugin + if inspect.isclass(item) and issubclass(item, Matcher): + return Ability.got(item) + if isinstance(item, Ability): + return item.matcher + raise TypeError(f"Unsupported type: {type(item)}") + + @classmethod + def load_service(cls, plugin: Plugin) -> Service: + """加载服务。 + + ### 参数 + plugin: 插件对象 + + ### 返回 + 服务对象 + """ + config = _from_metadata(plugin) + try: + with contextlib.suppress(AttributeError, FileNotFoundError): + config |= _from_file(plugin) + except Exception as e: + logger.opt(colors=True, exception=e).error( + f"Loading \"{getattr(plugin, 'full_name')}\" configuration failed! Use default configuration." + ) + config["id"] = f"{config['author']}.{getattr(plugin, 'full_name')}" + service = Service(**config) + service.bind(plugin) + cls.load_abilities(service) + return service + + @classmethod + def load_abilities(cls, service: Service) -> list[Ability]: + """加载功能。 + + ### 参数 + service: 服务对象 + + ### 返回 + 功能列表 + """ + matchers = cls.get_correspond(service).matcher + configs = { + i["name"]: i for i in service.default.get("matchers", []) if "name" in i + } + abilities = {} + for matcher in matchers: + if members := inspect.getmembers( + matcher.module, + lambda x: x is matcher, # noqa: B023 + ): + name, _ = members[0] + else: + try: + name = matcher.handlers[0].call.__name__ + except IndexError: + continue + if name in abilities: + raise ServiceError( + f"Ability name conflict! Duplicate with existing matcher name: {name}" + ) + ability = Ability( + id=f"{service.id}.{name}", **configs.get(name, {"name": name}) # type: ignore + ) + ability.bind(matcher) + abilities[name] = ability + service._abilities = sorted(abilities.values(), key=_sort_by_position) + return service._abilities + + @classmethod + def get_service(cls, key: str | Plugin | Matcher | Ability) -> Service: + """获取服务。 + + ### 参数 + key: 服务名或对应的插件、功能或事件响应器对象 + """ + if isinstance(key, Plugin): + plugin = key + elif isinstance(key, Matcher): + plugin = key.plugin + elif isinstance(key, Ability): + plugin = cls.get_correspond(key).plugin + elif not (plugin := nonebot.get_plugin(key)): + for service in cls.get_services(): + if key in {service.name} | service.alias: + plugin = cls.get_correspond(service) + break + + if not plugin: + raise ServiceError("查找的服务不存在") + + return cls.get_correspond(plugin) + + @classmethod + def get_services(cls, tag: str | None = None) -> Generator[Service, None, None]: + """获取所有服务。 + + ### 参数 + tag: 分类标签,为空则获取所有服务 + + ### 生成 + 服务对象 + """ + for service in Service.sp_map: + if tag and tag in service.tags or not tag: + yield service + + @classmethod + def get_ability(cls, source: Service | Plugin, index: int) -> Ability: + """获取功能。 + + ### 参数 + source: 服务或插件对象 + + index: 功能索引 + """ + if isinstance(source, Plugin): + source = cls.get_correspond(source) + return source.abilities[index - 1] + + @classmethod + def get_abilities(cls, source: Service | Plugin) -> Generator[Ability, None, None]: + """获取所有功能。 + + ### 参数 + source: 服务或插件对象 + + ### 生成 + 功能对象 + """ + if isinstance(source, Plugin): + source = cls.get_correspond(source) + yield from source.abilities diff --git a/kirami/service/service.py b/kirami/service/service.py new file mode 100644 index 0000000..ecbf3d8 --- /dev/null +++ b/kirami/service/service.py @@ -0,0 +1,334 @@ +"""本模块定义了服务与功能配置""" + +import asyncio +from enum import IntEnum, auto +from typing import Any, ClassVar, Literal + +from bidict import bidict +from mango import Document, Field +from nonebot.matcher import Matcher +from nonebot.plugin import Plugin +from pydantic import BaseModel, PrivateAttr, validator +from pydantic.fields import ModelField +from typing_extensions import Self + +from .limiter import LimitScope +from .subject import Subject + + +class State(IntEnum): + """运行状态枚举""" + + NORMAL = auto() + """正常,所有人都可使用""" + SHUTDOWN = auto() + """停用,所有人都无法使用,可能是因为滥用或无法修复的问题""" + MAINT = auto() + """维护,所有人都无法使用,可能是需要维护或正在修复问题""" + DEVELOP = auto() + """开发,仅超级管理员或测试用户可以使用""" + EXCEPTION = auto() + """异常,表示可能会出现问题,但是不影响使用""" + FAULT = auto() + """故障,表示已经无法正常使用,需要修复""" + + +class RoleConfig(BaseModel): + user: str = "normal" + manager: str = "admin" + + +class LimitConfig(BaseModel): + type: LimitScope = LimitScope.LOCAL + """默认为局部限制""" + prompt: str | None = None + """限制状态提示""" + + +class CooldownConfig(LimitConfig): + time: int + """默认为0,即不冷却""" + + +class ResetTime(BaseModel): + hour: int = 0 + minute: int = 0 + second: int = 0 + + +class QuotaConfig(LimitConfig): + limit: int + """默认为0,即不限制""" + reset: ResetTime = Field(default_factory=ResetTime) + """重置时间""" + + +LIMIT_KEY = {CooldownConfig: "time", QuotaConfig: "limit"} + + +class MixinConfig(Document): + enabled: bool = True + """默认启用状态""" + + position: int | None = None + """帮助列表中排序位置""" + + visible: bool = True + """是否在帮助菜单可见""" + + role: RoleConfig = Field(default_factory=RoleConfig) + """ + 角色权限。 + + 可选范围: + - `use`: 使用所需最低权限,默认为成员权限 + - `manage`: 管理所需最低权限,默认为管理员权限 + """ + + scope: Literal["group", "private", "all"] = "all" + """ + 消息作用域,决定事件的响应范围。 + + 可选范围: + - `group`: 只响应群聊消息 + - `private`: 只响应私聊消息 + - `all`: 响应私聊和群聊消息 + """ + + cooldown: CooldownConfig | None = None + """ + 每次使用后的冷却时间,不指定范围则默认为`局部冷却时间`。 + + 可选范围: + - `global`: 全局冷却时间,所有群组和用户的公共冷却时间 + - `local`: 局部冷却时间,群组内的单个成员的独有冷却时间 + - `group`: 群组冷却时间,群组内所有成员的公共冷却时间 + - `user`: 用户冷却时间,单个用户的独有冷却时间 + """ + + quota: QuotaConfig | None = None + """ + 每日可使用的次数,不指定范围则默认为`局部可用次数`。 + + 可选范围: + - `global`: 全局可用次数,所有群组和用户的公共可用次数 + - `local`: 局部可用次数,群组内的单个成员的独有可用次数 + - `group`: 群组可用次数,群组内所有成员的公共可用次数 + - `user`: 用户可用次数,单个用户的独有可用次数 + """ + + state: State = State.NORMAL + """运行状态""" + + status: dict[Subject, bool] = Field(default_factory=dict) + """主体状态列表""" + + @validator("cooldown", "quota", pre=True) + def validate_limiter( + cls, value: int | dict[str, Any] | None, field: ModelField + ) -> Any: + if value is None: + return None + if isinstance(value, int): + return field.type_(**{LIMIT_KEY[field.type_]: value}) + return field.type_(**value) + + async def enable(self, subject: Subject) -> None: + """启用服务或功能。 + + ### 参数 + subject: 主体 + """ + self.status[subject] = True + await self.save() + + async def disable(self, subject: Subject) -> None: + """禁用服务或功能。 + + ### 参数 + subject: 主体 + """ + self.status[subject] = False + await self.save() + + def check_enabled(self, *subjects: Subject) -> bool: + """检查服务或功能是否启用。 + + ### 参数 + *subjects: 主体 + """ + return all(self.status.get(subject, self.enabled) for subject in subjects) + + def get_enabled_subjects(self, *subjects: Subject) -> set[Subject]: + """获取启用的主体列表。 + + ### 参数 + *subjects: 主体 + + ### 返回 + 启用的主体集合 + """ + return { + subject for subject in subjects if self.status.get(subject, self.enabled) + } + + def get_disabled_subjects(self, *subjects: Subject) -> set[Subject]: + """获取禁用的主体列表。 + + ### 参数 + *subjects: 主体 + + ### 返回 + 禁用的主体集合 + """ + return set(subjects) - self.get_enabled_subjects(*subjects) + + +class BaseConfig(Document): + default: dict[str, Any] = Field(default_factory=dict) + """默认配置""" + + def __init__(self, **data: Any) -> None: + super().__init__(**data) + default = data.pop("default", {}) + if not default: + self.default = data + + def __hash__(self) -> int: + return hash(self.id) + + async def sync(self) -> None: + """从数据库中同步配置""" + if not (config := await self.get(self.id)): + return + + await self.update(**config.dict(exclude={"id", "version", "author", "default"})) + + class Meta: + by_alias = True + + +class Ability(BaseConfig, MixinConfig): + """功能配置""" + + id: str = Field(primary_key=True) + """唯一标识符,由插件标识符+功能名构成(eg. akirami.hello-world.hello)""" + name: str = "" + """功能名称""" + command: str = "" + """命令/指令示例""" + description: str = "" + """指令效果说明""" + exception: int = 0 + """异常发生次数""" + + am_map: ClassVar[bidict[Self, type[Matcher]]] = bidict() + """Matcher - Ability 映射表""" + + @property + def matcher(self) -> type[Matcher]: + """事件响应器""" + return self.am_map[self] + + @property + def service(self) -> "Service": + """所属服务""" + if plugin := self.matcher.plugin: + return Service.got(plugin) + raise ValueError("未找到对应的服务") + + @property + def index(self) -> int: + """功能序号""" + return self.service.abilities.index(self) + 1 + + def bind(self, matcher: type[Matcher]) -> None: + """绑定事件响应器。 + + ### 参数 + matcher: 事件响应器 + """ + self.am_map[self] = matcher + + @classmethod + def got(cls, matcher: type[Matcher] | Matcher) -> Self: + """获取对应的功能。 + + ### 参数 + matcher: 事件响应器 + """ + if isinstance(matcher, Matcher): + return cls.am_map.inverse[matcher.__class__] + return cls.am_map.inverse[matcher] + + +class Service(BaseConfig, MixinConfig): + """服务配置""" + + id: str = Field(primary_key=True) + """唯一标识符,由插件作者+插件名构成(eg. akirami.hello-world)""" + name: str + """服务名称""" + alias: set[str] = Field(default_factory=set) + """服务别名""" + summary: str = "" + """插件摘要,简短说明,用于展示在服务列表中""" + description: str = "" + """插件描述,详细介绍,用于展示在详细帮助界面。如果没有提供,则默认使用 summary""" + usage: str = "" + """使用方法""" + version: str = "0.0.0" + """插件版本""" + author: str = "unknown" + """插件作者""" + tags: set[str] = Field(default_factory=set) + """服务分类标签,在批量操作服务时使用,以及在服务列表中显示""" + extra: dict[str, Any] = Field(default_factory=dict) + """额外配置""" + + _abilities: list[Ability] = PrivateAttr(default_factory=list) + + sp_map: ClassVar[bidict[Self, Plugin]] = bidict() + """Service - Plugin 映射表""" + + @validator("id") + def validate_id(cls, value: str) -> str: + return value.lower().replace("_", "-") + + @validator("author") + def validate_author(cls, value: str) -> str: + if " " in value or "." in value: + raise ValueError("作者名不得包含空格或点符号") + return value + + @property + def plugin(self) -> Plugin: + """插件""" + return self.sp_map[self] + + @property + def abilities(self) -> list[Ability]: + """功能组""" + return self._abilities + + def bind(self, plugin: Plugin) -> None: + """绑定插件。 + + ### 参数 + plugin: 插件对象 + """ + self.sp_map[self] = plugin + + @classmethod + def got(cls, plugin: Plugin) -> Self: + """获取对应的服务。 + + ### 参数 + plugin: 插件对象 + """ + return cls.sp_map.inverse[plugin] + + async def init(self) -> None: + """加载配置""" + tasks = [ability.sync() for ability in self.abilities] + await asyncio.gather(self.sync(), *tasks) diff --git a/kirami/service/subject.py b/kirami/service/subject.py new file mode 100644 index 0000000..58b864d --- /dev/null +++ b/kirami/service/subject.py @@ -0,0 +1,113 @@ +"""本模块定义了主体及提取函数""" + +import asyncio +from collections.abc import Callable, Generator +from contextlib import AsyncExitStack +from typing import Annotated + +from nonebot.dependencies import Dependent +from nonebot.exception import SkippedException +from nonebot.message import EVENT_PCS_PARAMS +from nonebot.params import Depends +from nonebot.typing import _DependentCallable +from nonebot.utils import run_coro_with_catch +from typing_extensions import Self + +from kirami.log import logger +from kirami.typing import Bot, Event, GroupMessageEvent, MessageEvent, State + + +class Subject(str): + __slots__ = ("type", "id") + + type: str + """主体类型""" + id: str + """主体标识符,*表示所有""" + + def __new__(cls, type: str = "*", id: str | int = "*") -> Self: + obj = super().__new__(cls, f"{type}:{id}") + obj.type = type + obj.id = str(id) + return obj + + def __repr__(self) -> str: + return f"Subject({super().__repr__()})" + + @classmethod + def __get_validators__(cls) -> Generator[Callable[..., Self], None, None]: + yield cls.validate + + @classmethod + def validate(cls, value: str) -> Self: + if not isinstance(value, str): + raise TypeError("string required") + type, _, id = value.partition(":") + return cls(type, id) + + +T_SubjectExtractor = _DependentCallable[Subject] + +_extractors: set[Dependent[Subject]] = set() + + +def register_extractor(extractor: T_SubjectExtractor) -> T_SubjectExtractor: + """注册主体提取器""" + _extractors.add( + Dependent[Subject].parse( + call=extractor, + allow_types=EVENT_PCS_PARAMS, + ) + ) + return extractor + + +async def extractor_subjects(bot: Bot, event: Event, state: State) -> set[Subject]: + async with AsyncExitStack() as stack: + coros = [ + run_coro_with_catch( + extractor( + bot=bot, + event=event, + state=state, + stack=stack, + dependency_cache={}, + ), + (SkippedException,), + ) + for extractor in _extractors + ] + try: + subjects = await asyncio.gather(*coros) + except Exception as e: + logger.opt(colors=True, exception=e).error( + "提取主体时出现意外错误", + ) + raise + else: + return {subject for subject in subjects if subject} + raise RuntimeError("unreachable") + + +Subjects = Annotated[set[Subject], Depends(extractor_subjects)] +"""主体集合""" + + +@register_extractor +def extract_global() -> Subject: + return Subject("global", "*") + + +@register_extractor +def extract_bot(bot: Bot) -> Subject: + return Subject("bot", bot.self_id) + + +@register_extractor +def extract_user(event: MessageEvent) -> Subject: + return Subject("user", event.user_id) + + +@register_extractor +def extract_group(event: GroupMessageEvent) -> Subject: + return Subject("group", event.group_id) diff --git a/kirami/state.py b/kirami/state.py new file mode 100644 index 0000000..183c441 --- /dev/null +++ b/kirami/state.py @@ -0,0 +1,121 @@ +"""本模块定义了会话状态类""" + +from re import Match +from typing import Any + +from nonebot.adapters.onebot.v11 import Message, MessageSegment +from nonebot.consts import ( + CMD_ARG_KEY, + CMD_KEY, + CMD_START_KEY, + CMD_WHITESPACE_KEY, + ENDSWITH_KEY, + FULLMATCH_KEY, + KEYWORD_KEY, + PREFIX_KEY, + RAW_CMD_KEY, + REGEX_MATCHED, + SHELL_ARGS, + SHELL_ARGV, + STARTSWITH_KEY, +) +from nonebot.exception import ParserExit +from nonebot.rule import Namespace +from nonebot.typing import T_State +from typing_extensions import Self + + +class BaseState(T_State): + def __getattr__(self, name: str, /) -> Any: + if name in self: + item = super().__getitem__(name) + return self.__class__(item) if isinstance(item, dict) else item + return super().__getattribute__(name) + + def __setattr__(self, name: str, value: Any, /) -> None: + return super().__setitem__(name, value) + + def __delattr__(self, name: str, /) -> None: + return super().__delitem__(name) + + def copy(self) -> Self: + return self.__class__(super().copy()) + + +class State(BaseState): + """会话状态""" + + @property + def argot(self) -> dict[str, Any]: + """暗语""" + return self["_argot"] + + @property + def startswith(self) -> str: + """响应触发前缀""" + return self[STARTSWITH_KEY] + + @property + def prefix(self) -> str: + """响应触发前缀""" + return self[STARTSWITH_KEY] + + @property + def endswith(self) -> str: + """响应触发后缀""" + return self[ENDSWITH_KEY] + + @property + def suffix(self) -> str: + """响应触发后缀""" + return self[ENDSWITH_KEY] + + @property + def fullmatch(self) -> str: + """响应触发完整消息""" + return self[FULLMATCH_KEY] + + @property + def keyword(self) -> str: + """响应触发关键字""" + return self[KEYWORD_KEY] + + @property + def matched(self) -> Match[str]: + """正则匹配结果""" + return self[REGEX_MATCHED] + + @property + def shell_args(self) -> Namespace | ParserExit: + """shell 命令解析后的参数字典""" + return self[SHELL_ARGS] + + @property + def shell_argv(self) -> list[str | MessageSegment]: + """shell 命令原始参数列表""" + return self[SHELL_ARGV] + + @property + def command(self) -> tuple[str, ...]: + """消息命令元组""" + return self[PREFIX_KEY][CMD_KEY] + + @property + def raw_command(self) -> str: + """消息命令文本""" + return self[PREFIX_KEY][RAW_CMD_KEY] + + @property + def command_arg(self) -> Message: + """消息命令参数""" + return self[PREFIX_KEY][CMD_ARG_KEY] + + @property + def command_start(self) -> str: + """消息命令开头""" + return self[PREFIX_KEY][CMD_START_KEY] + + @property + def command_whitespace(self) -> str: + """消息命令与参数间空白符""" + return self[PREFIX_KEY][CMD_WHITESPACE_KEY] diff --git a/kirami/typing.py b/kirami/typing.py new file mode 100644 index 0000000..5f5017f --- /dev/null +++ b/kirami/typing.py @@ -0,0 +1,20 @@ +"""本模块集合了 KiramiBot 开发中的一些常用类型""" + +from argparse import Namespace as Namespace + +from flowery.typing import PILImage as PILImage +from httpx import AsyncClient as AsyncClient +from nonebot.adapters import MessageTemplate as MessageTemplate +from nonebot.adapters.onebot.v11 import Bot as Bot +from nonebot.exception import ParserExit as ParserExit +from nonebot.permission import Permission as Permission +from nonebot.plugin import PluginMetadata as PluginMetadata +from nonebot.typing import T_Handler as T_Handler +from nonebot.typing import T_PermissionChecker as T_PermissionChecker +from nonebot.typing import T_RuleChecker as T_RuleChecker + +from kirami.event import * # noqa: F403 # type: ignore +from kirami.matcher import Matcher as Matcher +from kirami.message import Message as Message +from kirami.message import MessageSegment as MessageSegment +from kirami.state import State as State diff --git a/kirami/utils/__init__.py b/kirami/utils/__init__.py new file mode 100644 index 0000000..228ccb6 --- /dev/null +++ b/kirami/utils/__init__.py @@ -0,0 +1,24 @@ +from .downloader import Downloader as Downloader +from .downloader import DownloadProgress as DownloadProgress +from .downloader import File as File +from .helpers import ( + extract_at_users as extract_at_users, +) +from .helpers import ( + extract_image_urls as extract_image_urls, +) +from .helpers import ( + extract_match as extract_match, +) +from .helpers import ( + extract_plain_text as extract_plain_text, +) +from .jsondata import JsonDict as JsonDict +from .jsondata import JsonModel as JsonModel +from .memcache import cache as cache +from .renderer import Renderer as Renderer +from .request import Request as Request +from .resource import Resource as Resource +from .scheduler import scheduler as scheduler +from .utils import * # noqa: F403 +from .webwright import WebWright as WebWright diff --git a/kirami/utils/downloader.py b/kirami/utils/downloader.py new file mode 100644 index 0000000..8f69921 --- /dev/null +++ b/kirami/utils/downloader.py @@ -0,0 +1,130 @@ +from collections.abc import Iterable +from pathlib import Path +from typing import NamedTuple +from urllib.parse import urlparse + +import filetype +from loguru import logger +from rich.panel import Panel +from rich.progress import ( + BarColumn, + DownloadColumn, + Progress, + Task, + TextColumn, + TimeRemainingColumn, + TransferSpeedColumn, +) +from rich.table import Table + +from .request import Request + + +class File(NamedTuple): + """文件信息""" + + path: Path + """路径""" + name: str + """文件名""" + extension: str + """文件扩展名""" + size: int + """文件大小""" + + +class DownloadProgress(Progress): + """下载进度条""" + + STATUS_DL = TextColumn("[blue]Downloading...") + STATUS_FIN = TextColumn("[green]Complete!") + STATUS_ROW = ( + TextColumn("[progress.percentage]{task.percentage:>3.0f}%", justify="center"), + TimeRemainingColumn(compact=True), + ) + PROG_ROW = (DownloadColumn(binary_units=True), BarColumn(), TransferSpeedColumn()) + + def make_tasks_table(self, tasks: Iterable[Task]) -> Table: + table = Table.grid(padding=(0, 1), expand=self.expand) + for task in tasks: + if task.visible: + status = self.STATUS_FIN if task.finished else self.STATUS_DL + itable = Table.grid(padding=(0, 1), expand=self.expand) + itable.add_row(*(column(task) for column in [status, *self.STATUS_ROW])) + itable.add_row(*(column(task) for column in self.PROG_ROW)) + table.add_row(Panel(itable, title=task.description, title_align="left")) + return table + + +class Downloader: + @classmethod + async def download_file( + cls, + url: str, + path: str | Path, + *, + file_name: str | None = None, + file_type: str | None = None, + chunk_size: int | None = None, + **kwargs, + ) -> File: + """下载文件并保存到本地。 + + ### 参数 + url: 文件的下载链接 + + path: 文件的保存路径,可以是文件夹或者文件路径。如果是文件夹,则自动获取文件名,如果是文件路径,则使用指定的文件名 + + file_name: 文件名。不指定则自动获取文件名,优先从 `path` 中获取,如果 `path` 中没有文件名,则从 `url` 中获取 + + file_type: 文件类型。不指定则自动识别文件类型,优先从 `path` 中获取,如果 `path` 中没有文件类型,则从 `url` 中获取 + + chunk_size: 指定文件下载的分块大小,不指定则不进行分块下载,如果文件大小大于 3M,则自动使用分块下载 + + **kwargs: 传递给 `request.Request.stream` 的参数 + + ### 返回 + `File` 对象 + """ + path = Path(path) + if path.suffix: + file_name = file_name or path.stem + path = path.parent + + url_file = Path(urlparse(url).path) + if url_file.suffix: + file_name = file_name or url_file.stem + + if not file_name: + raise ValueError("没有找到文件名") + + path.mkdir(parents=True, exist_ok=True) + + async with Request.stream("GET", url, **kwargs) as response: + file_extension = file_type or path.suffix or url_file.suffix + if content_type := filetype.get_type(response.headers["Content-Type"]): + file_extension = file_extension or content_type.extension + if not file_extension: + raise ValueError(f"{url} 无法确定文件类型") + file_extension = file_extension.split(".")[-1] + + file_size = int(response.headers["Content-Length"]) + if file_size > 1024**2 * 3 and not chunk_size: + chunk_size = 1024 * 4 + + file = f"{file_name}.{file_extension}" + file_path = path / file + + logger.debug(f'开始下载 "{file}", 下载地址: {url}') + + with file_path.open("wb") as f, DownloadProgress() as progress: + download_task = progress.add_task(file, total=file_size) + async for data in response.aiter_bytes(chunk_size): + f.write(data) + progress.update( + download_task, completed=response.num_bytes_downloaded + ) + + logger.debug(f'"{file}" 下载完成, 保存路径: {file_path.absolute()}') + + return File(file_path, file_name, file_extension, file_size) diff --git a/kirami/utils/helpers.py b/kirami/utils/helpers.py new file mode 100644 index 0000000..a9a4d6d --- /dev/null +++ b/kirami/utils/helpers.py @@ -0,0 +1,74 @@ +"""本模块提供了一些常用的工具函数""" + +from collections.abc import Callable +from typing import TypeVar + +from nonebot.adapters.onebot.v11 import Message, MessageEvent +from nonebot.matcher import Matcher + +T = TypeVar("T") + + +async def extract_match( + extract_func: Callable[..., T], + event: MessageEvent, + matcher: Matcher, + prompt: str | None = None, + from_reply: bool = False, +) -> T: + """从消息中提取指定内容。 + + ### 参数 + extract_func: 提取函数 + + event: 消息事件 + + matcher: 事件响应器 + + prompt: 当提取为空时的提示 + + from_reply: 是否从回复消息中提取 + """ + message = reply.message if (reply := event.reply) and from_reply else event.message + result = extract_func(message) + if not result and prompt: + await matcher.finish(prompt) + return result + + +def extract_image_urls(message: Message) -> list[str]: + """提取消息中的图片链接。 + + ### 参数 + message: 消息对象 + + ### 返回 + 图片链接列表 + """ + return [ + segment.data["url"] for segment in message["image"] if "url" in segment.data + ] + + +def extract_at_users(message: Message) -> list[str]: + """提取消息中提及的用户。 + + ### 参数 + message: 消息对象 + + ### 返回 + 提及用户列表 + """ + return [segment.data["qq"] for segment in message["at"]] + + +def extract_plain_text(message: Message) -> str: + """提取消息中纯文本消息。 + + ### 参数 + message: 消息对象 + + ### 返回 + 艾特用户列表 + """ + return message.extract_plain_text().strip() diff --git a/kirami/utils/jsondata.py b/kirami/utils/jsondata.py new file mode 100644 index 0000000..478fb18 --- /dev/null +++ b/kirami/utils/jsondata.py @@ -0,0 +1,176 @@ +"""本模块提供了 json 字典和 json 模型""" + +import json +from pathlib import Path +from typing import Any, ClassVar + +from pydantic import BaseModel, PrivateAttr, root_validator +from typing_extensions import Self + +from kirami.config import DATA_DIR +from kirami.exception import FileNotExistError, ReadFileError + + +class JsonDict(dict[str, Any]): + _file_path: Path + _auto_load: bool + _initial_data: dict[str, Any] + + def __init__( + self, + data: dict[str, Any] | None = None, + /, + *, + path: str | Path = DATA_DIR, + auto_load: bool = False, + ) -> None: + """创建 json 数据字典 + + ### 参数 + data: json 数据 + + path: 文件路径 + + auto_load: 是否自动加载文件 + """ + self._file_path = Path(path) + self._auto_load = auto_load + self._initial_data = data.copy() if data else {} + + if auto_load and self._file_path.is_file(): + json_data = json.loads(self._file_path.read_text("utf-8")) + else: + json_data = self._initial_data + self.file_path.parent.mkdir(parents=True, exist_ok=True) + + super().__init__(**json_data) + + @property + def file_path(self) -> Path: + """文件路径""" + return self._file_path + + def load(self) -> None: + """从文件加载数据""" + if self._auto_load: + raise RuntimeError("Auto load is enabled, cannot load manually.") + if not self._file_path.is_file(): + raise FileNotFoundError(self._file_path) + self.update(json.loads(self._file_path.read_text("utf-8"))) + + def save(self) -> None: + """保存数据到文件""" + self.file_path.write_text(json.dumps(self)) + + def clear(self) -> None: + """清除全部数据""" + super().clear() + self.save() + + def delete(self) -> None: + """删除文件""" + super().clear() + self.file_path.unlink(missing_ok=True) + + def reset(self) -> None: + """重置数据""" + super().clear() + self.update(self._initial_data) + self.save() + + +class JsonModel(BaseModel): + """json 模型""" + + _file_path: ClassVar[Path] + _auto_load: ClassVar[bool] + _scatter_fields: ClassVar[list[str]] + _initial_data: dict[str, Any] = PrivateAttr() + + def __init_subclass__( + cls, + path: str | Path = DATA_DIR, + auto_load: bool = False, + ) -> None: + cls._file_path = Path(path) / f"{cls.__name__.lower()}.json" + cls._auto_load = auto_load + scatter_fields = [] + for field in cls.__fields__.values(): + if field.field_info.extra.get("scatter", False): + scatter_fields.append(field.name) + field.field_info.allow_mutation = False + cls._scatter_fields = scatter_fields + if cls._auto_load and cls._scatter_fields: + raise ValueError("auto_load and scatter fields cannot be used together.") + return super().__init_subclass__() + + def __init__(self, **data: Any) -> None: + super().__init__(**data) + self.file_path.parent.mkdir(parents=True, exist_ok=True) + self._initial_data = self.dict() + + @root_validator(pre=True) + def _load_file(cls, values: dict[str, Any]) -> dict[str, Any]: + if cls._auto_load and cls._file_path.is_file(): + return json.loads(cls._file_path.read_text("utf-8")) + return values + + @property + def file_path(self) -> Path: + """文件路径""" + file_path = self.__class__._file_path + if self.__class__._scatter_fields: + return file_path.with_suffix("") / f"{self.scatter_key}.json" + return file_path + + @property + def scatter_key(self) -> str: + """离散键""" + return "_".join( + str(getattr(self, field)) for field in self.__class__._scatter_fields + ) + + @classmethod + def load(cls, scatter_key: str | None = None) -> Self: + """加载数据。 + + ### 参数 + scatter_key: 离散键 + """ + if cls._auto_load: + raise ReadFileError("Auto load is enabled, cannot load manually.") + if scatter_key: + file_path = cls._file_path.with_suffix("") / f"{scatter_key}.json" + else: + file_path = cls._file_path + if file_path.is_file(): + return cls(**json.loads(file_path.read_text("utf-8"))) + raise FileNotExistError + + @classmethod + def load_all(cls) -> list[Self]: + """加载全部数据""" + if cls._auto_load: + raise ReadFileError("Auto load is enabled, cannot load manually.") + if not cls._scatter_fields: + raise ReadFileError("No scatter fields.") + return [ + cls.load(file.name) + for file in cls._file_path.with_suffix("").glob("*.json") + ] + + def save(self) -> None: + """保存数据到文件""" + self.file_path.write_text(json.dumps(self.dict())) + + def delete(self) -> None: + """删除文件""" + self.file_path.unlink(missing_ok=True) + + def reset(self) -> None: + """重置数据""" + for field, value in self._initial_data.items(): + setattr(self, field, value) + + class Config: + validate_assignment = True diff --git a/kirami/utils/memcache.py b/kirami/utils/memcache.py new file mode 100644 index 0000000..a0ab244 --- /dev/null +++ b/kirami/utils/memcache.py @@ -0,0 +1,6 @@ +"""本模块提供了一个memcache的实例,用于缓存数据""" + +from cashews.wrapper import Cache + +cache = Cache("kirami") +cache.setup("mem://") diff --git a/kirami/utils/renderer/__init__.py b/kirami/utils/renderer/__init__.py new file mode 100644 index 0000000..ec326ac --- /dev/null +++ b/kirami/utils/renderer/__init__.py @@ -0,0 +1,115 @@ +"""本模块提供了一个渲染器,用于将 jinja2 模板和 markdown 渲染为 html""" + +from collections.abc import Sequence +from pathlib import Path +from typing import ClassVar, Literal + +from jinja2 import Environment +from markdown_it import MarkdownIt +from markdown_it.common.utils import escapeHtml, unescapeAll +from markdown_it.renderer import RendererHTML +from markdown_it.token import Token +from markdown_it.utils import EnvType, OptionsDict +from mdit_py_emoji import emoji_plugin +from mdit_py_plugins.dollarmath.index import dollarmath_plugin +from mdit_py_plugins.tasklists import tasklists_plugin +from pygments import highlight +from pygments.formatters import HtmlFormatter +from pygments.lexers import get_lexer_by_name + + +class CustomRendererHTML(RendererHTML): + def fence( + self, tokens: Sequence[Token], idx: int, _options: OptionsDict, env: EnvType + ) -> str: + token = tokens[idx] + info = unescapeAll(token.info).strip() if token.info else "" + langName = info.split(maxsplit=1)[0] if info else "" + + lexer = get_lexer_by_name(langName) + formatter = HtmlFormatter(style=env.get("theme") or "default") + + return highlight(token.content, lexer, formatter) or escapeHtml(token.content) + + +class Renderer: + env: ClassVar[Environment] = Environment(autoescape=True, enable_async=True) + md: ClassVar[MarkdownIt] = ( + MarkdownIt("gfm-like", renderer_cls=CustomRendererHTML) + .use(emoji_plugin) + .use(tasklists_plugin) + .use(dollarmath_plugin) + ) + + @classmethod + def _get_string(cls, fs: str | Path) -> str: + file = Path(fs) if isinstance(fs, str) else fs + if file.is_file() or isinstance(fs, Path): + return file.read_text(encoding="utf-8") + return fs + + @classmethod + async def template( + cls, tpl: str | Path, *, env: Environment | None = None, **kwargs + ) -> str: + """将 jinja2 模板渲染为 html。 + + ### 参数 + tpl: 模板文件路径或字符串 + + env: jinja2 环境,默认为类属性 env + + **kwargs: 传递给渲染后的 html 的参数 + + ### 返回 + html 字符串 + """ + string = cls._get_string(tpl) + env = env or cls.env + template = env.from_string(string) + return await template.render_async(**kwargs) + + @classmethod + async def markdown( + cls, + md: str | Path, + theme: Literal["light", "dark"] = "light", + highlight: str | None = "auto", + extra: str = "", + only_md: bool = False, + **kwargs, + ) -> str: + """将 markdown 渲染为 html。 + + ### 参数 + md: markdown 文件路径或字符串 + + theme: 主题,可选值为 "light" 或 "dark",默认为 "light" + + highlight: 代码高亮主题,可选值为 "auto" 或 pygments 支持的主题,默认为 "auto" + + extra: 额外的 head 标签内容,可以是 meta 标签、link 标签、script 标签、style 标签等 + + only_md: 是否只渲染 markdown,不渲染为完整的 html + + **kwargs: 传递给渲染后的 html 的参数 + + ### 返回 + html 字符串 + """ + string = cls._get_string(md) + env = {} + if highlight == "auto": + env["theme"] = "xcode" if theme == "light" else "lightbulb" + html = cls.md.render(string, env=env) + if only_md: + return await cls.template(html, **kwargs) + base_path = Path(__file__).parent / "template" + return await cls.template( + base_path / "markdown.html", + markdown=html, + theme=theme, + extra=extra, + base_path=base_path, + **kwargs, + ) diff --git a/kirami/utils/renderer/template/github-markdown.css b/kirami/utils/renderer/template/github-markdown.css new file mode 100644 index 0000000..8c93e57 --- /dev/null +++ b/kirami/utils/renderer/template/github-markdown.css @@ -0,0 +1,969 @@ +[data-color-mode="dark"] { + /*dark_dimmed*/ + color-scheme: dark; + --color-fg-default: #adbac7; + --color-fg-muted: #768390; + --color-fg-subtle: #636e7b; + --color-canvas-default: #22272e; + --color-canvas-subtle: #2d333b; + --color-border-default: #444c56; + --color-border-muted: #373e47; + --color-neutral-muted: rgba(99, 110, 123, 0.4); + --color-accent-fg: #539bf5; + --color-accent-emphasis: #316dca; + --color-attention-subtle: rgba(174, 124, 20, 0.15); + --color-danger-fg: #e5534b; +} + +[data-color-mode="light"] { + /*light*/ + color-scheme: light; + --color-fg-default: #1F2328; + --color-fg-muted: #656d76; + --color-fg-subtle: #6e7781; + --color-canvas-default: #ffffff; + --color-canvas-subtle: #f6f8fa; + --color-border-default: #d0d7de; + --color-border-muted: hsla(210, 18%, 87%, 1); + --color-neutral-muted: rgba(175, 184, 193, 0.2); + --color-accent-fg: #0969da; + --color-accent-emphasis: #0969da; + --color-attention-subtle: #fff8c5; + --color-danger-fg: #d1242f; +} + +body { + box-sizing: border-box; + margin: 0 auto; +} + +.markdown-body { + -ms-text-size-adjust: 100%; + -webkit-text-size-adjust: 100%; + margin: 0; + color: var(--fgColor-default, var(--color-fg-default)); + background-color: var(--bgColor-default, var(--color-canvas-default)); + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji"; + font-size: 16px; + line-height: 1.5; + word-wrap: break-word; + padding: 2rem; +} + +.markdown-body .octicon { + display: inline-block; + fill: currentColor; + vertical-align: text-bottom; +} + +.markdown-body h1:hover .anchor .octicon-link:before, +.markdown-body h2:hover .anchor .octicon-link:before, +.markdown-body h3:hover .anchor .octicon-link:before, +.markdown-body h4:hover .anchor .octicon-link:before, +.markdown-body h5:hover .anchor .octicon-link:before, +.markdown-body h6:hover .anchor .octicon-link:before { + width: 16px; + height: 16px; + content: ' '; + display: inline-block; + background-color: currentColor; + -webkit-mask-image: url("data:image/svg+xml,"); + mask-image: url("data:image/svg+xml,"); +} + +.markdown-body details, +.markdown-body figcaption, +.markdown-body figure { + display: block; +} + +.markdown-body summary { + display: list-item; +} + +.markdown-body [hidden] { + display: none !important; +} + +.markdown-body a { + background-color: transparent; + color: var(--fgColor-accent, var(--color-accent-fg)); + text-decoration: none; +} + +.markdown-body abbr[title] { + border-bottom: none; + text-decoration: underline dotted; +} + +.markdown-body b, +.markdown-body strong { + font-weight: var(--base-text-weight-semibold, 600); +} + +.markdown-body dfn { + font-style: italic; +} + +.markdown-body h1 { + margin: .67em 0; + font-weight: var(--base-text-weight-semibold, 600); + padding-bottom: .3em; + font-size: 2em; + border-bottom: 1px solid var(--borderColor-muted, var(--color-border-muted)); +} + +.markdown-body mark { + background-color: var(--bgColor-attention-muted, var(--color-attention-subtle)); + color: var(--fgColor-default, var(--color-fg-default)); +} + +.markdown-body small { + font-size: 90%; +} + +.markdown-body sub, +.markdown-body sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +.markdown-body sub { + bottom: -0.25em; +} + +.markdown-body sup { + top: -0.5em; +} + +.markdown-body img { + border-style: none; + max-width: 100%; + box-sizing: content-box; + background-color: var(--bgColor-default, var(--color-canvas-default)); +} + +.markdown-body code, +.markdown-body kbd, +.markdown-body pre, +.markdown-body samp { + font-family: monospace; + font-size: 1em; +} + +.markdown-body figure { + margin: 1em 40px; +} + +.markdown-body hr { + box-sizing: content-box; + overflow: hidden; + background: transparent; + border-bottom: 1px solid var(--borderColor-muted, var(--color-border-muted)); + height: .25em; + padding: 0; + margin: 24px 0; + background-color: var(--borderColor-default, var(--color-border-default)); + border: 0; +} + +.markdown-body input { + font: inherit; + margin: 0; + overflow: visible; + font-family: inherit; + font-size: inherit; + line-height: inherit; +} + +.markdown-body [type=button], +.markdown-body [type=reset], +.markdown-body [type=submit] { + -webkit-appearance: button; + appearance: auto; +} + +.markdown-body [type=checkbox], +.markdown-body [type=radio] { + box-sizing: border-box; + padding: 0; +} + +.markdown-body [type=number]::-webkit-inner-spin-button, +.markdown-body [type=number]::-webkit-outer-spin-button { + height: auto; +} + +.markdown-body [type=search]::-webkit-search-cancel-button, +.markdown-body [type=search]::-webkit-search-decoration { + -webkit-appearance: none; +} + +.markdown-body ::-webkit-input-placeholder { + color: inherit; + opacity: .54; +} + +.markdown-body ::-webkit-file-upload-button { + -webkit-appearance: button; + font: inherit; +} + +.markdown-body a:hover { + text-decoration: underline; +} + +.markdown-body ::placeholder { + color: var(--fgColor-muted, var(--color-fg-subtle)); + opacity: 1; +} + +.markdown-body hr::before { + display: table; + content: ""; +} + +.markdown-body hr::after { + display: table; + clear: both; + content: ""; +} + +.markdown-body table { + border-spacing: 0; + border-collapse: collapse; + display: block; + width: max-content; + max-width: 100%; + overflow: auto; +} + +.markdown-body td, +.markdown-body th { + padding: 0; +} + +.markdown-body details summary { + cursor: pointer; +} + +.markdown-body details:not([open])>*:not(summary) { + display: none !important; +} + +.markdown-body a:focus, +.markdown-body [role=button]:focus, +.markdown-body input[type=radio]:focus, +.markdown-body input[type=checkbox]:focus { + outline: 2px solid var(--focus-outlineColor, var(--color-accent-fg)); + outline-offset: -2px; + box-shadow: none; +} + +.markdown-body a:focus:not(:focus-visible), +.markdown-body [role=button]:focus:not(:focus-visible), +.markdown-body input[type=radio]:focus:not(:focus-visible), +.markdown-body input[type=checkbox]:focus:not(:focus-visible) { + outline: solid 1px transparent; +} + +.markdown-body a:focus-visible, +.markdown-body [role=button]:focus-visible, +.markdown-body input[type=radio]:focus-visible, +.markdown-body input[type=checkbox]:focus-visible { + outline: 2px solid var(--focus-outlineColor, var(--color-accent-fg)); + outline-offset: -2px; + box-shadow: none; +} + +.markdown-body a:not([class]):focus, +.markdown-body a:not([class]):focus-visible, +.markdown-body input[type=radio]:focus, +.markdown-body input[type=radio]:focus-visible, +.markdown-body input[type=checkbox]:focus, +.markdown-body input[type=checkbox]:focus-visible { + outline-offset: 0; +} + +.markdown-body kbd { + display: inline-block; + padding: 3px 5px; + font: 11px ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace; + line-height: 10px; + color: var(--fgColor-default, var(--color-fg-default)); + vertical-align: middle; + background-color: var(--bgColor-muted, var(--color-canvas-subtle)); + border: solid 1px var(--borderColor-neutral-muted, var(--color-neutral-muted)); + border-bottom-color: var(--borderColor-neutral-muted, var(--color-neutral-muted)); + border-radius: 6px; + box-shadow: inset 0 -1px 0 var(--borderColor-neutral-muted, var(--color-neutral-muted)); +} + +.markdown-body h1, +.markdown-body h2, +.markdown-body h3, +.markdown-body h4, +.markdown-body h5, +.markdown-body h6 { + margin-top: 24px; + margin-bottom: 16px; + font-weight: var(--base-text-weight-semibold, 600); + line-height: 1.25; +} + +.markdown-body h2 { + font-weight: var(--base-text-weight-semibold, 600); + padding-bottom: .3em; + font-size: 1.5em; + border-bottom: 1px solid var(--borderColor-muted, var(--color-border-muted)); +} + +.markdown-body h3 { + font-weight: var(--base-text-weight-semibold, 600); + font-size: 1.25em; +} + +.markdown-body h4 { + font-weight: var(--base-text-weight-semibold, 600); + font-size: 1em; +} + +.markdown-body h5 { + font-weight: var(--base-text-weight-semibold, 600); + font-size: .875em; +} + +.markdown-body h6 { + font-weight: var(--base-text-weight-semibold, 600); + font-size: .85em; + color: var(--fgColor-muted, var(--color-fg-muted)); +} + +.markdown-body p { + margin-top: 0; + margin-bottom: 10px; +} + +.markdown-body blockquote { + margin: 0; + padding: 0 1em; + color: var(--fgColor-muted, var(--color-fg-muted)); + border-left: .25em solid var(--borderColor-default, var(--color-border-default)); +} + +.markdown-body ul, +.markdown-body ol { + margin-top: 0; + margin-bottom: 0; + padding-left: 2em; +} + +.markdown-body ol ol, +.markdown-body ul ol { + list-style-type: lower-roman; +} + +.markdown-body ul ul ol, +.markdown-body ul ol ol, +.markdown-body ol ul ol, +.markdown-body ol ol ol { + list-style-type: lower-alpha; +} + +.markdown-body dd { + margin-left: 0; +} + +.markdown-body tt, +.markdown-body code, +.markdown-body samp { + font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace; + font-size: 12px; +} + +.markdown-body pre { + margin-top: 0; + margin-bottom: 0; + font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace; + font-size: 12px; + word-wrap: normal; + white-space: pre-wrap; +} + +.markdown-body .octicon { + display: inline-block; + overflow: visible !important; + vertical-align: text-bottom; + fill: currentColor; +} + +.markdown-body input::-webkit-outer-spin-button, +.markdown-body input::-webkit-inner-spin-button { + margin: 0; + -webkit-appearance: none; + appearance: none; +} + +.markdown-body::before { + display: table; + content: ""; +} + +.markdown-body::after { + display: table; + clear: both; + content: ""; +} + +.markdown-body>*:first-child { + margin-top: 0 !important; +} + +.markdown-body>*:last-child { + margin-bottom: 0 !important; +} + +.markdown-body a:not([href]) { + color: inherit; + text-decoration: none; +} + +.markdown-body .absent { + color: var(--fgColor-danger, var(--color-danger-fg)); +} + +.markdown-body .anchor { + float: left; + padding-right: 4px; + margin-left: -20px; + line-height: 1; +} + +.markdown-body .anchor:focus { + outline: none; +} + +.markdown-body p, +.markdown-body blockquote, +.markdown-body ul, +.markdown-body ol, +.markdown-body dl, +.markdown-body table, +.markdown-body pre, +.markdown-body details { + margin-top: 0; + margin-bottom: 16px; +} + +.markdown-body blockquote>:first-child { + margin-top: 0; +} + +.markdown-body blockquote>:last-child { + margin-bottom: 0; +} + +.markdown-body h1 .octicon-link, +.markdown-body h2 .octicon-link, +.markdown-body h3 .octicon-link, +.markdown-body h4 .octicon-link, +.markdown-body h5 .octicon-link, +.markdown-body h6 .octicon-link { + color: var(--fgColor-default, var(--color-fg-default)); + vertical-align: middle; + visibility: hidden; +} + +.markdown-body h1:hover .anchor, +.markdown-body h2:hover .anchor, +.markdown-body h3:hover .anchor, +.markdown-body h4:hover .anchor, +.markdown-body h5:hover .anchor, +.markdown-body h6:hover .anchor { + text-decoration: none; +} + +.markdown-body h1:hover .anchor .octicon-link, +.markdown-body h2:hover .anchor .octicon-link, +.markdown-body h3:hover .anchor .octicon-link, +.markdown-body h4:hover .anchor .octicon-link, +.markdown-body h5:hover .anchor .octicon-link, +.markdown-body h6:hover .anchor .octicon-link { + visibility: visible; +} + +.markdown-body h1 tt, +.markdown-body h1 code, +.markdown-body h2 tt, +.markdown-body h2 code, +.markdown-body h3 tt, +.markdown-body h3 code, +.markdown-body h4 tt, +.markdown-body h4 code, +.markdown-body h5 tt, +.markdown-body h5 code, +.markdown-body h6 tt, +.markdown-body h6 code { + padding: 0 .2em; + font-size: inherit; +} + +.markdown-body summary h1, +.markdown-body summary h2, +.markdown-body summary h3, +.markdown-body summary h4, +.markdown-body summary h5, +.markdown-body summary h6 { + display: inline-block; +} + +.markdown-body summary h1 .anchor, +.markdown-body summary h2 .anchor, +.markdown-body summary h3 .anchor, +.markdown-body summary h4 .anchor, +.markdown-body summary h5 .anchor, +.markdown-body summary h6 .anchor { + margin-left: -40px; +} + +.markdown-body summary h1, +.markdown-body summary h2 { + padding-bottom: 0; + border-bottom: 0; +} + +.markdown-body ul.no-list, +.markdown-body ol.no-list { + padding: 0; + list-style-type: none; +} + +.markdown-body ol[type="a s"] { + list-style-type: lower-alpha; +} + +.markdown-body ol[type="A s"] { + list-style-type: upper-alpha; +} + +.markdown-body ol[type="i s"] { + list-style-type: lower-roman; +} + +.markdown-body ol[type="I s"] { + list-style-type: upper-roman; +} + +.markdown-body ol[type="1"] { + list-style-type: decimal; +} + +.markdown-body div>ol:not([type]) { + list-style-type: decimal; +} + +.markdown-body ul ul, +.markdown-body ul ol, +.markdown-body ol ol, +.markdown-body ol ul { + margin-top: 0; + margin-bottom: 0; +} + +.markdown-body li>p { + margin-top: 16px; +} + +.markdown-body li+li { + margin-top: .25em; +} + +.markdown-body dl { + padding: 0; +} + +.markdown-body dl dt { + padding: 0; + margin-top: 16px; + font-size: 1em; + font-style: italic; + font-weight: var(--base-text-weight-semibold, 600); +} + +.markdown-body dl dd { + padding: 0 16px; + margin-bottom: 16px; +} + +.markdown-body table th { + font-weight: var(--base-text-weight-semibold, 600); +} + +.markdown-body table th, +.markdown-body table td { + padding: 6px 13px; + border: 1px solid var(--borderColor-default, var(--color-border-default)); +} + +.markdown-body table td>:last-child { + margin-bottom: 0; +} + +.markdown-body table tr { + background-color: var(--bgColor-default, var(--color-canvas-default)); + border-top: 1px solid var(--borderColor-muted, var(--color-border-muted)); +} + +.markdown-body table tr:nth-child(2n) { + background-color: var(--bgColor-muted, var(--color-canvas-subtle)); +} + +.markdown-body table img { + background-color: transparent; +} + +.markdown-body img[align=right] { + padding-left: 20px; +} + +.markdown-body img[align=left] { + padding-right: 20px; +} + +.markdown-body .emoji { + max-width: none; + vertical-align: text-top; + background-color: transparent; +} + +.markdown-body span.frame { + display: block; + overflow: hidden; +} + +.markdown-body span.frame>span { + display: block; + float: left; + width: auto; + padding: 7px; + margin: 13px 0 0; + overflow: hidden; + border: 1px solid var(--borderColor-default, var(--color-border-default)); +} + +.markdown-body span.frame span img { + display: block; + float: left; +} + +.markdown-body span.frame span span { + display: block; + padding: 5px 0 0; + clear: both; + color: var(--fgColor-default, var(--color-fg-default)); +} + +.markdown-body span.align-center { + display: block; + overflow: hidden; + clear: both; +} + +.markdown-body span.align-center>span { + display: block; + margin: 13px auto 0; + overflow: hidden; + text-align: center; +} + +.markdown-body span.align-center span img { + margin: 0 auto; + text-align: center; +} + +.markdown-body span.align-right { + display: block; + overflow: hidden; + clear: both; +} + +.markdown-body span.align-right>span { + display: block; + margin: 13px 0 0; + overflow: hidden; + text-align: right; +} + +.markdown-body span.align-right span img { + margin: 0; + text-align: right; +} + +.markdown-body span.float-left { + display: block; + float: left; + margin-right: 13px; + overflow: hidden; +} + +.markdown-body span.float-left span { + margin: 13px 0 0; +} + +.markdown-body span.float-right { + display: block; + float: right; + margin-left: 13px; + overflow: hidden; +} + +.markdown-body span.float-right>span { + display: block; + margin: 13px auto 0; + overflow: hidden; + text-align: right; +} + +.markdown-body code, +.markdown-body tt { + padding: .2em .4em; + margin: 0; + font-size: 85%; + white-space: break-spaces; + background-color: var(--bgColor-neutral-muted, var(--color-neutral-muted)); + border-radius: 6px; +} + +.markdown-body code br, +.markdown-body tt br { + display: none; +} + +.markdown-body del code { + text-decoration: inherit; +} + +.markdown-body samp { + font-size: 85%; +} + +.markdown-body pre code { + font-size: 100%; +} + +.markdown-body pre>code { + padding: 0; + margin: 0; + word-break: normal; + white-space: pre; + background: transparent; + border: 0; +} + +.markdown-body .highlight { + margin-bottom: 16px; +} + +.markdown-body .highlight pre { + margin-bottom: 0; + word-break: normal; +} + +.markdown-body .highlight pre, +.markdown-body pre { + padding: 16px; + overflow: auto; + font-size: 85%; + line-height: 1.45; + color: var(--fgColor-default, var(--color-fg-default)); + background-color: var(--bgColor-muted, var(--color-canvas-subtle)); + border-radius: 6px; +} + +.markdown-body pre code, +.markdown-body pre tt { + display: inline; + max-width: auto; + padding: 0; + margin: 0; + overflow: visible; + line-height: inherit; + word-wrap: normal; + background-color: transparent; + border: 0; +} + +.markdown-body .csv-data td, +.markdown-body .csv-data th { + padding: 5px; + overflow: hidden; + font-size: 12px; + line-height: 1; + text-align: left; + white-space: nowrap; +} + +.markdown-body .csv-data .blob-num { + padding: 10px 8px 9px; + text-align: right; + background: var(--bgColor-default, var(--color-canvas-default)); + border: 0; +} + +.markdown-body .csv-data tr { + border-top: 0; +} + +.markdown-body .csv-data th { + font-weight: var(--base-text-weight-semibold, 600); + background: var(--bgColor-muted, var(--color-canvas-subtle)); + border-top: 0; +} + +.markdown-body [data-footnote-ref]::before { + content: "["; +} + +.markdown-body [data-footnote-ref]::after { + content: "]"; +} + +.markdown-body .footnotes { + font-size: 12px; + color: var(--fgColor-muted, var(--color-fg-muted)); + border-top: 1px solid var(--borderColor-default, var(--color-border-default)); +} + +.markdown-body .footnotes ol { + padding-left: 16px; +} + +.markdown-body .footnotes ol ul { + display: inline-block; + padding-left: 16px; + margin-top: 16px; +} + +.markdown-body .footnotes li { + position: relative; +} + +.markdown-body .footnotes li:target::before { + position: absolute; + top: -8px; + right: -8px; + bottom: -8px; + left: -24px; + pointer-events: none; + content: ""; + border: 2px solid var(--borderColor-accent-emphasis, var(--color-accent-emphasis)); + border-radius: 6px; +} + +.markdown-body .footnotes li:target { + color: var(--fgColor-default, var(--color-fg-default)); +} + +.markdown-body .footnotes .data-footnote-backref g-emoji { + font-family: monospace; +} + +.markdown-body [popover] { + background-color: canvas; + border: initial solid; + border-color: initial; + border-image: initial; + color: initial; + height: fit-content; + inset: 0; + margin: auto; + overflow: auto; + padding: .25em; + position: fixed; + width: fit-content; + z-index: 2147483647; +} + +.markdown-body [popover]:not(.\:popover-open) { + display: none; +} + +.markdown-body [popover]:is(dialog[open]) { + display: revert; +} + +.markdown-body [anchor].\:popover-open { + inset: auto; +} + +.markdown-body g-emoji { + display: inline-block; + min-width: 1ch; + font-family: "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; + font-size: 1em; + font-style: normal !important; + font-weight: var(--base-text-weight-normal, 400); + line-height: 1; + vertical-align: -0.075em; +} + +.markdown-body g-emoji img { + width: 1em; + height: 1em; +} + +.markdown-body .task-list-item { + list-style-type: none; +} + +.markdown-body .task-list-item label { + font-weight: var(--base-text-weight-normal, 400); +} + +.markdown-body .task-list-item.enabled label { + cursor: pointer; +} + +.markdown-body .task-list-item+.task-list-item { + margin-top: 4px; +} + +.markdown-body .task-list-item .handle { + display: none; +} + +.markdown-body .task-list-item-checkbox { + margin: 0 .2em .25em -1.4em; + vertical-align: middle; +} + +.markdown-body .contains-task-list:dir(rtl) .task-list-item-checkbox { + margin: 0 -1.6em .25em .2em; +} + +.markdown-body .contains-task-list { + position: relative; +} + +.markdown-body .contains-task-list:hover .task-list-item-convert-container, +.markdown-body .contains-task-list:focus-within .task-list-item-convert-container { + display: block; + width: auto; + height: 24px; + overflow: visible; + clip: auto; +} + +.markdown-body .QueryBuilder .qb-entity { + color: var(--color-prettylights-syntax-entity); +} + +.markdown-body .QueryBuilder .qb-constant { + color: var(--color-prettylights-syntax-constant); +} + +.markdown-body ::-webkit-calendar-picker-indicator { + filter: invert(50%); +} diff --git a/kirami/utils/renderer/template/highlight-dark.css b/kirami/utils/renderer/template/highlight-dark.css new file mode 100644 index 0000000..622a867 --- /dev/null +++ b/kirami/utils/renderer/template/highlight-dark.css @@ -0,0 +1,85 @@ +/* lightbulb */ +pre { line-height: 125%; } +td.linenos .normal { color: #3c4354; background-color: transparent; padding-left: 5px; padding-right: 5px; } +span.linenos { color: #3c4354; background-color: transparent; padding-left: 5px; padding-right: 5px; } +td.linenos .special { color: #3c4354; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +span.linenos.special { color: #3c4354; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +.hll { background-color: #6e7681 } +.c { color: #7e8aa1 } /* Comment */ +.err { color: #f88f7f } /* Error */ +.esc { color: #d4d2c8 } /* Escape */ +.g { color: #d4d2c8 } /* Generic */ +.k { color: #FFAD66 } /* Keyword */ +.l { color: #D5FF80 } /* Literal */ +.n { color: #d4d2c8 } /* Name */ +.o { color: #FFAD66 } /* Operator */ +.x { color: #d4d2c8 } /* Other */ +.p { color: #d4d2c8 } /* Punctuation */ +.ch { color: #f88f7f; font-style: italic } /* Comment.Hashbang */ +.cm { color: #7e8aa1 } /* Comment.Multiline */ +.cp { color: #FFAD66; font-weight: bold } /* Comment.Preproc */ +.cpf { color: #7e8aa1 } /* Comment.PreprocFile */ +.c1 { color: #7e8aa1 } /* Comment.Single */ +.cs { color: #7e8aa1; font-style: italic } /* Comment.Special */ +.gd { color: #f88f7f; background-color: #3d1e20 } /* Generic.Deleted */ +.ge { color: #d4d2c8; font-style: italic } /* Generic.Emph */ +.ges { color: #d4d2c8 } /* Generic.EmphStrong */ +.gr { color: #f88f7f } /* Generic.Error */ +.gh { color: #d4d2c8 } /* Generic.Heading */ +.gi { color: #6ad4af; background-color: #19362c } /* Generic.Inserted */ +.go { color: #7e8aa1 } /* Generic.Output */ +.gp { color: #d4d2c8 } /* Generic.Prompt */ +.gs { color: #d4d2c8; font-weight: bold } /* Generic.Strong */ +.gu { color: #d4d2c8 } /* Generic.Subheading */ +.gt { color: #f88f7f } /* Generic.Traceback */ +.kc { color: #FFAD66 } /* Keyword.Constant */ +.kd { color: #FFAD66 } /* Keyword.Declaration */ +.kn { color: #FFAD66 } /* Keyword.Namespace */ +.kp { color: #FFAD66 } /* Keyword.Pseudo */ +.kr { color: #FFAD66 } /* Keyword.Reserved */ +.kt { color: #73D0FF } /* Keyword.Type */ +.ld { color: #D5FF80 } /* Literal.Date */ +.m { color: #DFBFFF } /* Literal.Number */ +.s { color: #D5FF80 } /* Literal.String */ +.na { color: #FFD173 } /* Name.Attribute */ +.nb { color: #FFD173 } /* Name.Builtin */ +.nc { color: #73D0FF } /* Name.Class */ +.no { color: #FFD173 } /* Name.Constant */ +.nd { color: #d4d2c8; font-weight: bold; font-style: italic } /* Name.Decorator */ +.ni { color: #95E6CB } /* Name.Entity */ +.ne { color: #73D0FF } /* Name.Exception */ +.nf { color: #FFD173 } /* Name.Function */ +.nl { color: #d4d2c8 } /* Name.Label */ +.nn { color: #d4d2c8 } /* Name.Namespace */ +.nx { color: #d4d2c8 } /* Name.Other */ +.py { color: #FFD173 } /* Name.Property */ +.nt { color: #5CCFE6 } /* Name.Tag */ +.nv { color: #d4d2c8 } /* Name.Variable */ +.ow { color: #FFAD66 } /* Operator.Word */ +.pm { color: #d4d2c8 } /* Punctuation.Marker */ +.w { color: #d4d2c8 } /* Text.Whitespace */ +.mb { color: #DFBFFF } /* Literal.Number.Bin */ +.mf { color: #DFBFFF } /* Literal.Number.Float */ +.mh { color: #DFBFFF } /* Literal.Number.Hex */ +.mi { color: #DFBFFF } /* Literal.Number.Integer */ +.mo { color: #DFBFFF } /* Literal.Number.Oct */ +.sa { color: #F29E74 } /* Literal.String.Affix */ +.sb { color: #D5FF80 } /* Literal.String.Backtick */ +.sc { color: #D5FF80 } /* Literal.String.Char */ +.dl { color: #D5FF80 } /* Literal.String.Delimiter */ +.sd { color: #7e8aa1 } /* Literal.String.Doc */ +.s2 { color: #D5FF80 } /* Literal.String.Double */ +.se { color: #95E6CB } /* Literal.String.Escape */ +.sh { color: #D5FF80 } /* Literal.String.Heredoc */ +.si { color: #95E6CB } /* Literal.String.Interpol */ +.sx { color: #95E6CB } /* Literal.String.Other */ +.sr { color: #95E6CB } /* Literal.String.Regex */ +.s1 { color: #D5FF80 } /* Literal.String.Single */ +.ss { color: #DFBFFF } /* Literal.String.Symbol */ +.bp { color: #5CCFE6 } /* Name.Builtin.Pseudo */ +.fm { color: #FFD173 } /* Name.Function.Magic */ +.vc { color: #d4d2c8 } /* Name.Variable.Class */ +.vg { color: #d4d2c8 } /* Name.Variable.Global */ +.vi { color: #d4d2c8 } /* Name.Variable.Instance */ +.vm { color: #d4d2c8 } /* Name.Variable.Magic */ +.il { color: #DFBFFF } /* Literal.Number.Integer.Long */ diff --git a/kirami/utils/renderer/template/highlight-light.css b/kirami/utils/renderer/template/highlight-light.css new file mode 100644 index 0000000..ffffdf3 --- /dev/null +++ b/kirami/utils/renderer/template/highlight-light.css @@ -0,0 +1,68 @@ +/* xcode */ +pre { line-height: 125%; } +td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } +span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } +td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +.hll { background-color: #ffffcc } +.c { color: #177500 } /* Comment */ +.err { color: #000000 } /* Error */ +.k { color: #A90D91 } /* Keyword */ +.l { color: #1C01CE } /* Literal */ +.n { color: #000000 } /* Name */ +.o { color: #000000 } /* Operator */ +.ch { color: #177500 } /* Comment.Hashbang */ +.cm { color: #177500 } /* Comment.Multiline */ +.cp { color: #633820 } /* Comment.Preproc */ +.cpf { color: #177500 } /* Comment.PreprocFile */ +.c1 { color: #177500 } /* Comment.Single */ +.cs { color: #177500 } /* Comment.Special */ +.kc { color: #A90D91 } /* Keyword.Constant */ +.kd { color: #A90D91 } /* Keyword.Declaration */ +.kn { color: #A90D91 } /* Keyword.Namespace */ +.kp { color: #A90D91 } /* Keyword.Pseudo */ +.kr { color: #A90D91 } /* Keyword.Reserved */ +.kt { color: #A90D91 } /* Keyword.Type */ +.ld { color: #1C01CE } /* Literal.Date */ +.m { color: #1C01CE } /* Literal.Number */ +.s { color: #C41A16 } /* Literal.String */ +.na { color: #836C28 } /* Name.Attribute */ +.nb { color: #A90D91 } /* Name.Builtin */ +.nc { color: #3F6E75 } /* Name.Class */ +.no { color: #000000 } /* Name.Constant */ +.nd { color: #000000 } /* Name.Decorator */ +.ni { color: #000000 } /* Name.Entity */ +.ne { color: #000000 } /* Name.Exception */ +.nf { color: #000000 } /* Name.Function */ +.nl { color: #000000 } /* Name.Label */ +.nn { color: #000000 } /* Name.Namespace */ +.nx { color: #000000 } /* Name.Other */ +.py { color: #000000 } /* Name.Property */ +.nt { color: #000000 } /* Name.Tag */ +.nv { color: #000000 } /* Name.Variable */ +.ow { color: #000000 } /* Operator.Word */ +.mb { color: #1C01CE } /* Literal.Number.Bin */ +.mf { color: #1C01CE } /* Literal.Number.Float */ +.mh { color: #1C01CE } /* Literal.Number.Hex */ +.mi { color: #1C01CE } /* Literal.Number.Integer */ +.mo { color: #1C01CE } /* Literal.Number.Oct */ +.sa { color: #C41A16 } /* Literal.String.Affix */ +.sb { color: #C41A16 } /* Literal.String.Backtick */ +.sc { color: #2300CE } /* Literal.String.Char */ +.dl { color: #C41A16 } /* Literal.String.Delimiter */ +.sd { color: #C41A16 } /* Literal.String.Doc */ +.s2 { color: #C41A16 } /* Literal.String.Double */ +.se { color: #C41A16 } /* Literal.String.Escape */ +.sh { color: #C41A16 } /* Literal.String.Heredoc */ +.si { color: #C41A16 } /* Literal.String.Interpol */ +.sx { color: #C41A16 } /* Literal.String.Other */ +.sr { color: #C41A16 } /* Literal.String.Regex */ +.s1 { color: #C41A16 } /* Literal.String.Single */ +.ss { color: #C41A16 } /* Literal.String.Symbol */ +.bp { color: #5B269A } /* Name.Builtin.Pseudo */ +.fm { color: #000000 } /* Name.Function.Magic */ +.vc { color: #000000 } /* Name.Variable.Class */ +.vg { color: #000000 } /* Name.Variable.Global */ +.vi { color: #000000 } /* Name.Variable.Instance */ +.vm { color: #000000 } /* Name.Variable.Magic */ +.il { color: #1C01CE } /* Literal.Number.Integer.Long */ diff --git a/kirami/utils/renderer/template/markdown.html b/kirami/utils/renderer/template/markdown.html new file mode 100644 index 0000000..d09bd99 --- /dev/null +++ b/kirami/utils/renderer/template/markdown.html @@ -0,0 +1,39 @@ + + + + + + + + + {% if 'class="math ' in markdown %} + + + + {% endif %} + {{ extra | safe }} + + + +
+ {{ markdown | safe }} +
+ + + diff --git a/kirami/utils/request/__init__.py b/kirami/utils/request/__init__.py new file mode 100644 index 0000000..79f3114 --- /dev/null +++ b/kirami/utils/request/__init__.py @@ -0,0 +1,665 @@ +""" +请求 +==== + +将 HTTPX 封装以便更好的使用 +""" + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager +from typing import Any, Literal + +import httpx +from httpx import Response +from httpx._types import ( + CookieTypes, + HeaderTypes, + ProxiesTypes, + QueryParamTypes, + RequestContent, + RequestData, + RequestFiles, + TimeoutTypes, + URLTypes, + VerifyTypes, +) + +from kirami.config import bot_config + +from .utils import add_user_agent + + +class Request: + """ + ## httpx 异步请求封装 + + [HTTPX官方文档](https://www.python-httpx.org/) + """ + + @classmethod + async def get( + cls, + url: URLTypes, + *, + params: QueryParamTypes | None = None, + headers: HeaderTypes | None = None, + cookies: CookieTypes | None = None, + follow_redirects: bool = True, + timeout: TimeoutTypes | None = None, + verify: VerifyTypes = True, + http2: bool = False, + proxies: ProxiesTypes | None = None, + **kwargs, + ) -> Response: + """发起 GET 请求。 + + ### 参数 + url: 请求地址 + + params: 请求参数 + + headers: 请求头 + + cookies: 请求 Cookie + + follow_redirects: 是否跟随重定向 + + timeout: 超时时间,单位: 秒 + + verify: 是否验证 SSL 证书 + + http2: 是否使用 HTTP/2 + + proxies: 代理地址 + + **kwargs: 传递给 `httpx.AsyncClient` 的其他参数 + + ### 返回 + `httpx.Response` 对象 + """ + async with httpx.AsyncClient( + verify=verify, + http2=http2, + proxies=proxies or bot_config.proxy_url, # type: ignore + **kwargs, + ) as client: + return await client.get( + url, + params=params, + headers=add_user_agent(headers), + cookies=cookies, + follow_redirects=follow_redirects, + timeout=timeout if timeout is not None else bot_config.http_timeout, + ) + + @classmethod + async def post( + cls, + url: URLTypes, + *, + content: RequestContent | None = None, + data: RequestData | None = None, + json: RequestContent | None = None, + files: RequestFiles | None = None, + params: QueryParamTypes | None = None, + headers: HeaderTypes | None = None, + cookies: CookieTypes | None = None, + follow_redirects: bool = True, + timeout: TimeoutTypes | None = None, + verify: VerifyTypes = True, + http2: bool = False, + proxies: ProxiesTypes | None = None, + **kwargs, + ) -> Response: + """发起 POST 请求。 + + ### 参数 + url: 请求地址 + + content: 请求内容 + + data: 请求数据 + + json: 请求 JSON + + files: 请求文件 + + params: 请求参数 + + headers: 请求头 + + cookies: 请求 Cookie + + follow_redirects: 是否跟随重定向 + + timeout: 超时时间,单位: 秒 + + verify: 是否验证 SSL 证书 + + http2: 是否使用 HTTP/2 + + proxies: 代理地址 + + **kwargs: 传递给 `httpx.AsyncClient` 的其他参数 + + ### 返回 + `httpx.Response` 对象 + """ + async with httpx.AsyncClient( + verify=verify, + http2=http2, + proxies=proxies or bot_config.proxy_url, # type: ignore + **kwargs, + ) as client: + return await client.post( + url, + content=content, + data=data, + files=files, + json=json, + params=params, + headers=add_user_agent(headers), + cookies=cookies, + follow_redirects=follow_redirects, + timeout=timeout if timeout is not None else bot_config.http_timeout, + ) + + @classmethod + async def put( + cls, + url: URLTypes, + *, + content: RequestContent | None = None, + data: RequestData | None = None, + files: RequestFiles | None = None, + json: Any = None, + params: QueryParamTypes | None = None, + headers: HeaderTypes | None = None, + cookies: CookieTypes | None = None, + follow_redirects: bool = True, + timeout: TimeoutTypes | None = None, + verify: VerifyTypes = True, + http2: bool = False, + proxies: ProxiesTypes | None = None, + **kwargs, + ) -> Response: + """发起 PUT 请求。 + + ### 参数 + url: 请求地址 + + content: 请求内容 + + data: 请求数据 + + files: 请求文件 + + json: 请求 JSON + + params: 请求参数 + + headers: 请求头 + + cookies: 请求 Cookie + + follow_redirects: 是否跟随重定向 + + timeout: 超时时间,单位: 秒 + + verify: 是否验证 SSL 证书 + + http2: 是否使用 HTTP/2 + + proxies: 代理地址 + + **kwargs: 传递给 `httpx.AsyncClient` 的其他参数 + + ### 返回 + `httpx.Response` 对象 + """ + async with httpx.AsyncClient( + verify=verify, + http2=http2, + proxies=proxies or bot_config.proxy_url, # type: ignore + **kwargs, + ) as client: + return await client.put( + url, + content=content, + data=data, + files=files, + json=json, + params=params, + headers=add_user_agent(headers), + cookies=cookies, + follow_redirects=follow_redirects, + timeout=timeout if timeout is not None else bot_config.http_timeout, + ) + + @classmethod + async def delete( + cls, + url: URLTypes, + *, + params: QueryParamTypes | None = None, + headers: HeaderTypes | None = None, + cookies: CookieTypes | None = None, + follow_redirects: bool = True, + timeout: TimeoutTypes | None = None, + verify: VerifyTypes = True, + http2: bool = False, + proxies: ProxiesTypes | None = None, + **kwargs, + ) -> Response: + """发起 DELETE 请求。 + + ### 参数 + url: 请求地址 + + params: 请求参数 + + headers: 请求头 + + cookies: 请求 Cookie + + follow_redirects: 是否跟随重定向 + + timeout: 超时时间,单位: 秒 + + verify: 是否验证 SSL 证书 + + http2: 是否使用 HTTP/2 + + proxies: 代理地址 + + **kwargs: 传递给 `httpx.AsyncClient` 的其他参数 + + ### 返回 + `httpx.Response` 对象 + """ + async with httpx.AsyncClient( + verify=verify, + http2=http2, + proxies=proxies or bot_config.proxy_url, # type: ignore + **kwargs, + ) as client: + return await client.delete( + url, + params=params, + headers=add_user_agent(headers), + cookies=cookies, + follow_redirects=follow_redirects, + timeout=timeout if timeout is not None else bot_config.http_timeout, + ) + + @classmethod + async def patch( + cls, + url: URLTypes, + *, + content: RequestContent | None = None, + data: RequestData | None = None, + files: RequestFiles | None = None, + json: Any = None, + params: QueryParamTypes | None = None, + headers: HeaderTypes | None = None, + cookies: CookieTypes | None = None, + follow_redirects: bool = True, + timeout: TimeoutTypes | None = None, + verify: VerifyTypes = True, + http2: bool = False, + proxies: ProxiesTypes | None = None, + **kwargs, + ) -> Response: + """发起 PATCH 请求。 + + ### 参数 + url: 请求地址 + + content: 请求内容 + + data: 请求数据 + + files: 请求文件 + + json: 请求 JSON + + params: 请求参数 + + headers: 请求头 + + cookies: 请求 Cookie + + follow_redirects: 是否跟随重定向 + + timeout: 超时时间,单位: 秒 + + verify: 是否验证 SSL 证书 + + http2: 是否使用 HTTP/2 + + proxies: 代理地址 + + **kwargs: 传递给 `httpx.AsyncClient` 的其他参数 + + ### 返回 + `httpx.Response` 对象 + """ + async with httpx.AsyncClient( + verify=verify, + http2=http2, + proxies=proxies or bot_config.proxy_url, # type: ignore + **kwargs, + ) as client: + return await client.patch( + url, + content=content, + data=data, + files=files, + json=json, + params=params, + headers=add_user_agent(headers), + cookies=cookies, + follow_redirects=follow_redirects, + timeout=timeout if timeout is not None else bot_config.http_timeout, + ) + + @classmethod + async def head( + cls, + url: URLTypes, + *, + params: QueryParamTypes | None = None, + headers: HeaderTypes | None = None, + cookies: CookieTypes | None = None, + follow_redirects: bool = True, + timeout: TimeoutTypes | None = None, + verify: VerifyTypes = True, + http2: bool = False, + proxies: ProxiesTypes | None = None, + **kwargs, + ) -> Response: + """发起 HEAD 请求。 + + ### 参数 + url: 请求地址 + + params: 请求参数 + + headers: 请求头 + + cookies: 请求 Cookie + + follow_redirects: 是否跟随重定向 + + timeout: 超时时间,单位: 秒 + + verify: 是否验证 SSL 证书 + + http2: 是否使用 HTTP/2 + + proxies: 代理地址 + + **kwargs: 传递给 `httpx.AsyncClient` 的其他参数 + + ### 返回 + `httpx.Response` 对象 + """ + async with httpx.AsyncClient( + verify=verify, + http2=http2, + proxies=proxies or bot_config.proxy_url, # type: ignore + **kwargs, + ) as client: + return await client.head( + url, + params=params, + headers=add_user_agent(headers), + cookies=cookies, + follow_redirects=follow_redirects, + timeout=timeout if timeout is not None else bot_config.http_timeout, + ) + + @classmethod + async def options( + cls, + url: URLTypes, + *, + params: QueryParamTypes | None = None, + headers: HeaderTypes | None = None, + cookies: CookieTypes | None = None, + follow_redirects: bool = True, + timeout: TimeoutTypes | None = None, + verify: VerifyTypes = True, + http2: bool = False, + proxies: ProxiesTypes | None = None, + **kwargs, + ) -> Response: + """发起 OPTIONS 请求。 + + ### 参数 + url: 请求地址 + + params: 请求参数 + + headers: 请求头 + + cookies: 请求 Cookie + + follow_redirects: 是否跟随重定向 + + timeout: 超时时间,单位: 秒 + + verify: 是否验证 SSL 证书 + + http2: 是否使用 HTTP/2 + + proxies: 代理地址 + + **kwargs: 传递给 `httpx.AsyncClient` 的其他参数 + + ### 返回 + `httpx.Response` 对象 + """ + async with httpx.AsyncClient( + verify=verify, + http2=http2, + proxies=proxies or bot_config.proxy_url, # type: ignore + **kwargs, + ) as client: + return await client.options( + url, + params=params, + headers=add_user_agent(headers), + cookies=cookies, + follow_redirects=follow_redirects, + timeout=timeout if timeout is not None else bot_config.http_timeout, + ) + + @classmethod + async def request( + cls, + method: Literal["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"], + url: URLTypes, + *, + content: RequestContent | None = None, + data: RequestData | None = None, + files: RequestFiles | None = None, + json: Any = None, + params: QueryParamTypes | None = None, + headers: HeaderTypes | None = None, + cookies: CookieTypes | None = None, + follow_redirects: bool = True, + timeout: TimeoutTypes | None = None, + verify: VerifyTypes = True, + http2: bool = False, + proxies: ProxiesTypes | None = None, + **kwargs, + ) -> Response: + """发起请求。 + + ### 参数 + method: 请求方法 + + url: 请求地址 + + content: 请求内容 + + data: 请求数据 + + files: 请求文件 + + json: 请求 JSON + + params: 请求参数 + + headers: 请求头 + + cookies: 请求 Cookie + + follow_redirects: 是否跟随重定向 + + timeout: 超时时间,单位: 秒 + + verify: 是否验证 SSL 证书 + + http2: 是否使用 HTTP/2 + + proxies: 代理地址 + + **kwargs: 传递给 `httpx.AsyncClient` 的其他参数 + + ### 返回 + `httpx.Response` 对象 + """ + async with httpx.AsyncClient( + verify=verify, + http2=http2, + proxies=proxies or bot_config.proxy_url, # type: ignore + **kwargs, + ) as client: + return await client.request( + method, + url, + content=content, + data=data, + files=files, + json=json, + params=params, + headers=add_user_agent(headers), + cookies=cookies, + follow_redirects=follow_redirects, + timeout=timeout if timeout is not None else bot_config.http_timeout, + ) + + @classmethod + @asynccontextmanager + async def stream( + cls, + method: Literal["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"], + url: URLTypes, + *, + content: RequestContent | None = None, + data: RequestData | None = None, + files: RequestFiles | None = None, + json: Any = None, + params: QueryParamTypes | None = None, + headers: HeaderTypes | None = None, + cookies: CookieTypes | None = None, + follow_redirects: bool = True, + timeout: TimeoutTypes | None = None, + verify: VerifyTypes = True, + http2: bool = False, + proxies: ProxiesTypes | None = None, + **kwargs, + ) -> AsyncGenerator[Response, None]: + """发起流式请求。 + + ### 参数 + method: 请求方法 + + url: 请求地址 + + content: 请求内容 + + data: 请求数据 + + files: 请求文件 + + json: 请求 JSON + + params: 请求参数 + + headers: 请求头 + + cookies: 请求 Cookie + + follow_redirects: 是否跟随重定向 + + timeout: 超时时间,单位: 秒 + + verify: 是否验证 SSL 证书 + + http2: 是否使用 HTTP/2 + + proxies: 代理地址 + + **kwargs: 传递给 `httpx.AsyncClient` 的其他参数 + + ### 生成 + `httpx.Response` 对象 + """ + async with httpx.AsyncClient( + verify=verify, + http2=http2, + proxies=proxies or bot_config.proxy_url, # type: ignore + **kwargs, + ) as client, client.stream( + method, + url, + content=content, + data=data, + files=files, + json=json, + params=params, + headers=add_user_agent(headers), + cookies=cookies, + follow_redirects=follow_redirects, + timeout=timeout if timeout is not None else bot_config.http_timeout, + ) as response: + yield response + + @classmethod + @asynccontextmanager + async def client_session( + cls, + verify: VerifyTypes = True, + http2: bool = False, + proxies: ProxiesTypes | None = None, + follow_redirects: bool = True, + **kwargs, + ) -> AsyncGenerator[httpx.AsyncClient, None]: + """创建 `httpx.AsyncClient` 会话。 + + ### 参数 + verify: 是否验证 SSL 证书 + + http2: 是否使用 HTTP/2 + + proxies: 代理地址 + + follow_redirects: 是否跟随重定向 + + **kwargs: 传递给 `httpx.AsyncClient` 的其他参数 + + ### 生成 + `httpx.AsyncClient` 对象 + """ + async with httpx.AsyncClient( + verify=verify, + http2=http2, + proxies=proxies or bot_config.proxy_url, # type: ignore + follow_redirects=follow_redirects, + **kwargs, + ) as client: + yield client diff --git a/kirami/utils/request/fake_user_agent.json b/kirami/utils/request/fake_user_agent.json new file mode 100644 index 0000000..710ba38 --- /dev/null +++ b/kirami/utils/request/fake_user_agent.json @@ -0,0 +1,1251 @@ +{ + "browsers": { + "chrome": [ + "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2227.1 Safari/537.36", + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2227.0 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2227.0 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2226.0 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.4; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2225.0 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2225.0 Safari/537.36", + "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2224.3 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.93 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2062.124 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2049.0 Safari/537.36", + "Mozilla/5.0 (Windows NT 4.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2049.0 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/36.0.1985.67 Safari/537.36", + "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/36.0.1985.67 Safari/537.36", + "Mozilla/5.0 (X11; OpenBSD i386) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/36.0.1985.125 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/36.0.1944.0 Safari/537.36", + "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.3319.102 Safari/537.36", + "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.2309.372 Safari/537.36", + "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.2117.157 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.1916.47 Safari/537.36", + "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/34.0.1866.237 Safari/537.36", + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/34.0.1847.137 Safari/4E423F", + "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/34.0.1847.116 Safari/537.36 Mozilla/5.0 (iPad; U; CPU OS 3_2 like Mac OS X; en-us) AppleWebKit/531.21.10 (KHTML, like Gecko) Version/4.0.4 Mobile/7B334b Safari/531.21.10", + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/33.0.1750.517 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.2; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1667.0 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1664.3 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1664.3 Safari/537.36", + "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.16 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1623.0 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/30.0.1599.17 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/29.0.1547.62 Safari/537.36", + "Mozilla/5.0 (X11; CrOS i686 4319.74.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/29.0.1547.57 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/29.0.1547.2 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/28.0.1468.0 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/28.0.1467.0 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/28.0.1464.0 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1500.55 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.93 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.93 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.93 Safari/537.36", + "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.93 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.93 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.93 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.90 Safari/537.36", + "Mozilla/5.0 (X11; NetBSD) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.116 Safari/537.36", + "Mozilla/5.0 (X11; CrOS i686 3912.101.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.116 Safari/537.36", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.17 (KHTML, like Gecko) Chrome/24.0.1312.60 Safari/537.17", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.17 (KHTML, like Gecko) Chrome/24.0.1309.0 Safari/537.17", + "Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.15 (KHTML, like Gecko) Chrome/24.0.1295.0 Safari/537.15", + "Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.14 (KHTML, like Gecko) Chrome/24.0.1292.0 Safari/537.14" + ], + "opera": [ + "Opera/9.80 (X11; Linux i686; Ubuntu/14.10) Presto/2.12.388 Version/12.16", + "Opera/9.80 (Windows NT 6.0) Presto/2.12.388 Version/12.14", + "Mozilla/5.0 (Windows NT 6.0; rv:2.0) Gecko/20100101 Firefox/4.0 Opera 12.14", + "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.0) Opera 12.14", + "Opera/12.80 (Windows NT 5.1; U; en) Presto/2.10.289 Version/12.02", + "Opera/9.80 (Windows NT 6.1; U; es-ES) Presto/2.9.181 Version/12.00", + "Opera/9.80 (Windows NT 5.1; U; zh-sg) Presto/2.9.181 Version/12.00", + "Opera/12.0(Windows NT 5.2;U;en)Presto/22.9.168 Version/12.00", + "Opera/12.0(Windows NT 5.1;U;en)Presto/22.9.168 Version/12.00", + "Mozilla/5.0 (Windows NT 5.1) Gecko/20100101 Firefox/14.0 Opera/12.0", + "Opera/9.80 (Windows NT 6.1; WOW64; U; pt) Presto/2.10.229 Version/11.62", + "Opera/9.80 (Windows NT 6.0; U; pl) Presto/2.10.229 Version/11.62", + "Opera/9.80 (Macintosh; Intel Mac OS X 10.6.8; U; fr) Presto/2.9.168 Version/11.52", + "Opera/9.80 (Macintosh; Intel Mac OS X 10.6.8; U; de) Presto/2.9.168 Version/11.52", + "Opera/9.80 (Windows NT 5.1; U; en) Presto/2.9.168 Version/11.51", + "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; de) Opera 11.51", + "Opera/9.80 (X11; Linux x86_64; U; fr) Presto/2.9.168 Version/11.50", + "Opera/9.80 (X11; Linux i686; U; hu) Presto/2.9.168 Version/11.50", + "Opera/9.80 (X11; Linux i686; U; ru) Presto/2.8.131 Version/11.11", + "Opera/9.80 (X11; Linux i686; U; es-ES) Presto/2.8.131 Version/11.11", + "Mozilla/5.0 (Windows NT 5.1; U; en; rv:1.8.1) Gecko/20061208 Firefox/5.0 Opera 11.11", + "Opera/9.80 (X11; Linux x86_64; U; bg) Presto/2.8.131 Version/11.10", + "Opera/9.80 (Windows NT 6.0; U; en) Presto/2.8.99 Version/11.10", + "Opera/9.80 (Windows NT 5.1; U; zh-tw) Presto/2.8.131 Version/11.10", + "Opera/9.80 (Windows NT 6.1; Opera Tablet/15165; U; en) Presto/2.8.149 Version/11.1", + "Opera/9.80 (X11; Linux x86_64; U; Ubuntu/10.10 (maverick); pl) Presto/2.7.62 Version/11.01", + "Opera/9.80 (X11; Linux i686; U; ja) Presto/2.7.62 Version/11.01", + "Opera/9.80 (X11; Linux i686; U; fr) Presto/2.7.62 Version/11.01", + "Opera/9.80 (Windows NT 6.1; U; zh-tw) Presto/2.7.62 Version/11.01", + "Opera/9.80 (Windows NT 6.1; U; zh-cn) Presto/2.7.62 Version/11.01", + "Opera/9.80 (Windows NT 6.1; U; sv) Presto/2.7.62 Version/11.01", + "Opera/9.80 (Windows NT 6.1; U; en-US) Presto/2.7.62 Version/11.01", + "Opera/9.80 (Windows NT 6.1; U; cs) Presto/2.7.62 Version/11.01", + "Opera/9.80 (Windows NT 6.0; U; pl) Presto/2.7.62 Version/11.01", + "Opera/9.80 (Windows NT 5.2; U; ru) Presto/2.7.62 Version/11.01", + "Opera/9.80 (Windows NT 5.1; U;) Presto/2.7.62 Version/11.01", + "Opera/9.80 (Windows NT 5.1; U; cs) Presto/2.7.62 Version/11.01", + "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.2.13) Gecko/20101213 Opera/9.80 (Windows NT 6.1; U; zh-tw) Presto/2.7.62 Version/11.01", + "Mozilla/5.0 (Windows NT 6.1; U; nl; rv:1.9.1.6) Gecko/20091201 Firefox/3.5.6 Opera 11.01", + "Mozilla/5.0 (Windows NT 6.1; U; de; rv:1.9.1.6) Gecko/20091201 Firefox/3.5.6 Opera 11.01", + "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; de) Opera 11.01", + "Opera/9.80 (X11; Linux x86_64; U; pl) Presto/2.7.62 Version/11.00", + "Opera/9.80 (X11; Linux i686; U; it) Presto/2.7.62 Version/11.00", + "Opera/9.80 (Windows NT 6.1; U; zh-cn) Presto/2.6.37 Version/11.00", + "Opera/9.80 (Windows NT 6.1; U; pl) Presto/2.7.62 Version/11.00", + "Opera/9.80 (Windows NT 6.1; U; ko) Presto/2.7.62 Version/11.00", + "Opera/9.80 (Windows NT 6.1; U; fi) Presto/2.7.62 Version/11.00", + "Opera/9.80 (Windows NT 6.1; U; en-GB) Presto/2.7.62 Version/11.00", + "Opera/9.80 (Windows NT 6.1 x64; U; en) Presto/2.7.62 Version/11.00", + "Opera/9.80 (Windows NT 6.0; U; en) Presto/2.7.39 Version/11.00" + ], + "firefox": [ + "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:40.0) Gecko/20100101 Firefox/40.1", + "Mozilla/5.0 (Windows NT 6.3; rv:36.0) Gecko/20100101 Firefox/36.0", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10; rv:33.0) Gecko/20100101 Firefox/33.0", + "Mozilla/5.0 (X11; Linux i586; rv:31.0) Gecko/20100101 Firefox/31.0", + "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:31.0) Gecko/20130401 Firefox/31.0", + "Mozilla/5.0 (Windows NT 5.1; rv:31.0) Gecko/20100101 Firefox/31.0", + "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:29.0) Gecko/20120101 Firefox/29.0", + "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:25.0) Gecko/20100101 Firefox/29.0", + "Mozilla/5.0 (X11; OpenBSD amd64; rv:28.0) Gecko/20100101 Firefox/28.0", + "Mozilla/5.0 (X11; Linux x86_64; rv:28.0) Gecko/20100101 Firefox/28.0", + "Mozilla/5.0 (Windows NT 6.1; rv:27.3) Gecko/20130101 Firefox/27.3", + "Mozilla/5.0 (Windows NT 6.2; Win64; x64; rv:27.0) Gecko/20121011 Firefox/27.0", + "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:25.0) Gecko/20100101 Firefox/25.0", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:25.0) Gecko/20100101 Firefox/25.0", + "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:24.0) Gecko/20100101 Firefox/24.0", + "Mozilla/5.0 (Windows NT 6.0; WOW64; rv:24.0) Gecko/20100101 Firefox/24.0", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:24.0) Gecko/20100101 Firefox/24.0", + "Mozilla/5.0 (Windows NT 6.2; rv:22.0) Gecko/20130405 Firefox/23.0", + "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:23.0) Gecko/20130406 Firefox/23.0", + "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:23.0) Gecko/20131011 Firefox/23.0", + "Mozilla/5.0 (Windows NT 6.2; rv:22.0) Gecko/20130405 Firefox/22.0", + "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:22.0) Gecko/20130328 Firefox/22.0", + "Mozilla/5.0 (Windows NT 6.1; rv:22.0) Gecko/20130405 Firefox/22.0", + "Mozilla/5.0 (Microsoft Windows NT 6.2.9200.0); rv:22.0) Gecko/20130405 Firefox/22.0", + "Mozilla/5.0 (Windows NT 6.2; Win64; x64; rv:16.0.1) Gecko/20121011 Firefox/21.0.1", + "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:16.0.1) Gecko/20121011 Firefox/21.0.1", + "Mozilla/5.0 (Windows NT 6.2; Win64; x64; rv:21.0.0) Gecko/20121011 Firefox/21.0.0", + "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:21.0) Gecko/20130331 Firefox/21.0", + "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:21.0) Gecko/20100101 Firefox/21.0", + "Mozilla/5.0 (X11; Linux i686; rv:21.0) Gecko/20100101 Firefox/21.0", + "Mozilla/5.0 (Windows NT 6.2; WOW64; rv:21.0) Gecko/20130514 Firefox/21.0", + "Mozilla/5.0 (Windows NT 6.2; rv:21.0) Gecko/20130326 Firefox/21.0", + "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:21.0) Gecko/20130401 Firefox/21.0", + "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:21.0) Gecko/20130331 Firefox/21.0", + "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:21.0) Gecko/20130330 Firefox/21.0", + "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:21.0) Gecko/20100101 Firefox/21.0", + "Mozilla/5.0 (Windows NT 6.1; rv:21.0) Gecko/20130401 Firefox/21.0", + "Mozilla/5.0 (Windows NT 6.1; rv:21.0) Gecko/20130328 Firefox/21.0", + "Mozilla/5.0 (Windows NT 6.1; rv:21.0) Gecko/20100101 Firefox/21.0", + "Mozilla/5.0 (Windows NT 5.1; rv:21.0) Gecko/20130401 Firefox/21.0", + "Mozilla/5.0 (Windows NT 5.1; rv:21.0) Gecko/20130331 Firefox/21.0", + "Mozilla/5.0 (Windows NT 5.1; rv:21.0) Gecko/20100101 Firefox/21.0", + "Mozilla/5.0 (Windows NT 5.0; rv:21.0) Gecko/20100101 Firefox/21.0", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:21.0) Gecko/20100101 Firefox/21.0", + "Mozilla/5.0 (Windows NT 6.2; Win64; x64;) Gecko/20100101 Firefox/20.0", + "Mozilla/5.0 (Windows x86; rv:19.0) Gecko/20100101 Firefox/19.0", + "Mozilla/5.0 (Windows NT 6.1; rv:6.0) Gecko/20100101 Firefox/19.0", + "Mozilla/5.0 (Windows NT 6.1; rv:14.0) Gecko/20100101 Firefox/18.0.1", + "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:18.0) Gecko/20100101 Firefox/18.0", + "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:17.0) Gecko/20100101 Firefox/17.0.6" + ], + "safari": [ + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_3) AppleWebKit/537.75.14 (KHTML, like Gecko) Version/7.0.3 Safari/7046A194A", + "Mozilla/5.0 (iPad; CPU OS 6_0 like Mac OS X) AppleWebKit/536.26 (KHTML, like Gecko) Version/6.0 Mobile/10A5355d Safari/8536.25", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/537.13+ (KHTML, like Gecko) Version/5.1.7 Safari/534.57.2", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_3) AppleWebKit/534.55.3 (KHTML, like Gecko) Version/5.1.3 Safari/534.53.10", + "Mozilla/5.0 (iPad; CPU OS 5_1 like Mac OS X) AppleWebKit/534.46 (KHTML, like Gecko ) Version/5.1 Mobile/9B176 Safari/7534.48.3", + "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_8; de-at) AppleWebKit/533.21.1 (KHTML, like Gecko) Version/5.0.5 Safari/533.21.1", + "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_7; da-dk) AppleWebKit/533.21.1 (KHTML, like Gecko) Version/5.0.5 Safari/533.21.1", + "Mozilla/5.0 (Windows; U; Windows NT 6.1; tr-TR) AppleWebKit/533.20.25 (KHTML, like Gecko) Version/5.0.4 Safari/533.20.27", + "Mozilla/5.0 (Windows; U; Windows NT 6.1; ko-KR) AppleWebKit/533.20.25 (KHTML, like Gecko) Version/5.0.4 Safari/533.20.27", + "Mozilla/5.0 (Windows; U; Windows NT 6.1; fr-FR) AppleWebKit/533.20.25 (KHTML, like Gecko) Version/5.0.4 Safari/533.20.27", + "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/533.20.25 (KHTML, like Gecko) Version/5.0.4 Safari/533.20.27", + "Mozilla/5.0 (Windows; U; Windows NT 6.1; cs-CZ) AppleWebKit/533.20.25 (KHTML, like Gecko) Version/5.0.4 Safari/533.20.27", + "Mozilla/5.0 (Windows; U; Windows NT 6.0; ja-JP) AppleWebKit/533.20.25 (KHTML, like Gecko) Version/5.0.4 Safari/533.20.27", + "Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/533.20.25 (KHTML, like Gecko) Version/5.0.4 Safari/533.20.27", + "Mozilla/5.0 (Macintosh; U; PPC Mac OS X 10_5_8; zh-cn) AppleWebKit/533.20.25 (KHTML, like Gecko) Version/5.0.4 Safari/533.20.27", + "Mozilla/5.0 (Macintosh; U; PPC Mac OS X 10_5_8; ja-jp) AppleWebKit/533.20.25 (KHTML, like Gecko) Version/5.0.4 Safari/533.20.27", + "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_7; ja-jp) AppleWebKit/533.20.25 (KHTML, like Gecko) Version/5.0.4 Safari/533.20.27", + "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_6; zh-cn) AppleWebKit/533.20.25 (KHTML, like Gecko) Version/5.0.4 Safari/533.20.27", + "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_6; sv-se) AppleWebKit/533.20.25 (KHTML, like Gecko) Version/5.0.4 Safari/533.20.27", + "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_6; ko-kr) AppleWebKit/533.20.25 (KHTML, like Gecko) Version/5.0.4 Safari/533.20.27", + "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_6; ja-jp) AppleWebKit/533.20.25 (KHTML, like Gecko) Version/5.0.4 Safari/533.20.27", + "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_6; it-it) AppleWebKit/533.20.25 (KHTML, like Gecko) Version/5.0.4 Safari/533.20.27", + "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_6; fr-fr) AppleWebKit/533.20.25 (KHTML, like Gecko) Version/5.0.4 Safari/533.20.27", + "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_6; es-es) AppleWebKit/533.20.25 (KHTML, like Gecko) Version/5.0.4 Safari/533.20.27", + "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_6; en-us) AppleWebKit/533.20.25 (KHTML, like Gecko) Version/5.0.4 Safari/533.20.27", + "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_6; en-gb) AppleWebKit/533.20.25 (KHTML, like Gecko) Version/5.0.4 Safari/533.20.27", + "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_6; de-de) AppleWebKit/533.20.25 (KHTML, like Gecko) Version/5.0.4 Safari/533.20.27", + "Mozilla/5.0 (Windows; U; Windows NT 6.1; sv-SE) AppleWebKit/533.19.4 (KHTML, like Gecko) Version/5.0.3 Safari/533.19.4", + "Mozilla/5.0 (Windows; U; Windows NT 6.1; ja-JP) AppleWebKit/533.20.25 (KHTML, like Gecko) Version/5.0.3 Safari/533.19.4", + "Mozilla/5.0 (Windows; U; Windows NT 6.1; de-DE) AppleWebKit/533.20.25 (KHTML, like Gecko) Version/5.0.3 Safari/533.19.4", + "Mozilla/5.0 (Windows; U; Windows NT 6.0; hu-HU) AppleWebKit/533.19.4 (KHTML, like Gecko) Version/5.0.3 Safari/533.19.4", + "Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/533.20.25 (KHTML, like Gecko) Version/5.0.3 Safari/533.19.4", + "Mozilla/5.0 (Windows; U; Windows NT 6.0; de-DE) AppleWebKit/533.20.25 (KHTML, like Gecko) Version/5.0.3 Safari/533.19.4", + "Mozilla/5.0 (Windows; U; Windows NT 5.1; ru-RU) AppleWebKit/533.19.4 (KHTML, like Gecko) Version/5.0.3 Safari/533.19.4", + "Mozilla/5.0 (Windows; U; Windows NT 5.1; ja-JP) AppleWebKit/533.20.25 (KHTML, like Gecko) Version/5.0.3 Safari/533.19.4", + "Mozilla/5.0 (Windows; U; Windows NT 5.1; it-IT) AppleWebKit/533.20.25 (KHTML, like Gecko) Version/5.0.3 Safari/533.19.4", + "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/533.20.25 (KHTML, like Gecko) Version/5.0.3 Safari/533.19.4", + "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_7; en-us) AppleWebKit/534.16+ (KHTML, like Gecko) Version/5.0.3 Safari/533.19.4", + "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_6; fr-ch) AppleWebKit/533.19.4 (KHTML, like Gecko) Version/5.0.3 Safari/533.19.4", + "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_5; de-de) AppleWebKit/534.15+ (KHTML, like Gecko) Version/5.0.3 Safari/533.19.4", + "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_5; ar) AppleWebKit/533.19.4 (KHTML, like Gecko) Version/5.0.3 Safari/533.19.4", + "Mozilla/5.0 (Android 2.2; Windows; U; Windows NT 6.1; en-US) AppleWebKit/533.19.4 (KHTML, like Gecko) Version/5.0.3 Safari/533.19.4", + "Mozilla/5.0 (Windows; U; Windows NT 6.1; zh-HK) AppleWebKit/533.18.1 (KHTML, like Gecko) Version/5.0.2 Safari/533.18.5", + "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/533.19.4 (KHTML, like Gecko) Version/5.0.2 Safari/533.18.5", + "Mozilla/5.0 (Windows; U; Windows NT 6.0; tr-TR) AppleWebKit/533.18.1 (KHTML, like Gecko) Version/5.0.2 Safari/533.18.5", + "Mozilla/5.0 (Windows; U; Windows NT 6.0; nb-NO) AppleWebKit/533.18.1 (KHTML, like Gecko) Version/5.0.2 Safari/533.18.5", + "Mozilla/5.0 (Windows; U; Windows NT 6.0; fr-FR) AppleWebKit/533.18.1 (KHTML, like Gecko) Version/5.0.2 Safari/533.18.5", + "Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-TW) AppleWebKit/533.19.4 (KHTML, like Gecko) Version/5.0.2 Safari/533.18.5", + "Mozilla/5.0 (Windows; U; Windows NT 5.1; ru-RU) AppleWebKit/533.18.1 (KHTML, like Gecko) Version/5.0.2 Safari/533.18.5", + "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_8; zh-cn) AppleWebKit/533.18.1 (KHTML, like Gecko) Version/5.0.2 Safari/533.18.5" + ], + "internetexplorer": [ + "Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; AS; rv:11.0) like Gecko", + "Mozilla/5.0 (compatible, MSIE 11, Windows NT 6.3; Trident/7.0; rv:11.0) like Gecko", + "Mozilla/5.0 (compatible; MSIE 10.6; Windows NT 6.1; Trident/5.0; InfoPath.2; SLCC1; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729; .NET CLR 2.0.50727) 3gpp-gba UNTRUSTED/1.0", + "Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 7.0; InfoPath.3; .NET CLR 3.1.40767; Trident/6.0; en-IN)", + "Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; WOW64; Trident/6.0)", + "Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; Trident/6.0)", + "Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; Trident/5.0)", + "Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; Trident/4.0; InfoPath.2; SV1; .NET CLR 2.0.50727; WOW64)", + "Mozilla/5.0 (compatible; MSIE 10.0; Macintosh; Intel Mac OS X 10_7_3; Trident/6.0)", + "Mozilla/4.0 (Compatible; MSIE 8.0; Windows NT 5.2; Trident/6.0)", + "Mozilla/4.0 (compatible; MSIE 10.0; Windows NT 6.1; Trident/5.0)", + "Mozilla/1.22 (compatible; MSIE 10.0; Windows 3.1)", + "Mozilla/5.0 (Windows; U; MSIE 9.0; WIndows NT 9.0; en-US))", + "Mozilla/5.0 (Windows; U; MSIE 9.0; Windows NT 9.0; en-US)", + "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 7.1; Trident/5.0)", + "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0; SLCC2; Media Center PC 6.0; InfoPath.3; MS-RTC LM 8; Zune 4.7)", + "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0; SLCC2; Media Center PC 6.0; InfoPath.3; MS-RTC LM 8; Zune 4.7", + "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; Zune 4.0; InfoPath.3; MS-RTC LM 8; .NET4.0C; .NET4.0E)", + "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0; chromeframe/12.0.742.112)", + "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0; .NET CLR 3.5.30729; .NET CLR 3.0.30729; .NET CLR 2.0.50727; Media Center PC 6.0)", + "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0; .NET CLR 3.5.30729; .NET CLR 3.0.30729; .NET CLR 2.0.50727; Media Center PC 6.0)", + "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0; .NET CLR 2.0.50727; SLCC2; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; Zune 4.0; Tablet PC 2.0; InfoPath.3; .NET4.0C; .NET4.0E)", + "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0", + "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0; yie8)", + "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; InfoPath.2; .NET CLR 1.1.4322; .NET4.0C; Tablet PC 2.0)", + "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0; FunWebProducts)", + "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0; chromeframe/13.0.782.215)", + "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0; chromeframe/11.0.696.57)", + "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0) chromeframe/10.0.648.205", + "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/4.0; GTB7.4; InfoPath.1; SV1; .NET CLR 2.8.52393; WOW64; en-US)", + "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.0; Trident/5.0; chromeframe/11.0.696.57)", + "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.0; Trident/4.0; GTB7.4; InfoPath.3; SV1; .NET CLR 3.1.76908; WOW64; en-US)", + "Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 6.1; Trident/4.0; GTB7.4; InfoPath.2; SV1; .NET CLR 3.3.69573; WOW64; en-US)", + "Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 6.0; Trident/4.0; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; .NET CLR 1.0.3705; .NET CLR 1.1.4322)", + "Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 6.0; Trident/4.0; InfoPath.1; SV1; .NET CLR 3.8.36217; WOW64; en-US)", + "Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 6.0; Trident/4.0; .NET CLR 2.7.58687; SLCC2; Media Center PC 5.0; Zune 3.4; Tablet PC 3.6; InfoPath.3)", + "Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 5.2; Trident/4.0; Media Center PC 4.0; SLCC1; .NET CLR 3.0.04320)", + "Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0; SLCC1; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729; .NET CLR 1.1.4322)", + "Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0; InfoPath.2; SLCC1; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729; .NET CLR 2.0.50727)", + "Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0; .NET CLR 1.1.4322; .NET CLR 2.0.50727)", + "Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 5.1; SLCC1; .NET CLR 1.1.4322)", + "Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 5.0; Trident/4.0; InfoPath.1; SV1; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729; .NET CLR 3.0.04506.30)", + "Mozilla/5.0 (compatible; MSIE 7.0; Windows NT 5.0; Trident/4.0; FBSMTWB; .NET CLR 2.0.34861; .NET CLR 3.0.3746.3218; .NET CLR 3.5.33652; msn OptimizedIE8;ENUS)", + "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.2; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0)", + "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; WOW64; Trident/4.0; SLCC2; Media Center PC 6.0; InfoPath.2; MS-RTC LM 8)", + "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; WOW64; Trident/4.0; SLCC2; Media Center PC 6.0; InfoPath.2; MS-RTC LM 8", + "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; Media Center PC 6.0; .NET CLR 3.5.30729; .NET CLR 3.0.30729; .NET4.0C)", + "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; InfoPath.3; .NET4.0C; .NET4.0E; .NET CLR 3.5.30729; .NET CLR 3.0.30729; MS-RTC LM 8)", + "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; InfoPath.2)", + "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; Zune 3.0)" + ] + }, + "randomize": { + "0": "chrome", + "1": "chrome", + "2": "chrome", + "3": "chrome", + "4": "chrome", + "5": "chrome", + "6": "chrome", + "7": "chrome", + "8": "chrome", + "9": "chrome", + "10": "chrome", + "11": "chrome", + "12": "chrome", + "13": "chrome", + "14": "chrome", + "15": "chrome", + "16": "chrome", + "17": "chrome", + "18": "chrome", + "19": "chrome", + "20": "chrome", + "21": "chrome", + "22": "chrome", + "23": "chrome", + "24": "chrome", + "25": "chrome", + "26": "chrome", + "27": "chrome", + "28": "chrome", + "29": "chrome", + "30": "chrome", + "31": "chrome", + "32": "chrome", + "33": "chrome", + "34": "chrome", + "35": "chrome", + "36": "chrome", + "37": "chrome", + "38": "chrome", + "39": "chrome", + "40": "chrome", + "41": "chrome", + "42": "chrome", + "43": "chrome", + "44": "chrome", + "45": "chrome", + "46": "chrome", + "47": "chrome", + "48": "chrome", + "49": "chrome", + "50": "chrome", + "51": "chrome", + "52": "chrome", + "53": "chrome", + "54": "chrome", + "55": "chrome", + "56": "chrome", + "57": "chrome", + "58": "chrome", + "59": "chrome", + "60": "chrome", + "61": "chrome", + "62": "chrome", + "63": "chrome", + "64": "chrome", + "65": "chrome", + "66": "chrome", + "67": "chrome", + "68": "chrome", + "69": "chrome", + "70": "chrome", + "71": "chrome", + "72": "chrome", + "73": "chrome", + "74": "chrome", + "75": "chrome", + "76": "chrome", + "77": "chrome", + "78": "chrome", + "79": "chrome", + "80": "chrome", + "81": "chrome", + "82": "chrome", + "83": "chrome", + "84": "chrome", + "85": "chrome", + "86": "chrome", + "87": "chrome", + "88": "chrome", + "89": "chrome", + "90": "chrome", + "91": "chrome", + "92": "chrome", + "93": "chrome", + "94": "chrome", + "95": "chrome", + "96": "chrome", + "97": "chrome", + "98": "chrome", + "99": "chrome", + "100": "chrome", + "101": "chrome", + "102": "chrome", + "103": "chrome", + "104": "chrome", + "105": "chrome", + "106": "chrome", + "107": "chrome", + "108": "chrome", + "109": "chrome", + "110": "chrome", + "111": "chrome", + "112": "chrome", + "113": "chrome", + "114": "chrome", + "115": "chrome", + "116": "chrome", + "117": "chrome", + "118": "chrome", + "119": "chrome", + "120": "chrome", + "121": "chrome", + "122": "chrome", + "123": "chrome", + "124": "chrome", + "125": "chrome", + "126": "chrome", + "127": "chrome", + "128": "chrome", + "129": "chrome", + "130": "chrome", + "131": "chrome", + "132": "chrome", + "133": "chrome", + "134": "chrome", + "135": "chrome", + "136": "chrome", + "137": "chrome", + "138": "chrome", + "139": "chrome", + "140": "chrome", + "141": "chrome", + "142": "chrome", + "143": "chrome", + "144": "chrome", + "145": "chrome", + "146": "chrome", + "147": "chrome", + "148": "chrome", + "149": "chrome", + "150": "chrome", + "151": "chrome", + "152": "chrome", + "153": "chrome", + "154": "chrome", + "155": "chrome", + "156": "chrome", + "157": "chrome", + "158": "chrome", + "159": "chrome", + "160": "chrome", + "161": "chrome", + "162": "chrome", + "163": "chrome", + "164": "chrome", + "165": "chrome", + "166": "chrome", + "167": "chrome", + "168": "chrome", + "169": "chrome", + "170": "chrome", + "171": "chrome", + "172": "chrome", + "173": "chrome", + "174": "chrome", + "175": "chrome", + "176": "chrome", + "177": "chrome", + "178": "chrome", + "179": "chrome", + "180": "chrome", + "181": "chrome", + "182": "chrome", + "183": "chrome", + "184": "chrome", + "185": "chrome", + "186": "chrome", + "187": "chrome", + "188": "chrome", + "189": "chrome", + "190": "chrome", + "191": "chrome", + "192": "chrome", + "193": "chrome", + "194": "chrome", + "195": "chrome", + "196": "chrome", + "197": "chrome", + "198": "chrome", + "199": "chrome", + "200": "chrome", + "201": "chrome", + "202": "chrome", + "203": "chrome", + "204": "chrome", + "205": "chrome", + "206": "chrome", + "207": "chrome", + "208": "chrome", + "209": "chrome", + "210": "chrome", + "211": "chrome", + "212": "chrome", + "213": "chrome", + "214": "chrome", + "215": "chrome", + "216": "chrome", + "217": "chrome", + "218": "chrome", + "219": "chrome", + "220": "chrome", + "221": "chrome", + "222": "chrome", + "223": "chrome", + "224": "chrome", + "225": "chrome", + "226": "chrome", + "227": "chrome", + "228": "chrome", + "229": "chrome", + "230": "chrome", + "231": "chrome", + "232": "chrome", + "233": "chrome", + "234": "chrome", + "235": "chrome", + "236": "chrome", + "237": "chrome", + "238": "chrome", + "239": "chrome", + "240": "chrome", + "241": "chrome", + "242": "chrome", + "243": "chrome", + "244": "chrome", + "245": "chrome", + "246": "chrome", + "247": "chrome", + "248": "chrome", + "249": "chrome", + "250": "chrome", + "251": "chrome", + "252": "chrome", + "253": "chrome", + "254": "chrome", + "255": "chrome", + "256": "chrome", + "257": "chrome", + "258": "chrome", + "259": "chrome", + "260": "chrome", + "261": "chrome", + "262": "chrome", + "263": "chrome", + "264": "chrome", + "265": "chrome", + "266": "chrome", + "267": "chrome", + "268": "chrome", + "269": "chrome", + "270": "chrome", + "271": "chrome", + "272": "chrome", + "273": "chrome", + "274": "chrome", + "275": "chrome", + "276": "chrome", + "277": "chrome", + "278": "chrome", + "279": "chrome", + "280": "chrome", + "281": "chrome", + "282": "chrome", + "283": "chrome", + "284": "chrome", + "285": "chrome", + "286": "chrome", + "287": "chrome", + "288": "chrome", + "289": "chrome", + "290": "chrome", + "291": "chrome", + "292": "chrome", + "293": "chrome", + "294": "chrome", + "295": "chrome", + "296": "chrome", + "297": "chrome", + "298": "chrome", + "299": "chrome", + "300": "chrome", + "301": "chrome", + "302": "chrome", + "303": "chrome", + "304": "chrome", + "305": "chrome", + "306": "chrome", + "307": "chrome", + "308": "chrome", + "309": "chrome", + "310": "chrome", + "311": "chrome", + "312": "chrome", + "313": "chrome", + "314": "chrome", + "315": "chrome", + "316": "chrome", + "317": "chrome", + "318": "chrome", + "319": "chrome", + "320": "chrome", + "321": "chrome", + "322": "chrome", + "323": "chrome", + "324": "chrome", + "325": "chrome", + "326": "chrome", + "327": "chrome", + "328": "chrome", + "329": "chrome", + "330": "chrome", + "331": "chrome", + "332": "chrome", + "333": "chrome", + "334": "chrome", + "335": "chrome", + "336": "chrome", + "337": "chrome", + "338": "chrome", + "339": "chrome", + "340": "chrome", + "341": "chrome", + "342": "chrome", + "343": "chrome", + "344": "chrome", + "345": "chrome", + "346": "chrome", + "347": "chrome", + "348": "chrome", + "349": "chrome", + "350": "chrome", + "351": "chrome", + "352": "chrome", + "353": "chrome", + "354": "chrome", + "355": "chrome", + "356": "chrome", + "357": "chrome", + "358": "chrome", + "359": "chrome", + "360": "chrome", + "361": "chrome", + "362": "chrome", + "363": "chrome", + "364": "chrome", + "365": "chrome", + "366": "chrome", + "367": "chrome", + "368": "chrome", + "369": "chrome", + "370": "chrome", + "371": "chrome", + "372": "chrome", + "373": "chrome", + "374": "chrome", + "375": "chrome", + "376": "chrome", + "377": "chrome", + "378": "chrome", + "379": "chrome", + "380": "chrome", + "381": "chrome", + "382": "chrome", + "383": "chrome", + "384": "chrome", + "385": "chrome", + "386": "chrome", + "387": "chrome", + "388": "chrome", + "389": "chrome", + "390": "chrome", + "391": "chrome", + "392": "chrome", + "393": "chrome", + "394": "chrome", + "395": "chrome", + "396": "chrome", + "397": "chrome", + "398": "chrome", + "399": "chrome", + "400": "chrome", + "401": "chrome", + "402": "chrome", + "403": "chrome", + "404": "chrome", + "405": "chrome", + "406": "chrome", + "407": "chrome", + "408": "chrome", + "409": "chrome", + "410": "chrome", + "411": "chrome", + "412": "chrome", + "413": "chrome", + "414": "chrome", + "415": "chrome", + "416": "chrome", + "417": "chrome", + "418": "chrome", + "419": "chrome", + "420": "chrome", + "421": "chrome", + "422": "chrome", + "423": "chrome", + "424": "chrome", + "425": "chrome", + "426": "chrome", + "427": "chrome", + "428": "chrome", + "429": "chrome", + "430": "chrome", + "431": "chrome", + "432": "chrome", + "433": "chrome", + "434": "chrome", + "435": "chrome", + "436": "chrome", + "437": "chrome", + "438": "chrome", + "439": "chrome", + "440": "chrome", + "441": "chrome", + "442": "chrome", + "443": "chrome", + "444": "chrome", + "445": "chrome", + "446": "chrome", + "447": "chrome", + "448": "chrome", + "449": "chrome", + "450": "chrome", + "451": "chrome", + "452": "chrome", + "453": "chrome", + "454": "chrome", + "455": "chrome", + "456": "chrome", + "457": "chrome", + "458": "chrome", + "459": "chrome", + "460": "chrome", + "461": "chrome", + "462": "chrome", + "463": "chrome", + "464": "chrome", + "465": "chrome", + "466": "chrome", + "467": "chrome", + "468": "chrome", + "469": "chrome", + "470": "chrome", + "471": "chrome", + "472": "chrome", + "473": "chrome", + "474": "chrome", + "475": "chrome", + "476": "chrome", + "477": "chrome", + "478": "chrome", + "479": "chrome", + "480": "chrome", + "481": "chrome", + "482": "chrome", + "483": "chrome", + "484": "chrome", + "485": "chrome", + "486": "chrome", + "487": "chrome", + "488": "chrome", + "489": "chrome", + "490": "chrome", + "491": "chrome", + "492": "chrome", + "493": "chrome", + "494": "chrome", + "495": "chrome", + "496": "chrome", + "497": "chrome", + "498": "chrome", + "499": "chrome", + "500": "chrome", + "501": "chrome", + "502": "chrome", + "503": "chrome", + "504": "chrome", + "505": "chrome", + "506": "chrome", + "507": "chrome", + "508": "chrome", + "509": "chrome", + "510": "chrome", + "511": "chrome", + "512": "chrome", + "513": "chrome", + "514": "chrome", + "515": "chrome", + "516": "chrome", + "517": "chrome", + "518": "chrome", + "519": "chrome", + "520": "chrome", + "521": "chrome", + "522": "chrome", + "523": "chrome", + "524": "chrome", + "525": "chrome", + "526": "chrome", + "527": "chrome", + "528": "chrome", + "529": "chrome", + "530": "chrome", + "531": "chrome", + "532": "chrome", + "533": "chrome", + "534": "chrome", + "535": "chrome", + "536": "chrome", + "537": "chrome", + "538": "chrome", + "539": "chrome", + "540": "chrome", + "541": "chrome", + "542": "chrome", + "543": "chrome", + "544": "chrome", + "545": "chrome", + "546": "chrome", + "547": "chrome", + "548": "chrome", + "549": "chrome", + "550": "chrome", + "551": "chrome", + "552": "chrome", + "553": "chrome", + "554": "chrome", + "555": "chrome", + "556": "chrome", + "557": "chrome", + "558": "chrome", + "559": "chrome", + "560": "chrome", + "561": "chrome", + "562": "chrome", + "563": "chrome", + "564": "chrome", + "565": "chrome", + "566": "chrome", + "567": "chrome", + "568": "chrome", + "569": "chrome", + "570": "chrome", + "571": "chrome", + "572": "chrome", + "573": "chrome", + "574": "chrome", + "575": "chrome", + "576": "chrome", + "577": "chrome", + "578": "chrome", + "579": "chrome", + "580": "chrome", + "581": "chrome", + "582": "chrome", + "583": "chrome", + "584": "chrome", + "585": "chrome", + "586": "chrome", + "587": "chrome", + "588": "chrome", + "589": "chrome", + "590": "chrome", + "591": "chrome", + "592": "chrome", + "593": "chrome", + "594": "chrome", + "595": "chrome", + "596": "chrome", + "597": "chrome", + "598": "chrome", + "599": "chrome", + "600": "chrome", + "601": "chrome", + "602": "chrome", + "603": "chrome", + "604": "chrome", + "605": "chrome", + "606": "chrome", + "607": "chrome", + "608": "chrome", + "609": "chrome", + "610": "chrome", + "611": "chrome", + "612": "chrome", + "613": "chrome", + "614": "chrome", + "615": "chrome", + "616": "chrome", + "617": "chrome", + "618": "chrome", + "619": "chrome", + "620": "chrome", + "621": "chrome", + "622": "chrome", + "623": "chrome", + "624": "chrome", + "625": "chrome", + "626": "chrome", + "627": "chrome", + "628": "chrome", + "629": "chrome", + "630": "chrome", + "631": "chrome", + "632": "chrome", + "633": "chrome", + "634": "chrome", + "635": "chrome", + "636": "chrome", + "637": "chrome", + "638": "chrome", + "639": "chrome", + "640": "chrome", + "641": "chrome", + "642": "chrome", + "643": "chrome", + "644": "chrome", + "645": "chrome", + "646": "chrome", + "647": "chrome", + "648": "chrome", + "649": "chrome", + "650": "chrome", + "651": "chrome", + "652": "chrome", + "653": "chrome", + "654": "chrome", + "655": "chrome", + "656": "chrome", + "657": "chrome", + "658": "chrome", + "659": "chrome", + "660": "chrome", + "661": "chrome", + "662": "chrome", + "663": "chrome", + "664": "chrome", + "665": "chrome", + "666": "chrome", + "667": "chrome", + "668": "chrome", + "669": "chrome", + "670": "chrome", + "671": "chrome", + "672": "chrome", + "673": "chrome", + "674": "chrome", + "675": "chrome", + "676": "chrome", + "677": "chrome", + "678": "chrome", + "679": "chrome", + "680": "chrome", + "681": "chrome", + "682": "chrome", + "683": "chrome", + "684": "chrome", + "685": "chrome", + "686": "chrome", + "687": "chrome", + "688": "chrome", + "689": "chrome", + "690": "chrome", + "691": "chrome", + "692": "chrome", + "693": "chrome", + "694": "chrome", + "695": "chrome", + "696": "chrome", + "697": "chrome", + "698": "chrome", + "699": "chrome", + "700": "chrome", + "701": "chrome", + "702": "chrome", + "703": "chrome", + "704": "chrome", + "705": "chrome", + "706": "chrome", + "707": "chrome", + "708": "chrome", + "709": "chrome", + "710": "chrome", + "711": "chrome", + "712": "chrome", + "713": "chrome", + "714": "chrome", + "715": "chrome", + "716": "chrome", + "717": "chrome", + "718": "chrome", + "719": "chrome", + "720": "chrome", + "721": "chrome", + "722": "chrome", + "723": "chrome", + "724": "chrome", + "725": "chrome", + "726": "chrome", + "727": "chrome", + "728": "chrome", + "729": "chrome", + "730": "chrome", + "731": "chrome", + "732": "chrome", + "733": "chrome", + "734": "chrome", + "735": "chrome", + "736": "chrome", + "737": "chrome", + "738": "chrome", + "739": "chrome", + "740": "chrome", + "741": "internetexplorer", + "742": "internetexplorer", + "743": "internetexplorer", + "744": "internetexplorer", + "745": "internetexplorer", + "746": "internetexplorer", + "747": "internetexplorer", + "748": "internetexplorer", + "749": "internetexplorer", + "750": "internetexplorer", + "751": "internetexplorer", + "752": "internetexplorer", + "753": "internetexplorer", + "754": "internetexplorer", + "755": "internetexplorer", + "756": "internetexplorer", + "757": "internetexplorer", + "758": "internetexplorer", + "759": "internetexplorer", + "760": "internetexplorer", + "761": "internetexplorer", + "762": "internetexplorer", + "763": "internetexplorer", + "764": "internetexplorer", + "765": "internetexplorer", + "766": "internetexplorer", + "767": "internetexplorer", + "768": "internetexplorer", + "769": "internetexplorer", + "770": "internetexplorer", + "771": "internetexplorer", + "772": "internetexplorer", + "773": "internetexplorer", + "774": "internetexplorer", + "775": "internetexplorer", + "776": "internetexplorer", + "777": "internetexplorer", + "778": "internetexplorer", + "779": "internetexplorer", + "780": "internetexplorer", + "781": "internetexplorer", + "782": "internetexplorer", + "783": "internetexplorer", + "784": "internetexplorer", + "785": "internetexplorer", + "786": "internetexplorer", + "787": "internetexplorer", + "788": "internetexplorer", + "789": "firefox", + "790": "firefox", + "791": "firefox", + "792": "firefox", + "793": "firefox", + "794": "firefox", + "795": "firefox", + "796": "firefox", + "797": "firefox", + "798": "firefox", + "799": "firefox", + "800": "firefox", + "801": "firefox", + "802": "firefox", + "803": "firefox", + "804": "firefox", + "805": "firefox", + "806": "firefox", + "807": "firefox", + "808": "firefox", + "809": "firefox", + "810": "firefox", + "811": "firefox", + "812": "firefox", + "813": "firefox", + "814": "firefox", + "815": "firefox", + "816": "firefox", + "817": "firefox", + "818": "firefox", + "819": "firefox", + "820": "firefox", + "821": "firefox", + "822": "firefox", + "823": "firefox", + "824": "firefox", + "825": "firefox", + "826": "firefox", + "827": "firefox", + "828": "firefox", + "829": "firefox", + "830": "firefox", + "831": "firefox", + "832": "firefox", + "833": "firefox", + "834": "firefox", + "835": "firefox", + "836": "firefox", + "837": "firefox", + "838": "firefox", + "839": "firefox", + "840": "firefox", + "841": "firefox", + "842": "firefox", + "843": "firefox", + "844": "firefox", + "845": "firefox", + "846": "firefox", + "847": "firefox", + "848": "firefox", + "849": "firefox", + "850": "firefox", + "851": "firefox", + "852": "firefox", + "853": "firefox", + "854": "firefox", + "855": "firefox", + "856": "firefox", + "857": "firefox", + "858": "firefox", + "859": "firefox", + "860": "firefox", + "861": "firefox", + "862": "firefox", + "863": "firefox", + "864": "firefox", + "865": "firefox", + "866": "firefox", + "867": "firefox", + "868": "firefox", + "869": "firefox", + "870": "firefox", + "871": "firefox", + "872": "firefox", + "873": "firefox", + "874": "firefox", + "875": "firefox", + "876": "firefox", + "877": "firefox", + "878": "firefox", + "879": "firefox", + "880": "firefox", + "881": "firefox", + "882": "firefox", + "883": "firefox", + "884": "firefox", + "885": "firefox", + "886": "firefox", + "887": "firefox", + "888": "firefox", + "889": "firefox", + "890": "firefox", + "891": "firefox", + "892": "firefox", + "893": "firefox", + "894": "firefox", + "895": "firefox", + "896": "firefox", + "897": "firefox", + "898": "firefox", + "899": "firefox", + "900": "firefox", + "901": "firefox", + "902": "firefox", + "903": "firefox", + "904": "firefox", + "905": "firefox", + "906": "firefox", + "907": "firefox", + "908": "firefox", + "909": "firefox", + "910": "firefox", + "911": "firefox", + "912": "firefox", + "913": "firefox", + "914": "firefox", + "915": "firefox", + "916": "firefox", + "917": "firefox", + "918": "firefox", + "919": "firefox", + "920": "firefox", + "921": "firefox", + "922": "firefox", + "923": "firefox", + "924": "firefox", + "925": "firefox", + "926": "firefox", + "927": "firefox", + "928": "firefox", + "929": "firefox", + "930": "firefox", + "931": "firefox", + "932": "firefox", + "933": "firefox", + "934": "firefox", + "935": "firefox", + "936": "firefox", + "937": "firefox", + "938": "firefox", + "939": "safari", + "940": "safari", + "941": "safari", + "942": "safari", + "943": "safari", + "944": "safari", + "945": "safari", + "946": "safari", + "947": "safari", + "948": "safari", + "949": "safari", + "950": "safari", + "951": "safari", + "952": "safari", + "953": "safari", + "954": "safari", + "955": "safari", + "956": "safari", + "957": "safari", + "958": "safari", + "959": "safari", + "960": "safari", + "961": "safari", + "962": "safari", + "963": "safari", + "964": "safari", + "965": "safari", + "966": "safari", + "967": "safari", + "968": "safari", + "969": "safari", + "970": "safari", + "971": "safari", + "972": "safari", + "973": "safari", + "974": "safari", + "975": "opera", + "976": "opera", + "977": "opera", + "978": "opera", + "979": "opera", + "980": "opera", + "981": "opera", + "982": "opera", + "983": "opera", + "984": "opera" + } +} \ No newline at end of file diff --git a/kirami/utils/request/utils.py b/kirami/utils/request/utils.py new file mode 100644 index 0000000..9dd4c58 --- /dev/null +++ b/kirami/utils/request/utils.py @@ -0,0 +1,41 @@ +import json +import secrets +from collections.abc import Sequence +from pathlib import Path +from typing import Literal + +from httpx._types import HeaderTypes + + +def fake_user_agent( + browser: Literal["chrome", "opera", "firefox", "safari", "internetexplorer"] + | None = None, +) -> dict[str, str]: + """获取一个随机的 User-Agent。 + + ### 参数 + browser: 浏览器类型,如果不指定则随机选择 + """ + fake_file = Path(__file__).parent / "fake_user_agent.json" + if not fake_file.is_file(): + raise FileNotFoundError(f"{fake_file} is not found.") + fake_data = json.loads(fake_file.read_text(encoding="utf-8")) + if not browser: + browser = secrets.choice(list(fake_data["randomize"].values())) + return {"User-Agent": secrets.choice(fake_data["browsers"][browser])} + + +def add_user_agent(headers: HeaderTypes | None = None) -> HeaderTypes: + """添加随机的 User-Agent 到请求头中。 + + ### 参数 + headers: 请求头字典或列表 + + ### 返回 + 添加了随机 User-Agent 的请求头字典 + """ + user_agent = fake_user_agent() + if headers: + _headers = dict(headers) if isinstance(headers, Sequence) else headers + return {**user_agent, **_headers} # type: ignore + return user_agent diff --git a/kirami/utils/resource.py b/kirami/utils/resource.py new file mode 100644 index 0000000..b39f6b3 --- /dev/null +++ b/kirami/utils/resource.py @@ -0,0 +1,329 @@ +"""本模块提供了多种资源管理工具""" + +import os +import random +import secrets +import tempfile +from abc import ABC, abstractmethod +from collections.abc import Generator +from contextlib import contextmanager +from pathlib import Path +from typing import Any, ClassVar + +from flowery import Imager +from typing_extensions import Self + +from kirami.config import AUDIO_DIR, FONT_DIR, IMAGE_DIR, RES_DIR, VIDEO_DIR +from kirami.exception import ResourceError, WriteFileError +from kirami.message import MessageSegment + + +class BaseResource: + __root__: ClassVar[Path] = RES_DIR + """资源根目录""" + __format__: ClassVar[set[str]] = set() + """支持的文件格式""" + + __slots__ = ("_path",) + + def __init__(self, path: str | Path | Self) -> None: + self._path = path.path if isinstance(path, BaseResource) else Path(path) + if not self._path.is_absolute(): + self._path = self.__root__ / self._path + + def __repr__(self) -> str: + return f"{self.__class__.__name__}(path={self.path})" + + def __truediv__(self, other: str | Path) -> Self: + return self.__class__(self.path / other) + + def __iter__(self) -> Generator[Self, None, None]: + for path in self.path.iterdir(): + if not self.__format__ or path.suffix in self.__format__: + yield self.__class__(path) + + def __getitem__(self, format: str) -> Generator[Self, None, None]: + format_ = format.lower().removeprefix(".") + for res in self: + if res.suffix == format_: + yield res + + @property + def path(self) -> Path: + """资源路径""" + return self._path + + @property + def name(self) -> str: + """资源名称""" + return self._path.name + + @property + def suffix(self) -> str: + """资源后缀名""" + return self._path.suffix + + @property + def stem(self) -> str: + """资源名称(无后缀)""" + return self._path.stem + + def is_file(self) -> bool: + """此资源是否为常规文件""" + return self.path.is_file() + + def is_dir(self) -> bool: + """此资源是否为目录""" + return self.path.is_dir() + + def exists(self) -> bool: + """该资源是否存在""" + return self.path.exists() + + def as_uri(self) -> str: + """将路径返回为文件 URI""" + return self.path.as_uri() + + def search( + self, pattern: str, recursion: bool = False + ) -> Generator[Self, None, None]: + """搜索所有匹配的资源。 + + ### 参数 + pattern: 搜索模式 + + recursion: 是否递归搜索 + + ### 生成 + 匹配的资源 + """ + searcher = self.path.rglob if recursion else self.path.glob + for path in searcher(pattern): + yield self.__class__(path) + + def choice(self) -> Self: + """随机抽取一个资源""" + return secrets.choice(list(self)) + + def random(self, num: int = 1) -> list[Self]: + """随机抽取指定数量的资源。 + + ### 参数 + num: 抽取数量,默认为1 + + ### 返回 + 抽取的资源列表 + """ + return random.sample(list(self), num) + + def save(self, file: str | bytes) -> None: + """保存文件。 + + ### 参数 + file: 要保存的文件 + + ### 异常 + WriteFileError: 资源不是有效的文件路径 + """ + if self.is_dir(): + raise WriteFileError(f"资源不是有效的文件路径: {self.path}") + if isinstance(file, str): + self.path.write_text(file, encoding="utf-8") + else: + self.path.write_bytes(file) + + def delete(self) -> None: + """删除资源""" + self.path.unlink() + + def move(self, target: str | Path) -> None: + """移动资源。 + + ### 参数 + target: 目标路径 + """ + self._path = self.path.rename(target) + + def rename(self, name: str) -> None: + """重命名资源。 + + ### 参数 + name: 新名称 + """ + self._path = self.path.rename(self.path.parent / name) + + @classmethod + def add_format(cls, format: str) -> None: + """添加支持的资源类型。 + + ### 参数 + format: 资源类型后缀名 + """ + format_ = format.lower().removeprefix(".") + cls.__format__.add(f".{format_}") + + +class MessageMixin(ABC): + @abstractmethod + def message(self) -> MessageSegment: + """将资源文件转换为 `MessageSegment` 对象""" + raise NotImplementedError + + +class OpenMixin(ABC): + @abstractmethod + def open(self) -> Any: + """打开资源文件""" + raise NotImplementedError + + +class Image(BaseResource, OpenMixin, MessageMixin): + __root__: ClassVar[Path] = IMAGE_DIR + __format__: ClassVar[set[str]] = { + ".png", + ".jpg", + ".jpeg", + ".gif", + ".webp", + ".avif", + ".svg", + } + + def open(self) -> Imager: + try: + return Imager.open(self.path) + except Exception as e: + raise ResourceError(f"无法打开图片: {self.path}") from e + + def message(self, *args, **kwargs) -> MessageSegment: + return MessageSegment.image(self.path, *args, **kwargs) + + +class Font(BaseResource): + __root__: ClassVar[Path] = FONT_DIR + __format__: ClassVar[set[str]] = { + ".ttf", + ".ttc", + ".otf", + ".otc", + ".woff", + ".woff2", + } + + +class Audio(BaseResource, MessageMixin): + __root__: ClassVar[Path] = AUDIO_DIR + __format__: ClassVar[set[str]] = { + ".wav", + ".flac", + ".ape", + ".alac", + ".mp3", + ".aac", + ".ogg", + } + + def message(self, *args, **kwargs) -> MessageSegment: + return MessageSegment.record(self.path, *args, **kwargs) + + +class Video(BaseResource, MessageMixin): + __root__: ClassVar[Path] = VIDEO_DIR + __format__: ClassVar[set[str]] = { + ".avi", + ".wmv", + ".mpeg", + ".mp4", + ".m4v", + ".mov", + ".mkv", + ".flv", + ".f4v", + ".webm", + } + + def message(self, *args, **kwargs) -> MessageSegment: + return MessageSegment.video(self.path, *args, **kwargs) + + +class Resource(BaseResource): + @staticmethod + def audio(path: str | Path | Audio) -> Audio: + """音频资源。 + + ### 参数 + path: 资源路径 + """ + return Audio(path) + + @staticmethod + def font(path: str | Path | Font) -> Font: + """字体资源。 + + ### 参数 + path: 资源路径 + """ + return Font(path) + + @staticmethod + def image(path: str | Path | Image) -> Image: + """图像资源。 + + ### 参数 + path: 资源路径 + """ + return Image(path) + + @staticmethod + def video(path: str | Path | Video) -> Video: + """视频资源。 + + ### 参数 + path: 资源路径 + """ + return Video(path) + + @classmethod + @contextmanager + def tempfile( + cls, suffix: str | None = None, prefix: str | None = None, text: bool = False + ) -> Generator[Self, None, None]: + """创建临时文件。 + + ### 参数 + suffix: 后缀名。如果 suffix 不是 None 则文件名将以该后缀结尾,是 None 则没有后缀 + + prefix: 前缀名。如果 prefix 不是 None,则文件名将以该前缀开头,是 None 则使用默认前缀。默认前缀是 `gettempprefix()` 或 `gettempprefixb()` 函数的返回值(自动调用合适的函数) + + text: 是否文本文件。如果 text 为 True,文件会以文本模式打开,否则以二进制模式打开 + + ### 生成 + 临时文件 + """ + fp, path = tempfile.mkstemp(suffix=suffix, prefix=prefix, text=text) + os.close(fp) + file = cls(path) + try: + yield file + finally: + file.delete() + + @classmethod + @contextmanager + def tempdir( + cls, suffix: str | None = None, prefix: str | None = None + ) -> Generator[Self, None, None]: + """创建临时目录。 + + ### 参数 + suffix: 后缀名。如果 suffix 不是 None 则文件名将以该后缀结尾,是 None 则没有后缀 + + prefix: 前缀名。如果 prefix 不是 None,则文件名将以该前缀开头,是 None 则使用默认前缀。默认前缀是 `gettempprefix()` 或 `gettempprefixb()` 函数的返回值(自动调用合适的函数) + + ### 生成 + 临时目录 + """ + with tempfile.TemporaryDirectory( + suffix=suffix, prefix=prefix, ignore_cleanup_errors=True + ) as tmpdir: + yield cls(tmpdir) diff --git a/kirami/utils/scheduler.py b/kirami/utils/scheduler.py new file mode 100644 index 0000000..cef62d2 --- /dev/null +++ b/kirami/utils/scheduler.py @@ -0,0 +1,33 @@ +"""本模块提供了一个异步定时任务调度器""" + +import logging + +from apscheduler.schedulers.asyncio import AsyncIOScheduler +from loguru import logger +from nonebot.log import LoguruHandler + +from kirami.config import bot_config +from kirami.hook import on_shutdown, on_startup + +scheduler = AsyncIOScheduler() +scheduler.configure(bot_config.apscheduler_config) + + +@on_startup +async def start_scheduler() -> None: + if not scheduler.running: + scheduler.start() + logger.opt(colors=True).info("Scheduler Started") + + +@on_shutdown +async def shutdown_scheduler() -> None: + if scheduler.running: + scheduler.shutdown() + logger.opt(colors=True).info("Scheduler Shutdown") + + +aps_logger = logging.getLogger("apscheduler") +aps_logger.setLevel(getattr(bot_config, "apscheduler_log_level", 30)) +aps_logger.handlers.clear() +aps_logger.addHandler(LoguruHandler()) diff --git a/kirami/utils/utils.py b/kirami/utils/utils.py new file mode 100644 index 0000000..5c30f15 --- /dev/null +++ b/kirami/utils/utils.py @@ -0,0 +1,477 @@ +import asyncio +import inspect +import json +from collections.abc import Callable, Coroutine +from datetime import datetime, time, timedelta +from functools import wraps +from io import BytesIO +from pathlib import Path +from typing import Any, Literal, ParamSpec, TypeVar, overload + +import arrow +import yaml +from flowery import Imager +from httpx import Response +from httpx._types import QueryParamTypes, RequestData +from nonebot.adapters.onebot.v11.utils import escape as escape +from nonebot.adapters.onebot.v11.utils import unescape as unescape +from PIL import Image as PILImg +from tenacity import AsyncRetrying, RetryError, stop_after_attempt + +from kirami.config import BOT_DIR, RES_DIR +from kirami.exception import FileNotExistError, NetworkError, ReadFileError + +from .renderer import Renderer +from .request import Request +from .webwright import WebWright + +try: # pragma: py-gte-311 + import tomllib # pyright: ignore[reportMissingImports] +except ModuleNotFoundError: # pragma: py-lt-311 + import tomli as tomllib + +R = TypeVar("R") +"""返回值泛型。""" + +P = ParamSpec("P") +"""参数泛型""" + + +def get_path(file: str | Path, *, depth: int = 0) -> Path: + """获取文件的绝对路径。 + + ### 参数 + file: 文件路径,如果为相对路径,则相对于当前文件的父目录 + + depth: 调用栈深度。默认为 0,即当前函数调用栈的深度 + + ### 异常 + FileNotExistError: 文件不存在 + """ + if Path(file).is_absolute(): + path = Path(file) + else: + path = Path(inspect.stack()[depth + 1].filename).parent / file + + if path.is_file(): + return path.resolve() + + raise FileNotExistError(f"找不到文件: {path}") + + +def load_data(file: str | Path = "data.json") -> dict[str, Any]: + """读取文件数据。 + + 支持的文件格式有:`json`、`yaml`、`toml`。 + + ### 参数 + file: 文件路径,如果为相对路径,则相对于当前文件的父目录。默认为 `data.json` + + ### 返回 + 解析后的数据 + + ### 异常 + FileNotFoundError: 文件不存在 + + ValueError: 文件格式不支持 + """ + data_path = get_path(file, depth=1) + data = data_path.read_text(encoding="utf-8") + + if data_path.suffix == ".json": + file_data = json.loads(data) + elif data_path.suffix in (".yml", ".yaml"): + file_data = yaml.safe_load(data) + elif data_path.suffix == ".toml": + file_data = tomllib.loads(data) + else: + raise ReadFileError(f"不支持的文件格式: {data_path.suffix}, 只能是 .json、.yaml 或 .toml。") + + if file_data is None: + raise ReadFileError(f"文件内容为空: {data_path}") + + return file_data + + +def new_dir(path: str | Path, root: str | Path = BOT_DIR) -> Path: + """创建一个新的目录。 + + ### 参数 + path: 新目录的路径 + + root: 相对于新目录的根目录。默认为 bot 根目录 + + ### 返回 + 新目录的绝对路径 + """ + root = Path(root) + + if root.is_file(): + raise RuntimeError("root 应该是一个目录, 而不是一个文件") + + dir_ = root / path + dir_.mkdir(parents=True, exist_ok=True) + + return dir_.resolve() + + +def is_path(fs: str | Path) -> bool: + """判断是否是一个文件路径。 + + ### 参数 + fs: 文件路径 + """ + file = Path(fs) if isinstance(fs, str) else fs + return file.is_file() or isinstance(fs, Path) + + +def str_of_size(size: int) -> str: + """将字节大小转换为带单位的字符串。 + + ### 参数 + size: 字节大小 + + ### 异常 + ValueError: 参数 `size` 小于0 + """ + if size < 0: + raise ValueError("size 不能小于0") + if size < 1024: # noqa: PLR2004 + return f"{size:.2f} B" + if size < 1024**2: + return f"{size / 1024:.2f} KB" + if size < 1024**3: + return f"{size / 1024 ** 2:.2f} MB" + if size < 1024**4: + return f"{size / 1024 ** 3:.2f} GB" + if size < 1024**5: + return f"{size / 1024 ** 4:.2f} TB" + return f"{size / 1024 ** 5:.2f} PB" + + +def singleton(cls: Callable[P, R]) -> Callable[P, R]: + """单例模式装饰器""" + _instance: dict[Callable[P, R], Any] = {} + + @wraps(cls) + def wrapper(*args: P.args, **kwargs: P.kwargs) -> R: + if cls not in _instance: + _instance[cls] = cls(*args, **kwargs) + return _instance[cls] + + return wrapper + + +def awaitable(func: Callable[P, R]) -> Callable[P, Coroutine[Any, Any, R]]: + """同步转异步装饰器""" + + @wraps(func) + async def wrapper(*args: P.args, **kwargs: P.kwargs) -> R: + return await asyncio.to_thread(func, *args, **kwargs) + + return wrapper + + +async def get_pic( + url: str, + size: tuple[int, int] | int | None = None, + convert: Literal["RGBA", "RGB", "L"] = "RGBA", + **kwargs, +) -> Imager: + """从网络获取图片,格式化为指定尺寸的指定图像模式。 + + ### 参数 + url:图片链接 + + size:调整尺寸大小 + + convert:转换图像模式 + + **kwargs:传递给 `Request.get` 的关键字参数 + """ + resp = await Request.get(url, stream=True, **kwargs) + if resp.is_error: + raise NetworkError("获取图片失败") + pic = Imager.open(BytesIO(resp.content)) + if convert: + pic = pic.convert(convert) + if size: + pic = pic.resize(size, PILImg.LANCZOS) + return pic + + +async def tpl2img( + tpl: str | Path, + *, + data: dict[str, Any] | None = None, + base_path: str | Path | None = None, + width: int = 480, + device_scale_factor: int | float = 1, + **kwargs, +) -> bytes: + """将 jinja2 模板转换为图片。 + + ### 参数 + tpl: 模板文件路径或字符串 + + data: 模板渲染所需的数据 + + base_path: 模板文件中的相对路径的根目录 + + width: 图片宽度 + + device_scale_factor: 设备缩放因子 + + **kwargs: 传递给 `html2pic` 的关键字参数 + + ### 返回 + 图片的二进制数据 + """ + html = await Renderer.template(tpl, **(data or {})) + if not base_path and is_path(tpl): + base_path = Path(tpl).parent + return await html2pic( + html, base_path, width=width, device_scale_factor=device_scale_factor, **kwargs + ) + + +async def md2img( + md: str | Path, + *, + theme: Literal["light", "dark"] = "light", + highlight: str | None = "auto", + extra: str = "", + base_path: str | Path | None = None, + width: int = 480, + device_scale_factor: int | float = 1, + **kwargs, +) -> bytes: + """将 markdown 转换为图片。 + + ### 参数 + md: markdown 文件路径或字符串 + + theme: 主题,可选值为 "light" 或 "dark",默认为 "light" + + highlight: 代码高亮主题,可选值为 "auto" 或 pygments 支持的主题,默认为 "auto" + + extra: 额外的 head 标签内容,可以是 meta 标签、link 标签、script 标签、style 标签等 + + base_path: markdown 文件中的相对路径的根目录 + + width: 图片宽度 + + device_scale_factor: 设备缩放因子 + + **kwargs: 传递给 `html2pic` 的关键字参数 + + ### 返回 + 图片的二进制数据 + """ + html = await Renderer.markdown(md, theme, highlight, extra) + if not base_path and is_path(md): + base_path = Path(md).parent + return await html2pic( + html, base_path, width=width, device_scale_factor=device_scale_factor, **kwargs + ) + + +async def html2pic( + html: str, + /, + base_path: str | Path | None = None, + *, + width: int = 1280, + wait_until: Literal[ + "commit", "domcontentloaded", "load", "networkidle" + ] = "networkidle", + wait: int | float = 0, + device_scale_factor: int | float = 1, + locator: str | None = None, + **kwargs, +) -> bytes: + """将网页转换为图片。 + + ### 参数 + html: 网页文本 + + base_path: 网页中的相对路径的根目录 + + width: 网页宽度,单位为像素 + + wait_until: 等待网页加载的事件,可选值为 "commit"、"domcontentloaded"、"load"、"networkidle" + + wait: 等待网页加载的时间,单位为毫秒 + + device_scale_factor: 设备缩放因子 + + locator: 网页中的元素定位器,如果指定了该参数,则只截取该元素的图像 + + **kwargs: 传递给 `playwright.async_api.Page.screenshot` 的关键字参数 + + ### 返回 + 图片的二进制数据 + """ + base_path = Path(base_path) if base_path else RES_DIR + async with WebWright.new_page( + viewport={"width": width, "height": 1}, device_scale_factor=device_scale_factor + ) as page: + await page.goto(base_path.absolute().as_uri()) + await page.set_content(html, wait_until=wait_until) + await page.wait_for_timeout(wait) + if locator: + return await page.locator(locator).screenshot(**kwargs) + return await page.screenshot(full_page=True, **kwargs) + + +@overload +async def get_api_data( + url: str, + params: QueryParamTypes | None = None, + data: RequestData | None = None, + retry: int = 3, + to_json: Literal[True] = True, + **kwargs, +) -> Any: + ... + + +@overload +async def get_api_data( + url: str, + params: QueryParamTypes | None = None, + data: RequestData | None = None, + retry: int = 3, + to_json: Literal[False] = False, + **kwargs, +) -> Response: + ... + + +async def get_api_data( + url: str, + params: QueryParamTypes | None = None, + data: RequestData | None = None, + retry: int = 3, + to_json: bool = True, + **kwargs, +) -> Response | Any: + """请求 API 获取数据。 + + ### 参数 + url: API 地址 + + params: get 请求参数 + + data: post 请求数据 + + retry: 重试次数 + + to_json: 是否将结果转换为 json + + ### 返回 + API 请求结果 + + ### 异常 + NetworkError: API 请求失败 + """ + try: + async for attempt in AsyncRetrying(stop=stop_after_attempt(retry)): + with attempt: + if data: + result = await Request.post(url, data=data, params=params, **kwargs) + else: + result = await Request.get(url, params=params, **kwargs) + if result.is_error: + raise NetworkError(f"{result.status_code}") + + return result.json() if to_json else result + except RetryError as e: + raise NetworkError("API 请求失败") from e + + +def human_readable_time(seconds: int) -> str: + """将给定的秒数转换为易读的时间格式。 + + ### 参数 + seconds: 要转换的秒数 + + ### 返回 + 转换后的时间字符串,格式为 "X小时Y分Z秒" + """ + seconds = max(seconds, 0) + minutes, seconds = divmod(seconds, 60) + hours, minutes = divmod(minutes, 60) + + time_string = "" + + if hours: + time_string += f"{hours}小时" + if minutes: + time_string += f"{minutes}分" + if seconds or not time_string: + time_string += f"{seconds}秒" + + return time_string + + +def get_shift_now( + year: int = 0, + month: int = 0, + day: int = 0, + hour: int = 0, + minute: int = 0, + second: int = 0, + **kwargs: Any, +) -> datetime: + """从当前时间推移一定时间 + + ### 参数 + year: 推移的年数 + + month: 推移的月数 + + day: 推移的天数 + + hour: 推移的小时数 + + minute: 推移的分钟数 + + second: 推移的秒数 + + **kwargs: 其他关键字参数,参考 `arrow.arrow.Arrow.shift` 的参数 + + ### 返回 + 推移后的 `datetime` 对象 + """ + now = arrow.now() + shift_date = now.shift( + years=year, + months=month, + days=day, + hours=hour, + minutes=minute, + seconds=second, + **kwargs, + ) + return shift_date.datetime + + +def get_daily_datetime(datetime_time: time) -> datetime: + """返回今天指定时间的 `datetime` 对象,如果当前时间已经过了指定时间,则返回明天的 `datetime` 对象。 + + ### 参数 + datetime_time: 指定的时间,类型为 `datetime.time` 对象 + + ### 返回 + 今天或明天的指定时间 `datetime` 对象 + """ + current_datetime = datetime.now() + daily_datetime = datetime.combine(current_datetime, datetime_time) + + if current_datetime > daily_datetime: + daily_datetime = datetime.combine( + current_datetime + timedelta(days=1), datetime_time + ) + + return daily_datetime.astimezone() diff --git a/kirami/utils/webwright.py b/kirami/utils/webwright.py new file mode 100644 index 0000000..83438ab --- /dev/null +++ b/kirami/utils/webwright.py @@ -0,0 +1,130 @@ +"""本模块提供了一个简单的浏览器管理器""" + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager +from typing import ClassVar, Literal, cast + +from loguru import logger +from playwright.async_api import ( + Browser, + BrowserContext, + BrowserType, + Error, + Page, + Playwright, + async_playwright, +) + +from kirami.config import bot_config +from kirami.hook import on_shutdown + + +class WebWright: + _playwright: ClassVar[Playwright | None] = None + _browser: ClassVar[Browser | None] = None + + @classmethod + async def launch( + cls, + browser: Literal["chromium", "firefox", "webkit"] | None = None, + *args, + **kwargs, + ) -> None: + """启动浏览器。 + + ### 参数 + browser: 浏览器类型,可选值为 `chromium`,`firefox`,`webkit`,未指定时使用配置文件中的默认值 + + *args: 传递给 [playwright.async_api.BrowserType.launch](https://playwright.dev/python/docs/api/class-browsertype#browser-type-launch) 的位置参数 + + **kwargs: 传递给 [playwright.async_api.BrowserType.launch](https://playwright.dev/python/docs/api/class-browsertype#browser-type-launch) 的关键字参数 + """ + try: + if not cls._playwright: + cls._playwright = await async_playwright().start() + if not cls._browser: + browser_type: BrowserType = getattr( + cls._playwright, browser or bot_config.browser + ) + cls._browser = await browser_type.launch(*args, **kwargs) + except Error as e: + logger.opt(exception=e).error(f"{bot_config.browser} 浏览器未安装,正在尝试自动安装") + await install_browser() + + @classmethod + async def stop(cls) -> None: + """关闭浏览器""" + if cls._browser: + await cls._browser.close() + if cls._playwright: + await cls._playwright.stop() + + @classmethod + async def get_browser(cls) -> Browser: + """获取浏览器实例""" + if not cls._browser: + await cls.launch() + return cast(Browser, cls._browser) + + @classmethod + @asynccontextmanager + async def new_context(cls, **kwargs) -> AsyncGenerator[BrowserContext, None]: + """新建浏览器上下文。 + + ### 参数 + **kwargs: 传递给 [playwright.async_api.Browser.new_context](https://playwright.dev/python/docs/api/class-browser#browser-new-context) 的关键字参数 + """ + browser = await cls.get_browser() + context = await browser.new_context(**kwargs) + try: + yield context + finally: + await context.close() + + @classmethod + @asynccontextmanager + async def new_page(cls, **kwargs) -> AsyncGenerator[Page, None]: + """新建页面。 + + ### 参数 + **kwargs: 传递给 [playwright.async_api.Browser.new_page](https://playwright.dev/python/docs/api/class-browser#browser-new-page) 的关键字参数 + """ + browser = await cls.get_browser() + page = await browser.new_page(**kwargs) + try: + yield page + finally: + await page.close() + + +async def install_browser() -> None: + import os + import shutil + import subprocess + import sys + + from playwright.__main__ import main + + from kirami.config import bot_config + + if not (playwright_cmd := shutil.which("playwright")): + raise RuntimeError("Playwright is not installed") from None + + os.environ["PLAYWRIGHT_DOWNLOAD_HOST"] = "https://npmmirror.com/mirrors/playwright/" + sys.argv = ["", "install", bot_config.browser] + + logger.debug(f"Running {playwright_cmd} install {bot_config.browser}") + + try: + logger.debug("Install dependencies necessary to run browsers") + subprocess.run([playwright_cmd, "install-deps"], check=True) # noqa: S603 + logger.debug("Install browsers for this version of Playwright") + main() + logger.success(f"{bot_config.browser} 浏览器安装成功") + except subprocess.CalledProcessError as e: + logger.opt(colors=True, exception=e).error( + f"{bot_config.browser} 浏览器安装失败,请检查网络状况或尝试手动安装" + ) + + +on_shutdown(WebWright.stop) diff --git a/kirami/version.py b/kirami/version.py new file mode 100644 index 0000000..48e839e --- /dev/null +++ b/kirami/version.py @@ -0,0 +1,19 @@ +"""本模块用于获取元数据及版本信息""" + +from importlib.metadata import metadata + +from pydantic import BaseModel + + +class Metadata(BaseModel): + name: str + version: str + summary: str + + class Config: + extra = "allow" + + +__metadata__ = Metadata(**metadata("kiramibot").json) # type: ignore + +__version__ = __metadata__.version diff --git a/pdm.lock b/pdm.lock new file mode 100644 index 0000000..92a8f3d --- /dev/null +++ b/pdm.lock @@ -0,0 +1,1994 @@ +# This file is @generated by PDM. +# It is not intended for manual editing. + +[metadata] +groups = ["default", "dev", "hyper", "test"] +cross_platform = true +static_urls = false +lock_version = "4.3" +content_hash = "sha256:793ad5ba61761f100f1a6c55f41265ba874cffdb94b5998d7e1c3a60d4e34be6" + +[[package]] +name = "anyio" +version = "3.6.2" +requires_python = ">=3.6.2" +summary = "High level compatibility layer for multiple asynchronous event loop implementations" +dependencies = [ + "idna>=2.8", + "sniffio>=1.1", +] +files = [ + {file = "anyio-3.6.2-py3-none-any.whl", hash = "sha256:fbbe32bd270d2a2ef3ed1c5d45041250284e31fc0a4df4a5a6071842051a51e3"}, + {file = "anyio-3.6.2.tar.gz", hash = "sha256:25ea0d673ae30af41a0c442f81cf3b38c7e79fdc7b60335a4c14e05eb0947421"}, +] + +[[package]] +name = "apscheduler" +version = "3.10.1" +requires_python = ">=3.6" +summary = "In-process task scheduler with Cron-like capabilities" +dependencies = [ + "pytz", + "setuptools>=0.7", + "six>=1.4.0", + "tzlocal!=3.*,>=2.0", +] +files = [ + {file = "APScheduler-3.10.1-py3-none-any.whl", hash = "sha256:e813ad5ada7aff36fb08cdda746b520531eaac7757832abc204868ba78e0c8f6"}, + {file = "APScheduler-3.10.1.tar.gz", hash = "sha256:0293937d8f6051a0f493359440c1a1b93e882c57daf0197afeff0e727777b96e"}, +] + +[[package]] +name = "arrow" +version = "1.2.3" +requires_python = ">=3.6" +summary = "Better dates & times for Python" +dependencies = [ + "python-dateutil>=2.7.0", +] +files = [ + {file = "arrow-1.2.3-py3-none-any.whl", hash = "sha256:5a49ab92e3b7b71d96cd6bfcc4df14efefc9dfa96ea19045815914a6ab6b1fe2"}, + {file = "arrow-1.2.3.tar.gz", hash = "sha256:3934b30ca1b9f292376d9db15b19446088d12ec58629bc3f0da28fd55fb633a1"}, +] + +[[package]] +name = "bidict" +version = "0.22.1" +requires_python = ">=3.7" +summary = "The bidirectional mapping library for Python." +files = [ + {file = "bidict-0.22.1-py3-none-any.whl", hash = "sha256:6ef212238eb884b664f28da76f33f1d28b260f665fc737b413b287d5487d1e7b"}, + {file = "bidict-0.22.1.tar.gz", hash = "sha256:1e0f7f74e4860e6d0943a05d4134c63a2fad86f3d4732fb265bd79e4e856d81d"}, +] + +[[package]] +name = "black" +version = "23.7.0" +requires_python = ">=3.8" +summary = "The uncompromising code formatter." +dependencies = [ + "click>=8.0.0", + "mypy-extensions>=0.4.3", + "packaging>=22.0", + "pathspec>=0.9.0", + "platformdirs>=2", + "tomli>=1.1.0; python_version < \"3.11\"", +] +files = [ + {file = "black-23.7.0-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:5c4bc552ab52f6c1c506ccae05681fab58c3f72d59ae6e6639e8885e94fe2587"}, + {file = "black-23.7.0-cp310-cp310-macosx_10_16_universal2.whl", hash = "sha256:552513d5cd5694590d7ef6f46e1767a4df9af168d449ff767b13b084c020e63f"}, + {file = "black-23.7.0-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:86cee259349b4448adb4ef9b204bb4467aae74a386bce85d56ba4f5dc0da27be"}, + {file = "black-23.7.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:501387a9edcb75d7ae8a4412bb8749900386eaef258f1aefab18adddea1936bc"}, + {file = "black-23.7.0-cp310-cp310-win_amd64.whl", hash = "sha256:fb074d8b213749fa1d077d630db0d5f8cc3b2ae63587ad4116e8a436e9bbe995"}, + {file = "black-23.7.0-cp311-cp311-macosx_10_16_arm64.whl", hash = "sha256:b5b0ee6d96b345a8b420100b7d71ebfdd19fab5e8301aff48ec270042cd40ac2"}, + {file = "black-23.7.0-cp311-cp311-macosx_10_16_universal2.whl", hash = "sha256:893695a76b140881531062d48476ebe4a48f5d1e9388177e175d76234ca247cd"}, + {file = "black-23.7.0-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:c333286dc3ddca6fdff74670b911cccedacb4ef0a60b34e491b8a67c833b343a"}, + {file = "black-23.7.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:831d8f54c3a8c8cf55f64d0422ee875eecac26f5f649fb6c1df65316b67c8926"}, + {file = "black-23.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:7f3bf2dec7d541b4619b8ce526bda74a6b0bffc480a163fed32eb8b3c9aed8ad"}, + {file = "black-23.7.0-cp38-cp38-macosx_10_16_arm64.whl", hash = "sha256:f9062af71c59c004cd519e2fb8f5d25d39e46d3af011b41ab43b9c74e27e236f"}, + {file = "black-23.7.0-cp38-cp38-macosx_10_16_universal2.whl", hash = "sha256:01ede61aac8c154b55f35301fac3e730baf0c9cf8120f65a9cd61a81cfb4a0c3"}, + {file = "black-23.7.0-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:327a8c2550ddc573b51e2c352adb88143464bb9d92c10416feb86b0f5aee5ff6"}, + {file = "black-23.7.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d1c6022b86f83b632d06f2b02774134def5d4d4f1dac8bef16d90cda18ba28a"}, + {file = "black-23.7.0-cp38-cp38-win_amd64.whl", hash = "sha256:27eb7a0c71604d5de083757fbdb245b1a4fae60e9596514c6ec497eb63f95320"}, + {file = "black-23.7.0-cp39-cp39-macosx_10_16_arm64.whl", hash = "sha256:8417dbd2f57b5701492cd46edcecc4f9208dc75529bcf76c514864e48da867d9"}, + {file = "black-23.7.0-cp39-cp39-macosx_10_16_universal2.whl", hash = "sha256:47e56d83aad53ca140da0af87678fb38e44fd6bc0af71eebab2d1f59b1acf1d3"}, + {file = "black-23.7.0-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:25cc308838fe71f7065df53aedd20327969d05671bac95b38fdf37ebe70ac087"}, + {file = "black-23.7.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:642496b675095d423f9b8448243336f8ec71c9d4d57ec17bf795b67f08132a91"}, + {file = "black-23.7.0-cp39-cp39-win_amd64.whl", hash = "sha256:ad0014efc7acf0bd745792bd0d8857413652979200ab924fbf239062adc12491"}, + {file = "black-23.7.0-py3-none-any.whl", hash = "sha256:9fd59d418c60c0348505f2ddf9609c1e1de8e7493eab96198fc89d9f865e7a96"}, + {file = "black-23.7.0.tar.gz", hash = "sha256:022a582720b0d9480ed82576c920a8c1dde97cc38ff11d8d8859b3bd6ca9eedb"}, +] + +[[package]] +name = "cachetools" +version = "5.3.0" +requires_python = "~=3.7" +summary = "Extensible memoizing collections and decorators" +files = [ + {file = "cachetools-5.3.0-py3-none-any.whl", hash = "sha256:429e1a1e845c008ea6c85aa35d4b98b65d6a9763eeef3e37e92728a12d1de9d4"}, + {file = "cachetools-5.3.0.tar.gz", hash = "sha256:13dfddc7b8df938c21a940dfa6557ce6e94a2f1cdfa58eb90c805721d58f2c14"}, +] + +[[package]] +name = "cashews" +version = "6.2.0" +requires_python = ">=3.7" +summary = "cache tools with async power" +files = [ + {file = "cashews-6.2.0-py3-none-any.whl", hash = "sha256:8a005fdb429efad8a99e2d8c3024a0a59bed35d49fe67da35a2cd9cb1d56cd89"}, + {file = "cashews-6.2.0.tar.gz", hash = "sha256:c197202336d1bfde732bf43c30c8fd3fdb5836700e8a81bdd25abcc5ea1df9c8"}, +] + +[[package]] +name = "certifi" +version = "2022.12.7" +requires_python = ">=3.6" +summary = "Python package for providing Mozilla's CA Bundle." +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 = "cfgv" +version = "3.3.1" +requires_python = ">=3.6.1" +summary = "Validate configuration and produce human readable error messages." +files = [ + {file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"}, + {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"}, +] + +[[package]] +name = "charset-normalizer" +version = "3.1.0" +requires_python = ">=3.7.0" +summary = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +files = [ + {file = "charset-normalizer-3.1.0.tar.gz", hash = "sha256:34e0a2f9c370eb95597aae63bf85eb5e96826d81e3dcf88b8886012906f509b5"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e0ac8959c929593fee38da1c2b64ee9778733cdf03c482c9ff1d508b6b593b2b"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d7fc3fca01da18fbabe4625d64bb612b533533ed10045a2ac3dd194bfa656b60"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:04eefcee095f58eaabe6dc3cc2262f3bcd776d2c67005880894f447b3f2cb9c1"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20064ead0717cf9a73a6d1e779b23d149b53daf971169289ed2ed43a71e8d3b0"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1435ae15108b1cb6fffbcea2af3d468683b7afed0169ad718451f8db5d1aff6f"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c84132a54c750fda57729d1e2599bb598f5fa0344085dbde5003ba429a4798c0"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75f2568b4189dda1c567339b48cba4ac7384accb9c2a7ed655cd86b04055c795"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:11d3bcb7be35e7b1bba2c23beedac81ee893ac9871d0ba79effc7fc01167db6c"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:891cf9b48776b5c61c700b55a598621fdb7b1e301a550365571e9624f270c203"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:5f008525e02908b20e04707a4f704cd286d94718f48bb33edddc7d7b584dddc1"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:b06f0d3bf045158d2fb8837c5785fe9ff9b8c93358be64461a1089f5da983137"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:49919f8400b5e49e961f320c735388ee686a62327e773fa5b3ce6721f7e785ce"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:22908891a380d50738e1f978667536f6c6b526a2064156203d418f4856d6e86a"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-win32.whl", hash = "sha256:12d1a39aa6b8c6f6248bb54550efcc1c38ce0d8096a146638fd4738e42284448"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:65ed923f84a6844de5fd29726b888e58c62820e0769b76565480e1fdc3d062f8"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9a3267620866c9d17b959a84dd0bd2d45719b817245e49371ead79ed4f710d19"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6734e606355834f13445b6adc38b53c0fd45f1a56a9ba06c2058f86893ae8017"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f8303414c7b03f794347ad062c0516cee0e15f7a612abd0ce1e25caf6ceb47df"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aaf53a6cebad0eae578f062c7d462155eada9c172bd8c4d250b8c1d8eb7f916a"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3dc5b6a8ecfdc5748a7e429782598e4f17ef378e3e272eeb1340ea57c9109f41"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e1b25e3ad6c909f398df8921780d6a3d120d8c09466720226fc621605b6f92b1"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ca564606d2caafb0abe6d1b5311c2649e8071eb241b2d64e75a0d0065107e62"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b82fab78e0b1329e183a65260581de4375f619167478dddab510c6c6fb04d9b6"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bd7163182133c0c7701b25e604cf1611c0d87712e56e88e7ee5d72deab3e76b5"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:11d117e6c63e8f495412d37e7dc2e2fff09c34b2d09dbe2bee3c6229577818be"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:cf6511efa4801b9b38dc5546d7547d5b5c6ef4b081c60b23e4d941d0eba9cbeb"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:abc1185d79f47c0a7aaf7e2412a0eb2c03b724581139193d2d82b3ad8cbb00ac"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cb7b2ab0188829593b9de646545175547a70d9a6e2b63bf2cd87a0a391599324"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-win32.whl", hash = "sha256:c36bcbc0d5174a80d6cccf43a0ecaca44e81d25be4b7f90f0ed7bcfbb5a00909"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:cca4def576f47a09a943666b8f829606bcb17e2bc2d5911a46c8f8da45f56755"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0c95f12b74681e9ae127728f7e5409cbbef9cd914d5896ef238cc779b8152373"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fca62a8301b605b954ad2e9c3666f9d97f63872aa4efcae5492baca2056b74ab"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac0aa6cd53ab9a31d397f8303f92c42f534693528fafbdb997c82bae6e477ad9"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c3af8e0f07399d3176b179f2e2634c3ce9c1301379a6b8c9c9aeecd481da494f"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a5fc78f9e3f501a1614a98f7c54d3969f3ad9bba8ba3d9b438c3bc5d047dd28"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:628c985afb2c7d27a4800bfb609e03985aaecb42f955049957814e0491d4006d"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:74db0052d985cf37fa111828d0dd230776ac99c740e1a758ad99094be4f1803d"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:1e8fcdd8f672a1c4fc8d0bd3a2b576b152d2a349782d1eb0f6b8e52e9954731d"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:04afa6387e2b282cf78ff3dbce20f0cc071c12dc8f685bd40960cc68644cfea6"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:dd5653e67b149503c68c4018bf07e42eeed6b4e956b24c00ccdf93ac79cdff84"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d2686f91611f9e17f4548dbf050e75b079bbc2a82be565832bc8ea9047b61c8c"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-win32.whl", hash = "sha256:4155b51ae05ed47199dc5b2a4e62abccb274cee6b01da5b895099b61b1982974"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:322102cdf1ab682ecc7d9b1c5eed4ec59657a65e1c146a0da342b78f4112db23"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e633940f28c1e913615fd624fcdd72fdba807bf53ea6925d6a588e84e1151531"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3a06f32c9634a8705f4ca9946d667609f52cf130d5548881401f1eb2c39b1e2c"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7381c66e0561c5757ffe616af869b916c8b4e42b367ab29fedc98481d1e74e14"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3573d376454d956553c356df45bb824262c397c6e26ce43e8203c4c540ee0acb"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e89df2958e5159b811af9ff0f92614dabf4ff617c03a4c1c6ff53bf1c399e0e1"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:78cacd03e79d009d95635e7d6ff12c21eb89b894c354bd2b2ed0b4763373693b"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de5695a6f1d8340b12a5d6d4484290ee74d61e467c39ff03b39e30df62cf83a0"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1c60b9c202d00052183c9be85e5eaf18a4ada0a47d188a83c8f5c5b23252f649"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f645caaf0008bacf349875a974220f1f1da349c5dbe7c4ec93048cdc785a3326"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:ea9f9c6034ea2d93d9147818f17c2a0860d41b71c38b9ce4d55f21b6f9165a11"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:80d1543d58bd3d6c271b66abf454d437a438dff01c3e62fdbcd68f2a11310d4b"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:73dc03a6a7e30b7edc5b01b601e53e7fc924b04e1835e8e407c12c037e81adbd"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6f5c2e7bc8a4bf7c426599765b1bd33217ec84023033672c1e9a8b35eaeaaaf8"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-win32.whl", hash = "sha256:12a2b561af122e3d94cdb97fe6fb2bb2b82cef0cdca131646fdb940a1eda04f0"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:3160a0fd9754aab7d47f95a6b63ab355388d890163eb03b2d2b87ab0a30cfa59"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:38e812a197bf8e71a59fe55b757a84c1f946d0ac114acafaafaf21667a7e169e"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6baf0baf0d5d265fa7944feb9f7451cc316bfe30e8df1a61b1bb08577c554f31"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8f25e17ab3039b05f762b0a55ae0b3632b2e073d9c8fc88e89aca31a6198e88f"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3747443b6a904001473370d7810aa19c3a180ccd52a7157aacc264a5ac79265e"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b116502087ce8a6b7a5f1814568ccbd0e9f6cfd99948aa59b0e241dc57cf739f"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d16fd5252f883eb074ca55cb622bc0bee49b979ae4e8639fff6ca3ff44f9f854"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21fa558996782fc226b529fdd2ed7866c2c6ec91cee82735c98a197fae39f706"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f6c7a8a57e9405cad7485f4c9d3172ae486cfef1344b5ddd8e5239582d7355e"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ac3775e3311661d4adace3697a52ac0bab17edd166087d493b52d4f4f553f9f0"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:10c93628d7497c81686e8e5e557aafa78f230cd9e77dd0c40032ef90c18f2230"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:6f4f4668e1831850ebcc2fd0b1cd11721947b6dc7c00bf1c6bd3c929ae14f2c7"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:0be65ccf618c1e7ac9b849c315cc2e8a8751d9cfdaa43027d4f6624bd587ab7e"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:53d0a3fa5f8af98a1e261de6a3943ca631c526635eb5817a87a59d9a57ebf48f"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-win32.whl", hash = "sha256:a04f86f41a8916fe45ac5024ec477f41f886b3c435da2d4e3d2709b22ab02af1"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:830d2948a5ec37c386d3170c483063798d7879037492540f10a475e3fd6f244b"}, + {file = "charset_normalizer-3.1.0-py3-none-any.whl", hash = "sha256:3d9098b479e78c85080c98e1e35ff40b4a31d8953102bb0fd7d1b6f8a2111a3d"}, +] + +[[package]] +name = "click" +version = "8.1.3" +requires_python = ">=3.7" +summary = "Composable command line interface toolkit" +dependencies = [ + "colorama; platform_system == \"Windows\"", +] +files = [ + {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, + {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, +] + +[[package]] +name = "colorama" +version = "0.4.6" +requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +summary = "Cross-platform colored terminal text." +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 = "cssselect" +version = "1.2.0" +requires_python = ">=3.7" +summary = "cssselect parses CSS3 Selectors and translates them to XPath 1.0" +files = [ + {file = "cssselect-1.2.0-py2.py3-none-any.whl", hash = "sha256:da1885f0c10b60c03ed5eccbb6b68d6eff248d91976fcde348f395d54c9fd35e"}, + {file = "cssselect-1.2.0.tar.gz", hash = "sha256:666b19839cfaddb9ce9d36bfe4c969132c647b92fc9088c4e23f786b30f1b3dc"}, +] + +[[package]] +name = "cssutils" +version = "2.6.0" +requires_python = ">=3.7" +summary = "A CSS Cascading Style Sheets library for Python" +files = [ + {file = "cssutils-2.6.0-py3-none-any.whl", hash = "sha256:30c72f3a5c5951a11151640600aae7b3bf10e4c0d5c87f5bc505c2cd4a26e0c2"}, + {file = "cssutils-2.6.0.tar.gz", hash = "sha256:f7dcd23c1cec909fdf3630de346e1413b7b2555936dec14ba2ebb9913bf0818e"}, +] + +[[package]] +name = "distlib" +version = "0.3.6" +summary = "Distribution utilities" +files = [ + {file = "distlib-0.3.6-py2.py3-none-any.whl", hash = "sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e"}, + {file = "distlib-0.3.6.tar.gz", hash = "sha256:14bad2d9b04d3a36127ac97f30b12a19268f211063d8f8ee4f47108896e11b46"}, +] + +[[package]] +name = "dnspython" +version = "2.3.0" +requires_python = ">=3.7,<4.0" +summary = "DNS toolkit" +files = [ + {file = "dnspython-2.3.0-py3-none-any.whl", hash = "sha256:89141536394f909066cabd112e3e1a37e4e654db00a25308b0f130bc3152eb46"}, + {file = "dnspython-2.3.0.tar.gz", hash = "sha256:224e32b03eb46be70e12ef6d64e0be123a64e621ab4c0822ff6d450d52a540b9"}, +] + +[[package]] +name = "fastapi" +version = "0.95.0" +requires_python = ">=3.7" +summary = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" +dependencies = [ + "pydantic!=1.7,!=1.7.1,!=1.7.2,!=1.7.3,!=1.8,!=1.8.1,<2.0.0,>=1.6.2", + "starlette<0.27.0,>=0.26.1", +] +files = [ + {file = "fastapi-0.95.0-py3-none-any.whl", hash = "sha256:daf73bbe844180200be7966f68e8ec9fd8be57079dff1bacb366db32729e6eb5"}, + {file = "fastapi-0.95.0.tar.gz", hash = "sha256:99d4fdb10e9dd9a24027ac1d0bd4b56702652056ca17a6c8721eec4ad2f14e18"}, +] + +[[package]] +name = "filelock" +version = "3.12.0" +requires_python = ">=3.7" +summary = "A platform independent file lock." +files = [ + {file = "filelock-3.12.0-py3-none-any.whl", hash = "sha256:ad98852315c2ab702aeb628412cbf7e95b7ce8c3bf9565670b4eaecf1db370a9"}, + {file = "filelock-3.12.0.tar.gz", hash = "sha256:fc03ae43288c013d2ea83c8597001b1129db351aad9c57fe2409327916b8e718"}, +] + +[[package]] +name = "filetype" +version = "1.2.0" +summary = "Infer file type and MIME type of any file/buffer. No external dependencies." +files = [ + {file = "filetype-1.2.0-py2.py3-none-any.whl", hash = "sha256:7ce71b6880181241cf7ac8697a2f1eb6a8bd9b429f7ad6d27b8db9ba5f1c2d25"}, + {file = "filetype-1.2.0.tar.gz", hash = "sha256:66b56cd6474bf41d8c54660347d37afcc3f7d1970648de365c102ef77548aadb"}, +] + +[[package]] +name = "flowery" +version = "0.0.1" +requires_python = ">=3.10,<4.0" +summary = "💐 Python image processing library focusing on drawing" +dependencies = [ + "pillow<10.0.0,>=9.5.0", + "typing-extensions>=4.5.0", +] +files = [ + {file = "flowery-0.0.1-py3-none-any.whl", hash = "sha256:d5f3abeb74a21d50b52ad2fec5f6de4a2e878775cec4f4fe0ae2a7d61abc1821"}, + {file = "flowery-0.0.1.tar.gz", hash = "sha256:66978d3a72def44e0b493fd9ad5ab2c9b7df4820f22c45f4ab843175de62e263"}, +] + +[[package]] +name = "greenlet" +version = "2.0.2" +requires_python = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*" +summary = "Lightweight in-process concurrent programming" +files = [ + {file = "greenlet-2.0.2-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:bdfea8c661e80d3c1c99ad7c3ff74e6e87184895bbaca6ee8cc61209f8b9b85d"}, + {file = "greenlet-2.0.2-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:9d14b83fab60d5e8abe587d51c75b252bcc21683f24699ada8fb275d7712f5a9"}, + {file = "greenlet-2.0.2-cp27-cp27m-win32.whl", hash = "sha256:6c3acb79b0bfd4fe733dff8bc62695283b57949ebcca05ae5c129eb606ff2d74"}, + {file = "greenlet-2.0.2-cp27-cp27m-win_amd64.whl", hash = "sha256:283737e0da3f08bd637b5ad058507e578dd462db259f7f6e4c5c365ba4ee9343"}, + {file = "greenlet-2.0.2-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:d27ec7509b9c18b6d73f2f5ede2622441de812e7b1a80bbd446cb0633bd3d5ae"}, + {file = "greenlet-2.0.2-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:30bcf80dda7f15ac77ba5af2b961bdd9dbc77fd4ac6105cee85b0d0a5fcf74df"}, + {file = "greenlet-2.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26fbfce90728d82bc9e6c38ea4d038cba20b7faf8a0ca53a9c07b67318d46088"}, + {file = "greenlet-2.0.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9190f09060ea4debddd24665d6804b995a9c122ef5917ab26e1566dcc712ceeb"}, + {file = "greenlet-2.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d75209eed723105f9596807495d58d10b3470fa6732dd6756595e89925ce2470"}, + {file = "greenlet-2.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:3a51c9751078733d88e013587b108f1b7a1fb106d402fb390740f002b6f6551a"}, + {file = "greenlet-2.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:76ae285c8104046b3a7f06b42f29c7b73f77683df18c49ab5af7983994c2dd91"}, + {file = "greenlet-2.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:2d4686f195e32d36b4d7cf2d166857dbd0ee9f3d20ae349b6bf8afc8485b3645"}, + {file = "greenlet-2.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c4302695ad8027363e96311df24ee28978162cdcdd2006476c43970b384a244c"}, + {file = "greenlet-2.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c48f54ef8e05f04d6eff74b8233f6063cb1ed960243eacc474ee73a2ea8573ca"}, + {file = "greenlet-2.0.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a1846f1b999e78e13837c93c778dcfc3365902cfb8d1bdb7dd73ead37059f0d0"}, + {file = "greenlet-2.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a06ad5312349fec0ab944664b01d26f8d1f05009566339ac6f63f56589bc1a2"}, + {file = "greenlet-2.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:eff4eb9b7eb3e4d0cae3d28c283dc16d9bed6b193c2e1ace3ed86ce48ea8df19"}, + {file = "greenlet-2.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5454276c07d27a740c5892f4907c86327b632127dd9abec42ee62e12427ff7e3"}, + {file = "greenlet-2.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:7cafd1208fdbe93b67c7086876f061f660cfddc44f404279c1585bbf3cdc64c5"}, + {file = "greenlet-2.0.2-cp35-cp35m-macosx_10_14_x86_64.whl", hash = "sha256:910841381caba4f744a44bf81bfd573c94e10b3045ee00de0cbf436fe50673a6"}, + {file = "greenlet-2.0.2-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:18a7f18b82b52ee85322d7a7874e676f34ab319b9f8cce5de06067384aa8ff43"}, + {file = "greenlet-2.0.2-cp35-cp35m-win32.whl", hash = "sha256:03a8f4f3430c3b3ff8d10a2a86028c660355ab637cee9333d63d66b56f09d52a"}, + {file = "greenlet-2.0.2-cp35-cp35m-win_amd64.whl", hash = "sha256:4b58adb399c4d61d912c4c331984d60eb66565175cdf4a34792cd9600f21b394"}, + {file = "greenlet-2.0.2-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:703f18f3fda276b9a916f0934d2fb6d989bf0b4fb5a64825260eb9bfd52d78f0"}, + {file = "greenlet-2.0.2-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:32e5b64b148966d9cccc2c8d35a671409e45f195864560829f395a54226408d3"}, + {file = "greenlet-2.0.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2dd11f291565a81d71dab10b7033395b7a3a5456e637cf997a6f33ebdf06f8db"}, + {file = "greenlet-2.0.2-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e0f72c9ddb8cd28532185f54cc1453f2c16fb417a08b53a855c4e6a418edd099"}, + {file = "greenlet-2.0.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cd021c754b162c0fb55ad5d6b9d960db667faad0fa2ff25bb6e1301b0b6e6a75"}, + {file = "greenlet-2.0.2-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:3c9b12575734155d0c09d6c3e10dbd81665d5c18e1a7c6597df72fd05990c8cf"}, + {file = "greenlet-2.0.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:b9ec052b06a0524f0e35bd8790686a1da006bd911dd1ef7d50b77bfbad74e292"}, + {file = "greenlet-2.0.2-cp36-cp36m-win32.whl", hash = "sha256:dbfcfc0218093a19c252ca8eb9aee3d29cfdcb586df21049b9d777fd32c14fd9"}, + {file = "greenlet-2.0.2-cp36-cp36m-win_amd64.whl", hash = "sha256:9f35ec95538f50292f6d8f2c9c9f8a3c6540bbfec21c9e5b4b751e0a7c20864f"}, + {file = "greenlet-2.0.2-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:d5508f0b173e6aa47273bdc0a0b5ba055b59662ba7c7ee5119528f466585526b"}, + {file = "greenlet-2.0.2-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:f82d4d717d8ef19188687aa32b8363e96062911e63ba22a0cff7802a8e58e5f1"}, + {file = "greenlet-2.0.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9c59a2120b55788e800d82dfa99b9e156ff8f2227f07c5e3012a45a399620b7"}, + {file = "greenlet-2.0.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2780572ec463d44c1d3ae850239508dbeb9fed38e294c68d19a24d925d9223ca"}, + {file = "greenlet-2.0.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:937e9020b514ceedb9c830c55d5c9872abc90f4b5862f89c0887033ae33c6f73"}, + {file = "greenlet-2.0.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:36abbf031e1c0f79dd5d596bfaf8e921c41df2bdf54ee1eed921ce1f52999a86"}, + {file = "greenlet-2.0.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:18e98fb3de7dba1c0a852731c3070cf022d14f0d68b4c87a19cc1016f3bb8b33"}, + {file = "greenlet-2.0.2-cp37-cp37m-win32.whl", hash = "sha256:3f6ea9bd35eb450837a3d80e77b517ea5bc56b4647f5502cd28de13675ee12f7"}, + {file = "greenlet-2.0.2-cp37-cp37m-win_amd64.whl", hash = "sha256:7492e2b7bd7c9b9916388d9df23fa49d9b88ac0640db0a5b4ecc2b653bf451e3"}, + {file = "greenlet-2.0.2-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:b864ba53912b6c3ab6bcb2beb19f19edd01a6bfcbdfe1f37ddd1778abfe75a30"}, + {file = "greenlet-2.0.2-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:ba2956617f1c42598a308a84c6cf021a90ff3862eddafd20c3333d50f0edb45b"}, + {file = "greenlet-2.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc3a569657468b6f3fb60587e48356fe512c1754ca05a564f11366ac9e306526"}, + {file = "greenlet-2.0.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8eab883b3b2a38cc1e050819ef06a7e6344d4a990d24d45bc6f2cf959045a45b"}, + {file = "greenlet-2.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acd2162a36d3de67ee896c43effcd5ee3de247eb00354db411feb025aa319857"}, + {file = "greenlet-2.0.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:0bf60faf0bc2468089bdc5edd10555bab6e85152191df713e2ab1fcc86382b5a"}, + {file = "greenlet-2.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b0ef99cdbe2b682b9ccbb964743a6aca37905fda5e0452e5ee239b1654d37f2a"}, + {file = "greenlet-2.0.2-cp38-cp38-win32.whl", hash = "sha256:b80f600eddddce72320dbbc8e3784d16bd3fb7b517e82476d8da921f27d4b249"}, + {file = "greenlet-2.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:4d2e11331fc0c02b6e84b0d28ece3a36e0548ee1a1ce9ddde03752d9b79bba40"}, + {file = "greenlet-2.0.2-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:88d9ab96491d38a5ab7c56dd7a3cc37d83336ecc564e4e8816dbed12e5aaefc8"}, + {file = "greenlet-2.0.2-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:561091a7be172ab497a3527602d467e2b3fbe75f9e783d8b8ce403fa414f71a6"}, + {file = "greenlet-2.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:971ce5e14dc5e73715755d0ca2975ac88cfdaefcaab078a284fea6cfabf866df"}, + {file = "greenlet-2.0.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be4ed120b52ae4d974aa40215fcdfde9194d63541c7ded40ee12eb4dda57b76b"}, + {file = "greenlet-2.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94c817e84245513926588caf1152e3b559ff794d505555211ca041f032abbb6b"}, + {file = "greenlet-2.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:1a819eef4b0e0b96bb0d98d797bef17dc1b4a10e8d7446be32d1da33e095dbb8"}, + {file = "greenlet-2.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7efde645ca1cc441d6dc4b48c0f7101e8d86b54c8530141b09fd31cef5149ec9"}, + {file = "greenlet-2.0.2-cp39-cp39-win32.whl", hash = "sha256:ea9872c80c132f4663822dd2a08d404073a5a9b5ba6155bea72fb2a79d1093b5"}, + {file = "greenlet-2.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:db1a39669102a1d8d12b57de2bb7e2ec9066a6f2b3da35ae511ff93b01b5d564"}, + {file = "greenlet-2.0.2.tar.gz", hash = "sha256:e7c8dc13af7db097bed64a051d2dd49e9f0af495c26995c00a9ee842690d34c0"}, +] + +[[package]] +name = "h11" +version = "0.14.0" +requires_python = ">=3.7" +summary = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +files = [ + {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, + {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, +] + +[[package]] +name = "h2" +version = "4.1.0" +requires_python = ">=3.6.1" +summary = "HTTP/2 State-Machine based protocol implementation" +dependencies = [ + "hpack<5,>=4.0", + "hyperframe<7,>=6.0", +] +files = [ + {file = "h2-4.1.0-py3-none-any.whl", hash = "sha256:03a46bcf682256c95b5fd9e9a99c1323584c3eec6440d379b9903d709476bc6d"}, + {file = "h2-4.1.0.tar.gz", hash = "sha256:a83aca08fbe7aacb79fec788c9c0bac936343560ed9ec18b82a13a12c28d2abb"}, +] + +[[package]] +name = "hpack" +version = "4.0.0" +requires_python = ">=3.6.1" +summary = "Pure-Python HPACK header compression" +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.16.3" +requires_python = ">=3.7" +summary = "A minimal low-level HTTP client." +dependencies = [ + "anyio<5.0,>=3.0", + "certifi", + "h11<0.15,>=0.13", + "sniffio==1.*", +] +files = [ + {file = "httpcore-0.16.3-py3-none-any.whl", hash = "sha256:da1fb708784a938aa084bde4feb8317056c55037247c787bd7e19eb2c2949dc0"}, + {file = "httpcore-0.16.3.tar.gz", hash = "sha256:c5d6f04e2fc530f39e0c077e6a30caa53f1451096120f1f38b954afd0b17c0cb"}, +] + +[[package]] +name = "httptools" +version = "0.5.0" +requires_python = ">=3.5.0" +summary = "A collection of framework independent HTTP protocol utils." +files = [ + {file = "httptools-0.5.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8f470c79061599a126d74385623ff4744c4e0f4a0997a353a44923c0b561ee51"}, + {file = "httptools-0.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e90491a4d77d0cb82e0e7a9cb35d86284c677402e4ce7ba6b448ccc7325c5421"}, + {file = "httptools-0.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1d2357f791b12d86faced7b5736dea9ef4f5ecdc6c3f253e445ee82da579449"}, + {file = "httptools-0.5.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f90cd6fd97c9a1b7fe9215e60c3bd97336742a0857f00a4cb31547bc22560c2"}, + {file = "httptools-0.5.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:5230a99e724a1bdbbf236a1b58d6e8504b912b0552721c7c6b8570925ee0ccde"}, + {file = "httptools-0.5.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:3a47a34f6015dd52c9eb629c0f5a8a5193e47bf2a12d9a3194d231eaf1bc451a"}, + {file = "httptools-0.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:24bb4bb8ac3882f90aa95403a1cb48465de877e2d5298ad6ddcfdebec060787d"}, + {file = "httptools-0.5.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e67d4f8734f8054d2c4858570cc4b233bf753f56e85217de4dfb2495904cf02e"}, + {file = "httptools-0.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7e5eefc58d20e4c2da82c78d91b2906f1a947ef42bd668db05f4ab4201a99f49"}, + {file = "httptools-0.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0297822cea9f90a38df29f48e40b42ac3d48a28637368f3ec6d15eebefd182f9"}, + {file = "httptools-0.5.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:557be7fbf2bfa4a2ec65192c254e151684545ebab45eca5d50477d562c40f986"}, + {file = "httptools-0.5.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:54465401dbbec9a6a42cf737627fb0f014d50dc7365a6b6cd57753f151a86ff0"}, + {file = "httptools-0.5.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:4d9ebac23d2de960726ce45f49d70eb5466725c0087a078866043dad115f850f"}, + {file = "httptools-0.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:e8a34e4c0ab7b1ca17b8763613783e2458e77938092c18ac919420ab8655c8c1"}, + {file = "httptools-0.5.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f659d7a48401158c59933904040085c200b4be631cb5f23a7d561fbae593ec1f"}, + {file = "httptools-0.5.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef1616b3ba965cd68e6f759eeb5d34fbf596a79e84215eeceebf34ba3f61fdc7"}, + {file = "httptools-0.5.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3625a55886257755cb15194efbf209584754e31d336e09e2ffe0685a76cb4b60"}, + {file = "httptools-0.5.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:72ad589ba5e4a87e1d404cc1cb1b5780bfcb16e2aec957b88ce15fe879cc08ca"}, + {file = "httptools-0.5.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:850fec36c48df5a790aa735417dca8ce7d4b48d59b3ebd6f83e88a8125cde324"}, + {file = "httptools-0.5.0-cp36-cp36m-win_amd64.whl", hash = "sha256:f222e1e9d3f13b68ff8a835574eda02e67277d51631d69d7cf7f8e07df678c86"}, + {file = "httptools-0.5.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3cb8acf8f951363b617a8420768a9f249099b92e703c052f9a51b66342eea89b"}, + {file = "httptools-0.5.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:550059885dc9c19a072ca6d6735739d879be3b5959ec218ba3e013fd2255a11b"}, + {file = "httptools-0.5.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a04fe458a4597aa559b79c7f48fe3dceabef0f69f562daf5c5e926b153817281"}, + {file = "httptools-0.5.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7d0c1044bce274ec6711f0770fd2d5544fe392591d204c68328e60a46f88843b"}, + {file = "httptools-0.5.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c6eeefd4435055a8ebb6c5cc36111b8591c192c56a95b45fe2af22d9881eee25"}, + {file = "httptools-0.5.0-cp37-cp37m-win_amd64.whl", hash = "sha256:5b65be160adcd9de7a7e6413a4966665756e263f0d5ddeffde277ffeee0576a5"}, + {file = "httptools-0.5.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:fe9c766a0c35b7e3d6b6939393c8dfdd5da3ac5dec7f971ec9134f284c6c36d6"}, + {file = "httptools-0.5.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:85b392aba273566c3d5596a0a490978c085b79700814fb22bfd537d381dd230c"}, + {file = "httptools-0.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f5e3088f4ed33947e16fd865b8200f9cfae1144f41b64a8cf19b599508e096bc"}, + {file = "httptools-0.5.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c2a56b6aad7cc8f5551d8e04ff5a319d203f9d870398b94702300de50190f63"}, + {file = "httptools-0.5.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9b571b281a19762adb3f48a7731f6842f920fa71108aff9be49888320ac3e24d"}, + {file = "httptools-0.5.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:aa47ffcf70ba6f7848349b8a6f9b481ee0f7637931d91a9860a1838bfc586901"}, + {file = "httptools-0.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:bede7ee075e54b9a5bde695b4fc8f569f30185891796b2e4e09e2226801d09bd"}, + {file = "httptools-0.5.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:64eba6f168803a7469866a9c9b5263a7463fa8b7a25b35e547492aa7322036b6"}, + {file = "httptools-0.5.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4b098e4bb1174096a93f48f6193e7d9aa7071506a5877da09a783509ca5fff42"}, + {file = "httptools-0.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9423a2de923820c7e82e18980b937893f4aa8251c43684fa1772e341f6e06887"}, + {file = "httptools-0.5.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca1b7becf7d9d3ccdbb2f038f665c0f4857e08e1d8481cbcc1a86a0afcfb62b2"}, + {file = "httptools-0.5.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:50d4613025f15f4b11f1c54bbed4761c0020f7f921b95143ad6d58c151198142"}, + {file = "httptools-0.5.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8ffce9d81c825ac1deaa13bc9694c0562e2840a48ba21cfc9f3b4c922c16f372"}, + {file = "httptools-0.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:1af91b3650ce518d226466f30bbba5b6376dbd3ddb1b2be8b0658c6799dd450b"}, + {file = "httptools-0.5.0.tar.gz", hash = "sha256:295874861c173f9101960bba332429bb77ed4dcd8cdf5cee9922eb00e4f6bc09"}, +] + +[[package]] +name = "httpx" +version = "0.24.1" +requires_python = ">=3.7" +summary = "The next generation HTTP client." +dependencies = [ + "certifi", + "httpcore<0.18.0,>=0.15.0", + "idna", + "sniffio", +] +files = [ + {file = "httpx-0.24.1-py3-none-any.whl", hash = "sha256:06781eb9ac53cde990577af654bd990a4949de37a28bdb4a230d434f3a30b9bd"}, + {file = "httpx-0.24.1.tar.gz", hash = "sha256:5853a43053df830c20f8110c5e69fe44d035d850b2dfe795e196f00fdb774bdd"}, +] + +[[package]] +name = "httpx" +version = "0.24.1" +extras = ["http2"] +requires_python = ">=3.7" +summary = "The next generation HTTP client." +dependencies = [ + "h2<5,>=3", + "httpx==0.24.1", +] +files = [ + {file = "httpx-0.24.1-py3-none-any.whl", hash = "sha256:06781eb9ac53cde990577af654bd990a4949de37a28bdb4a230d434f3a30b9bd"}, + {file = "httpx-0.24.1.tar.gz", hash = "sha256:5853a43053df830c20f8110c5e69fe44d035d850b2dfe795e196f00fdb774bdd"}, +] + +[[package]] +name = "hyperframe" +version = "6.0.1" +requires_python = ">=3.6.1" +summary = "HTTP/2 framing layer for Python" +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 = "identify" +version = "2.5.24" +requires_python = ">=3.7" +summary = "File identification library for Python" +files = [ + {file = "identify-2.5.24-py2.py3-none-any.whl", hash = "sha256:986dbfb38b1140e763e413e6feb44cd731faf72d1909543178aa79b0e258265d"}, + {file = "identify-2.5.24.tar.gz", hash = "sha256:0aac67d5b4812498056d28a9a512a483f5085cc28640b02b258a59dac34301d4"}, +] + +[[package]] +name = "idna" +version = "3.4" +requires_python = ">=3.5" +summary = "Internationalized Domain Names in Applications (IDNA)" +files = [ + {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, + {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, +] + +[[package]] +name = "jinja2" +version = "3.1.2" +requires_python = ">=3.7" +summary = "A very fast and expressive template engine." +dependencies = [ + "MarkupSafe>=2.0", +] +files = [ + {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"}, + {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"}, +] + +[[package]] +name = "linkify-it-py" +version = "2.0.2" +requires_python = ">=3.7" +summary = "Links recognition library with FULL unicode support." +dependencies = [ + "uc-micro-py", +] +files = [ + {file = "linkify-it-py-2.0.2.tar.gz", hash = "sha256:19f3060727842c254c808e99d465c80c49d2c7306788140987a1a7a29b0d6ad2"}, + {file = "linkify_it_py-2.0.2-py3-none-any.whl", hash = "sha256:a3a24428f6c96f27370d7fe61d2ac0be09017be5190d68d8658233171f1b6541"}, +] + +[[package]] +name = "loguru" +version = "0.7.0" +requires_python = ">=3.5" +summary = "Python logging made (stupidly) simple" +dependencies = [ + "colorama>=0.3.4; sys_platform == \"win32\"", + "win32-setctime>=1.0.0; sys_platform == \"win32\"", +] +files = [ + {file = "loguru-0.7.0-py3-none-any.whl", hash = "sha256:b93aa30099fa6860d4727f1b81f8718e965bb96253fa190fab2077aaad6d15d3"}, + {file = "loguru-0.7.0.tar.gz", hash = "sha256:1612053ced6ae84d7959dd7d5e431a0532642237ec21f7fd83ac73fe539e03e1"}, +] + +[[package]] +name = "lxml" +version = "4.9.2" +requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, != 3.4.*" +summary = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." +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]] +name = "mango-odm" +version = "0.3.2" +requires_python = "<4.0,>=3.10" +summary = "🥭 Async MongoDB ODM with type hints in Python" +dependencies = [ + "motor>=3.2.0", + "pydantic<2.0.0,>=1.10.12", +] +files = [ + {file = "mango_odm-0.3.2-py3-none-any.whl", hash = "sha256:318a104d4c4ff3e313b9b0217531789fab8ae60b3d1d8d676767716974adeecd"}, + {file = "mango_odm-0.3.2.tar.gz", hash = "sha256:c808415bebf604d77408747a5e5911dd1ae362daa382df01e9fc8d67096dc406"}, +] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +requires_python = ">=3.8" +summary = "Python port of markdown-it. Markdown parsing, done right!" +dependencies = [ + "mdurl~=0.1", +] +files = [ + {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, + {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, +] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +extras = ["linkify", "plugins"] +requires_python = ">=3.8" +summary = "Python port of markdown-it. Markdown parsing, done right!" +dependencies = [ + "linkify-it-py<3,>=1", + "markdown-it-py==3.0.0", + "mdit-py-plugins", +] +files = [ + {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, + {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, +] + +[[package]] +name = "markupsafe" +version = "2.1.2" +requires_python = ">=3.7" +summary = "Safely add untrusted strings to HTML/XML markup." +files = [ + {file = "MarkupSafe-2.1.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:665a36ae6f8f20a4676b53224e33d456a6f5a72657d9c83c2aa00765072f31f7"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:340bea174e9761308703ae988e982005aedf427de816d1afe98147668cc03036"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22152d00bf4a9c7c83960521fc558f55a1adbc0631fbb00a9471e097b19d72e1"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28057e985dace2f478e042eaa15606c7efccb700797660629da387eb289b9323"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca244fa73f50a800cf8c3ebf7fd93149ec37f5cb9596aa8873ae2c1d23498601"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d9d971ec1e79906046aa3ca266de79eac42f1dbf3612a05dc9368125952bd1a1"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7e007132af78ea9df29495dbf7b5824cb71648d7133cf7848a2a5dd00d36f9ff"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7313ce6a199651c4ed9d7e4cfb4aa56fe923b1adf9af3b420ee14e6d9a73df65"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-win32.whl", hash = "sha256:c4a549890a45f57f1ebf99c067a4ad0cb423a05544accaf2b065246827ed9603"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:835fb5e38fd89328e9c81067fd642b3593c33e1e17e2fdbf77f5676abb14a156"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2ec4f2d48ae59bbb9d1f9d7efb9236ab81429a764dedca114f5fdabbc3788013"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:608e7073dfa9e38a85d38474c082d4281f4ce276ac0010224eaba11e929dd53a"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:65608c35bfb8a76763f37036547f7adfd09270fbdbf96608be2bead319728fcd"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2bfb563d0211ce16b63c7cb9395d2c682a23187f54c3d79bfec33e6705473c6"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:da25303d91526aac3672ee6d49a2f3db2d9502a4a60b55519feb1a4c7714e07d"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9cad97ab29dfc3f0249b483412c85c8ef4766d96cdf9dcf5a1e3caa3f3661cf1"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:085fd3201e7b12809f9e6e9bc1e5c96a368c8523fad5afb02afe3c051ae4afcc"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1bea30e9bf331f3fef67e0a3877b2288593c98a21ccb2cf29b74c581a4eb3af0"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-win32.whl", hash = "sha256:7df70907e00c970c60b9ef2938d894a9381f38e6b9db73c5be35e59d92e06625"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:e55e40ff0cc8cc5c07996915ad367fa47da6b3fc091fdadca7f5403239c5fec3"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a6e40afa7f45939ca356f348c8e23048e02cb109ced1eb8420961b2f40fb373a"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf877ab4ed6e302ec1d04952ca358b381a882fbd9d1b07cccbfd61783561f98a"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63ba06c9941e46fa389d389644e2d8225e0e3e5ebcc4ff1ea8506dce646f8c8a"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f1cd098434e83e656abf198f103a8207a8187c0fc110306691a2e94a78d0abb2"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:55f44b440d491028addb3b88f72207d71eeebfb7b5dbf0643f7c023ae1fba619"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:a6f2fcca746e8d5910e18782f976489939d54a91f9411c32051b4aab2bd7c513"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0b462104ba25f1ac006fdab8b6a01ebbfbce9ed37fd37fd4acd70c67c973e460"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-win32.whl", hash = "sha256:7668b52e102d0ed87cb082380a7e2e1e78737ddecdde129acadb0eccc5423859"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6d6607f98fcf17e534162f0709aaad3ab7a96032723d8ac8750ffe17ae5a0666"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a806db027852538d2ad7555b203300173dd1b77ba116de92da9afbc3a3be3eed"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a4abaec6ca3ad8660690236d11bfe28dfd707778e2442b45addd2f086d6ef094"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f03a532d7dee1bed20bc4884194a16160a2de9ffc6354b3878ec9682bb623c54"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4cf06cdc1dda95223e9d2d3c58d3b178aa5dacb35ee7e3bbac10e4e1faacb419"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:22731d79ed2eb25059ae3df1dfc9cb1546691cc41f4e3130fe6bfbc3ecbbecfa"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f8ffb705ffcf5ddd0e80b65ddf7bed7ee4f5a441ea7d3419e861a12eaf41af58"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8db032bf0ce9022a8e41a22598eefc802314e81b879ae093f36ce9ddf39ab1ba"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2298c859cfc5463f1b64bd55cb3e602528db6fa0f3cfd568d3605c50678f8f03"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-win32.whl", hash = "sha256:50c42830a633fa0cf9e7d27664637532791bfc31c731a87b202d2d8ac40c3ea2"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-win_amd64.whl", hash = "sha256:bb06feb762bade6bf3c8b844462274db0c76acc95c52abe8dbed28ae3d44a147"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:99625a92da8229df6d44335e6fcc558a5037dd0a760e11d84be2260e6f37002f"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8bca7e26c1dd751236cfb0c6c72d4ad61d986e9a41bbf76cb445f69488b2a2bd"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40dfd3fefbef579ee058f139733ac336312663c6706d1163b82b3003fb1925c4"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:090376d812fb6ac5f171e5938e82e7f2d7adc2b629101cec0db8b267815c85e2"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2e7821bffe00aa6bd07a23913b7f4e01328c3d5cc0b40b36c0bd81d362faeb65"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c0a33bc9f02c2b17c3ea382f91b4db0e6cde90b63b296422a939886a7a80de1c"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b8526c6d437855442cdd3d87eede9c425c4445ea011ca38d937db299382e6fa3"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-win32.whl", hash = "sha256:137678c63c977754abe9086a3ec011e8fd985ab90631145dfb9294ad09c102a7"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:0576fe974b40a400449768941d5d0858cc624e3249dfd1e0c33674e5c7ca7aed"}, + {file = "MarkupSafe-2.1.2.tar.gz", hash = "sha256:abcabc8c2b26036d62d4c746381a6f7cf60aafcc653198ad678306986b09450d"}, +] + +[[package]] +name = "mdit-py-emoji" +version = "0.1.1" +requires_python = ">=3.8" +summary = "Emoji plugin for markdown-it-py." +dependencies = [ + "markdown-it-py<4.0.0,>=1.0.0", +] +files = [ + {file = "mdit-py-emoji-0.1.1.tar.gz", hash = "sha256:1d08f2cfc0c512f879fe905c7a572ca55590ea359951825fc2073967d3b9daef"}, + {file = "mdit_py_emoji-0.1.1-py3-none-any.whl", hash = "sha256:59f9fef016eeffbabc1ca524d765ce3f5b1aceb4b5320c46cf6ee120a52371b6"}, +] + +[[package]] +name = "mdit-py-plugins" +version = "0.4.0" +requires_python = ">=3.8" +summary = "Collection of plugins for markdown-it-py" +dependencies = [ + "markdown-it-py<4.0.0,>=1.0.0", +] +files = [ + {file = "mdit_py_plugins-0.4.0-py3-none-any.whl", hash = "sha256:b51b3bb70691f57f974e257e367107857a93b36f322a9e6d44ca5bf28ec2def9"}, + {file = "mdit_py_plugins-0.4.0.tar.gz", hash = "sha256:d8ab27e9aed6c38aa716819fedfde15ca275715955f8a185a8e1cf90fb1d2c1b"}, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +requires_python = ">=3.7" +summary = "Markdown URL utilities" +files = [ + {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, + {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, +] + +[[package]] +name = "motor" +version = "3.2.0" +requires_python = ">=3.7" +summary = "Non-blocking MongoDB driver for Tornado or asyncio" +dependencies = [ + "pymongo<5,>=4.4", +] +files = [ + {file = "motor-3.2.0-py3-none-any.whl", hash = "sha256:82cd3d8a3b57e322c3fa382a393b52828c9a2e98b315c78af36f01bae78af6a6"}, + {file = "motor-3.2.0.tar.gz", hash = "sha256:4fb1e8502260f853554f24115421584e83904a6debb577354d33e9711ee99008"}, +] + +[[package]] +name = "msgpack" +version = "1.0.5" +summary = "MessagePack serializer" +files = [ + {file = "msgpack-1.0.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:525228efd79bb831cf6830a732e2e80bc1b05436b086d4264814b4b2955b2fa9"}, + {file = "msgpack-1.0.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4f8d8b3bf1ff2672567d6b5c725a1b347fe838b912772aa8ae2bf70338d5a198"}, + {file = "msgpack-1.0.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cdc793c50be3f01106245a61b739328f7dccc2c648b501e237f0699fe1395b81"}, + {file = "msgpack-1.0.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5cb47c21a8a65b165ce29f2bec852790cbc04936f502966768e4aae9fa763cb7"}, + {file = "msgpack-1.0.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e42b9594cc3bf4d838d67d6ed62b9e59e201862a25e9a157019e171fbe672dd3"}, + {file = "msgpack-1.0.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:55b56a24893105dc52c1253649b60f475f36b3aa0fc66115bffafb624d7cb30b"}, + {file = "msgpack-1.0.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:1967f6129fc50a43bfe0951c35acbb729be89a55d849fab7686004da85103f1c"}, + {file = "msgpack-1.0.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:20a97bf595a232c3ee6d57ddaadd5453d174a52594bf9c21d10407e2a2d9b3bd"}, + {file = "msgpack-1.0.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d25dd59bbbbb996eacf7be6b4ad082ed7eacc4e8f3d2df1ba43822da9bfa122a"}, + {file = "msgpack-1.0.5-cp310-cp310-win32.whl", hash = "sha256:382b2c77589331f2cb80b67cc058c00f225e19827dbc818d700f61513ab47bea"}, + {file = "msgpack-1.0.5-cp310-cp310-win_amd64.whl", hash = "sha256:4867aa2df9e2a5fa5f76d7d5565d25ec76e84c106b55509e78c1ede0f152659a"}, + {file = "msgpack-1.0.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9f5ae84c5c8a857ec44dc180a8b0cc08238e021f57abdf51a8182e915e6299f0"}, + {file = "msgpack-1.0.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9e6ca5d5699bcd89ae605c150aee83b5321f2115695e741b99618f4856c50898"}, + {file = "msgpack-1.0.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5494ea30d517a3576749cad32fa27f7585c65f5f38309c88c6d137877fa28a5a"}, + {file = "msgpack-1.0.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1ab2f3331cb1b54165976a9d976cb251a83183631c88076613c6c780f0d6e45a"}, + {file = "msgpack-1.0.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28592e20bbb1620848256ebc105fc420436af59515793ed27d5c77a217477705"}, + {file = "msgpack-1.0.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fe5c63197c55bce6385d9aee16c4d0641684628f63ace85f73571e65ad1c1e8d"}, + {file = "msgpack-1.0.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ed40e926fa2f297e8a653c954b732f125ef97bdd4c889f243182299de27e2aa9"}, + {file = "msgpack-1.0.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:b2de4c1c0538dcb7010902a2b97f4e00fc4ddf2c8cda9749af0e594d3b7fa3d7"}, + {file = "msgpack-1.0.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:bf22a83f973b50f9d38e55c6aade04c41ddda19b00c4ebc558930d78eecc64ed"}, + {file = "msgpack-1.0.5-cp311-cp311-win32.whl", hash = "sha256:c396e2cc213d12ce017b686e0f53497f94f8ba2b24799c25d913d46c08ec422c"}, + {file = "msgpack-1.0.5-cp311-cp311-win_amd64.whl", hash = "sha256:6c4c68d87497f66f96d50142a2b73b97972130d93677ce930718f68828b382e2"}, + {file = "msgpack-1.0.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:a2b031c2e9b9af485d5e3c4520f4220d74f4d222a5b8dc8c1a3ab9448ca79c57"}, + {file = "msgpack-1.0.5-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f837b93669ce4336e24d08286c38761132bc7ab29782727f8557e1eb21b2080"}, + {file = "msgpack-1.0.5-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1d46dfe3832660f53b13b925d4e0fa1432b00f5f7210eb3ad3bb9a13c6204a6"}, + {file = "msgpack-1.0.5-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:366c9a7b9057e1547f4ad51d8facad8b406bab69c7d72c0eb6f529cf76d4b85f"}, + {file = "msgpack-1.0.5-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:4c075728a1095efd0634a7dccb06204919a2f67d1893b6aa8e00497258bf926c"}, + {file = "msgpack-1.0.5-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:f933bbda5a3ee63b8834179096923b094b76f0c7a73c1cfe8f07ad608c58844b"}, + {file = "msgpack-1.0.5-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:36961b0568c36027c76e2ae3ca1132e35123dcec0706c4b7992683cc26c1320c"}, + {file = "msgpack-1.0.5-cp36-cp36m-win32.whl", hash = "sha256:b5ef2f015b95f912c2fcab19c36814963b5463f1fb9049846994b007962743e9"}, + {file = "msgpack-1.0.5-cp36-cp36m-win_amd64.whl", hash = "sha256:288e32b47e67f7b171f86b030e527e302c91bd3f40fd9033483f2cacc37f327a"}, + {file = "msgpack-1.0.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:137850656634abddfb88236008339fdaba3178f4751b28f270d2ebe77a563b6c"}, + {file = "msgpack-1.0.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c05a4a96585525916b109bb85f8cb6511db1c6f5b9d9cbcbc940dc6b4be944b"}, + {file = "msgpack-1.0.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56a62ec00b636583e5cb6ad313bbed36bb7ead5fa3a3e38938503142c72cba4f"}, + {file = "msgpack-1.0.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef8108f8dedf204bb7b42994abf93882da1159728a2d4c5e82012edd92c9da9f"}, + {file = "msgpack-1.0.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:1835c84d65f46900920b3708f5ba829fb19b1096c1800ad60bae8418652a951d"}, + {file = "msgpack-1.0.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:e57916ef1bd0fee4f21c4600e9d1da352d8816b52a599c46460e93a6e9f17086"}, + {file = "msgpack-1.0.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:17358523b85973e5f242ad74aa4712b7ee560715562554aa2134d96e7aa4cbbf"}, + {file = "msgpack-1.0.5-cp37-cp37m-win32.whl", hash = "sha256:cb5aaa8c17760909ec6cb15e744c3ebc2ca8918e727216e79607b7bbce9c8f77"}, + {file = "msgpack-1.0.5-cp37-cp37m-win_amd64.whl", hash = "sha256:ab31e908d8424d55601ad7075e471b7d0140d4d3dd3272daf39c5c19d936bd82"}, + {file = "msgpack-1.0.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:b72d0698f86e8d9ddf9442bdedec15b71df3598199ba33322d9711a19f08145c"}, + {file = "msgpack-1.0.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:379026812e49258016dd84ad79ac8446922234d498058ae1d415f04b522d5b2d"}, + {file = "msgpack-1.0.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:332360ff25469c346a1c5e47cbe2a725517919892eda5cfaffe6046656f0b7bb"}, + {file = "msgpack-1.0.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:476a8fe8fae289fdf273d6d2a6cb6e35b5a58541693e8f9f019bfe990a51e4ba"}, + {file = "msgpack-1.0.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9985b214f33311df47e274eb788a5893a761d025e2b92c723ba4c63936b69b1"}, + {file = "msgpack-1.0.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:48296af57cdb1d885843afd73c4656be5c76c0c6328db3440c9601a98f303d87"}, + {file = "msgpack-1.0.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:addab7e2e1fcc04bd08e4eb631c2a90960c340e40dfc4a5e24d2ff0d5a3b3edb"}, + {file = "msgpack-1.0.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:916723458c25dfb77ff07f4c66aed34e47503b2eb3188b3adbec8d8aa6e00f48"}, + {file = "msgpack-1.0.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:821c7e677cc6acf0fd3f7ac664c98803827ae6de594a9f99563e48c5a2f27eb0"}, + {file = "msgpack-1.0.5-cp38-cp38-win32.whl", hash = "sha256:1c0f7c47f0087ffda62961d425e4407961a7ffd2aa004c81b9c07d9269512f6e"}, + {file = "msgpack-1.0.5-cp38-cp38-win_amd64.whl", hash = "sha256:bae7de2026cbfe3782c8b78b0db9cbfc5455e079f1937cb0ab8d133496ac55e1"}, + {file = "msgpack-1.0.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:20c784e66b613c7f16f632e7b5e8a1651aa5702463d61394671ba07b2fc9e025"}, + {file = "msgpack-1.0.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:266fa4202c0eb94d26822d9bfd7af25d1e2c088927fe8de9033d929dd5ba24c5"}, + {file = "msgpack-1.0.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:18334484eafc2b1aa47a6d42427da7fa8f2ab3d60b674120bce7a895a0a85bdd"}, + {file = "msgpack-1.0.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:57e1f3528bd95cc44684beda696f74d3aaa8a5e58c816214b9046512240ef437"}, + {file = "msgpack-1.0.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:586d0d636f9a628ddc6a17bfd45aa5b5efaf1606d2b60fa5d87b8986326e933f"}, + {file = "msgpack-1.0.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a740fa0e4087a734455f0fc3abf5e746004c9da72fbd541e9b113013c8dc3282"}, + {file = "msgpack-1.0.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:3055b0455e45810820db1f29d900bf39466df96ddca11dfa6d074fa47054376d"}, + {file = "msgpack-1.0.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:a61215eac016f391129a013c9e46f3ab308db5f5ec9f25811e811f96962599a8"}, + {file = "msgpack-1.0.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:362d9655cd369b08fda06b6657a303eb7172d5279997abe094512e919cf74b11"}, + {file = "msgpack-1.0.5-cp39-cp39-win32.whl", hash = "sha256:ac9dd47af78cae935901a9a500104e2dea2e253207c924cc95de149606dc43cc"}, + {file = "msgpack-1.0.5-cp39-cp39-win_amd64.whl", hash = "sha256:06f5174b5f8ed0ed919da0e62cbd4ffde676a374aba4020034da05fab67b9164"}, + {file = "msgpack-1.0.5.tar.gz", hash = "sha256:c075544284eadc5cddc70f4757331d99dcbc16b2bbd4849d15f8aae4cf36d31c"}, +] + +[[package]] +name = "multidict" +version = "6.0.4" +requires_python = ">=3.7" +summary = "multidict implementation" +files = [ + {file = "multidict-6.0.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0b1a97283e0c85772d613878028fec909f003993e1007eafa715b24b377cb9b8"}, + {file = "multidict-6.0.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:eeb6dcc05e911516ae3d1f207d4b0520d07f54484c49dfc294d6e7d63b734171"}, + {file = "multidict-6.0.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d6d635d5209b82a3492508cf5b365f3446afb65ae7ebd755e70e18f287b0adf7"}, + {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c048099e4c9e9d615545e2001d3d8a4380bd403e1a0578734e0d31703d1b0c0b"}, + {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ea20853c6dbbb53ed34cb4d080382169b6f4554d394015f1bef35e881bf83547"}, + {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:16d232d4e5396c2efbbf4f6d4df89bfa905eb0d4dc5b3549d872ab898451f569"}, + {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36c63aaa167f6c6b04ef2c85704e93af16c11d20de1d133e39de6a0e84582a93"}, + {file = "multidict-6.0.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:64bdf1086b6043bf519869678f5f2757f473dee970d7abf6da91ec00acb9cb98"}, + {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:43644e38f42e3af682690876cff722d301ac585c5b9e1eacc013b7a3f7b696a0"}, + {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7582a1d1030e15422262de9f58711774e02fa80df0d1578995c76214f6954988"}, + {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:ddff9c4e225a63a5afab9dd15590432c22e8057e1a9a13d28ed128ecf047bbdc"}, + {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:ee2a1ece51b9b9e7752e742cfb661d2a29e7bcdba2d27e66e28a99f1890e4fa0"}, + {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a2e4369eb3d47d2034032a26c7a80fcb21a2cb22e1173d761a162f11e562caa5"}, + {file = "multidict-6.0.4-cp310-cp310-win32.whl", hash = "sha256:574b7eae1ab267e5f8285f0fe881f17efe4b98c39a40858247720935b893bba8"}, + {file = "multidict-6.0.4-cp310-cp310-win_amd64.whl", hash = "sha256:4dcbb0906e38440fa3e325df2359ac6cb043df8e58c965bb45f4e406ecb162cc"}, + {file = "multidict-6.0.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0dfad7a5a1e39c53ed00d2dd0c2e36aed4650936dc18fd9a1826a5ae1cad6f03"}, + {file = "multidict-6.0.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:64da238a09d6039e3bd39bb3aee9c21a5e34f28bfa5aa22518581f910ff94af3"}, + {file = "multidict-6.0.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ff959bee35038c4624250473988b24f846cbeb2c6639de3602c073f10410ceba"}, + {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:01a3a55bd90018c9c080fbb0b9f4891db37d148a0a18722b42f94694f8b6d4c9"}, + {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c5cb09abb18c1ea940fb99360ea0396f34d46566f157122c92dfa069d3e0e982"}, + {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:666daae833559deb2d609afa4490b85830ab0dfca811a98b70a205621a6109fe"}, + {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11bdf3f5e1518b24530b8241529d2050014c884cf18b6fc69c0c2b30ca248710"}, + {file = "multidict-6.0.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7d18748f2d30f94f498e852c67d61261c643b349b9d2a581131725595c45ec6c"}, + {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:458f37be2d9e4c95e2d8866a851663cbc76e865b78395090786f6cd9b3bbf4f4"}, + {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:b1a2eeedcead3a41694130495593a559a668f382eee0727352b9a41e1c45759a"}, + {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:7d6ae9d593ef8641544d6263c7fa6408cc90370c8cb2bbb65f8d43e5b0351d9c"}, + {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:5979b5632c3e3534e42ca6ff856bb24b2e3071b37861c2c727ce220d80eee9ed"}, + {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:dcfe792765fab89c365123c81046ad4103fcabbc4f56d1c1997e6715e8015461"}, + {file = "multidict-6.0.4-cp311-cp311-win32.whl", hash = "sha256:3601a3cece3819534b11d4efc1eb76047488fddd0c85a3948099d5da4d504636"}, + {file = "multidict-6.0.4-cp311-cp311-win_amd64.whl", hash = "sha256:81a4f0b34bd92df3da93315c6a59034df95866014ac08535fc819f043bfd51f0"}, + {file = "multidict-6.0.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:67040058f37a2a51ed8ea8f6b0e6ee5bd78ca67f169ce6122f3e2ec80dfe9b78"}, + {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:853888594621e6604c978ce2a0444a1e6e70c8d253ab65ba11657659dcc9100f"}, + {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:39ff62e7d0f26c248b15e364517a72932a611a9b75f35b45be078d81bdb86603"}, + {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:af048912e045a2dc732847d33821a9d84ba553f5c5f028adbd364dd4765092ac"}, + {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1e8b901e607795ec06c9e42530788c45ac21ef3aaa11dbd0c69de543bfb79a9"}, + {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62501642008a8b9871ddfccbf83e4222cf8ac0d5aeedf73da36153ef2ec222d2"}, + {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:99b76c052e9f1bc0721f7541e5e8c05db3941eb9ebe7b8553c625ef88d6eefde"}, + {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:509eac6cf09c794aa27bcacfd4d62c885cce62bef7b2c3e8b2e49d365b5003fe"}, + {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:21a12c4eb6ddc9952c415f24eef97e3e55ba3af61f67c7bc388dcdec1404a067"}, + {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:5cad9430ab3e2e4fa4a2ef4450f548768400a2ac635841bc2a56a2052cdbeb87"}, + {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ab55edc2e84460694295f401215f4a58597f8f7c9466faec545093045476327d"}, + {file = "multidict-6.0.4-cp37-cp37m-win32.whl", hash = "sha256:5a4dcf02b908c3b8b17a45fb0f15b695bf117a67b76b7ad18b73cf8e92608775"}, + {file = "multidict-6.0.4-cp37-cp37m-win_amd64.whl", hash = "sha256:6ed5f161328b7df384d71b07317f4d8656434e34591f20552c7bcef27b0ab88e"}, + {file = "multidict-6.0.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5fc1b16f586f049820c5c5b17bb4ee7583092fa0d1c4e28b5239181ff9532e0c"}, + {file = "multidict-6.0.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1502e24330eb681bdaa3eb70d6358e818e8e8f908a22a1851dfd4e15bc2f8161"}, + {file = "multidict-6.0.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b692f419760c0e65d060959df05f2a531945af31fda0c8a3b3195d4efd06de11"}, + {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45e1ecb0379bfaab5eef059f50115b54571acfbe422a14f668fc8c27ba410e7e"}, + {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ddd3915998d93fbcd2566ddf9cf62cdb35c9e093075f862935573d265cf8f65d"}, + {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:59d43b61c59d82f2effb39a93c48b845efe23a3852d201ed2d24ba830d0b4cf2"}, + {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc8e1d0c705233c5dd0c5e6460fbad7827d5d36f310a0fadfd45cc3029762258"}, + {file = "multidict-6.0.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d6aa0418fcc838522256761b3415822626f866758ee0bc6632c9486b179d0b52"}, + {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6748717bb10339c4760c1e63da040f5f29f5ed6e59d76daee30305894069a660"}, + {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:4d1a3d7ef5e96b1c9e92f973e43aa5e5b96c659c9bc3124acbbd81b0b9c8a951"}, + {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4372381634485bec7e46718edc71528024fcdc6f835baefe517b34a33c731d60"}, + {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:fc35cb4676846ef752816d5be2193a1e8367b4c1397b74a565a9d0389c433a1d"}, + {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:4b9d9e4e2b37daddb5c23ea33a3417901fa7c7b3dee2d855f63ee67a0b21e5b1"}, + {file = "multidict-6.0.4-cp38-cp38-win32.whl", hash = "sha256:e41b7e2b59679edfa309e8db64fdf22399eec4b0b24694e1b2104fb789207779"}, + {file = "multidict-6.0.4-cp38-cp38-win_amd64.whl", hash = "sha256:d6c254ba6e45d8e72739281ebc46ea5eb5f101234f3ce171f0e9f5cc86991480"}, + {file = "multidict-6.0.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:16ab77bbeb596e14212e7bab8429f24c1579234a3a462105cda4a66904998664"}, + {file = "multidict-6.0.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bc779e9e6f7fda81b3f9aa58e3a6091d49ad528b11ed19f6621408806204ad35"}, + {file = "multidict-6.0.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4ceef517eca3e03c1cceb22030a3e39cb399ac86bff4e426d4fc6ae49052cc60"}, + {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:281af09f488903fde97923c7744bb001a9b23b039a909460d0f14edc7bf59706"}, + {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:52f2dffc8acaba9a2f27174c41c9e57f60b907bb9f096b36b1a1f3be71c6284d"}, + {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b41156839806aecb3641f3208c0dafd3ac7775b9c4c422d82ee2a45c34ba81ca"}, + {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5e3fc56f88cc98ef8139255cf8cd63eb2c586531e43310ff859d6bb3a6b51f1"}, + {file = "multidict-6.0.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8316a77808c501004802f9beebde51c9f857054a0c871bd6da8280e718444449"}, + {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f70b98cd94886b49d91170ef23ec5c0e8ebb6f242d734ed7ed677b24d50c82cf"}, + {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bf6774e60d67a9efe02b3616fee22441d86fab4c6d335f9d2051d19d90a40063"}, + {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:e69924bfcdda39b722ef4d9aa762b2dd38e4632b3641b1d9a57ca9cd18f2f83a"}, + {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:6b181d8c23da913d4ff585afd1155a0e1194c0b50c54fcfe286f70cdaf2b7176"}, + {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:52509b5be062d9eafc8170e53026fbc54cf3b32759a23d07fd935fb04fc22d95"}, + {file = "multidict-6.0.4-cp39-cp39-win32.whl", hash = "sha256:27c523fbfbdfd19c6867af7346332b62b586eed663887392cff78d614f9ec313"}, + {file = "multidict-6.0.4-cp39-cp39-win_amd64.whl", hash = "sha256:33029f5734336aa0d4c0384525da0387ef89148dc7191aae00ca5fb23d7aafc2"}, + {file = "multidict-6.0.4.tar.gz", hash = "sha256:3666906492efb76453c0e7b97f2cf459b0682e7402c0489a95484965dbc1da49"}, +] + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +requires_python = ">=3.5" +summary = "Type system extensions for programs checked with the mypy type checker." +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" +requires_python = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" +summary = "Node.js virtual environment builder" +dependencies = [ + "setuptools", +] +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]] +name = "nonebot-adapter-onebot" +version = "2.2.4" +requires_python = ">=3.8,<4.0" +summary = "OneBot(CQHTTP) adapter for nonebot2" +dependencies = [ + "msgpack<2.0.0,>=1.0.3", + "nonebot2<3.0.0,>=2.0.0-beta.3", + "typing-extensions<5.0.0,>=4.0.0", +] +files = [ + {file = "nonebot_adapter_onebot-2.2.4-py3-none-any.whl", hash = "sha256:ae9971bb77a2984d6ca097d5565132723b051dafdfd1cef954a62f45684ae62c"}, + {file = "nonebot_adapter_onebot-2.2.4.tar.gz", hash = "sha256:1024b503514f87d6262adf1bde6f160b3a159afc4f6c21987eece3dacb4762dd"}, +] + +[[package]] +name = "nonebot2" +version = "2.0.1" +requires_python = ">=3.8,<4.0" +summary = "An asynchronous python bot framework." +dependencies = [ + "loguru<1.0.0,>=0.6.0", + "pydantic[dotenv]<2.0.0,>=1.10.0", + "pygtrie<3.0.0,>=2.4.1", + "tomli<3.0.0,>=2.0.1; python_version < \"3.11\"", + "typing-extensions<5.0.0,>=4.0.0", + "yarl<2.0.0,>=1.7.2", +] +files = [ + {file = "nonebot2-2.0.1-py3-none-any.whl", hash = "sha256:58111068df7a6c13cca2a412dd0f6f88d7bf2a2af3e92ae770fd913a9421743e"}, + {file = "nonebot2-2.0.1.tar.gz", hash = "sha256:c61294644aef08f2b427301ca1c358d34e6cfaa7025d694a502ad66e9508e7c2"}, +] + +[[package]] +name = "nonebot2" +version = "2.0.1" +extras = ["fastapi"] +requires_python = ">=3.8,<4.0" +summary = "An asynchronous python bot framework." +dependencies = [ + "fastapi<1.0.0,>=0.93.0", + "nonebot2==2.0.1", + "uvicorn[standard]<1.0.0,>=0.20.0", +] +files = [ + {file = "nonebot2-2.0.1-py3-none-any.whl", hash = "sha256:58111068df7a6c13cca2a412dd0f6f88d7bf2a2af3e92ae770fd913a9421743e"}, + {file = "nonebot2-2.0.1.tar.gz", hash = "sha256:c61294644aef08f2b427301ca1c358d34e6cfaa7025d694a502ad66e9508e7c2"}, +] + +[[package]] +name = "packaging" +version = "23.0" +requires_python = ">=3.7" +summary = "Core utilities for Python packages" +files = [ + {file = "packaging-23.0-py3-none-any.whl", hash = "sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2"}, + {file = "packaging-23.0.tar.gz", hash = "sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97"}, +] + +[[package]] +name = "pathspec" +version = "0.11.1" +requires_python = ">=3.7" +summary = "Utility library for gitignore style pattern matching of file paths." +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 = "pillow" +version = "9.5.0" +requires_python = ">=3.7" +summary = "Python Imaging Library (Fork)" +files = [ + {file = "Pillow-9.5.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:ace6ca218308447b9077c14ea4ef381ba0b67ee78d64046b3f19cf4e1139ad16"}, + {file = "Pillow-9.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d3d403753c9d5adc04d4694d35cf0391f0f3d57c8e0030aac09d7678fa8030aa"}, + {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ba1b81ee69573fe7124881762bb4cd2e4b6ed9dd28c9c60a632902fe8db8b38"}, + {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fe7e1c262d3392afcf5071df9afa574544f28eac825284596ac6db56e6d11062"}, + {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f36397bf3f7d7c6a3abdea815ecf6fd14e7fcd4418ab24bae01008d8d8ca15e"}, + {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:252a03f1bdddce077eff2354c3861bf437c892fb1832f75ce813ee94347aa9b5"}, + {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:85ec677246533e27770b0de5cf0f9d6e4ec0c212a1f89dfc941b64b21226009d"}, + {file = "Pillow-9.5.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b416f03d37d27290cb93597335a2f85ed446731200705b22bb927405320de903"}, + {file = "Pillow-9.5.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1781a624c229cb35a2ac31cc4a77e28cafc8900733a864870c49bfeedacd106a"}, + {file = "Pillow-9.5.0-cp310-cp310-win32.whl", hash = "sha256:8507eda3cd0608a1f94f58c64817e83ec12fa93a9436938b191b80d9e4c0fc44"}, + {file = "Pillow-9.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:d3c6b54e304c60c4181da1c9dadf83e4a54fd266a99c70ba646a9baa626819eb"}, + {file = "Pillow-9.5.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:7ec6f6ce99dab90b52da21cf0dc519e21095e332ff3b399a357c187b1a5eee32"}, + {file = "Pillow-9.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:560737e70cb9c6255d6dcba3de6578a9e2ec4b573659943a5e7e4af13f298f5c"}, + {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:96e88745a55b88a7c64fa49bceff363a1a27d9a64e04019c2281049444a571e3"}, + {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d9c206c29b46cfd343ea7cdfe1232443072bbb270d6a46f59c259460db76779a"}, + {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cfcc2c53c06f2ccb8976fb5c71d448bdd0a07d26d8e07e321c103416444c7ad1"}, + {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:a0f9bb6c80e6efcde93ffc51256d5cfb2155ff8f78292f074f60f9e70b942d99"}, + {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:8d935f924bbab8f0a9a28404422da8af4904e36d5c33fc6f677e4c4485515625"}, + {file = "Pillow-9.5.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fed1e1cf6a42577953abbe8e6cf2fe2f566daebde7c34724ec8803c4c0cda579"}, + {file = "Pillow-9.5.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c1170d6b195555644f0616fd6ed929dfcf6333b8675fcca044ae5ab110ded296"}, + {file = "Pillow-9.5.0-cp311-cp311-win32.whl", hash = "sha256:54f7102ad31a3de5666827526e248c3530b3a33539dbda27c6843d19d72644ec"}, + {file = "Pillow-9.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:cfa4561277f677ecf651e2b22dc43e8f5368b74a25a8f7d1d4a3a243e573f2d4"}, + {file = "Pillow-9.5.0-cp311-cp311-win_arm64.whl", hash = "sha256:965e4a05ef364e7b973dd17fc765f42233415974d773e82144c9bbaaaea5d089"}, + {file = "Pillow-9.5.0-cp312-cp312-win32.whl", hash = "sha256:22baf0c3cf0c7f26e82d6e1adf118027afb325e703922c8dfc1d5d0156bb2eeb"}, + {file = "Pillow-9.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:432b975c009cf649420615388561c0ce7cc31ce9b2e374db659ee4f7d57a1f8b"}, + {file = "Pillow-9.5.0-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:5d4ebf8e1db4441a55c509c4baa7a0587a0210f7cd25fcfe74dbbce7a4bd1906"}, + {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:375f6e5ee9620a271acb6820b3d1e94ffa8e741c0601db4c0c4d3cb0a9c224bf"}, + {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:99eb6cafb6ba90e436684e08dad8be1637efb71c4f2180ee6b8f940739406e78"}, + {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2dfaaf10b6172697b9bceb9a3bd7b951819d1ca339a5ef294d1f1ac6d7f63270"}, + {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:763782b2e03e45e2c77d7779875f4432e25121ef002a41829d8868700d119392"}, + {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:35f6e77122a0c0762268216315bf239cf52b88865bba522999dc38f1c52b9b47"}, + {file = "Pillow-9.5.0-cp37-cp37m-win32.whl", hash = "sha256:aca1c196f407ec7cf04dcbb15d19a43c507a81f7ffc45b690899d6a76ac9fda7"}, + {file = "Pillow-9.5.0-cp37-cp37m-win_amd64.whl", hash = "sha256:322724c0032af6692456cd6ed554bb85f8149214d97398bb80613b04e33769f6"}, + {file = "Pillow-9.5.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:a0aa9417994d91301056f3d0038af1199eb7adc86e646a36b9e050b06f526597"}, + {file = "Pillow-9.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f8286396b351785801a976b1e85ea88e937712ee2c3ac653710a4a57a8da5d9c"}, + {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c830a02caeb789633863b466b9de10c015bded434deb3ec87c768e53752ad22a"}, + {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fbd359831c1657d69bb81f0db962905ee05e5e9451913b18b831febfe0519082"}, + {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8fc330c3370a81bbf3f88557097d1ea26cd8b019d6433aa59f71195f5ddebbf"}, + {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:7002d0797a3e4193c7cdee3198d7c14f92c0836d6b4a3f3046a64bd1ce8df2bf"}, + {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:229e2c79c00e85989a34b5981a2b67aa079fd08c903f0aaead522a1d68d79e51"}, + {file = "Pillow-9.5.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9adf58f5d64e474bed00d69bcd86ec4bcaa4123bfa70a65ce72e424bfb88ed96"}, + {file = "Pillow-9.5.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:662da1f3f89a302cc22faa9f14a262c2e3951f9dbc9617609a47521c69dd9f8f"}, + {file = "Pillow-9.5.0-cp38-cp38-win32.whl", hash = "sha256:6608ff3bf781eee0cd14d0901a2b9cc3d3834516532e3bd673a0a204dc8615fc"}, + {file = "Pillow-9.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:e49eb4e95ff6fd7c0c402508894b1ef0e01b99a44320ba7d8ecbabefddcc5569"}, + {file = "Pillow-9.5.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:482877592e927fd263028c105b36272398e3e1be3269efda09f6ba21fd83ec66"}, + {file = "Pillow-9.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3ded42b9ad70e5f1754fb7c2e2d6465a9c842e41d178f262e08b8c85ed8a1d8e"}, + {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c446d2245ba29820d405315083d55299a796695d747efceb5717a8b450324115"}, + {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8aca1152d93dcc27dc55395604dcfc55bed5f25ef4c98716a928bacba90d33a3"}, + {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:608488bdcbdb4ba7837461442b90ea6f3079397ddc968c31265c1e056964f1ef"}, + {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:60037a8db8750e474af7ffc9faa9b5859e6c6d0a50e55c45576bf28be7419705"}, + {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:07999f5834bdc404c442146942a2ecadd1cb6292f5229f4ed3b31e0a108746b1"}, + {file = "Pillow-9.5.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:a127ae76092974abfbfa38ca2d12cbeddcdeac0fb71f9627cc1135bedaf9d51a"}, + {file = "Pillow-9.5.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:489f8389261e5ed43ac8ff7b453162af39c3e8abd730af8363587ba64bb2e865"}, + {file = "Pillow-9.5.0-cp39-cp39-win32.whl", hash = "sha256:9b1af95c3a967bf1da94f253e56b6286b50af23392a886720f563c547e48e964"}, + {file = "Pillow-9.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:77165c4a5e7d5a284f10a6efaa39a0ae8ba839da344f20b111d62cc932fa4e5d"}, + {file = "Pillow-9.5.0-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:833b86a98e0ede388fa29363159c9b1a294b0905b5128baf01db683672f230f5"}, + {file = "Pillow-9.5.0-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aaf305d6d40bd9632198c766fb64f0c1a83ca5b667f16c1e79e1661ab5060140"}, + {file = "Pillow-9.5.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0852ddb76d85f127c135b6dd1f0bb88dbb9ee990d2cd9aa9e28526c93e794fba"}, + {file = "Pillow-9.5.0-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:91ec6fe47b5eb5a9968c79ad9ed78c342b1f97a091677ba0e012701add857829"}, + {file = "Pillow-9.5.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:cb841572862f629b99725ebaec3287fc6d275be9b14443ea746c1dd325053cbd"}, + {file = "Pillow-9.5.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:c380b27d041209b849ed246b111b7c166ba36d7933ec6e41175fd15ab9eb1572"}, + {file = "Pillow-9.5.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7c9af5a3b406a50e313467e3565fc99929717f780164fe6fbb7704edba0cebbe"}, + {file = "Pillow-9.5.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5671583eab84af046a397d6d0ba25343c00cd50bce03787948e0fff01d4fd9b1"}, + {file = "Pillow-9.5.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:84a6f19ce086c1bf894644b43cd129702f781ba5751ca8572f08aa40ef0ab7b7"}, + {file = "Pillow-9.5.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:1e7723bd90ef94eda669a3c2c19d549874dd5badaeefabefd26053304abe5799"}, + {file = "Pillow-9.5.0.tar.gz", hash = "sha256:bf548479d336726d7a0eceb6e767e179fbde37833ae42794602631a070d630f1"}, +] + +[[package]] +name = "platformdirs" +version = "3.5.0" +requires_python = ">=3.7" +summary = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +files = [ + {file = "platformdirs-3.5.0-py3-none-any.whl", hash = "sha256:47692bc24c1958e8b0f13dd727307cff1db103fca36399f457da8e05f222fdc4"}, + {file = "platformdirs-3.5.0.tar.gz", hash = "sha256:7954a68d0ba23558d753f73437c55f89027cf8f5108c19844d4b82e5af396335"}, +] + +[[package]] +name = "playwright" +version = "1.36.0" +requires_python = ">=3.8" +summary = "A high-level API to automate web browsers" +dependencies = [ + "greenlet==2.0.2", + "pyee==9.0.4", +] +files = [ + {file = "playwright-1.36.0-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:b7c6ddfca2b141b0385387cc56c125b14ea867902c39e3fc650ddd6c429b17da"}, + {file = "playwright-1.36.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:428a719a6c7e40781c19860ed813840ac2d63678f7587abe12e800ea030d4b7e"}, + {file = "playwright-1.36.0-py3-none-macosx_11_0_universal2.whl", hash = "sha256:4e396853034742b76654cdab27422155d238f46e4dc6369ea75854fafb935586"}, + {file = "playwright-1.36.0-py3-none-manylinux1_x86_64.whl", hash = "sha256:72e80076e595f5fcd8ebd89bf6635ad78e4bafa633119faed8b2568d17dbd398"}, + {file = "playwright-1.36.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffbb927679b62fad5071439d5fe0840af46ad1844bc44bf80e1a0ad706140c98"}, + {file = "playwright-1.36.0-py3-none-win32.whl", hash = "sha256:84213339f179fd2a70f77ea7faea0616d74871349d556c53a1ecb7dd5097973c"}, + {file = "playwright-1.36.0-py3-none-win_amd64.whl", hash = "sha256:89ca2261bb00b67d3dff97691cf18f4347ee0529a11e431e47df67b703d4d8fa"}, +] + +[[package]] +name = "pre-commit" +version = "3.3.3" +requires_python = ">=3.8" +summary = "A framework for managing and maintaining multi-language pre-commit hooks." +dependencies = [ + "cfgv>=2.0.0", + "identify>=1.0.0", + "nodeenv>=0.11.1", + "pyyaml>=5.1", + "virtualenv>=20.10.0", +] +files = [ + {file = "pre_commit-3.3.3-py2.py3-none-any.whl", hash = "sha256:10badb65d6a38caff29703362271d7dca483d01da88f9d7e05d0b97171c136cb"}, + {file = "pre_commit-3.3.3.tar.gz", hash = "sha256:a2256f489cd913d575c145132ae196fe335da32d91a8294b7afe6622335dd023"}, +] + +[[package]] +name = "premailer" +version = "3.10.0" +summary = "Turns CSS blocks into style attributes" +dependencies = [ + "cachetools", + "cssselect", + "cssutils", + "lxml", + "requests", +] +files = [ + {file = "premailer-3.10.0-py2.py3-none-any.whl", hash = "sha256:021b8196364d7df96d04f9ade51b794d0b77bcc19e998321c515633a2273be1a"}, + {file = "premailer-3.10.0.tar.gz", hash = "sha256:d1875a8411f5dc92b53ef9f193db6c0f879dc378d618e0ad292723e388bfe4c2"}, +] + +[[package]] +name = "pydantic" +version = "1.10.12" +requires_python = ">=3.7" +summary = "Data validation and settings management using python type hints" +dependencies = [ + "typing-extensions>=4.2.0", +] +files = [ + {file = "pydantic-1.10.12-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a1fcb59f2f355ec350073af41d927bf83a63b50e640f4dbaa01053a28b7a7718"}, + {file = "pydantic-1.10.12-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b7ccf02d7eb340b216ec33e53a3a629856afe1c6e0ef91d84a4e6f2fb2ca70fe"}, + {file = "pydantic-1.10.12-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8fb2aa3ab3728d950bcc885a2e9eff6c8fc40bc0b7bb434e555c215491bcf48b"}, + {file = "pydantic-1.10.12-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:771735dc43cf8383959dc9b90aa281f0b6092321ca98677c5fb6125a6f56d58d"}, + {file = "pydantic-1.10.12-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:ca48477862372ac3770969b9d75f1bf66131d386dba79506c46d75e6b48c1e09"}, + {file = "pydantic-1.10.12-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a5e7add47a5b5a40c49b3036d464e3c7802f8ae0d1e66035ea16aa5b7a3923ed"}, + {file = "pydantic-1.10.12-cp310-cp310-win_amd64.whl", hash = "sha256:e4129b528c6baa99a429f97ce733fff478ec955513630e61b49804b6cf9b224a"}, + {file = "pydantic-1.10.12-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b0d191db0f92dfcb1dec210ca244fdae5cbe918c6050b342d619c09d31eea0cc"}, + {file = "pydantic-1.10.12-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:795e34e6cc065f8f498c89b894a3c6da294a936ee71e644e4bd44de048af1405"}, + {file = "pydantic-1.10.12-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:69328e15cfda2c392da4e713443c7dbffa1505bc9d566e71e55abe14c97ddc62"}, + {file = "pydantic-1.10.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2031de0967c279df0d8a1c72b4ffc411ecd06bac607a212892757db7462fc494"}, + {file = "pydantic-1.10.12-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:ba5b2e6fe6ca2b7e013398bc7d7b170e21cce322d266ffcd57cca313e54fb246"}, + {file = "pydantic-1.10.12-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:2a7bac939fa326db1ab741c9d7f44c565a1d1e80908b3797f7f81a4f86bc8d33"}, + {file = "pydantic-1.10.12-cp311-cp311-win_amd64.whl", hash = "sha256:87afda5539d5140cb8ba9e8b8c8865cb5b1463924d38490d73d3ccfd80896b3f"}, + {file = "pydantic-1.10.12-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:549a8e3d81df0a85226963611950b12d2d334f214436a19537b2efed61b7639a"}, + {file = "pydantic-1.10.12-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:598da88dfa127b666852bef6d0d796573a8cf5009ffd62104094a4fe39599565"}, + {file = "pydantic-1.10.12-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ba5c4a8552bff16c61882db58544116d021d0b31ee7c66958d14cf386a5b5350"}, + {file = "pydantic-1.10.12-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c79e6a11a07da7374f46970410b41d5e266f7f38f6a17a9c4823db80dadf4303"}, + {file = "pydantic-1.10.12-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ab26038b8375581dc832a63c948f261ae0aa21f1d34c1293469f135fa92972a5"}, + {file = "pydantic-1.10.12-cp37-cp37m-win_amd64.whl", hash = "sha256:e0a16d274b588767602b7646fa05af2782576a6cf1022f4ba74cbb4db66f6ca8"}, + {file = "pydantic-1.10.12-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6a9dfa722316f4acf4460afdf5d41d5246a80e249c7ff475c43a3a1e9d75cf62"}, + {file = "pydantic-1.10.12-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a73f489aebd0c2121ed974054cb2759af8a9f747de120acd2c3394cf84176ccb"}, + {file = "pydantic-1.10.12-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b30bcb8cbfccfcf02acb8f1a261143fab622831d9c0989707e0e659f77a18e0"}, + {file = "pydantic-1.10.12-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2fcfb5296d7877af406ba1547dfde9943b1256d8928732267e2653c26938cd9c"}, + {file = "pydantic-1.10.12-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:2f9a6fab5f82ada41d56b0602606a5506aab165ca54e52bc4545028382ef1c5d"}, + {file = "pydantic-1.10.12-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:dea7adcc33d5d105896401a1f37d56b47d443a2b2605ff8a969a0ed5543f7e33"}, + {file = "pydantic-1.10.12-cp38-cp38-win_amd64.whl", hash = "sha256:1eb2085c13bce1612da8537b2d90f549c8cbb05c67e8f22854e201bde5d98a47"}, + {file = "pydantic-1.10.12-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ef6c96b2baa2100ec91a4b428f80d8f28a3c9e53568219b6c298c1125572ebc6"}, + {file = "pydantic-1.10.12-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6c076be61cd0177a8433c0adcb03475baf4ee91edf5a4e550161ad57fc90f523"}, + {file = "pydantic-1.10.12-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2d5a58feb9a39f481eda4d5ca220aa8b9d4f21a41274760b9bc66bfd72595b86"}, + {file = "pydantic-1.10.12-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5f805d2d5d0a41633651a73fa4ecdd0b3d7a49de4ec3fadf062fe16501ddbf1"}, + {file = "pydantic-1.10.12-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:1289c180abd4bd4555bb927c42ee42abc3aee02b0fb2d1223fb7c6e5bef87dbe"}, + {file = "pydantic-1.10.12-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5d1197e462e0364906cbc19681605cb7c036f2475c899b6f296104ad42b9f5fb"}, + {file = "pydantic-1.10.12-cp39-cp39-win_amd64.whl", hash = "sha256:fdbdd1d630195689f325c9ef1a12900524dceb503b00a987663ff4f58669b93d"}, + {file = "pydantic-1.10.12-py3-none-any.whl", hash = "sha256:b749a43aa51e32839c9d71dc67eb1e4221bb04af1033a32e3923d46f9effa942"}, + {file = "pydantic-1.10.12.tar.gz", hash = "sha256:0fe8a415cea8f340e7a9af9c54fc71a649b43e8ca3cc732986116b3cb135d303"}, +] + +[[package]] +name = "pydantic" +version = "1.10.12" +extras = ["dotenv"] +requires_python = ">=3.7" +summary = "Data validation and settings management using python type hints" +dependencies = [ + "pydantic==1.10.12", + "python-dotenv>=0.10.4", +] +files = [ + {file = "pydantic-1.10.12-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a1fcb59f2f355ec350073af41d927bf83a63b50e640f4dbaa01053a28b7a7718"}, + {file = "pydantic-1.10.12-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b7ccf02d7eb340b216ec33e53a3a629856afe1c6e0ef91d84a4e6f2fb2ca70fe"}, + {file = "pydantic-1.10.12-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8fb2aa3ab3728d950bcc885a2e9eff6c8fc40bc0b7bb434e555c215491bcf48b"}, + {file = "pydantic-1.10.12-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:771735dc43cf8383959dc9b90aa281f0b6092321ca98677c5fb6125a6f56d58d"}, + {file = "pydantic-1.10.12-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:ca48477862372ac3770969b9d75f1bf66131d386dba79506c46d75e6b48c1e09"}, + {file = "pydantic-1.10.12-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a5e7add47a5b5a40c49b3036d464e3c7802f8ae0d1e66035ea16aa5b7a3923ed"}, + {file = "pydantic-1.10.12-cp310-cp310-win_amd64.whl", hash = "sha256:e4129b528c6baa99a429f97ce733fff478ec955513630e61b49804b6cf9b224a"}, + {file = "pydantic-1.10.12-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b0d191db0f92dfcb1dec210ca244fdae5cbe918c6050b342d619c09d31eea0cc"}, + {file = "pydantic-1.10.12-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:795e34e6cc065f8f498c89b894a3c6da294a936ee71e644e4bd44de048af1405"}, + {file = "pydantic-1.10.12-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:69328e15cfda2c392da4e713443c7dbffa1505bc9d566e71e55abe14c97ddc62"}, + {file = "pydantic-1.10.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2031de0967c279df0d8a1c72b4ffc411ecd06bac607a212892757db7462fc494"}, + {file = "pydantic-1.10.12-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:ba5b2e6fe6ca2b7e013398bc7d7b170e21cce322d266ffcd57cca313e54fb246"}, + {file = "pydantic-1.10.12-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:2a7bac939fa326db1ab741c9d7f44c565a1d1e80908b3797f7f81a4f86bc8d33"}, + {file = "pydantic-1.10.12-cp311-cp311-win_amd64.whl", hash = "sha256:87afda5539d5140cb8ba9e8b8c8865cb5b1463924d38490d73d3ccfd80896b3f"}, + {file = "pydantic-1.10.12-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:549a8e3d81df0a85226963611950b12d2d334f214436a19537b2efed61b7639a"}, + {file = "pydantic-1.10.12-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:598da88dfa127b666852bef6d0d796573a8cf5009ffd62104094a4fe39599565"}, + {file = "pydantic-1.10.12-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ba5c4a8552bff16c61882db58544116d021d0b31ee7c66958d14cf386a5b5350"}, + {file = "pydantic-1.10.12-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c79e6a11a07da7374f46970410b41d5e266f7f38f6a17a9c4823db80dadf4303"}, + {file = "pydantic-1.10.12-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ab26038b8375581dc832a63c948f261ae0aa21f1d34c1293469f135fa92972a5"}, + {file = "pydantic-1.10.12-cp37-cp37m-win_amd64.whl", hash = "sha256:e0a16d274b588767602b7646fa05af2782576a6cf1022f4ba74cbb4db66f6ca8"}, + {file = "pydantic-1.10.12-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6a9dfa722316f4acf4460afdf5d41d5246a80e249c7ff475c43a3a1e9d75cf62"}, + {file = "pydantic-1.10.12-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a73f489aebd0c2121ed974054cb2759af8a9f747de120acd2c3394cf84176ccb"}, + {file = "pydantic-1.10.12-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b30bcb8cbfccfcf02acb8f1a261143fab622831d9c0989707e0e659f77a18e0"}, + {file = "pydantic-1.10.12-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2fcfb5296d7877af406ba1547dfde9943b1256d8928732267e2653c26938cd9c"}, + {file = "pydantic-1.10.12-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:2f9a6fab5f82ada41d56b0602606a5506aab165ca54e52bc4545028382ef1c5d"}, + {file = "pydantic-1.10.12-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:dea7adcc33d5d105896401a1f37d56b47d443a2b2605ff8a969a0ed5543f7e33"}, + {file = "pydantic-1.10.12-cp38-cp38-win_amd64.whl", hash = "sha256:1eb2085c13bce1612da8537b2d90f549c8cbb05c67e8f22854e201bde5d98a47"}, + {file = "pydantic-1.10.12-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ef6c96b2baa2100ec91a4b428f80d8f28a3c9e53568219b6c298c1125572ebc6"}, + {file = "pydantic-1.10.12-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6c076be61cd0177a8433c0adcb03475baf4ee91edf5a4e550161ad57fc90f523"}, + {file = "pydantic-1.10.12-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2d5a58feb9a39f481eda4d5ca220aa8b9d4f21a41274760b9bc66bfd72595b86"}, + {file = "pydantic-1.10.12-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5f805d2d5d0a41633651a73fa4ecdd0b3d7a49de4ec3fadf062fe16501ddbf1"}, + {file = "pydantic-1.10.12-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:1289c180abd4bd4555bb927c42ee42abc3aee02b0fb2d1223fb7c6e5bef87dbe"}, + {file = "pydantic-1.10.12-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5d1197e462e0364906cbc19681605cb7c036f2475c899b6f296104ad42b9f5fb"}, + {file = "pydantic-1.10.12-cp39-cp39-win_amd64.whl", hash = "sha256:fdbdd1d630195689f325c9ef1a12900524dceb503b00a987663ff4f58669b93d"}, + {file = "pydantic-1.10.12-py3-none-any.whl", hash = "sha256:b749a43aa51e32839c9d71dc67eb1e4221bb04af1033a32e3923d46f9effa942"}, + {file = "pydantic-1.10.12.tar.gz", hash = "sha256:0fe8a415cea8f340e7a9af9c54fc71a649b43e8ca3cc732986116b3cb135d303"}, +] + +[[package]] +name = "pyee" +version = "9.0.4" +summary = "A port of node.js's EventEmitter to python." +dependencies = [ + "typing-extensions", +] +files = [ + {file = "pyee-9.0.4-py2.py3-none-any.whl", hash = "sha256:9f066570130c554e9cc12de5a9d86f57c7ee47fece163bbdaa3e9c933cfbdfa5"}, + {file = "pyee-9.0.4.tar.gz", hash = "sha256:2770c4928abc721f46b705e6a72b0c59480c4a69c9a83ca0b00bb994f1ea4b32"}, +] + +[[package]] +name = "pygments" +version = "2.16.1" +requires_python = ">=3.7" +summary = "Pygments is a syntax highlighting package written in Python." +files = [ + {file = "Pygments-2.16.1-py3-none-any.whl", hash = "sha256:13fc09fa63bc8d8671a6d247e1eb303c4b343eaee81d861f3404db2935653692"}, + {file = "Pygments-2.16.1.tar.gz", hash = "sha256:1daff0494820c69bc8941e407aa20f577374ee88364ee10a98fdbe0aece96e29"}, +] + +[[package]] +name = "pygtrie" +version = "2.5.0" +summary = "A pure Python trie data structure implementation." +files = [ + {file = "pygtrie-2.5.0-py3-none-any.whl", hash = "sha256:8795cda8105493d5ae159a5bef313ff13156c5d4d72feddefacaad59f8c8ce16"}, + {file = "pygtrie-2.5.0.tar.gz", hash = "sha256:203514ad826eb403dab1d2e2ddd034e0d1534bbe4dbe0213bb0593f66beba4e2"}, +] + +[[package]] +name = "pymongo" +version = "4.4.1" +requires_python = ">=3.7" +summary = "Python driver for MongoDB " +dependencies = [ + "dnspython<3.0.0,>=1.16.0", +] +files = [ + {file = "pymongo-4.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:bbdd6c719cc2ea440d7245ba71ecdda507275071753c6ffe9c8232647246f575"}, + {file = "pymongo-4.4.1-cp310-cp310-manylinux1_i686.whl", hash = "sha256:a438508dd8007a4a724601c3790db46fe0edc3d7d172acafc5f148ceb4a07815"}, + {file = "pymongo-4.4.1-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:3a350d03959f9d5b7f2ea0621f5bb2eb3927b8fc1c4031d12cfd3949839d4f66"}, + {file = "pymongo-4.4.1-cp310-cp310-manylinux2014_i686.whl", hash = "sha256:e6d5d2c97c35f83dc65ccd5d64c7ed16eba6d9403e3744e847aee648c432f0bb"}, + {file = "pymongo-4.4.1-cp310-cp310-manylinux2014_ppc64le.whl", hash = "sha256:1944b16ffef3573ae064196460de43eb1c865a64fed23551b5eac1951d80acca"}, + {file = "pymongo-4.4.1-cp310-cp310-manylinux2014_s390x.whl", hash = "sha256:912b0fdc16500125dc1837be8b13c99d6782d93d6cd099d0e090e2aca0b6d100"}, + {file = "pymongo-4.4.1-cp310-cp310-manylinux2014_x86_64.whl", hash = "sha256:d1b1c8eb21de4cb5e296614e8b775d5ecf9c56b7d3c6000f4bfdb17f9e244e72"}, + {file = "pymongo-4.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e3b508e0de613b906267f2c484cb5e9afd3a64680e1af23386ca8f99a29c6145"}, + {file = "pymongo-4.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f41feb8cf429799ac43ed34504839954aa7d907f8bd9ecb52ed5ff0d2ea84245"}, + {file = "pymongo-4.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1897123c4bede1af0c264a3bc389a2505bae50d85e4f211288d352928c02d017"}, + {file = "pymongo-4.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b4c4bcd285bf0f5272d50628e4ea3989738e3af1251b2dd7bf50da2d593f3a56"}, + {file = "pymongo-4.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:995b868ccc9df8d36cb28142363e3911846fe9f43348d942951f60cdd7f62224"}, + {file = "pymongo-4.4.1-cp310-cp310-win32.whl", hash = "sha256:a5198beca36778f19a98b56f541a0529502046bc867b352dda5b6322e1ddc4fd"}, + {file = "pymongo-4.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:a86d20210c9805a032cda14225087ec483613aff0955327c7871a3c980562c5b"}, + {file = "pymongo-4.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5a2a1da505ea78787b0382c92dc21a45d19918014394b220c4734857e9c73694"}, + {file = "pymongo-4.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35545583396684ea70a0b005034a469bf3f447732396e5b3d50bec94890b8d5c"}, + {file = "pymongo-4.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5248fdf7244a5e976279fe154d116c73f6206e0be71074ea9d9b1e73b5893dd5"}, + {file = "pymongo-4.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:44381b817eeb47a41bbfbd279594a7fb21017e0e3e15550eb0fd3758333097f3"}, + {file = "pymongo-4.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f0bd25de90b804cc95e548f55f430df2b47f242a4d7bbce486db62f3b3c981f"}, + {file = "pymongo-4.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d67f4029c57b36a0278aeae044ce382752c078c7625cef71b5e2cf3e576961f9"}, + {file = "pymongo-4.4.1-cp311-cp311-win32.whl", hash = "sha256:8082eef0d8c711c9c272906fa469965e52b44dbdb8a589b54857b1351dc2e511"}, + {file = "pymongo-4.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:980da627edc1275896d7d4670596433ec66e1f452ec244e07bbb2f91c955b581"}, + {file = "pymongo-4.4.1-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:6cf08997d3ecf9a1eabe12c35aa82a5c588f53fac054ed46fe5c16a0a20ea43d"}, + {file = "pymongo-4.4.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:a6750449759f0a83adc9df3a469483a8c3eef077490b76f30c03dc8f7a4b1d66"}, + {file = "pymongo-4.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:efa67f46c1678df541e8f41247d22430905f80a3296d9c914aaa793f2c9fa1db"}, + {file = "pymongo-4.4.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:d9a5e16a32fb1000c72a8734ddd8ae291974deb5d38d40d1bdd01dbe4024eeb0"}, + {file = "pymongo-4.4.1-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:36b0b06c6e830d190215fced82872e5fd8239771063afa206f9adc09574018a3"}, + {file = "pymongo-4.4.1-cp37-cp37m-manylinux2014_ppc64le.whl", hash = "sha256:4ec9c6d4547c93cf39787c249969f7348ef6c4d36439af10d57b5ee65f3dfbf9"}, + {file = "pymongo-4.4.1-cp37-cp37m-manylinux2014_s390x.whl", hash = "sha256:5368801ca6b66aacc5cc013258f11899cd6a4c3bb28cec435dd67f835905e9d2"}, + {file = "pymongo-4.4.1-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:91848d555155ad4594de5e575b6452adc471bc7bc4b4d2b1f4f15a78a8af7843"}, + {file = "pymongo-4.4.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e0f08a2dba1469252462c414b66cb416c7f7295f2c85e50f735122a251fcb131"}, + {file = "pymongo-4.4.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2fe4bbf2b2c91e4690b5658b0fbb98ca6e0a8fba9ececd65b4e7d2d1df3e9b01"}, + {file = "pymongo-4.4.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7e307d67641d0e2f7e7d6ee3dad880d090dace96cc1d95c99d15bd9f545a1168"}, + {file = "pymongo-4.4.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d43634594f2486cc9bb604a1dc0914234878c4faf6604574a25260cb2faaa06"}, + {file = "pymongo-4.4.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef0e3279e72cccc3dc7be75b12b1e54cc938d7ce13f5f22bea844b9d9d5fecd4"}, + {file = "pymongo-4.4.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05935f5a4bbae0a99482147588351b7b17999f4a4e6e55abfb74367ac58c0634"}, + {file = "pymongo-4.4.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:854d92d2437e3496742e17342496e1f3d9efb22455501fd6010aa3658138e457"}, + {file = "pymongo-4.4.1-cp37-cp37m-win32.whl", hash = "sha256:ddffc0c6d0e92cf43dc6c47639d1ef9ab3c280db2998a33dbb9953bd864841e1"}, + {file = "pymongo-4.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:2259302d8ab51cd56c3d9d5cca325977e35a0bb3a15a297ec124d2da56c214f7"}, + {file = "pymongo-4.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:262a4073d2ee0654f0314ef4d9aab1d8c13dc8dae5c102312e152c02bfa7bdb7"}, + {file = "pymongo-4.4.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:022c91e2a41eefbcddc844c534520a13c6f613666c37b9fb9ed039eff47bd2e4"}, + {file = "pymongo-4.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:a0d326c3ba989091026fbc4827638dc169abdbb0c0bbe593716921543f530af6"}, + {file = "pymongo-4.4.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:5a1e5b931bf729b2eacd720a0e40201c2d5ed0e2bada60863f19b069bb5016c4"}, + {file = "pymongo-4.4.1-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:54d0b8b6f2548e15b09232827d9ba8e03a599c9a30534f7f2c7bae79df2d1f91"}, + {file = "pymongo-4.4.1-cp38-cp38-manylinux2014_ppc64le.whl", hash = "sha256:e426e213ab07a73f8759ab8d69e87d05d7a60b3ecbf7673965948dcf8ebc1c9f"}, + {file = "pymongo-4.4.1-cp38-cp38-manylinux2014_s390x.whl", hash = "sha256:53831effe4dc0243231a944dfbd87896e42b1cf081776930de5cc74371405e3b"}, + {file = "pymongo-4.4.1-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:977c34b5b0b50bd169fbca1a4dd06fbfdfd8ac47734fdc3473532c10098e16ce"}, + {file = "pymongo-4.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fab52db4d3aa3b73bcf920fb375dbea63bf0df0cb4bdb38c5a0a69e16568cc21"}, + {file = "pymongo-4.4.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3bb935789276422d8875f051837356edfccdb886e673444d91e4941a8142bd48"}, + {file = "pymongo-4.4.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9d45243ff4800320c842c45e01c91037e281840e8c6ed2949ed82a70f55c0e6a"}, + {file = "pymongo-4.4.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:32d6d2b7e14bb6bc052f6cba0c1cf4d47a2b49c56ea1ed0f960a02bc9afaefb2"}, + {file = "pymongo-4.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:85b92b3828b2c923ed448f820c147ee51fa4566e35c9bf88415586eb0192ced2"}, + {file = "pymongo-4.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3f345380f6d6d6d1dc6db9fa5c8480c439ea79553b71a2cbe3030a1f20676595"}, + {file = "pymongo-4.4.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:0dcc64747b628a96bcfc6405c42acae3762c85d8ae8c1ce18834b8151cad7486"}, + {file = "pymongo-4.4.1-cp38-cp38-win32.whl", hash = "sha256:ebe1683ec85d8bca389183d01ecf4640c797d6f22e6dac3453a6c492920d5ec3"}, + {file = "pymongo-4.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:58c492e28057838792bed67875f982ffbd3c9ceb67341cc03811859fddb8efbf"}, + {file = "pymongo-4.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:aed21b3142311ad139629c4e101b54f25447ec40d6f42c72ad5c1a6f4f851f3a"}, + {file = "pymongo-4.4.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:98764ae13de0ab80ba824ca0b84177006dec51f48dfb7c944d8fa78ab645c67f"}, + {file = "pymongo-4.4.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:7b7127bb35f10d974ec1bd5573389e99054c558b821c9f23bb8ff94e7ae6e612"}, + {file = "pymongo-4.4.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:48409bac0f6a62825c306c9a124698df920afdc396132908a8e88b466925a248"}, + {file = "pymongo-4.4.1-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:55b6ebeeabe32a9d2e38eeb90f07c020cb91098b34b5fca42ff3991cb6e6e621"}, + {file = "pymongo-4.4.1-cp39-cp39-manylinux2014_ppc64le.whl", hash = "sha256:4e6a70c9d437b043fb07eef1796060f476359e5b7d8e23baa49f1a70379d6543"}, + {file = "pymongo-4.4.1-cp39-cp39-manylinux2014_s390x.whl", hash = "sha256:0bdbbcc1ef3a56347630c57eda5cd9536bdbdb82754b3108c66cbc51b5233dfb"}, + {file = "pymongo-4.4.1-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:04ec1c5451ad358fdbff28ddc6e8a3d1b5f62178d38cd08007a251bc3f59445a"}, + {file = "pymongo-4.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a7739bcebdbeb5648edb15af00fd38f2ab5de20851a1341d229494a638284cc"}, + {file = "pymongo-4.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:02dba4ea2a6f22de4b50864d3957a0110b75d3eeb40aeab0b0ff64bcb5a063e6"}, + {file = "pymongo-4.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:884a35c0740744a48f67210692841581ab83a4608d3a031e7125022989ef65f8"}, + {file = "pymongo-4.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2aab6d1cff00d68212eca75d2260980202b14038d9298fed7d5c455fe3285c7c"}, + {file = "pymongo-4.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae1f85223193f249320f695eec4242cdcc311357f5f5064c2e72cfd18017e8ee"}, + {file = "pymongo-4.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b25d2ccdb2901655cc56c0fc978c5ddb35029c46bfd30d182d0e23fffd55b14b"}, + {file = "pymongo-4.4.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:334d41649f157c56a47fb289bae3b647a867c1a74f5f3a8a371fb361580bd9d3"}, + {file = "pymongo-4.4.1-cp39-cp39-win32.whl", hash = "sha256:c409e5888a94a3ff99783fffd9477128ffab8416e3f8b2c633993eecdcd5c267"}, + {file = "pymongo-4.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:3681caf37edbe05f72f0d351e4a6cb5874ec7ab5eeb99df3a277dbf110093739"}, + {file = "pymongo-4.4.1.tar.gz", hash = "sha256:a4df87dbbd03ac6372d24f2a8054b4dc33de497d5227b50ec649f436ad574284"}, +] + +[[package]] +name = "python-dateutil" +version = "2.8.2" +requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +summary = "Extensions to the standard Python datetime module" +dependencies = [ + "six>=1.5", +] +files = [ + {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, + {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, +] + +[[package]] +name = "python-dotenv" +version = "1.0.0" +requires_python = ">=3.8" +summary = "Read key-value pairs from a .env file and set them as environment variables" +files = [ + {file = "python-dotenv-1.0.0.tar.gz", hash = "sha256:a8df96034aae6d2d50a4ebe8216326c61c3eb64836776504fcca410e5937a3ba"}, + {file = "python_dotenv-1.0.0-py3-none-any.whl", hash = "sha256:f5971a9226b701070a4bf2c38c89e5a3f0d64de8debda981d1db98583009122a"}, +] + +[[package]] +name = "pytz" +version = "2022.7.1" +summary = "World timezone definitions, modern and historical" +files = [ + {file = "pytz-2022.7.1-py2.py3-none-any.whl", hash = "sha256:78f4f37d8198e0627c5f1143240bb0206b8691d8d7ac6d78fee88b78733f8c4a"}, + {file = "pytz-2022.7.1.tar.gz", hash = "sha256:01a0681c4b9684a28304615eba55d1ab31ae00bf68ec157ec3708a8182dbbcd0"}, +] + +[[package]] +name = "pytz-deprecation-shim" +version = "0.1.0.post0" +requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" +summary = "Shims to make deprecation of pytz easier" +dependencies = [ + "tzdata; python_version >= \"3.6\"", +] +files = [ + {file = "pytz_deprecation_shim-0.1.0.post0-py2.py3-none-any.whl", hash = "sha256:8314c9692a636c8eb3bda879b9f119e350e93223ae83e70e80c31675a0fdc1a6"}, + {file = "pytz_deprecation_shim-0.1.0.post0.tar.gz", hash = "sha256:af097bae1b616dde5c5744441e2ddc69e74dfdcb0c263129610d85b87445a59d"}, +] + +[[package]] +name = "pyyaml" +version = "6.0.1" +requires_python = ">=3.6" +summary = "YAML parser and emitter for Python" +files = [ + {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, + {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, + {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, + {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, + {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, + {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, + {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, + {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, + {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, + {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, + {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, + {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, + {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, + {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, +] + +[[package]] +name = "requests" +version = "2.28.2" +requires_python = ">=3.7, <4" +summary = "Python HTTP for Humans." +dependencies = [ + "certifi>=2017.4.17", + "charset-normalizer<4,>=2", + "idna<4,>=2.5", + "urllib3<1.27,>=1.21.1", +] +files = [ + {file = "requests-2.28.2-py3-none-any.whl", hash = "sha256:64299f4909223da747622c030b781c0d7811e359c37124b4bd368fb8c6518baa"}, + {file = "requests-2.28.2.tar.gz", hash = "sha256:98b1b2782e3c6c4904938b84c0eb932721069dfdb9134313beff7c83c2df24bf"}, +] + +[[package]] +name = "rich" +version = "13.5.2" +requires_python = ">=3.7.0" +summary = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" +dependencies = [ + "markdown-it-py>=2.2.0", + "pygments<3.0.0,>=2.13.0", +] +files = [ + {file = "rich-13.5.2-py3-none-any.whl", hash = "sha256:146a90b3b6b47cac4a73c12866a499e9817426423f57c5a66949c086191a8808"}, + {file = "rich-13.5.2.tar.gz", hash = "sha256:fb9d6c0a0f643c99eed3875b5377a184132ba9be4d61516a55273d3554d75a39"}, +] + +[[package]] +name = "ruff" +version = "0.0.282" +requires_python = ">=3.7" +summary = "An extremely fast Python linter, written in Rust." +files = [ + {file = "ruff-0.0.282-py3-none-macosx_10_7_x86_64.whl", hash = "sha256:01b76309ddab16eb258dabc5e86e73e6542f59f3ea6b4ab886ecbcfc80ce062c"}, + {file = "ruff-0.0.282-py3-none-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:e177cbb6dc0b1dbef5e999900d798b73e33602abf9b6c62d5d2cbe101026d931"}, + {file = "ruff-0.0.282-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5374b40b6d860d334d28678a53a92f0bf04b53acdf0395900361ad54ce71cd1d"}, + {file = "ruff-0.0.282-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d1ccbceb44e94fe2205b63996166e98a513a19ed23ec01d7193b7494b94ba30d"}, + {file = "ruff-0.0.282-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eee9c8c50bc77eb9c0811c91d9d67ff39fe4f394c2f44ada37dac6d45e50c9f1"}, + {file = "ruff-0.0.282-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:826e4de98e91450a6fe699a4e4a7cf33b9a90a2c5c270dc5b202241c37359ff8"}, + {file = "ruff-0.0.282-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d99758f8bbcb8f8da99acabf711ffad5e7a015247adf27211100b3586777fd56"}, + {file = "ruff-0.0.282-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3f30c9958ab9cb02bf0c574c629e87c19454cbbdb82750e49e3d1559a5a8f216"}, + {file = "ruff-0.0.282-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47a7a9366ab8e4ee20df9339bef172eec7b2e9e123643bf3ede005058f5b114e"}, + {file = "ruff-0.0.282-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1f05f5e6d6df6f8b1974c08f963c33f0a4d8cfa15cba12d35ca3ece8e9be5b1f"}, + {file = "ruff-0.0.282-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:0710ea2cadc504b96c1d94c414a7802369d0fff2ab7c94460344bba69135cb40"}, + {file = "ruff-0.0.282-py3-none-musllinux_1_2_i686.whl", hash = "sha256:2ca52536e1c7603fe4cbb5ad9dc141df47c3200df782f5ec559364716ea27f96"}, + {file = "ruff-0.0.282-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:aab9ed5bfba6b0a2242a7ec9a72858c802ceeaf0076fe72b2ad455639275f22c"}, + {file = "ruff-0.0.282-py3-none-win32.whl", hash = "sha256:f51bbb64f8f29e444c16d21b269ba82e25f8d536beda3df7c9fe1816297e508e"}, + {file = "ruff-0.0.282-py3-none-win_amd64.whl", hash = "sha256:bd25085c42ebaffe336ed7bda8a0ae7b6c454a5f386ec8b2299503f79bd12bdf"}, + {file = "ruff-0.0.282-py3-none-win_arm64.whl", hash = "sha256:f03fba9621533d67d7ab995847467d78b9337e3697779ef2cea6f1deaee5fbef"}, + {file = "ruff-0.0.282.tar.gz", hash = "sha256:ef677c26bae756e4c98af6d8972da83caea550bc92ffef97a6e939ca5b24ad06"}, +] + +[[package]] +name = "setuptools" +version = "67.6.0" +requires_python = ">=3.7" +summary = "Easily download, build, install, upgrade, and uninstall Python packages" +files = [ + {file = "setuptools-67.6.0-py3-none-any.whl", hash = "sha256:b78aaa36f6b90a074c1fa651168723acbf45d14cb1196b6f02c0fd07f17623b2"}, + {file = "setuptools-67.6.0.tar.gz", hash = "sha256:2ee892cd5f29f3373097f5a814697e397cf3ce313616df0af11231e2ad118077"}, +] + +[[package]] +name = "six" +version = "1.16.0" +requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +summary = "Python 2 and 3 compatibility utilities" +files = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] + +[[package]] +name = "sniffio" +version = "1.3.0" +requires_python = ">=3.7" +summary = "Sniff out which async library your code is running under" +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 = "starlette" +version = "0.26.1" +requires_python = ">=3.7" +summary = "The little ASGI library that shines." +dependencies = [ + "anyio<5,>=3.4.0", +] +files = [ + {file = "starlette-0.26.1-py3-none-any.whl", hash = "sha256:e87fce5d7cbdde34b76f0ac69013fd9d190d581d80681493016666e6f96c6d5e"}, + {file = "starlette-0.26.1.tar.gz", hash = "sha256:41da799057ea8620e4667a3e69a5b1923ebd32b1819c8fa75634bbe8d8bea9bd"}, +] + +[[package]] +name = "tenacity" +version = "8.2.2" +requires_python = ">=3.6" +summary = "Retry code until it succeeds" +files = [ + {file = "tenacity-8.2.2-py3-none-any.whl", hash = "sha256:2f277afb21b851637e8f52e6a613ff08734c347dc19ade928e519d7d2d8569b0"}, + {file = "tenacity-8.2.2.tar.gz", hash = "sha256:43af037822bd0029025877f3b2d97cc4d7bb0c2991000a3d59d71517c5c969e0"}, +] + +[[package]] +name = "tomli" +version = "2.0.1" +requires_python = ">=3.7" +summary = "A lil' TOML parser" +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 = "typing-extensions" +version = "4.7.1" +requires_python = ">=3.7" +summary = "Backported and Experimental Type Hints for Python 3.7+" +files = [ + {file = "typing_extensions-4.7.1-py3-none-any.whl", hash = "sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36"}, + {file = "typing_extensions-4.7.1.tar.gz", hash = "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2"}, +] + +[[package]] +name = "tzdata" +version = "2022.7" +requires_python = ">=2" +summary = "Provider of IANA time zone data" +files = [ + {file = "tzdata-2022.7-py2.py3-none-any.whl", hash = "sha256:2b88858b0e3120792a3c0635c23daf36a7d7eeeca657c323da299d2094402a0d"}, + {file = "tzdata-2022.7.tar.gz", hash = "sha256:fe5f866eddd8b96e9fcba978f8e503c909b19ea7efda11e52e39494bad3a7bfa"}, +] + +[[package]] +name = "tzlocal" +version = "4.3" +requires_python = ">=3.7" +summary = "tzinfo object for the local timezone" +dependencies = [ + "pytz-deprecation-shim", + "tzdata; platform_system == \"Windows\"", +] +files = [ + {file = "tzlocal-4.3-py3-none-any.whl", hash = "sha256:b44c4388f3d34f25862cfbb387578a4d70fec417649da694a132f628a23367e2"}, + {file = "tzlocal-4.3.tar.gz", hash = "sha256:3f21d09e1b2aa9f2dacca12da240ca37de3ba5237a93addfd6d593afe9073355"}, +] + +[[package]] +name = "uc-micro-py" +version = "1.0.2" +requires_python = ">=3.7" +summary = "Micro subset of unicode data files for linkify-it-py projects." +files = [ + {file = "uc-micro-py-1.0.2.tar.gz", hash = "sha256:30ae2ac9c49f39ac6dce743bd187fcd2b574b16ca095fa74cd9396795c954c54"}, + {file = "uc_micro_py-1.0.2-py3-none-any.whl", hash = "sha256:8c9110c309db9d9e87302e2f4ad2c3152770930d88ab385cd544e7a7e75f3de0"}, +] + +[[package]] +name = "urllib3" +version = "1.26.15" +requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" +summary = "HTTP library with thread-safe connection pooling, file post, and more." +files = [ + {file = "urllib3-1.26.15-py2.py3-none-any.whl", hash = "sha256:aa751d169e23c7479ce47a0cb0da579e3ede798f994f5816a74e4f4500dcea42"}, + {file = "urllib3-1.26.15.tar.gz", hash = "sha256:8a388717b9476f934a21484e8c8e61875ab60644d29b9b39e11e4b9dc1c6b305"}, +] + +[[package]] +name = "uvicorn" +version = "0.21.1" +requires_python = ">=3.7" +summary = "The lightning-fast ASGI server." +dependencies = [ + "click>=7.0", + "h11>=0.8", +] +files = [ + {file = "uvicorn-0.21.1-py3-none-any.whl", hash = "sha256:e47cac98a6da10cd41e6fd036d472c6f58ede6c5dbee3dbee3ef7a100ed97742"}, + {file = "uvicorn-0.21.1.tar.gz", hash = "sha256:0fac9cb342ba099e0d582966005f3fdba5b0290579fed4a6266dc702ca7bb032"}, +] + +[[package]] +name = "uvicorn" +version = "0.21.1" +extras = ["standard"] +requires_python = ">=3.7" +summary = "The lightning-fast ASGI server." +dependencies = [ + "colorama>=0.4; sys_platform == \"win32\"", + "httptools>=0.5.0", + "python-dotenv>=0.13", + "pyyaml>=5.1", + "uvicorn==0.21.1", + "uvloop!=0.15.0,!=0.15.1,>=0.14.0; sys_platform != \"win32\" and (sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\")", + "watchfiles>=0.13", + "websockets>=10.4", +] +files = [ + {file = "uvicorn-0.21.1-py3-none-any.whl", hash = "sha256:e47cac98a6da10cd41e6fd036d472c6f58ede6c5dbee3dbee3ef7a100ed97742"}, + {file = "uvicorn-0.21.1.tar.gz", hash = "sha256:0fac9cb342ba099e0d582966005f3fdba5b0290579fed4a6266dc702ca7bb032"}, +] + +[[package]] +name = "uvloop" +version = "0.17.0" +requires_python = ">=3.7" +summary = "Fast implementation of asyncio event loop on top of libuv" +files = [ + {file = "uvloop-0.17.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ce9f61938d7155f79d3cb2ffa663147d4a76d16e08f65e2c66b77bd41b356718"}, + {file = "uvloop-0.17.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:68532f4349fd3900b839f588972b3392ee56042e440dd5873dfbbcd2cc67617c"}, + {file = "uvloop-0.17.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0949caf774b9fcefc7c5756bacbbbd3fc4c05a6b7eebc7c7ad6f825b23998d6d"}, + {file = "uvloop-0.17.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff3d00b70ce95adce264462c930fbaecb29718ba6563db354608f37e49e09024"}, + {file = "uvloop-0.17.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:a5abddb3558d3f0a78949c750644a67be31e47936042d4f6c888dd6f3c95f4aa"}, + {file = "uvloop-0.17.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8efcadc5a0003d3a6e887ccc1fb44dec25594f117a94e3127954c05cf144d811"}, + {file = "uvloop-0.17.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3378eb62c63bf336ae2070599e49089005771cc651c8769aaad72d1bd9385a7c"}, + {file = "uvloop-0.17.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6aafa5a78b9e62493539456f8b646f85abc7093dd997f4976bb105537cf2635e"}, + {file = "uvloop-0.17.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c686a47d57ca910a2572fddfe9912819880b8765e2f01dc0dd12a9bf8573e539"}, + {file = "uvloop-0.17.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:864e1197139d651a76c81757db5eb199db8866e13acb0dfe96e6fc5d1cf45fc4"}, + {file = "uvloop-0.17.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:2a6149e1defac0faf505406259561bc14b034cdf1d4711a3ddcdfbaa8d825a05"}, + {file = "uvloop-0.17.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6708f30db9117f115eadc4f125c2a10c1a50d711461699a0cbfaa45b9a78e376"}, + {file = "uvloop-0.17.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:23609ca361a7fc587031429fa25ad2ed7242941adec948f9d10c045bfecab06b"}, + {file = "uvloop-0.17.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2deae0b0fb00a6af41fe60a675cec079615b01d68beb4cc7b722424406b126a8"}, + {file = "uvloop-0.17.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45cea33b208971e87a31c17622e4b440cac231766ec11e5d22c76fab3bf9df62"}, + {file = "uvloop-0.17.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:9b09e0f0ac29eee0451d71798878eae5a4e6a91aa275e114037b27f7db72702d"}, + {file = "uvloop-0.17.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:dbbaf9da2ee98ee2531e0c780455f2841e4675ff580ecf93fe5c48fe733b5667"}, + {file = "uvloop-0.17.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a4aee22ece20958888eedbad20e4dbb03c37533e010fb824161b4f05e641f738"}, + {file = "uvloop-0.17.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:307958f9fc5c8bb01fad752d1345168c0abc5d62c1b72a4a8c6c06f042b45b20"}, + {file = "uvloop-0.17.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ebeeec6a6641d0adb2ea71dcfb76017602ee2bfd8213e3fcc18d8f699c5104f"}, + {file = "uvloop-0.17.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1436c8673c1563422213ac6907789ecb2b070f5939b9cbff9ef7113f2b531595"}, + {file = "uvloop-0.17.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8887d675a64cfc59f4ecd34382e5b4f0ef4ae1da37ed665adba0c2badf0d6578"}, + {file = "uvloop-0.17.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:3db8de10ed684995a7f34a001f15b374c230f7655ae840964d51496e2f8a8474"}, + {file = "uvloop-0.17.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7d37dccc7ae63e61f7b96ee2e19c40f153ba6ce730d8ba4d3b4e9738c1dccc1b"}, + {file = "uvloop-0.17.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:cbbe908fda687e39afd6ea2a2f14c2c3e43f2ca88e3a11964b297822358d0e6c"}, + {file = "uvloop-0.17.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3d97672dc709fa4447ab83276f344a165075fd9f366a97b712bdd3fee05efae8"}, + {file = "uvloop-0.17.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f1e507c9ee39c61bfddd79714e4f85900656db1aec4d40c6de55648e85c2799c"}, + {file = "uvloop-0.17.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:c092a2c1e736086d59ac8e41f9c98f26bbf9b9222a76f21af9dfe949b99b2eb9"}, + {file = "uvloop-0.17.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:30babd84706115626ea78ea5dbc7dd8d0d01a2e9f9b306d24ca4ed5796c66ded"}, + {file = "uvloop-0.17.0.tar.gz", hash = "sha256:0ddf6baf9cf11a1a22c71487f39f15b2cf78eb5bde7e5b45fbb99e8a9d91b9e1"}, +] + +[[package]] +name = "virtualenv" +version = "20.23.0" +requires_python = ">=3.7" +summary = "Virtual Python Environment builder" +dependencies = [ + "distlib<1,>=0.3.6", + "filelock<4,>=3.11", + "platformdirs<4,>=3.2", +] +files = [ + {file = "virtualenv-20.23.0-py3-none-any.whl", hash = "sha256:6abec7670e5802a528357fdc75b26b9f57d5d92f29c5462ba0fbe45feacc685e"}, + {file = "virtualenv-20.23.0.tar.gz", hash = "sha256:a85caa554ced0c0afbd0d638e7e2d7b5f92d23478d05d17a76daeac8f279f924"}, +] + +[[package]] +name = "watchfiles" +version = "0.18.1" +requires_python = ">=3.7" +summary = "Simple, modern and high performance file watching and code reload in python." +dependencies = [ + "anyio>=3.0.0", +] +files = [ + {file = "watchfiles-0.18.1-cp37-abi3-macosx_10_7_x86_64.whl", hash = "sha256:9891d3c94272108bcecf5597a592e61105279def1313521e637f2d5acbe08bc9"}, + {file = "watchfiles-0.18.1-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:7102342d60207fa635e24c02a51c6628bf0472e5fef067f78a612386840407fc"}, + {file = "watchfiles-0.18.1-cp37-abi3-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:00ea0081eca5e8e695cffbc3a726bb90da77f4e3f78ce29b86f0d95db4e70ef7"}, + {file = "watchfiles-0.18.1-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b8e6db99e49cd7125d8a4c9d33c0735eea7b75a942c6ad68b75be3e91c242fb"}, + {file = "watchfiles-0.18.1-cp37-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bc7c726855f04f22ac79131b51bf0c9f728cb2117419ed830a43828b2c4a5fcb"}, + {file = "watchfiles-0.18.1-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cbaff354d12235002e62d9d3fa8bcf326a8490c1179aa5c17195a300a9e5952f"}, + {file = "watchfiles-0.18.1-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:888db233e06907c555eccd10da99b9cd5ed45deca47e41766954292dc9f7b198"}, + {file = "watchfiles-0.18.1-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:dde79930d1b28f15994ad6613aa2865fc7a403d2bb14585a8714a53233b15717"}, + {file = "watchfiles-0.18.1-cp37-abi3-win32.whl", hash = "sha256:e2b2bdd26bf8d6ed90763e6020b475f7634f919dbd1730ea1b6f8cb88e21de5d"}, + {file = "watchfiles-0.18.1-cp37-abi3-win_amd64.whl", hash = "sha256:c541e0f2c3e95e83e4f84561c893284ba984e9d0025352057396d96dceb09f44"}, + {file = "watchfiles-0.18.1-cp37-abi3-win_arm64.whl", hash = "sha256:9a26272ef3e930330fc0c2c148cc29706cc2c40d25760c7ccea8d768a8feef8b"}, + {file = "watchfiles-0.18.1-pp38-pypy38_pp73-macosx_10_7_x86_64.whl", hash = "sha256:9fb12a5e2b42e0b53769455ff93546e6bc9ab14007fbd436978d827a95ca5bd1"}, + {file = "watchfiles-0.18.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:548d6b42303d40264118178053c78820533b683b20dfbb254a8706ca48467357"}, + {file = "watchfiles-0.18.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e0d8fdfebc50ac7569358f5c75f2b98bb473befccf9498cf23b3e39993bb45a"}, + {file = "watchfiles-0.18.1-pp39-pypy39_pp73-macosx_10_7_x86_64.whl", hash = "sha256:0f9a22fff1745e2bb930b1e971c4c5b67ea3b38ae17a6adb9019371f80961219"}, + {file = "watchfiles-0.18.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b02e7fa03cd4059dd61ff0600080a5a9e7a893a85cb8e5178943533656eec65e"}, + {file = "watchfiles-0.18.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a868ce2c7565137f852bd4c863a164dc81306cae7378dbdbe4e2aca51ddb8857"}, + {file = "watchfiles-0.18.1.tar.gz", hash = "sha256:4ec0134a5e31797eb3c6c624dbe9354f2a8ee9c720e0b46fc5b7bab472b7c6d4"}, +] + +[[package]] +name = "websockets" +version = "10.4" +requires_python = ">=3.7" +summary = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" +files = [ + {file = "websockets-10.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d58804e996d7d2307173d56c297cf7bc132c52df27a3efaac5e8d43e36c21c48"}, + {file = "websockets-10.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc0b82d728fe21a0d03e65f81980abbbcb13b5387f733a1a870672c5be26edab"}, + {file = "websockets-10.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ba089c499e1f4155d2a3c2a05d2878a3428cf321c848f2b5a45ce55f0d7d310c"}, + {file = "websockets-10.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:33d69ca7612f0ddff3316b0c7b33ca180d464ecac2d115805c044bf0a3b0d032"}, + {file = "websockets-10.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62e627f6b6d4aed919a2052efc408da7a545c606268d5ab5bfab4432734b82b4"}, + {file = "websockets-10.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:38ea7b82bfcae927eeffc55d2ffa31665dc7fec7b8dc654506b8e5a518eb4d50"}, + {file = "websockets-10.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e0cb5cc6ece6ffa75baccfd5c02cffe776f3f5c8bf486811f9d3ea3453676ce8"}, + {file = "websockets-10.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:ae5e95cfb53ab1da62185e23b3130e11d64431179debac6dc3c6acf08760e9b1"}, + {file = "websockets-10.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7c584f366f46ba667cfa66020344886cf47088e79c9b9d39c84ce9ea98aaa331"}, + {file = "websockets-10.4-cp310-cp310-win32.whl", hash = "sha256:b029fb2032ae4724d8ae8d4f6b363f2cc39e4c7b12454df8df7f0f563ed3e61a"}, + {file = "websockets-10.4-cp310-cp310-win_amd64.whl", hash = "sha256:8dc96f64ae43dde92530775e9cb169979f414dcf5cff670455d81a6823b42089"}, + {file = "websockets-10.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:47a2964021f2110116cc1125b3e6d87ab5ad16dea161949e7244ec583b905bb4"}, + {file = "websockets-10.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e789376b52c295c4946403bd0efecf27ab98f05319df4583d3c48e43c7342c2f"}, + {file = "websockets-10.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7d3f0b61c45c3fa9a349cf484962c559a8a1d80dae6977276df8fd1fa5e3cb8c"}, + {file = "websockets-10.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f55b5905705725af31ccef50e55391621532cd64fbf0bc6f4bac935f0fccec46"}, + {file = "websockets-10.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:00c870522cdb69cd625b93f002961ffb0c095394f06ba8c48f17eef7c1541f96"}, + {file = "websockets-10.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f38706e0b15d3c20ef6259fd4bc1700cd133b06c3c1bb108ffe3f8947be15fa"}, + {file = "websockets-10.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f2c38d588887a609191d30e902df2a32711f708abfd85d318ca9b367258cfd0c"}, + {file = "websockets-10.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:fe10ddc59b304cb19a1bdf5bd0a7719cbbc9fbdd57ac80ed436b709fcf889106"}, + {file = "websockets-10.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:90fcf8929836d4a0e964d799a58823547df5a5e9afa83081761630553be731f9"}, + {file = "websockets-10.4-cp311-cp311-win32.whl", hash = "sha256:b9968694c5f467bf67ef97ae7ad4d56d14be2751000c1207d31bf3bb8860bae8"}, + {file = "websockets-10.4-cp311-cp311-win_amd64.whl", hash = "sha256:a7a240d7a74bf8d5cb3bfe6be7f21697a28ec4b1a437607bae08ac7acf5b4882"}, + {file = "websockets-10.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:74de2b894b47f1d21cbd0b37a5e2b2392ad95d17ae983e64727e18eb281fe7cb"}, + {file = "websockets-10.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e3a686ecb4aa0d64ae60c9c9f1a7d5d46cab9bfb5d91a2d303d00e2cd4c4c5cc"}, + {file = "websockets-10.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b0d15c968ea7a65211e084f523151dbf8ae44634de03c801b8bd070b74e85033"}, + {file = "websockets-10.4-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00213676a2e46b6ebf6045bc11d0f529d9120baa6f58d122b4021ad92adabd41"}, + {file = "websockets-10.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:e23173580d740bf8822fd0379e4bf30aa1d5a92a4f252d34e893070c081050df"}, + {file = "websockets-10.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:dd500e0a5e11969cdd3320935ca2ff1e936f2358f9c2e61f100a1660933320ea"}, + {file = "websockets-10.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:4239b6027e3d66a89446908ff3027d2737afc1a375f8fd3eea630a4842ec9a0c"}, + {file = "websockets-10.4-cp37-cp37m-win32.whl", hash = "sha256:8a5cc00546e0a701da4639aa0bbcb0ae2bb678c87f46da01ac2d789e1f2d2038"}, + {file = "websockets-10.4-cp37-cp37m-win_amd64.whl", hash = "sha256:a9f9a735deaf9a0cadc2d8c50d1a5bcdbae8b6e539c6e08237bc4082d7c13f28"}, + {file = "websockets-10.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5c1289596042fad2cdceb05e1ebf7aadf9995c928e0da2b7a4e99494953b1b94"}, + {file = "websockets-10.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0cff816f51fb33c26d6e2b16b5c7d48eaa31dae5488ace6aae468b361f422b63"}, + {file = "websockets-10.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:dd9becd5fe29773d140d68d607d66a38f60e31b86df75332703757ee645b6faf"}, + {file = "websockets-10.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45ec8e75b7dbc9539cbfafa570742fe4f676eb8b0d3694b67dabe2f2ceed8aa6"}, + {file = "websockets-10.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f72e5cd0f18f262f5da20efa9e241699e0cf3a766317a17392550c9ad7b37d8"}, + {file = "websockets-10.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:185929b4808b36a79c65b7865783b87b6841e852ef5407a2fb0c03381092fa3b"}, + {file = "websockets-10.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:7d27a7e34c313b3a7f91adcd05134315002aaf8540d7b4f90336beafaea6217c"}, + {file = "websockets-10.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:884be66c76a444c59f801ac13f40c76f176f1bfa815ef5b8ed44321e74f1600b"}, + {file = "websockets-10.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:931c039af54fc195fe6ad536fde4b0de04da9d5916e78e55405436348cfb0e56"}, + {file = "websockets-10.4-cp38-cp38-win32.whl", hash = "sha256:db3c336f9eda2532ec0fd8ea49fef7a8df8f6c804cdf4f39e5c5c0d4a4ad9a7a"}, + {file = "websockets-10.4-cp38-cp38-win_amd64.whl", hash = "sha256:48c08473563323f9c9debac781ecf66f94ad5a3680a38fe84dee5388cf5acaf6"}, + {file = "websockets-10.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:40e826de3085721dabc7cf9bfd41682dadc02286d8cf149b3ad05bff89311e4f"}, + {file = "websockets-10.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:56029457f219ade1f2fc12a6504ea61e14ee227a815531f9738e41203a429112"}, + {file = "websockets-10.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f5fc088b7a32f244c519a048c170f14cf2251b849ef0e20cbbb0fdf0fdaf556f"}, + {file = "websockets-10.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2fc8709c00704194213d45e455adc106ff9e87658297f72d544220e32029cd3d"}, + {file = "websockets-10.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0154f7691e4fe6c2b2bc275b5701e8b158dae92a1ab229e2b940efe11905dff4"}, + {file = "websockets-10.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c6d2264f485f0b53adf22697ac11e261ce84805c232ed5dbe6b1bcb84b00ff0"}, + {file = "websockets-10.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9bc42e8402dc5e9905fb8b9649f57efcb2056693b7e88faa8fb029256ba9c68c"}, + {file = "websockets-10.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:edc344de4dac1d89300a053ac973299e82d3db56330f3494905643bb68801269"}, + {file = "websockets-10.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:84bc2a7d075f32f6ed98652db3a680a17a4edb21ca7f80fe42e38753a58ee02b"}, + {file = "websockets-10.4-cp39-cp39-win32.whl", hash = "sha256:c94ae4faf2d09f7c81847c63843f84fe47bf6253c9d60b20f25edfd30fb12588"}, + {file = "websockets-10.4-cp39-cp39-win_amd64.whl", hash = "sha256:bbccd847aa0c3a69b5f691a84d2341a4f8a629c6922558f2a70611305f902d74"}, + {file = "websockets-10.4-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:82ff5e1cae4e855147fd57a2863376ed7454134c2bf49ec604dfe71e446e2193"}, + {file = "websockets-10.4-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d210abe51b5da0ffdbf7b43eed0cfdff8a55a1ab17abbec4301c9ff077dd0342"}, + {file = "websockets-10.4-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:942de28af58f352a6f588bc72490ae0f4ccd6dfc2bd3de5945b882a078e4e179"}, + {file = "websockets-10.4-pp37-pypy37_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9b27d6c1c6cd53dc93614967e9ce00ae7f864a2d9f99fe5ed86706e1ecbf485"}, + {file = "websockets-10.4-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:3d3cac3e32b2c8414f4f87c1b2ab686fa6284a980ba283617404377cd448f631"}, + {file = "websockets-10.4-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:da39dd03d130162deb63da51f6e66ed73032ae62e74aaccc4236e30edccddbb0"}, + {file = "websockets-10.4-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:389f8dbb5c489e305fb113ca1b6bdcdaa130923f77485db5b189de343a179393"}, + {file = "websockets-10.4-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09a1814bb15eff7069e51fed0826df0bc0702652b5cb8f87697d469d79c23576"}, + {file = "websockets-10.4-pp38-pypy38_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff64a1d38d156d429404aaa84b27305e957fd10c30e5880d1765c9480bea490f"}, + {file = "websockets-10.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:b343f521b047493dc4022dd338fc6db9d9282658862756b4f6fd0e996c1380e1"}, + {file = "websockets-10.4-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:932af322458da7e4e35df32f050389e13d3d96b09d274b22a7aa1808f292fee4"}, + {file = "websockets-10.4-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6a4162139374a49eb18ef5b2f4da1dd95c994588f5033d64e0bbfda4b6b6fcf"}, + {file = "websockets-10.4-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c57e4c1349fbe0e446c9fa7b19ed2f8a4417233b6984277cce392819123142d3"}, + {file = "websockets-10.4-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b627c266f295de9dea86bd1112ed3d5fafb69a348af30a2422e16590a8ecba13"}, + {file = "websockets-10.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:05a7233089f8bd355e8cbe127c2e8ca0b4ea55467861906b80d2ebc7db4d6b72"}, + {file = "websockets-10.4.tar.gz", hash = "sha256:eef610b23933c54d5d921c92578ae5f89813438fded840c2e9809d378dc765d3"}, +] + +[[package]] +name = "win32-setctime" +version = "1.1.0" +requires_python = ">=3.5" +summary = "A small Python utility to set file creation time on Windows" +files = [ + {file = "win32_setctime-1.1.0-py3-none-any.whl", hash = "sha256:231db239e959c2fe7eb1d7dc129f11172354f98361c4fa2d6d2d7e278baa8aad"}, + {file = "win32_setctime-1.1.0.tar.gz", hash = "sha256:15cf5750465118d6929ae4de4eb46e8edae9a5634350c01ba582df868e932cb2"}, +] + +[[package]] +name = "yagmail" +version = "0.15.293" +summary = "Yet Another GMAIL client" +dependencies = [ + "premailer", +] +files = [ + {file = "yagmail-0.15.293-py2.py3-none-any.whl", hash = "sha256:947a0864e4a64452c8e6b58c80b5bf45389bf8842d779701febfd34fa09649c7"}, + {file = "yagmail-0.15.293.tar.gz", hash = "sha256:44e8d0cda4f63e22a14902cc9096d52197fd0ced023d50b0409325f401585296"}, +] + +[[package]] +name = "yarl" +version = "1.8.2" +requires_python = ">=3.7" +summary = "Yet another URL library" +dependencies = [ + "idna>=2.0", + "multidict>=4.0", +] +files = [ + {file = "yarl-1.8.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:bb81f753c815f6b8e2ddd2eef3c855cf7da193b82396ac013c661aaa6cc6b0a5"}, + {file = "yarl-1.8.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:47d49ac96156f0928f002e2424299b2c91d9db73e08c4cd6742923a086f1c863"}, + {file = "yarl-1.8.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3fc056e35fa6fba63248d93ff6e672c096f95f7836938241ebc8260e062832fe"}, + {file = "yarl-1.8.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:58a3c13d1c3005dbbac5c9f0d3210b60220a65a999b1833aa46bd6677c69b08e"}, + {file = "yarl-1.8.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:10b08293cda921157f1e7c2790999d903b3fd28cd5c208cf8826b3b508026996"}, + {file = "yarl-1.8.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:de986979bbd87272fe557e0a8fcb66fd40ae2ddfe28a8b1ce4eae22681728fef"}, + {file = "yarl-1.8.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c4fcfa71e2c6a3cb568cf81aadc12768b9995323186a10827beccf5fa23d4f8"}, + {file = "yarl-1.8.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae4d7ff1049f36accde9e1ef7301912a751e5bae0a9d142459646114c70ecba6"}, + {file = "yarl-1.8.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:bf071f797aec5b96abfc735ab97da9fd8f8768b43ce2abd85356a3127909d146"}, + {file = "yarl-1.8.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:74dece2bfc60f0f70907c34b857ee98f2c6dd0f75185db133770cd67300d505f"}, + {file = "yarl-1.8.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:df60a94d332158b444301c7f569659c926168e4d4aad2cfbf4bce0e8fb8be826"}, + {file = "yarl-1.8.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:63243b21c6e28ec2375f932a10ce7eda65139b5b854c0f6b82ed945ba526bff3"}, + {file = "yarl-1.8.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:cfa2bbca929aa742b5084fd4663dd4b87c191c844326fcb21c3afd2d11497f80"}, + {file = "yarl-1.8.2-cp310-cp310-win32.whl", hash = "sha256:b05df9ea7496df11b710081bd90ecc3a3db6adb4fee36f6a411e7bc91a18aa42"}, + {file = "yarl-1.8.2-cp310-cp310-win_amd64.whl", hash = "sha256:24ad1d10c9db1953291f56b5fe76203977f1ed05f82d09ec97acb623a7976574"}, + {file = "yarl-1.8.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2a1fca9588f360036242f379bfea2b8b44cae2721859b1c56d033adfd5893634"}, + {file = "yarl-1.8.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f37db05c6051eff17bc832914fe46869f8849de5b92dc4a3466cd63095d23dfd"}, + {file = "yarl-1.8.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:77e913b846a6b9c5f767b14dc1e759e5aff05502fe73079f6f4176359d832581"}, + {file = "yarl-1.8.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0978f29222e649c351b173da2b9b4665ad1feb8d1daa9d971eb90df08702668a"}, + {file = "yarl-1.8.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:388a45dc77198b2460eac0aca1efd6a7c09e976ee768b0d5109173e521a19daf"}, + {file = "yarl-1.8.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2305517e332a862ef75be8fad3606ea10108662bc6fe08509d5ca99503ac2aee"}, + {file = "yarl-1.8.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42430ff511571940d51e75cf42f1e4dbdded477e71c1b7a17f4da76c1da8ea76"}, + {file = "yarl-1.8.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3150078118f62371375e1e69b13b48288e44f6691c1069340081c3fd12c94d5b"}, + {file = "yarl-1.8.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c15163b6125db87c8f53c98baa5e785782078fbd2dbeaa04c6141935eb6dab7a"}, + {file = "yarl-1.8.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4d04acba75c72e6eb90745447d69f84e6c9056390f7a9724605ca9c56b4afcc6"}, + {file = "yarl-1.8.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:e7fd20d6576c10306dea2d6a5765f46f0ac5d6f53436217913e952d19237efc4"}, + {file = "yarl-1.8.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:75c16b2a900b3536dfc7014905a128a2bea8fb01f9ee26d2d7d8db0a08e7cb2c"}, + {file = "yarl-1.8.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6d88056a04860a98341a0cf53e950e3ac9f4e51d1b6f61a53b0609df342cc8b2"}, + {file = "yarl-1.8.2-cp311-cp311-win32.whl", hash = "sha256:fb742dcdd5eec9f26b61224c23baea46c9055cf16f62475e11b9b15dfd5c117b"}, + {file = "yarl-1.8.2-cp311-cp311-win_amd64.whl", hash = "sha256:8c46d3d89902c393a1d1e243ac847e0442d0196bbd81aecc94fcebbc2fd5857c"}, + {file = "yarl-1.8.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:ceff9722e0df2e0a9e8a79c610842004fa54e5b309fe6d218e47cd52f791d7ef"}, + {file = "yarl-1.8.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f6b4aca43b602ba0f1459de647af954769919c4714706be36af670a5f44c9c1"}, + {file = "yarl-1.8.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1684a9bd9077e922300ecd48003ddae7a7474e0412bea38d4631443a91d61077"}, + {file = "yarl-1.8.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ebb78745273e51b9832ef90c0898501006670d6e059f2cdb0e999494eb1450c2"}, + {file = "yarl-1.8.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3adeef150d528ded2a8e734ebf9ae2e658f4c49bf413f5f157a470e17a4a2e89"}, + {file = "yarl-1.8.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57a7c87927a468e5a1dc60c17caf9597161d66457a34273ab1760219953f7f4c"}, + {file = "yarl-1.8.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:efff27bd8cbe1f9bd127e7894942ccc20c857aa8b5a0327874f30201e5ce83d0"}, + {file = "yarl-1.8.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:a783cd344113cb88c5ff7ca32f1f16532a6f2142185147822187913eb989f739"}, + {file = "yarl-1.8.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:705227dccbe96ab02c7cb2c43e1228e2826e7ead880bb19ec94ef279e9555b5b"}, + {file = "yarl-1.8.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:34c09b43bd538bf6c4b891ecce94b6fa4f1f10663a8d4ca589a079a5018f6ed7"}, + {file = "yarl-1.8.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a48f4f7fea9a51098b02209d90297ac324241bf37ff6be6d2b0149ab2bd51b37"}, + {file = "yarl-1.8.2-cp37-cp37m-win32.whl", hash = "sha256:0414fd91ce0b763d4eadb4456795b307a71524dbacd015c657bb2a39db2eab89"}, + {file = "yarl-1.8.2-cp37-cp37m-win_amd64.whl", hash = "sha256:d881d152ae0007809c2c02e22aa534e702f12071e6b285e90945aa3c376463c5"}, + {file = "yarl-1.8.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5df5e3d04101c1e5c3b1d69710b0574171cc02fddc4b23d1b2813e75f35a30b1"}, + {file = "yarl-1.8.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7a66c506ec67eb3159eea5096acd05f5e788ceec7b96087d30c7d2865a243918"}, + {file = "yarl-1.8.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2b4fa2606adf392051d990c3b3877d768771adc3faf2e117b9de7eb977741229"}, + {file = "yarl-1.8.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e21fb44e1eff06dd6ef971d4bdc611807d6bd3691223d9c01a18cec3677939e"}, + {file = "yarl-1.8.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:93202666046d9edadfe9f2e7bf5e0782ea0d497b6d63da322e541665d65a044e"}, + {file = "yarl-1.8.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fc77086ce244453e074e445104f0ecb27530d6fd3a46698e33f6c38951d5a0f1"}, + {file = "yarl-1.8.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dd68a92cab699a233641f5929a40f02a4ede8c009068ca8aa1fe87b8c20ae3"}, + {file = "yarl-1.8.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1b372aad2b5f81db66ee7ec085cbad72c4da660d994e8e590c997e9b01e44901"}, + {file = "yarl-1.8.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e6f3515aafe0209dd17fb9bdd3b4e892963370b3de781f53e1746a521fb39fc0"}, + {file = "yarl-1.8.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:dfef7350ee369197106805e193d420b75467b6cceac646ea5ed3049fcc950a05"}, + {file = "yarl-1.8.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:728be34f70a190566d20aa13dc1f01dc44b6aa74580e10a3fb159691bc76909d"}, + {file = "yarl-1.8.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:ff205b58dc2929191f68162633d5e10e8044398d7a45265f90a0f1d51f85f72c"}, + {file = "yarl-1.8.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:baf211dcad448a87a0d9047dc8282d7de59473ade7d7fdf22150b1d23859f946"}, + {file = "yarl-1.8.2-cp38-cp38-win32.whl", hash = "sha256:272b4f1599f1b621bf2aabe4e5b54f39a933971f4e7c9aa311d6d7dc06965165"}, + {file = "yarl-1.8.2-cp38-cp38-win_amd64.whl", hash = "sha256:326dd1d3caf910cd26a26ccbfb84c03b608ba32499b5d6eeb09252c920bcbe4f"}, + {file = "yarl-1.8.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:f8ca8ad414c85bbc50f49c0a106f951613dfa5f948ab69c10ce9b128d368baf8"}, + {file = "yarl-1.8.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:418857f837347e8aaef682679f41e36c24250097f9e2f315d39bae3a99a34cbf"}, + {file = "yarl-1.8.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ae0eec05ab49e91a78700761777f284c2df119376e391db42c38ab46fd662b77"}, + {file = "yarl-1.8.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:009a028127e0a1755c38b03244c0bea9d5565630db9c4cf9572496e947137a87"}, + {file = "yarl-1.8.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3edac5d74bb3209c418805bda77f973117836e1de7c000e9755e572c1f7850d0"}, + {file = "yarl-1.8.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:da65c3f263729e47351261351b8679c6429151ef9649bba08ef2528ff2c423b2"}, + {file = "yarl-1.8.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ef8fb25e52663a1c85d608f6dd72e19bd390e2ecaf29c17fb08f730226e3a08"}, + {file = "yarl-1.8.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bcd7bb1e5c45274af9a1dd7494d3c52b2be5e6bd8d7e49c612705fd45420b12d"}, + {file = "yarl-1.8.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:44ceac0450e648de86da8e42674f9b7077d763ea80c8ceb9d1c3e41f0f0a9951"}, + {file = "yarl-1.8.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:97209cc91189b48e7cfe777237c04af8e7cc51eb369004e061809bcdf4e55220"}, + {file = "yarl-1.8.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:48dd18adcf98ea9cd721a25313aef49d70d413a999d7d89df44f469edfb38a06"}, + {file = "yarl-1.8.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:e59399dda559688461762800d7fb34d9e8a6a7444fd76ec33220a926c8be1516"}, + {file = "yarl-1.8.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d617c241c8c3ad5c4e78a08429fa49e4b04bedfc507b34b4d8dceb83b4af3588"}, + {file = "yarl-1.8.2-cp39-cp39-win32.whl", hash = "sha256:cb6d48d80a41f68de41212f3dfd1a9d9898d7841c8f7ce6696cf2fd9cb57ef83"}, + {file = "yarl-1.8.2-cp39-cp39-win_amd64.whl", hash = "sha256:6604711362f2dbf7160df21c416f81fac0de6dbcf0b5445a2ef25478ecc4c778"}, + {file = "yarl-1.8.2.tar.gz", hash = "sha256:49d43402c6e3013ad0978602bf6bf5328535c48d192304b91b97a3c6790b1562"}, +] diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..5ceaa84 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,124 @@ +[project] +name = "KiramiBot" +version = "0.0.0" +description = "🌟 读作 Kirami,写作星见,简明轻快的聊天机器人应用。" +authors = [{ name = "Akirami", email = "akiramiaya@outlook.com" }] +requires-python = ">=3.10,<4.0" +license = { text = "AGPL-3.0" } +readme = "README.md" +keywords = ["kirami", "bot", "chatbot", "onebot"] +classifiers = [ + "Development Status :: 4 - Beta", + "Framework :: Robot Framework", + "Framework :: Robot Framework :: Library", + "License :: OSI Approved :: GNU Affero General Public License v3", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Topic :: Software Development", + "Typing :: Typed", +] +dependencies = [ + "nonebot2[fastapi]>=2.0.1", + "nonebot-adapter-onebot>=2.2.4", + "typing-extensions>=4.7.1", + "APScheduler>=3.10.1", + "mango-odm>=0.3.2", + "arrow>=1.2.3", + "bidict>=0.22.1", + "cashews>=6.2.0", + "flowery>=0.0.1", + "rich>=13.5.2", + "loguru>=0.7.0", + "tomli>=2.0.1; python_version < \"3.11\"", + "pyyaml>=6.0.1", + "playwright>=1.36.0", + "httpx[http2]>=0.24.1", + "Jinja2>=3.1.2", + "tenacity>=8.2.2", + "yagmail>=0.15.293", + "filetype>=1.2.0", + "markdown-it-py[linkify,plugins]>=3.0.0", + "mdit-py-emoji>=0.1.1", + "pygments>=2.16.1", +] + +[project.urls] +homepage = "https://kiramibot.dev" +repository = "https://github.com/A-kirami/KiramiBot" +documentation = "https://kiramibot.dev/docs" + +[tool.pdm.dev-dependencies] +dev = [ + "black>=23.7.0", + "ruff>=0.0.282", + "pre-commit>=3.3.3", +] + +[tool.ruff] +select = [ + "F", # Pyflakes + "E", # pycodestyle + "W", # pycodestyle + "UP", # pyupgrade + "ANN", # flake8-annotations + "S", # flake8-bandit + "B", # flake8-bugbear + "C4", # flake8-comprehensions + "ISC", # flake8-implicit-str-concat + "INP", # flake8-no-pep420 + "PIE", # flake8-pie + "T20", # flake8-print + "PYI", # flake8-pyi + "PT", # flake8-pytest-style + "Q", # flake8-quotes + "RSE", # flake8-raise + "RET", # flake8-return + "SIM", # flake8-simplify + "TID", # flake8-tidy-imports + "TCH", # flake8-type-checking + "ARG", # flake8-unused-arguments + "PTH", # flake8-use-pathlib + "ERA", # eradicate + "PL", # Pylint + "TRY", # tryceratops + "PERF", # Perflint + "RUF", # Ruff-specific rules +] +ignore = [ + "E402", + "E501", + "B008", + "B009", + "B010", + "ANN002", + "ANN003", + "ANN101", + "ANN102", + "ANN401", + "ERA001", + "PLC0414", + "PLR0913", + "PLW2901", + "TRY003", + "RUF001", + "RUF002", + "RUF003", + "RUF012", +] +unfixable = ["F401", "F841", "ERA001"] + +[tool.ruff.flake8-annotations] +suppress-dummy-args = true + +[tool.pdm.scripts] +lint = "black --check --diff --color ." +fix = "black ." +post_install = "pre-commit install" + +[build-system] +requires = ["pdm-backend"] +build-backend = "pdm.backend"