├── .editorconfig ├── .gitignore ├── .vscode ├── extensions.json └── settings.json ├── JKCommentCrawler.example.ini ├── JKCommentCrawler.sh ├── License.txt ├── Readme.md ├── jkcommentcrawler ├── __init__.py ├── __main__.py └── nx_client.py ├── poetry.lock ├── poetry.toml └── pyproject.toml /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | indent_size = 4 8 | indent_style = space 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | indent_size = 2 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.toptal.com/developers/gitignore/api/python 3 | # Edit at https://www.toptal.com/developers/gitignore?templates=python 4 | 5 | ### Python ### 6 | # Byte-compiled / optimized / DLL files 7 | __pycache__/ 8 | *.py[cod] 9 | *$py.class 10 | 11 | # C extensions 12 | *.so 13 | 14 | # Distribution / packaging 15 | .Python 16 | build/ 17 | develop-eggs/ 18 | dist/ 19 | downloads/ 20 | eggs/ 21 | .eggs/ 22 | lib/ 23 | lib64/ 24 | parts/ 25 | sdist/ 26 | var/ 27 | wheels/ 28 | pip-wheel-metadata/ 29 | share/python-wheels/ 30 | *.egg-info/ 31 | .installed.cfg 32 | *.egg 33 | MANIFEST 34 | 35 | # PyInstaller 36 | # Usually these files are written by a python script from a template 37 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 38 | *.manifest 39 | *.spec 40 | 41 | # Installer logs 42 | pip-log.txt 43 | pip-delete-this-directory.txt 44 | 45 | # Unit test / coverage reports 46 | htmlcov/ 47 | .tox/ 48 | .nox/ 49 | .coverage 50 | .coverage.* 51 | .cache 52 | nosetests.xml 53 | coverage.xml 54 | *.cover 55 | *.py,cover 56 | .hypothesis/ 57 | .pytest_cache/ 58 | pytestdebug.log 59 | 60 | # Translations 61 | *.mo 62 | *.pot 63 | 64 | # Django stuff: 65 | *.log 66 | local_settings.py 67 | db.sqlite3 68 | db.sqlite3-journal 69 | 70 | # Flask stuff: 71 | instance/ 72 | .webassets-cache 73 | 74 | # Scrapy stuff: 75 | .scrapy 76 | 77 | # Sphinx documentation 78 | docs/_build/ 79 | doc/_build/ 80 | 81 | # PyBuilder 82 | target/ 83 | 84 | # Jupyter Notebook 85 | .ipynb_checkpoints 86 | 87 | # IPython 88 | profile_default/ 89 | ipython_config.py 90 | 91 | # pyenv 92 | .python-version 93 | 94 | # pipenv 95 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 96 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 97 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 98 | # install all needed dependencies. 99 | #Pipfile.lock 100 | 101 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 102 | __pypackages__/ 103 | 104 | # Celery stuff 105 | celerybeat-schedule 106 | celerybeat.pid 107 | 108 | # SageMath parsed files 109 | *.sage.py 110 | 111 | # Environments 112 | .env 113 | .venv 114 | env/ 115 | venv/ 116 | ENV/ 117 | env.bak/ 118 | venv.bak/ 119 | pythonenv* 120 | 121 | # Spyder project settings 122 | .spyderproject 123 | .spyproject 124 | 125 | # Rope project settings 126 | .ropeproject 127 | 128 | # mkdocs documentation 129 | /site 130 | 131 | # mypy 132 | .mypy_cache/ 133 | .dmypy.json 134 | dmypy.json 135 | 136 | # Pyre type checker 137 | .pyre/ 138 | 139 | # pytype static type analyzer 140 | .pytype/ 141 | 142 | # profiling data 143 | .prof 144 | 145 | # End of https://www.toptal.com/developers/gitignore/api/python 146 | 147 | # JKCommentCrawler 148 | JKCommentCrawler.ini 149 | cookie.dump 150 | cookies.json 151 | log* 152 | kakolog* 153 | !.gitkeep 154 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "editorconfig.editorconfig", 4 | "ms-python.python", 5 | "ms-python.vscode-pylance", 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | // Pylance の Type Checking を有効化 3 | "python.languageServer": "Pylance", 4 | "python.analysis.typeCheckingMode": "strict", 5 | // Pylance の Type Checking のうち、いくつかのエラー報告を抑制する 6 | "python.analysis.diagnosticSeverityOverrides": { 7 | "reportConstantRedefinition": "none", 8 | "reportMissingTypeStubs": "none", 9 | "reportPrivateImportUsage": "none", 10 | "reportShadowedImports": "none", 11 | "reportUnnecessaryComparison": "none", 12 | "reportUnknownArgumentType": "none", 13 | "reportUnknownMemberType": "none", 14 | "reportUnknownVariableType": "none", 15 | "reportUnusedFunction": "none", 16 | }, 17 | } 18 | -------------------------------------------------------------------------------- /JKCommentCrawler.example.ini: -------------------------------------------------------------------------------- 1 | [Default] 2 | 3 | # ==================== 環境設定 ==================== 4 | # JKCommentCrawler.ini にコピーした上で、各自の環境に合わせて編集してください 5 | # メールアドレス・パスワードを変更した時は、cookies.json ファイルを削除してから実行してください 6 | 7 | # 過去ログを保存するフォルダ 8 | jkcomment_folder = ./kakolog/ 9 | 10 | # ニコニコにログインするメールアドレス 11 | nicologin_mail = example@example.com 12 | 13 | # ニコニコにログインするパスワード 14 | nicologin_password = example_password 15 | -------------------------------------------------------------------------------- /JKCommentCrawler.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Cron の設定例 4 | ## 毎日 00:01・00:15 (予備)・12:01・12:15 (予備) に実行 5 | # 01 00 * * * sudo -u ubuntu /home/ubuntu/JKCommentCrawler/JKCommentCrawler.sh cron_daily 6 | # 15 00 * * * sudo -u ubuntu /home/ubuntu/JKCommentCrawler/JKCommentCrawler.sh cron_daily 7 | # 01 12 * * * sudo -u ubuntu /home/ubuntu/JKCommentCrawler/JKCommentCrawler.sh cron_daily 8 | # 15 12 * * * sudo -u ubuntu /home/ubuntu/JKCommentCrawler/JKCommentCrawler.sh cron_daily 9 | ## 5分おきに実行 10 | # */5 * * * * sudo -u ubuntu /home/ubuntu/JKCommentCrawler/JKCommentCrawler.sh cron_minutes 11 | 12 | # 現在時刻 13 | current_time=`date +"%Y/%m/%d %H:%M"` 14 | 15 | # 自身のスクリプトのフルパスを取得 16 | SCRIPT_PATH="$(readlink -f "$0")" 17 | 18 | # スクリプトが存在するディレクトリのパスを取得 19 | SCRIPT_DIR="$(dirname "$SCRIPT_PATH")" 20 | cd ${SCRIPT_DIR} 21 | 22 | # ログフォルダがなければ作成 23 | if [ ! -d ${SCRIPT_DIR}/log ]; then 24 | mkdir ${SCRIPT_DIR}/log 25 | fi 26 | 27 | # JKCommentCrawler を実行 28 | # Cron(5分ごと) 29 | if [[ $1 = 'cron_minutes' ]]; then 30 | 31 | # 今日分の JKCommentCrawler を実行 32 | echo 'JKCommentCrawler.sh (Cron minutes)' 33 | ${SCRIPT_DIR}/.venv/bin/python -m jkcommentcrawler all `date +"%Y/%m/%d"` --save-dataset-structure-json \ 34 | 1> ${SCRIPT_DIR}/log/minutes.log \ 35 | 2>> ${SCRIPT_DIR}/log/minutes.error.log 36 | 37 | # Cron(1日ごと) 38 | elif [[ $1 = 'cron_daily' ]]; then 39 | 40 | # 前日分の JKCommentCrawler を実行(取りこぼし防止) 41 | ## --force パラメータを付けると、以前取得したログよりサイズが小さくても強制的に保存する 42 | ## ニコ生の実況番組の放送終了後にスパム判定されたコメント (特に AA) がごっそり削除されることがあり、 43 | ## それによって新しいログが保存されなくなる事態を避ける 44 | echo 'JKCommentCrawler.sh (Cron daily)' 45 | ${SCRIPT_DIR}/.venv/bin/python -m jkcommentcrawler all `date -d '-1 day' +"%Y/%m/%d"` --save-dataset-structure-json --force \ 46 | 1> ${SCRIPT_DIR}/log/daily.log \ 47 | 2>> ${SCRIPT_DIR}/log/daily.error.log 48 | 49 | # 通常実行 50 | else 51 | echo 'JKCommentCrawler.sh (Nornal)' 52 | ${SCRIPT_DIR}/.venv/bin/python -m jkcommentcrawler all `date +"%Y/%m/%d"` --save-dataset-structure-json 53 | fi 54 | 55 | # Hugging Face (KakologArchives) に commit & push する 56 | # デフォルトでは前回のコミットに上書き追記し、コミット数を削減する 57 | # 前回のコミットメッセージが Add kakolog until 20xx/xx/xx 00:00 or 12:00 の時だけ、前回コミットを上書きせずそのままコミットする 58 | # こうすることで、1日に2回は必ずコミットされるようになる 59 | cd ${SCRIPT_DIR}/kakolog/ 60 | last_commit_message=$(git log -1 --pretty=%B) 61 | git add . 62 | if [[ $last_commit_message == *"00:00" ]] || [[ $last_commit_message == *"12:00" ]]; then 63 | git commit -m "Add kakolog until ${current_time}" 64 | else 65 | git commit -m "Add kakolog until ${current_time}" --amend --date=now 66 | fi 67 | git push -f 68 | 69 | # 不要な LFS オブジェクトを削除する 70 | ## --verify-remote で、リモートに存在することを確認してから削除する 71 | git lfs prune --recent --verify-remote 72 | -------------------------------------------------------------------------------- /License.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020-2025 tsukumi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | 2 | # JKCommentCrawler 3 | 4 | ![Screenshot](https://github.com/user-attachments/assets/6cbf9bb8-dbd8-473a-a25a-e5f78264bcf4) 5 | 6 | **ニコニコ生放送統合後の [ニコニコ実況](https://jk.nicovideo.jp/)・[NX-Jikkyo](https://nx-jikkyo.tsukumijima.net/) の過去ログを日付ごとに一括で収集・保存するツールです。** 7 | かつて [Nekopanda](https://github.com/nekopanda) 氏が公開されていた、旧ニコニコ実況の過去ログデータ一式と互換性のあるファイル・フォルダ構造で保存します。 8 | 9 | > [!WARNING] 10 | > **JKCommentCrawler v2 では、すべてのコードが全面的に書き直されています。** 11 | > コマンドライン引数や ini ファイルの構成には互換性がありますが、コンソール表示や実装仕様は大幅に変更されています。 12 | > **メッセージサーバーの仕様が変更された 2024/08/05 復旧以降のニコニコ生放送上の [ニコニコ実況](https://jk.nicovideo.jp/) に加え、ニコニコ実況が復旧するまでの避難所(現在は主に [ニコニコミュニティの廃止](https://blog.nicovideo.jp/niconews/225559.html) により存続困難になった非公式チャンネル向けの実況用代替コメントサーバー)である [NX-Jikkyo](https://nx-jikkyo.tsukumijima.net/) の両方のコメントを収集できます。** 13 | 14 | > [!TIP] 15 | > JKCommentCrawler v2 では、ニコニコ生放送の新メッセージサーバーからのコメント取得処理に、今回新規開発した [NDGRClient](https://github.com/tsukumijima/NDGRClient) を使用しています。 16 | > ニコニコ実況以外のニコニコ生放送番組のコメントを取得する用途でも使えますので、よろしければご活用ください。 17 | 18 | > [!TIP] 19 | > **JKCommentCrawler で5分おきに収集したニコニコ実況・NX-Jikkyo の過去ログは、[Hugging Face (KakologArchives)](https://huggingface.co/datasets/KakologArchives/KakologArchives) に公開しています!** 20 | > **また Hugging Face に保存された過去ログデータセットをデータソースとして、[ニコニコ実況 過去ログ API](https://jikkyo.tsukumijima.net/) を運営中です。** 21 | > 22 | > JKCommentCrawler 自体、元々 [ニコニコ実況 過去ログ API](https://jikkyo.tsukumijima.net/) で配信する過去ログコメントを収集する目的で開発されたものです。 23 | > 通常は [ニコニコ実況 過去ログ API](https://jikkyo.tsukumijima.net/) からのコメント取得をおすすめします。実況チャンネル・開始日時・終了日時で膨大なコメントを絞り込み、XML または JSON 形式で取得できます。 24 | 25 | ## 対応実況チャンネル 26 | 27 | > [!NOTE] 28 | > `jk` から始まる実況チャンネル ID は、2020/12/14 までの旧ニコニコ実況で使われていた ID 表記を概ね継承しています。 29 | > 一方 `jk260` や `jk333` など、旧ニコニコ実況では存在しなかったものの、各クライアントでの慣行を継承して「`jk` + チャンネル番号」形式で振られている実況チャンネル ID もあります。 30 | 31 | ### 地上波 32 | 33 | - `jk1` : NHK総合 - [[ニコニコ実況]](https://live.nicovideo.jp/watch/ch2646436) [[NX-Jikkyo]](https://nx-jikkyo.tsukumijima.net/watch/jk1) 34 | - `jk2` : NHK Eテレ - [[ニコニコ実況]](https://live.nicovideo.jp/watch/ch2646437) [[NX-Jikkyo]](https://nx-jikkyo.tsukumijima.net/watch/jk2) 35 | - `jk4` : 日本テレビ - [[ニコニコ実況]](https://live.nicovideo.jp/watch/ch2646438) [[NX-Jikkyo]](https://nx-jikkyo.tsukumijima.net/watch/jk4) 36 | - `jk5` : テレビ朝日 - [[ニコニコ実況]](https://live.nicovideo.jp/watch/ch2646439) [[NX-Jikkyo]](https://nx-jikkyo.tsukumijima.net/watch/jk5) 37 | - `jk6` : TBSテレビ - [[ニコニコ実況]](https://live.nicovideo.jp/watch/ch2646440) [[NX-Jikkyo]](https://nx-jikkyo.tsukumijima.net/watch/jk6) 38 | - `jk7` : テレビ東京 - [[ニコニコ実況]](https://live.nicovideo.jp/watch/ch2646441) [[NX-Jikkyo]](https://nx-jikkyo.tsukumijima.net/watch/jk7) 39 | - `jk8` : フジテレビ - [[ニコニコ実況]](https://live.nicovideo.jp/watch/ch2646442) [[NX-Jikkyo]](https://nx-jikkyo.tsukumijima.net/watch/jk8) 40 | - `jk9` : TOKYO MX - [[ニコニコ実況]](https://live.nicovideo.jp/watch/ch2646485) [[NX-Jikkyo]](https://nx-jikkyo.tsukumijima.net/watch/jk9) 41 | - `jk10` : テレ玉 - [[NX-Jikkyo]](https://nx-jikkyo.tsukumijima.net/watch/jk10) 42 | - `jk11` : tvk - [[NX-Jikkyo]](https://nx-jikkyo.tsukumijima.net/watch/jk11) 43 | - `jk12` : チバテレビ - [[NX-Jikkyo]](https://nx-jikkyo.tsukumijima.net/watch/jk12) 44 | - `jk13` : サンテレビ - [[NX-Jikkyo]](https://nx-jikkyo.tsukumijima.net/watch/jk13) 45 | - `jk14` : KBS京都 - [[NX-Jikkyo]](https://nx-jikkyo.tsukumijima.net/watch/jk14) 46 | 47 | ### BS・CS 48 | 49 | - `jk101` : NHK BS - [[ニコニコ実況]](https://live.nicovideo.jp/watch/ch2647992) [[NX-Jikkyo]](https://nx-jikkyo.tsukumijima.net/watch/jk101) 50 | - `jk103` : NHK BSプレミアム - [[NX-Jikkyo]](https://nx-jikkyo.tsukumijima.net/watch/jk103) 51 | - `jk141` : BS日テレ - [[NX-Jikkyo]](https://nx-jikkyo.tsukumijima.net/watch/jk141) 52 | - `jk151` : BS朝日 - [[NX-Jikkyo]](https://nx-jikkyo.tsukumijima.net/watch/jk151) 53 | - `jk161` : BS-TBS - [[NX-Jikkyo]](https://nx-jikkyo.tsukumijima.net/watch/jk161) 54 | - `jk171` : BSテレ東 - [[NX-Jikkyo]](https://nx-jikkyo.tsukumijima.net/watch/jk171) 55 | - `jk181` : BSフジ - [[NX-Jikkyo]](https://nx-jikkyo.tsukumijima.net/watch/jk181) 56 | - `jk191` : WOWOW PRIME - [[NX-Jikkyo]](https://nx-jikkyo.tsukumijima.net/watch/jk191) 57 | - `jk192` : WOWOW LIVE - [[NX-Jikkyo]](https://nx-jikkyo.tsukumijima.net/watch/jk192) 58 | - `jk193` : WOWOW CINEMA - [[NX-Jikkyo]](https://nx-jikkyo.tsukumijima.net/watch/jk193) 59 | - `jk200` : BS10 - [[NX-Jikkyo]](https://nx-jikkyo.tsukumijima.net/watch/jk200) 60 | - `jk201` : BS10スターチャンネル - [[NX-Jikkyo]](https://nx-jikkyo.tsukumijima.net/watch/jk201) 61 | - `jk211` : BS11 - [[ニコニコ実況]](https://live.nicovideo.jp/watch/ch2646846) [[NX-Jikkyo]](https://nx-jikkyo.tsukumijima.net/watch/jk211) 62 | - `jk222` : BS12 - [[NX-Jikkyo]](https://nx-jikkyo.tsukumijima.net/watch/jk222) 63 | - `jk236` : BSアニマックス - [[NX-Jikkyo]](https://nx-jikkyo.tsukumijima.net/watch/jk236) 64 | - `jk252` : WOWOW PLUS - [[NX-Jikkyo]](https://nx-jikkyo.tsukumijima.net/watch/jk252) 65 | - `jk260` : BS松竹東急 - [[NX-Jikkyo]](https://nx-jikkyo.tsukumijima.net/watch/jk260) 66 | - `jk263` : BSJapanext - [[NX-Jikkyo]](https://nx-jikkyo.tsukumijima.net/watch/jk263) 67 | - `jk265` : BSよしもと - [[NX-Jikkyo]](https://nx-jikkyo.tsukumijima.net/watch/jk265) 68 | - `jk333` : AT-X - [[NX-Jikkyo]](https://nx-jikkyo.tsukumijima.net/watch/jk333) 69 | 70 | ## いわゆる「コミュニティ実況」と NX-Jikkyo の関係について 71 | 72 | **2020/12/15 のニコニコ実況のニコニコ生放送への統合により、tvk などの地上波独立局と、BS11 を除く大半の BS チャンネルの実況チャンネルが「公式には」廃止されてしまいました。**(スポーツ実況で需要が多いからか、2021 年に NHK BS1 のみ復活しています。) 73 | 統合後のニコニコ実況は「チャンネル生放送」扱いのため、24時間365日真っ暗な映像が無駄に 1080p で放送され続けることによる多大な配信コストが、実況チャンネル数縮小の理由だと推測されています。 74 | しかし、**特に TOKYO MX が映らない地方在住の BS アニメ実況民などから「廃止された実況チャンネルでも実況を続けたい」という要望が根強く出ていました。** 75 | 76 | > [!NOTE] 77 | > 「ニコニコ実況のためにチャンネル生放送に映像・音声なし(せめて音声のみ)で配信できる仕組みを作れば良かったのでは?」という疑問は残りますが、不採算事業であるニコニコ実況のためだけに、複雑怪奇なニコニコ生放送のシステムを改修するコストと手間は掛けられないという経営判断なのでしょう。 78 | 79 | 一方、かつて存在したニコニコミュニティでは「不特定多数のユーザーが、コミュニティに紐づいて生放送番組を放送できる」「コミュニティ内で同時に放送できるのは一人だけ」という、ニコニコ実況的なものを不特定多数のユーザーで協力しながら実現するのに大変都合の良い仕組みが用意されていました。 80 | さらに旧来のニコニコ実況ページに代わって設置された [ニコニコ実況の新ポータル](https://jk.nicovideo.jp) は「ニコニコ実況」タグがつけられたユーザー生放送番組が、公式実況チャンネルへのリンクの下にランダム表示される仕様となっていました。 81 | 82 | **そうした状況を踏まえ、有志らの尽力で公式では廃止された各チャンネルごとに [ニコニコミュニティ](https://web.archive.org/web/20240522154158/https://com.nicovideo.jp/community/co5117214) が開設され、プレミアム会員の有志が「ユーザー生放送」で実況用番組の放送枠を取ることによる、いわゆる「コミュニティ実況」の慣習が自然発生的に成立しました。** 83 | 84 | この「コミュニティ実況」はプレミアム会員を辞めて継続できなくなる人が現れるなど何度も存続の危機に陥りつつも、私が開発した [JKLiveReserver](https://github.com/tsukumijima/JKLiveReserver) で自動枠取りできるようになった効果もあり、綱渡りながら長年維持されてきました。 85 | 86 | 徐々に TVTest ([NicoJK](https://github.com/xtne6f/NicoJK)) などのサードパーティー実況クライアントでも「コミュニティ実況」への対応が進み、事実上のデファクトスタンダートとなっていった経緯があります。 87 | 88 | > [!NOTE] 89 | > 技術的にも「コミュニティ実況」方式であれば、サードパーティークライアント側で各チャンネルごとの実況コミュニティの ID をハードコードし、当該コミュニティで放送中の番組を問答無用で「実況用番組枠」とみなせば、各 BS チャンネルに対応する実況用番組のコメントを流せる(=表面上は公式チャンネルと変わらない UI でコメントを表示・投稿できる)メリットがありました。 90 | 91 | > [!NOTE] 92 | > ニコニコ生放送では、実際には OBS などから配信を行わなくても(真っ黒画面ではありますが)放送できる仕様です。他方、一般会員の最大配信時間は 30 分までに制限されています。 93 | > さらに YouTube Live と異なり、一つのアカウントで同時に配信できる番組は一つまでの制限があります。 94 | > このため廃止された全実況チャンネルで「コミュニティ実況」を行うには、チャンネル数分のプレミアム会員が連携をとる必要がありました。 95 | 96 | しかし、**2024/06/08 のニコニコへの大規模なサイバー攻撃により、ニコニコ生放送/ニコニコ実況にも2ヶ月近くにわたりアクセスできなくなりました。** 97 | 何ヶ月も実況できない時間が続くことで実況過去ログに大きな穴があき、ひいては「ニコニコ実況」という文化自体が壊滅することを危惧した私は、**急遽 [NX-Jikkyo](https://nx-jikkyo.tsukumijima.net/) を開発し、運営を開始しました。** 98 | 99 | > [!NOTE] 100 | **NX-Jikkyo は、ニコニコ生放送の WebSocket API 仕様とのほぼ完全な互換性を持つ、実況用代替コメントサーバーです。** 101 | ニコニコ生放送向けのコメント受信処理を置き換えるだけで対応できる設計にしたことが奏功し、すぐに各サードパーティークライアントでも対応していただきました。 102 | 103 | その後ニコニコ生放送は 2024/08/05 に復旧しましたが、**今まで「コミュニティ実況」で利用されていたニコニコミュニティは復旧が困難として、そのままサービスを終了してしまいました。** 104 | 105 | - 元から近年プレミアム会員費の値上げなどで「コミュニティ実況」が存続の危機に瀕していたこと 106 | - 「ニコニコ実況避難所」として jkcommentviewer をはじめとした各種のサードパーティークライアントで対応済みなこと 107 | - ニコニコ生放送復旧までの2ヶ月間実際に使ってみて概ね問題なく、むしろ使いやすいと評する人が多かったこと 108 | - 過去ログデータ取得の事実上のデファクトスタンダードである [ニコニコ実況 過去ログ API](https://jikkyo.tsukumijima.net/) に過去ログが反映され、この API に依存している各クライアントでもニコニコ実況に投稿されたコメント同様に表示できること 109 | - ニコニコ実況公式で用意されている実況チャンネルのコメントもリアルタイム表示できること 110 | 111 | 上記条件が積み重なった結果、**BS アニメ実況民らによる暗黙の了解として「公式にない民放 BS などの実況チャンネルは引き続き NX-Jikkyo で実況する」流れになりつつあります。** 112 | 113 | **JKCommentCrawler v2 ではこうした流れを踏まえ、本家ニコニコ実況と NX-Jikkyo 両方に投稿されたコメントを時系列で統合して保存するようになりました。** 114 | [ニコニコ実況 過去ログ API](https://jikkyo.tsukumijima.net/) の API レスポンスには引き続き互換性を持たせており、既存クライアントをなるべく改修することなく、ニコニコ実況・NX-Jikkyo 双方のコメントをシームレスに一緒に楽しめる形を目指していきます。 115 | 116 | > [!NOTE] 117 | > NX-Jikkyo では、本家ニコニコ実況に投稿されたコメントをリアルタイムにマージし、随時配信しています。 118 | > しかしリアルタイムマージの性質上完璧なマージは難しく、サーバー再起動などによりコメントを取りこぼしてしまうことも稀にあります。 119 | > **オリジナルに近い情報量のコメントデータを、可能な限り収集して後世に残していくために、改めて JKCommentCrawler にて双方の過去ログコメントを収集し、時系列に並べ替え統合した上で保存・反映しています。** 120 | 121 | ## 注意 122 | 123 | - **JKCommentCrawler は、3 週間で消えてしまう新ニコニコ実況の過去ログを、一括で自動収集するために開発されました。** 124 | - 可能な限りすべての過去ログコメントを継続的に収集するユースケースを前提に設計しており、一部時間の過去ログだけを取得する用途は想定していません。 125 | - 一部時間の過去ログを抜き出して取得したい方は、代わりに [ニコニコ実況 過去ログ API](https://jikkyo.tsukumijima.net/) を利用してください。 126 | - [JKCommentGetter](https://github.com/ACUVE/JKCommentGetter) のように、詳細に時刻を指定して保存するなどの高度な機能はありません。 127 | - **JKCommentCrawler を利用するには、基本的にニコニコのプレミアムアカウントが必要です。** 128 | - プレミアムアカウントがなくても、事前にタイムシフトを予約しておいた番組であれば過去ログ収集が可能です。 129 | - ただし、一般会員ではタイムシフトの同時予約数が 10 に制限されています。そのため(複垢でも使わない限りは)全てのチャンネルの過去ログを収集することはできず、利用に堪えないと思います。 130 | - **Nekopanda 氏の過去ログデータ一式と互換性を持たせるため、コメントは番組(スレッド)ごとではなく、日付ごとに保存されます。** 131 | - 同じ日に放送された実況用番組が複数回あるケースでは、一旦同じ日に放送された実況用番組のコメントをすべてダウンロードした上で、指定された日付以外に投稿されたコメントを除外し、日付ごとのファイルに保存します。 132 | - **JKCommentCrawler.ini に記載されたニコニコアカウントのログイン情報を変更したときは、cookies.json を一旦削除してから再度 JKCommentCrawler を実行してください。** 133 | - cookies.json は Cookie を保存しているファイルです。このファイルが配置された状態では、ログインセッションが切れるまで再ログインを行いません。 134 | 135 | ## インストール 136 | 137 | 事前に Python 3.11 がインストールされている必要があります。 138 | 139 | ```bash 140 | git clone https://github.com/tsukumijima/JKCommentCrawler.git 141 | cd JKCommentCrawler 142 | pip install poetry 143 | poetry install 144 | ``` 145 | 146 | JKCommentCrawler を使う前には設定が必要です。 147 | まずは `JKCommentCrawler.example.ini` を `JKCommentCrawler.ini` にコピーしましょう。 148 | 149 | その後、`JKCommentCrawler.ini` を編集します。 150 | 編集箇所は「過去ログを保存するフォルダ」「ニコニコアカウントのメールアドレス」「ニコニコアカウントのパスワード」の 3 つです。 151 | 152 | ```ini 153 | [Default] 154 | 155 | # ==================== 環境設定 ==================== 156 | # JKCommentCrawler.ini にコピーした上で、各自の環境に合わせて編集してください 157 | # メールアドレス・パスワードを変更した時は、cookies.json ファイルを削除してから実行してください 158 | 159 | # 過去ログを保存するフォルダ 160 | jkcomment_folder = ./kakolog/ 161 | 162 | # ニコニコにログインするメールアドレス 163 | nicologin_mail = example@example.com 164 | 165 | # ニコニコにログインするパスワード 166 | nicologin_password = example_password 167 | ``` 168 | 169 | 過去ログの保存先フォルダは標準では `./kakolog/` になっていますが、これだと JKCommentCrawler を実行したカレントディレクトリによってパスが変わってしまいます。 170 | 念のため、できるだけ絶対パスで指定することを推奨します。 171 | 172 | また、ニコニコアカウントのメールアドレス / パスワードも指定します。前述の通り、基本的にプレミアムアカウントのログイン情報が必要です。 173 | 174 | > [!IMPORTANT] 175 | > ニコニコアカウントの 2 要素認証 (2FA) には対応していません。 176 | > お手元のニコニコアカウントで 2FA が有効な場合は、一旦解除してから再度 JKCommentCrawler を実行してください。 177 | 178 | これで設定は完了です。 179 | 180 | ## 使い方 181 | 182 | ```bash 183 | Usage: python -m jkcommentcrawler [OPTIONS] CHANNEL_ID DATE 184 | 185 | JKCommentCrawler: Nico Nico Jikkyo Comment Crawler 186 | 187 | ╭─ Arguments ──────────────────────────────────────────────────────────────────────────────────────╮ 188 | │ * channel_id TEXT コメントを収集する実況チャンネル。(ex: jk211) all │ 189 | │ を指定すると全チャンネルのコメントを収集する。 │ 190 | │ [default: None] │ 191 | │ [required] │ 192 | │ * date TEXT コメントを収集する日付。(ex: 2024/08/05) [default: None] [required] │ 193 | ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ 194 | ╭─ Options ────────────────────────────────────────────────────────────────────────────────────────╮ 195 | │ --save-dataset-structure-json 過去ログデータのフォルダ/ファイル構造を示す JSON │ 196 | │ ファイルを出力する。 │ 197 | │ --force -f 以前取得したログの方が文字数が多い場合でも上書きする。 │ 198 | │ --verbose -v 詳細なログを表示する。 │ 199 | │ --version バージョン情報を表示する。 │ 200 | │ --install-completion Install completion for the current shell. │ 201 | │ --show-completion Show completion for the current shell, to copy it or │ 202 | │ customize the installation. │ 203 | │ --help Show this message and exit. │ 204 | ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ 205 | ``` 206 | 207 | ```bash 208 | poetry run python -m jkcommentcrawler jk1 2024/08/05 209 | ``` 210 | 211 | `jk1` には実況チャンネル ID (ex: BS11 なら `jk211`) が、`2024/08/05` には収集対象の過去ログが投稿された日付が入ります。 212 | 213 | 各実況チャンネルのコメントは、日付ごとに `./kakolog/jk1/2024/20240805.nicojk` に保存されます。 214 | 215 | > [!NOTE] 216 | > .nicojk という拡張子ではありますが、実際はヘッダーなしの XML ファイルです。 217 | > Nekopanda 氏がかつて公開されていた過去ログデータ一式の拡張子が .nicojk だったため、それに合わせています。 218 | 219 | > [!IMPORTANT] 220 | > 従来の JKCommentCrawler v1 では、`/emotion` や `/nicoad` などの運営コメントも保存されていました。 221 | > しかしメッセージサーバーの仕様が変更された 2024/08/05 復旧以降のニコニコ生放送ではコメント配信形式が Protocol Buffers で構造化された関係で、「運営コメント」という概念自体が廃止されています。 222 | > その関係で、JKCommentCrawler v2 では運営コメントは保存されなくなっています。 223 | 224 | ![Screenshot](https://github.com/user-attachments/assets/5dbae17e-4646-4f80-82ca-c3545d0bd46c) 225 | 226 | ```bash 227 | poetry run python -m jkcommentcrawler all 2024/08/05 228 | ``` 229 | 230 | 実況チャンネル ID を指定する代わりに、`all` を指定することもできます。 231 | **`all` を指定すると、指定された日付の全実況チャンネルの過去ログを一括で収集します。** 232 | 233 | > [!TIP] 234 | > この例では、 2024/08/05 内に放送された(開始時間・終了時間の片方だけ 2024/08/05 に掛かっている場合も含む)`jk1` ~ `jk333` までの全実況チャンネルの過去ログを収集し、そのうち 2024/08/05 中のコメントのみを抽出して各実況チャンネルごとに保存します。 235 | 236 | 大方不具合は直したつもりですが、もし不具合を見つけられた場合は [Issues](https://github.com/tsukumijima/JKCommentCrawler/issues) までお願いします。 237 | 238 | ## License 239 | 240 | [MIT License](License.txt) 241 | -------------------------------------------------------------------------------- /jkcommentcrawler/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | __version__ = '2.0.6' 3 | 4 | from jkcommentcrawler.nx_client import * 5 | -------------------------------------------------------------------------------- /jkcommentcrawler/__main__.py: -------------------------------------------------------------------------------- 1 | 2 | import asyncio 3 | import configparser 4 | import json 5 | import traceback 6 | import typer 7 | from datetime import datetime 8 | from ndgr_client import NDGRClient, XMLCompatibleComment 9 | from ndgr_client.utils import AsyncTyper 10 | from pathlib import Path 11 | from rich import print 12 | from rich.rule import Rule 13 | from rich.style import Style 14 | 15 | from jkcommentcrawler import NXClient, __version__ 16 | 17 | 18 | app = AsyncTyper() 19 | 20 | def version(value: bool): 21 | if value is True: 22 | typer.echo(f'JKCommentCrawler version {__version__}') 23 | raise typer.Exit() 24 | 25 | @app.command(help='JKCommentCrawler: Nico Nico Jikkyo Comment Crawler') 26 | async def main( 27 | channel_id: str = typer.Argument(help='コメントを収集する実況チャンネル。(ex: jk211) all を指定すると全チャンネルのコメントを収集する。'), 28 | date: str = typer.Argument(help='コメントを収集する日付。(ex: 2024/08/05)'), 29 | save_dataset_structure_json: bool = typer.Option(False, '--save-dataset-structure-json', help='過去ログデータのフォルダ/ファイル構造を示す JSON ファイルを出力する。'), 30 | force: bool = typer.Option(False, '-f', '--force', help='以前取得したログの方が文字数が多い場合でも上書きする。'), 31 | verbose: bool = typer.Option(False, '-v', '--verbose', help='詳細なログを表示する。'), 32 | version: bool = typer.Option(None, '--version', callback=version, is_eager=True, help='バージョン情報を表示する。'), 33 | ): 34 | print(Rule(characters='=', style=Style(color='#E33157'))) 35 | target_date = datetime.strptime(date, '%Y/%m/%d').date() 36 | if target_date > datetime.now().date(): 37 | raise Exception('Target date is in the future.') 38 | 39 | # 設定読み込み 40 | config_ini = Path(__file__).parent.parent / 'JKCommentCrawler.ini' 41 | if not config_ini.exists(): 42 | raise Exception('JKCommentCrawler.ini not found. Copy from JKCommentCrawler.example.ini and edit it as needed.') 43 | config = configparser.ConfigParser() 44 | config.read(config_ini, encoding='utf-8') 45 | kakolog_dir: Path = Path(config.get('Default', 'jkcomment_folder').rstrip('/')).resolve() 46 | niconico_mail: str = config.get('Default', 'nicologin_mail') 47 | niconico_password: str = config.get('Default', 'nicologin_password') 48 | 49 | # jikkyo_id に 'all' が指定された場合は全てのチャンネルをダウンロード 50 | if channel_id == 'all': 51 | jikkyo_channel_ids = NXClient.JIKKYO_CHANNEL_ID_LIST.copy() 52 | else: 53 | jikkyo_channel_ids = [channel_id] 54 | 55 | # 過去ログ収集対象のニコニコ実況チャンネルごとに 56 | comment_counts: dict[str, int] = {} 57 | for jikkyo_channel_id in jikkyo_channel_ids: 58 | 59 | # 3回までリトライ 60 | for retry_count in range(3): 61 | try: 62 | print(f'[{datetime.now().strftime("%Y/%m/%d %H:%M:%S.%f")}]\\[{jikkyo_channel_id}] ' 63 | f'Retrieve comments broadcast during {target_date.strftime("%Y/%m/%d")}.') 64 | 65 | # 指定された日付に一部でも放送されたニコニコ生放送番組を取得 66 | ## NX-Jikkyo にはあるが本家ニコニコ実況に存在しない実況チャンネル (ex: jk141) では実行しない 67 | if jikkyo_channel_id not in NDGRClient.JIKKYO_CHANNEL_ID_MAP: 68 | nicolive_program_ids = [] 69 | print(f'Skipping retrieval of Nicolive comments as the channel {jikkyo_channel_id} does not exist on Nicolive.') 70 | else: 71 | nicolive_program_ids = await NDGRClient.getProgramIDsOnDate(jikkyo_channel_id, target_date) 72 | print(f'Retrieving Nicolive comments from {len(nicolive_program_ids)} programs.' + 73 | (f' ({", ".join(nicolive_program_ids)})' if len(nicolive_program_ids) > 0 else '')) 74 | 75 | # 指定された日付に一部でも放送された NX-Jikkyo スレッドを取得 76 | nx_thread_ids = await NXClient.getThreadIDsOnDate(jikkyo_channel_id, target_date) 77 | print(f'Retrieving NX-Jikkyo comments from {len(nx_thread_ids)} threads.' + 78 | (f' ({", ".join(map(str, nx_thread_ids))})' if len(nx_thread_ids) > 0 else '')) 79 | print(Rule(characters='-', style=Style(color='#E33157'))) 80 | 81 | # ダウンロードしたコメントを格納するリスト 82 | comments: list[XMLCompatibleComment] = [] 83 | 84 | # ニコニコ生放送番組 ID ごとに 85 | for nicolive_program_id in nicolive_program_ids: 86 | 87 | # NDGRClient を初期化 88 | ndgr_client = NDGRClient(nicolive_program_id, verbose=verbose, console_output=True) 89 | 90 | # ニコニコアカウントにログイン (タイムシフト再生に必要) 91 | ## すでにログイン済みの Cookie が cookies.json にあれば Cookie を再利用し、ない場合は新規ログインを行う 92 | cookies_json = Path(__file__).parent.parent / 'cookies.json' 93 | if cookies_json.exists(): 94 | with open(cookies_json, 'r', encoding='utf-8') as f: 95 | cookies_dict = json.load(f) 96 | cookies_dict = await ndgr_client.login(cookies=cookies_dict) 97 | # もし None が返る場合はログインセッションが切れた可能性が高いので、メールアドレスとパスワードを指定して再ログインを実行 98 | if cookies_dict is None: 99 | cookies_dict = await ndgr_client.login(mail=niconico_mail, password=niconico_password) 100 | if cookies_dict is None: 101 | raise Exception('Failed to login to niconico.') 102 | with open(cookies_json, 'w', encoding='utf-8') as f: 103 | json.dump(cookies_dict, f) 104 | else: 105 | # cookies.json が存在しない場合は新規ログインを実行 106 | cookies_dict = await ndgr_client.login(mail=niconico_mail, password=niconico_password) 107 | if cookies_dict is None: 108 | raise Exception('Failed to login to niconico.') 109 | with open(cookies_json, 'w', encoding='utf-8') as f: 110 | json.dump(cookies_dict, f) 111 | 112 | # コメントをダウンロードしてリストに追加 113 | comments.extend([ 114 | NDGRClient.convertToXMLCompatibleComment(comment) 115 | for comment in await ndgr_client.downloadBackwardComments() 116 | ]) 117 | 118 | # NX-Jikkyo スレッドごとに 119 | for nx_thread_id in nx_thread_ids: 120 | 121 | # NXClient を初期化 122 | nx_client = NXClient(nx_thread_id, verbose=verbose, console_output=True) 123 | 124 | # コメントをダウンロードしてリストに追加 125 | comments.extend(await nx_client.downloadBackwardComments()) 126 | 127 | # 指定された日付以外に投稿されたコメントを除外 128 | print(f'Total comments for {jikkyo_channel_id}: {len(comments)}') 129 | comments = [comment for comment in comments if datetime.fromtimestamp(comment.date_with_usec).date() == target_date] 130 | print(f'Excluding comments posted on dates other than {target_date.strftime("%Y/%m/%d")} ...') 131 | print(f'Final comments for {jikkyo_channel_id}: {len(comments)}') 132 | comment_counts[jikkyo_channel_id] = len(comments) 133 | 134 | # コメント投稿日時昇順で並び替え 135 | ## ニコニコ実況と NX-Jikkyo のコメントを時系列でマージするためにこの処理が必要 136 | comments.sort(key=lambda comment: comment.date_with_usec) 137 | 138 | # {kakolog_dir}/{jikkyo_channel_id}/{date.year}/{date.strftime('%Y%m%d')}.nicojk に保存 139 | ## 取得できたコメントが1つもない場合は実行しない 140 | if len(comments) > 0: 141 | output_dir = kakolog_dir / jikkyo_channel_id / str(target_date.year) 142 | output_dir.mkdir(parents=True, exist_ok=True) 143 | output_file = output_dir / f'{target_date.strftime("%Y%m%d")}.nicojk' 144 | 145 | # コメントリストを XML 文字列に変換 146 | xml_content = NDGRClient.convertToXMLString(comments) 147 | 148 | # 既存の XML ファイルがあれば文字数を取得 149 | if output_file.exists(): 150 | with open(output_file, 'r', encoding='utf-8') as f: 151 | existing_length = len(f.read()) 152 | else: 153 | existing_length = 0 154 | 155 | # コメントが1件も取得できていない場合は過去ログを保存しない 156 | if len(xml_content) == 0: 157 | print(f"Skipping log save for {target_date.strftime('%Y/%m/%d')} as there are 0 comments.") 158 | 159 | # 既存のファイルの方が文字数が多い場合は過去ログを保存しない 160 | elif existing_length > len(xml_content) and not force: 161 | print(f'Skipping log save as the previously retrieved log has more characters. ' 162 | f'(Previous: {existing_length} chars, Current: {len(xml_content)} chars)') 163 | 164 | # 過去ログを保存 165 | else: 166 | # 既存のファイルの方が文字数が多いが、--force が指定されている場合は上書きする 167 | if existing_length > len(xml_content) and force: 168 | print(f'The previously retrieved log has more characters, but overwriting as --force is specified. ' 169 | f'(Previous: {existing_length} chars, Current: {len(xml_content)} chars)') 170 | # ファイルに書き込む 171 | with open(output_file, 'w', encoding='utf-8') as f: 172 | f.write(xml_content) 173 | print(f"Log saved to {output_file}.") 174 | 175 | # コメントが1件も取得できていない場合はスキップ 176 | elif len(comments) == 0: 177 | print(f'No comments found for {jikkyo_channel_id} on {target_date.strftime("%Y/%m/%d")}. Skipping ...') 178 | print(Rule(characters='=', style=Style(color='#E33157'))) 179 | 180 | # 正常にダウンロードできたらループを抜ける 181 | break 182 | 183 | except Exception: 184 | if retry_count < 3: 185 | # エラー発生時は3回までリトライ 186 | print(f'[{datetime.now().strftime("%Y/%m/%d %H:%M:%S.%f")}]\\[{jikkyo_channel_id}] ' 187 | f'Unexpected error occurred. Retrying ({retry_count + 1}/3) after 3 seconds ...') 188 | print(traceback.format_exc()) 189 | await asyncio.sleep(3) 190 | else: 191 | # リトライ失敗、このチャンネルはスキップして次の実況チャンネルへ 192 | print(f'[{datetime.now().strftime("%Y/%m/%d %H:%M:%S.%f")}]\\[{jikkyo_channel_id}] ' 193 | f'Unexpected error occurred. Retrying failed. Skipping ...') 194 | print(traceback.format_exc()) 195 | print(Rule(characters='=', style=Style(color='#E33157'))) 196 | 197 | # 全チャンネルをダウンロードしたときは、各チャンネルごとの合計コメント数を表示 198 | if channel_id == 'all': 199 | print('Download completed for all channels.') 200 | for jikkyo_channel_id, count in comment_counts.items(): 201 | print(f'{jikkyo_channel_id:>5}: {count:>5} comments') 202 | print(Rule(characters='=', style=Style(color='#E33157'))) 203 | 204 | # --save-dataset-structure-json が指定されているときは、データセットの構造を JSON ファイルに保存 205 | if save_dataset_structure_json is True: 206 | def get_directory_contents(directory_path: Path, nest: bool = False) -> dict[str, dict[str, dict[str, None]]]: 207 | if not directory_path.exists(): 208 | raise FileNotFoundError(f'Directory "{directory_path}" does not exist.') 209 | data = {} 210 | for item in sorted(directory_path.iterdir()): 211 | if item.is_dir() and (item.name.startswith('jk') or nest is True): 212 | data[item.name] = get_directory_contents(item, nest=True) 213 | elif item.is_file() and nest is True: 214 | data[item.name] = None 215 | return data 216 | 217 | dataset_structure = get_directory_contents(kakolog_dir) 218 | with open(f'{kakolog_dir}/dataset_structure.json', 'w', encoding='utf-8') as f: 219 | json.dump(dataset_structure, f, ensure_ascii=False, indent=4) 220 | print(f'Dataset structure saved to {kakolog_dir}/dataset_structure.json.') 221 | print(Rule(characters='=', style=Style(color='#E33157'))) 222 | 223 | 224 | if __name__ == '__main__': 225 | app() 226 | -------------------------------------------------------------------------------- /jkcommentcrawler/nx_client.py: -------------------------------------------------------------------------------- 1 | 2 | import httpx 3 | from datetime import date, datetime 4 | from ndgr_client import XMLCompatibleComment 5 | from pathlib import Path 6 | from pydantic import BaseModel, TypeAdapter 7 | from rich import print 8 | from rich.rule import Rule 9 | from rich.style import Style 10 | from typing import Any, Literal 11 | 12 | from jkcommentcrawler import __version__ 13 | 14 | 15 | class NXClient: 16 | """ 17 | NX-Jikkyo メッセージサーバーのクライアント実装 18 | NX-Jikkyo の WebSocket API は 2024/06/08 以前のニコニコ生放送の API 仕様と互換性がある 19 | このクラスの設計は意図的に NDGRClient クラスに似せてある 20 | ref: https://github.com/tsukumijima/NX-Jikkyo 21 | ref: https://github.com/tsukumijima/NDGRClient 22 | """ 23 | 24 | # NX-Jikkyo 通信時の User-Agent 25 | USER_AGENT = f'JKCommentCrawler/{__version__}' 26 | 27 | # NX-Jikkyo で運用されているニコニコ実況チャンネル ID のリスト (2024/08/15 時点) 28 | JIKKYO_CHANNEL_ID_LIST: list[str] = [ 29 | 'jk1', 30 | 'jk2', 31 | 'jk4', 32 | 'jk5', 33 | 'jk6', 34 | 'jk7', 35 | 'jk8', 36 | 'jk9', 37 | 'jk10', 38 | 'jk11', 39 | 'jk12', 40 | 'jk13', 41 | 'jk14', 42 | 'jk101', 43 | 'jk103', 44 | 'jk141', 45 | 'jk151', 46 | 'jk161', 47 | 'jk171', 48 | 'jk181', 49 | 'jk191', 50 | 'jk192', 51 | 'jk193', 52 | 'jk200', 53 | 'jk201', 54 | 'jk211', 55 | 'jk222', 56 | 'jk236', 57 | 'jk252', 58 | 'jk260', 59 | 'jk263', 60 | 'jk265', 61 | 'jk333', 62 | ] 63 | 64 | 65 | def __init__(self, thread_id: int, verbose: bool = False, console_output: bool = False, log_path: Path | None = None) -> None: 66 | """ 67 | NXClient のコンストラクタ 68 | 69 | Args: 70 | thread_id (int): NX-Jikkyo のスレッド ID 71 | verbose (bool, default=False): 詳細な動作ログを出力するかどうか 72 | console_output (bool, default=False): 動作ログをコンソールに出力するかどうか 73 | log_path (Path | None, default=None): 動作ログをファイルに出力する場合のパス (show_log と併用可能) 74 | """ 75 | 76 | self.thread_id = thread_id 77 | self.verbose = verbose 78 | self.show_log = console_output 79 | self.log_path = log_path 80 | 81 | # httpx の非同期 HTTP クライアントのインスタンスを作成 82 | self.httpx_client = httpx.AsyncClient(headers={'User-Agent': self.USER_AGENT}, follow_redirects=True) 83 | 84 | 85 | @classmethod 86 | async def getThreadIDsOnDate(cls, jikkyo_channel_id: str, date: date) -> list[int]: 87 | """ 88 | 指定した日付に少なくとも一部が放送されている/放送された NX-Jikkyo スレッドの ID を取得する 89 | 90 | Args: 91 | jikkyo_channel_id (str): ニコニコ実況互換のチャンネル ID 92 | date (date): NX-Jikkyo のスレッド (通常毎日 04:00 ~ 翌日 04:00) を取得する日付 93 | 94 | Returns: 95 | list[int]: 指定した日付に少なくとも一部が放送されている/放送された NX-Jikkyo スレッドの ID のリスト (放送開始日時昇順) 96 | 97 | Raises: 98 | ValueError: ニコニコ実況互換のチャンネル ID が指定されていない場合 99 | httpx.HTTPStatusError: NX-Jikkyo API へのリクエストに失敗した場合 100 | """ 101 | 102 | if jikkyo_channel_id.startswith('jk') is False: 103 | raise ValueError(f'Invalid jikkyo_channel_id: {jikkyo_channel_id}') 104 | 105 | class ThreadInfo(BaseModel): 106 | id: int 107 | start_at: datetime 108 | end_at: datetime 109 | title: str 110 | description: str 111 | status: str 112 | 113 | # クラスメソッドから self.httpx_client にはアクセスできないため、新しい httpx.AsyncClient を作成している 114 | async with httpx.AsyncClient(headers={'User-Agent': cls.USER_AGENT}, follow_redirects=True) as client: 115 | 116 | # スレッド情報取得 API にリクエスト 117 | ## 実況チャンネル ID に紐づく過去全スレッドの情報を取得できる 118 | ## 割と重いのでタイムアウトを 30 秒まで余裕を持って設定している 119 | response = await client.get(f'https://nx-jikkyo.tsukumijima.net/api/v1/channels/{jikkyo_channel_id}/threads', timeout=30) 120 | response.raise_for_status() 121 | threads = TypeAdapter(list[ThreadInfo]).validate_json(response.content) 122 | 123 | # 指定された日付に放送されているスレッドをフィルタリングし、その ID をリストで返す 124 | threads = [ 125 | thread for thread in threads 126 | if thread.start_at.date() <= date <= thread.end_at.date() 127 | ] 128 | 129 | # ID を放送開始日時が早い順に並べ替えてから返す 130 | threads.sort(key=lambda x: x.start_at) 131 | return [thread.id for thread in threads] 132 | 133 | 134 | async def downloadBackwardComments(self, ignore_nicolive_comments: bool = True) -> list[XMLCompatibleComment]: 135 | """ 136 | NX-Jikkyo メッセージサーバーから過去に投稿されたコメントを遡ってダウンロードする 137 | 138 | Args: 139 | ignore_nicolive_comments (bool, default=True): ニコニコ実況に投稿され NX-Jikkyo にリアルタイムマージされたコメントを除外するかどうか 140 | 141 | Returns: 142 | list[XMLCompatibleComment]: 過去に投稿されたコメントのリスト (投稿日時昇順) 143 | 144 | Raises: 145 | httpx.HTTPStatusError: HTTP リクエストが失敗した場合 146 | AssertionError: 解析に失敗した場合 147 | """ 148 | 149 | class CommentResponse(BaseModel): 150 | id: int 151 | thread_id: int 152 | no: int 153 | vpos: int 154 | date: datetime 155 | mail: str 156 | user_id: str 157 | premium: bool 158 | anonymity: bool 159 | content: str 160 | 161 | class ThreadResponse(BaseModel): 162 | id: int 163 | channel_id: str 164 | start_at: datetime 165 | end_at: datetime 166 | duration: int 167 | title: str 168 | description: str 169 | status: Literal['ACTIVE', 'UPCOMING', 'PAST'] 170 | comments: list[CommentResponse] 171 | 172 | # スレッド取得 API にリクエスト 173 | ## 割と重いのでタイムアウトを 30 秒まで余裕を持って設定している 174 | response = await self.httpx_client.get(f'https://nx-jikkyo.tsukumijima.net/api/v1/threads/{self.thread_id}', timeout=30) 175 | response.raise_for_status() 176 | thread: ThreadResponse = TypeAdapter(ThreadResponse).validate_json(response.content) 177 | self.print(f'Title: {thread.title} [{thread.status}] ({thread.id})') 178 | self.print(f'Period: {thread.start_at.strftime("%Y-%m-%d %H:%M:%S")} ~ {thread.end_at.strftime("%Y-%m-%d %H:%M:%S")} ' 179 | f'({thread.end_at - thread.start_at}h)') 180 | self.print(Rule(characters='-', style=Style(color='#E33157')), verbose_log=True) 181 | 182 | # 基本投稿日時昇順でソートされているはずだが、念のためここでもソートする 183 | ## この後の処理で date は秒単位とミリ秒単位に分割するため、ここでソートしておかないと色々面倒 184 | thread.comments.sort(key=lambda x: x.date) 185 | 186 | # NX-Jikkyo から取得したコメントデータをニコニコ XML 互換コメント形式に変換する 187 | xml_compatible_comments: list[XMLCompatibleComment] = [] 188 | for comment in thread.comments: 189 | xml_comment = XMLCompatibleComment( 190 | # スレッド ID は NX-Jikkyo のスレッド ID を文字列化したものをそのまま入れる 191 | thread = str(comment.thread_id), 192 | no = comment.no, 193 | vpos = comment.vpos, 194 | date = int(comment.date.timestamp()), 195 | date_usec = int((comment.date.timestamp() % 1) * 1000000), 196 | mail = comment.mail, 197 | user_id = comment.user_id, 198 | premium = 1 if comment.premium is True else None, 199 | anonymity = 1 if comment.anonymity is True else None, 200 | content = comment.content, 201 | ) 202 | # ニコニコ実況に投稿され NX-Jikkyo にリアルタイムマージされたコメントを除外する 203 | if ignore_nicolive_comments is True and comment.user_id.startswith('nicolive:') is True: 204 | xml_comment.user_id = xml_comment.user_id.replace('nicolive:', '') 205 | self.print(str(xml_comment), verbose_log=True) 206 | self.print('[yellow]Skipped a comment from nicolive.[/yellow]', verbose_log=True) 207 | else: 208 | self.print(str(xml_comment), verbose_log=True) 209 | xml_compatible_comments.append(xml_comment) 210 | self.print(Rule(characters='-', style=Style(color='#E33157')), verbose_log=True) 211 | 212 | self.print(f'Retrieved a total of {len(xml_compatible_comments)} comments.') 213 | self.print(Rule(characters='-', style=Style(color='#E33157'))) 214 | return xml_compatible_comments 215 | 216 | 217 | def print(self, *args: Any, verbose_log: bool = False, **kwargs: Any) -> None: 218 | """ 219 | NXClient の動作ログをコンソールやファイルに出力する 220 | 221 | Args: 222 | verbose_log (bool, default=False): 詳細な動作ログかどうか (指定された場合、コンストラクタで verbose が指定された時のみ出力する) 223 | """ 224 | 225 | # このログが詳細な動作ログで、かつ詳細な動作ログの出力が有効でない場合は何もしない 226 | if verbose_log is True and self.verbose is False: 227 | return 228 | 229 | # 有効ならログをコンソールに出力する 230 | if self.show_log is True: 231 | print(*args, **kwargs) 232 | 233 | # ログファイルのパスが指定されている場合は、ログをファイルにも出力 234 | if self.log_path is not None: 235 | with self.log_path.open('a') as f: 236 | print(*args, **kwargs, file=f) 237 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 1.8.5 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "annotated-types" 5 | version = "0.7.0" 6 | description = "Reusable constraint types to use with typing.Annotated" 7 | optional = false 8 | python-versions = ">=3.8" 9 | files = [ 10 | {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, 11 | {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, 12 | ] 13 | 14 | [[package]] 15 | name = "anyio" 16 | version = "4.9.0" 17 | description = "High level compatibility layer for multiple asynchronous event loop implementations" 18 | optional = false 19 | python-versions = ">=3.9" 20 | files = [ 21 | {file = "anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c"}, 22 | {file = "anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028"}, 23 | ] 24 | 25 | [package.dependencies] 26 | idna = ">=2.8" 27 | sniffio = ">=1.1" 28 | typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""} 29 | 30 | [package.extras] 31 | doc = ["Sphinx (>=8.2,<9.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx_rtd_theme"] 32 | test = ["anyio[trio]", "blockbuster (>=1.5.23)", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21)"] 33 | trio = ["trio (>=0.26.1)"] 34 | 35 | [[package]] 36 | name = "beautifulsoup4" 37 | version = "4.13.4" 38 | description = "Screen-scraping library" 39 | optional = false 40 | python-versions = ">=3.7.0" 41 | files = [ 42 | {file = "beautifulsoup4-4.13.4-py3-none-any.whl", hash = "sha256:9bbbb14bfde9d79f38b8cd5f8c7c85f4b8f2523190ebed90e950a8dea4cb1c4b"}, 43 | {file = "beautifulsoup4-4.13.4.tar.gz", hash = "sha256:dbb3c4e1ceae6aefebdaf2423247260cd062430a410e38c66f2baa50a8437195"}, 44 | ] 45 | 46 | [package.dependencies] 47 | soupsieve = ">1.2" 48 | typing-extensions = ">=4.0.0" 49 | 50 | [package.extras] 51 | cchardet = ["cchardet"] 52 | chardet = ["chardet"] 53 | charset-normalizer = ["charset-normalizer"] 54 | html5lib = ["html5lib"] 55 | lxml = ["lxml"] 56 | 57 | [[package]] 58 | name = "certifi" 59 | version = "2025.4.26" 60 | description = "Python package for providing Mozilla's CA Bundle." 61 | optional = false 62 | python-versions = ">=3.6" 63 | files = [ 64 | {file = "certifi-2025.4.26-py3-none-any.whl", hash = "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3"}, 65 | {file = "certifi-2025.4.26.tar.gz", hash = "sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6"}, 66 | ] 67 | 68 | [[package]] 69 | name = "click" 70 | version = "8.1.8" 71 | description = "Composable command line interface toolkit" 72 | optional = false 73 | python-versions = ">=3.7" 74 | files = [ 75 | {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, 76 | {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, 77 | ] 78 | 79 | [package.dependencies] 80 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 81 | 82 | [[package]] 83 | name = "colorama" 84 | version = "0.4.6" 85 | description = "Cross-platform colored terminal text." 86 | optional = false 87 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 88 | files = [ 89 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 90 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 91 | ] 92 | 93 | [[package]] 94 | name = "h11" 95 | version = "0.16.0" 96 | description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" 97 | optional = false 98 | python-versions = ">=3.8" 99 | files = [ 100 | {file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"}, 101 | {file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"}, 102 | ] 103 | 104 | [[package]] 105 | name = "httpcore" 106 | version = "1.0.9" 107 | description = "A minimal low-level HTTP client." 108 | optional = false 109 | python-versions = ">=3.8" 110 | files = [ 111 | {file = "httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55"}, 112 | {file = "httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8"}, 113 | ] 114 | 115 | [package.dependencies] 116 | certifi = "*" 117 | h11 = ">=0.16" 118 | 119 | [package.extras] 120 | asyncio = ["anyio (>=4.0,<5.0)"] 121 | http2 = ["h2 (>=3,<5)"] 122 | socks = ["socksio (==1.*)"] 123 | trio = ["trio (>=0.22.0,<1.0)"] 124 | 125 | [[package]] 126 | name = "httpx" 127 | version = "0.28.1" 128 | description = "The next generation HTTP client." 129 | optional = false 130 | python-versions = ">=3.8" 131 | files = [ 132 | {file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"}, 133 | {file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"}, 134 | ] 135 | 136 | [package.dependencies] 137 | anyio = "*" 138 | certifi = "*" 139 | httpcore = "==1.*" 140 | idna = "*" 141 | 142 | [package.extras] 143 | brotli = ["brotli", "brotlicffi"] 144 | cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] 145 | http2 = ["h2 (>=3,<5)"] 146 | socks = ["socksio (==1.*)"] 147 | zstd = ["zstandard (>=0.18.0)"] 148 | 149 | [[package]] 150 | name = "idna" 151 | version = "3.10" 152 | description = "Internationalized Domain Names in Applications (IDNA)" 153 | optional = false 154 | python-versions = ">=3.6" 155 | files = [ 156 | {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, 157 | {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, 158 | ] 159 | 160 | [package.extras] 161 | all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] 162 | 163 | [[package]] 164 | name = "lxml" 165 | version = "5.4.0" 166 | description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." 167 | optional = false 168 | python-versions = ">=3.6" 169 | files = [ 170 | {file = "lxml-5.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e7bc6df34d42322c5289e37e9971d6ed114e3776b45fa879f734bded9d1fea9c"}, 171 | {file = "lxml-5.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6854f8bd8a1536f8a1d9a3655e6354faa6406621cf857dc27b681b69860645c7"}, 172 | {file = "lxml-5.4.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:696ea9e87442467819ac22394ca36cb3d01848dad1be6fac3fb612d3bd5a12cf"}, 173 | {file = "lxml-5.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ef80aeac414f33c24b3815ecd560cee272786c3adfa5f31316d8b349bfade28"}, 174 | {file = "lxml-5.4.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b9c2754cef6963f3408ab381ea55f47dabc6f78f4b8ebb0f0b25cf1ac1f7609"}, 175 | {file = "lxml-5.4.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7a62cc23d754bb449d63ff35334acc9f5c02e6dae830d78dab4dd12b78a524f4"}, 176 | {file = "lxml-5.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f82125bc7203c5ae8633a7d5d20bcfdff0ba33e436e4ab0abc026a53a8960b7"}, 177 | {file = "lxml-5.4.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:b67319b4aef1a6c56576ff544b67a2a6fbd7eaee485b241cabf53115e8908b8f"}, 178 | {file = "lxml-5.4.0-cp310-cp310-manylinux_2_28_ppc64le.whl", hash = "sha256:a8ef956fce64c8551221f395ba21d0724fed6b9b6242ca4f2f7beb4ce2f41997"}, 179 | {file = "lxml-5.4.0-cp310-cp310-manylinux_2_28_s390x.whl", hash = "sha256:0a01ce7d8479dce84fc03324e3b0c9c90b1ece9a9bb6a1b6c9025e7e4520e78c"}, 180 | {file = "lxml-5.4.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:91505d3ddebf268bb1588eb0f63821f738d20e1e7f05d3c647a5ca900288760b"}, 181 | {file = "lxml-5.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a3bcdde35d82ff385f4ede021df801b5c4a5bcdfb61ea87caabcebfc4945dc1b"}, 182 | {file = "lxml-5.4.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:aea7c06667b987787c7d1f5e1dfcd70419b711cdb47d6b4bb4ad4b76777a0563"}, 183 | {file = "lxml-5.4.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:a7fb111eef4d05909b82152721a59c1b14d0f365e2be4c742a473c5d7372f4f5"}, 184 | {file = "lxml-5.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:43d549b876ce64aa18b2328faff70f5877f8c6dede415f80a2f799d31644d776"}, 185 | {file = "lxml-5.4.0-cp310-cp310-win32.whl", hash = "sha256:75133890e40d229d6c5837b0312abbe5bac1c342452cf0e12523477cd3aa21e7"}, 186 | {file = "lxml-5.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:de5b4e1088523e2b6f730d0509a9a813355b7f5659d70eb4f319c76beea2e250"}, 187 | {file = "lxml-5.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:98a3912194c079ef37e716ed228ae0dcb960992100461b704aea4e93af6b0bb9"}, 188 | {file = "lxml-5.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0ea0252b51d296a75f6118ed0d8696888e7403408ad42345d7dfd0d1e93309a7"}, 189 | {file = "lxml-5.4.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b92b69441d1bd39f4940f9eadfa417a25862242ca2c396b406f9272ef09cdcaa"}, 190 | {file = "lxml-5.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20e16c08254b9b6466526bc1828d9370ee6c0d60a4b64836bc3ac2917d1e16df"}, 191 | {file = "lxml-5.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7605c1c32c3d6e8c990dd28a0970a3cbbf1429d5b92279e37fda05fb0c92190e"}, 192 | {file = "lxml-5.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ecf4c4b83f1ab3d5a7ace10bafcb6f11df6156857a3c418244cef41ca9fa3e44"}, 193 | {file = "lxml-5.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0cef4feae82709eed352cd7e97ae062ef6ae9c7b5dbe3663f104cd2c0e8d94ba"}, 194 | {file = "lxml-5.4.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:df53330a3bff250f10472ce96a9af28628ff1f4efc51ccba351a8820bca2a8ba"}, 195 | {file = "lxml-5.4.0-cp311-cp311-manylinux_2_28_ppc64le.whl", hash = "sha256:aefe1a7cb852fa61150fcb21a8c8fcea7b58c4cb11fbe59c97a0a4b31cae3c8c"}, 196 | {file = "lxml-5.4.0-cp311-cp311-manylinux_2_28_s390x.whl", hash = "sha256:ef5a7178fcc73b7d8c07229e89f8eb45b2908a9238eb90dcfc46571ccf0383b8"}, 197 | {file = "lxml-5.4.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:d2ed1b3cb9ff1c10e6e8b00941bb2e5bb568b307bfc6b17dffbbe8be5eecba86"}, 198 | {file = "lxml-5.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:72ac9762a9f8ce74c9eed4a4e74306f2f18613a6b71fa065495a67ac227b3056"}, 199 | {file = "lxml-5.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f5cb182f6396706dc6cc1896dd02b1c889d644c081b0cdec38747573db88a7d7"}, 200 | {file = "lxml-5.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:3a3178b4873df8ef9457a4875703488eb1622632a9cee6d76464b60e90adbfcd"}, 201 | {file = "lxml-5.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e094ec83694b59d263802ed03a8384594fcce477ce484b0cbcd0008a211ca751"}, 202 | {file = "lxml-5.4.0-cp311-cp311-win32.whl", hash = "sha256:4329422de653cdb2b72afa39b0aa04252fca9071550044904b2e7036d9d97fe4"}, 203 | {file = "lxml-5.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:fd3be6481ef54b8cfd0e1e953323b7aa9d9789b94842d0e5b142ef4bb7999539"}, 204 | {file = "lxml-5.4.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b5aff6f3e818e6bdbbb38e5967520f174b18f539c2b9de867b1e7fde6f8d95a4"}, 205 | {file = "lxml-5.4.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:942a5d73f739ad7c452bf739a62a0f83e2578afd6b8e5406308731f4ce78b16d"}, 206 | {file = "lxml-5.4.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:460508a4b07364d6abf53acaa0a90b6d370fafde5693ef37602566613a9b0779"}, 207 | {file = "lxml-5.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:529024ab3a505fed78fe3cc5ddc079464e709f6c892733e3f5842007cec8ac6e"}, 208 | {file = "lxml-5.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ca56ebc2c474e8f3d5761debfd9283b8b18c76c4fc0967b74aeafba1f5647f9"}, 209 | {file = "lxml-5.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a81e1196f0a5b4167a8dafe3a66aa67c4addac1b22dc47947abd5d5c7a3f24b5"}, 210 | {file = "lxml-5.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00b8686694423ddae324cf614e1b9659c2edb754de617703c3d29ff568448df5"}, 211 | {file = "lxml-5.4.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:c5681160758d3f6ac5b4fea370495c48aac0989d6a0f01bb9a72ad8ef5ab75c4"}, 212 | {file = "lxml-5.4.0-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:2dc191e60425ad70e75a68c9fd90ab284df64d9cd410ba8d2b641c0c45bc006e"}, 213 | {file = "lxml-5.4.0-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:67f779374c6b9753ae0a0195a892a1c234ce8416e4448fe1e9f34746482070a7"}, 214 | {file = "lxml-5.4.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:79d5bfa9c1b455336f52343130b2067164040604e41f6dc4d8313867ed540079"}, 215 | {file = "lxml-5.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3d3c30ba1c9b48c68489dc1829a6eede9873f52edca1dda900066542528d6b20"}, 216 | {file = "lxml-5.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1af80c6316ae68aded77e91cd9d80648f7dd40406cef73df841aa3c36f6907c8"}, 217 | {file = "lxml-5.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4d885698f5019abe0de3d352caf9466d5de2baded00a06ef3f1216c1a58ae78f"}, 218 | {file = "lxml-5.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:aea53d51859b6c64e7c51d522c03cc2c48b9b5d6172126854cc7f01aa11f52bc"}, 219 | {file = "lxml-5.4.0-cp312-cp312-win32.whl", hash = "sha256:d90b729fd2732df28130c064aac9bb8aff14ba20baa4aee7bd0795ff1187545f"}, 220 | {file = "lxml-5.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:1dc4ca99e89c335a7ed47d38964abcb36c5910790f9bd106f2a8fa2ee0b909d2"}, 221 | {file = "lxml-5.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:773e27b62920199c6197130632c18fb7ead3257fce1ffb7d286912e56ddb79e0"}, 222 | {file = "lxml-5.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ce9c671845de9699904b1e9df95acfe8dfc183f2310f163cdaa91a3535af95de"}, 223 | {file = "lxml-5.4.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9454b8d8200ec99a224df8854786262b1bd6461f4280064c807303c642c05e76"}, 224 | {file = "lxml-5.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cccd007d5c95279e529c146d095f1d39ac05139de26c098166c4beb9374b0f4d"}, 225 | {file = "lxml-5.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0fce1294a0497edb034cb416ad3e77ecc89b313cff7adbee5334e4dc0d11f422"}, 226 | {file = "lxml-5.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:24974f774f3a78ac12b95e3a20ef0931795ff04dbb16db81a90c37f589819551"}, 227 | {file = "lxml-5.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:497cab4d8254c2a90bf988f162ace2ddbfdd806fce3bda3f581b9d24c852e03c"}, 228 | {file = "lxml-5.4.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:e794f698ae4c5084414efea0f5cc9f4ac562ec02d66e1484ff822ef97c2cadff"}, 229 | {file = "lxml-5.4.0-cp313-cp313-manylinux_2_28_ppc64le.whl", hash = "sha256:2c62891b1ea3094bb12097822b3d44b93fc6c325f2043c4d2736a8ff09e65f60"}, 230 | {file = "lxml-5.4.0-cp313-cp313-manylinux_2_28_s390x.whl", hash = "sha256:142accb3e4d1edae4b392bd165a9abdee8a3c432a2cca193df995bc3886249c8"}, 231 | {file = "lxml-5.4.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:1a42b3a19346e5601d1b8296ff6ef3d76038058f311902edd574461e9c036982"}, 232 | {file = "lxml-5.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4291d3c409a17febf817259cb37bc62cb7eb398bcc95c1356947e2871911ae61"}, 233 | {file = "lxml-5.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4f5322cf38fe0e21c2d73901abf68e6329dc02a4994e483adbcf92b568a09a54"}, 234 | {file = "lxml-5.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:0be91891bdb06ebe65122aa6bf3fc94489960cf7e03033c6f83a90863b23c58b"}, 235 | {file = "lxml-5.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:15a665ad90054a3d4f397bc40f73948d48e36e4c09f9bcffc7d90c87410e478a"}, 236 | {file = "lxml-5.4.0-cp313-cp313-win32.whl", hash = "sha256:d5663bc1b471c79f5c833cffbc9b87d7bf13f87e055a5c86c363ccd2348d7e82"}, 237 | {file = "lxml-5.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:bcb7a1096b4b6b24ce1ac24d4942ad98f983cd3810f9711bcd0293f43a9d8b9f"}, 238 | {file = "lxml-5.4.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:7be701c24e7f843e6788353c055d806e8bd8466b52907bafe5d13ec6a6dbaecd"}, 239 | {file = "lxml-5.4.0-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fb54f7c6bafaa808f27166569b1511fc42701a7713858dddc08afdde9746849e"}, 240 | {file = "lxml-5.4.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97dac543661e84a284502e0cf8a67b5c711b0ad5fb661d1bd505c02f8cf716d7"}, 241 | {file = "lxml-5.4.0-cp36-cp36m-manylinux_2_28_x86_64.whl", hash = "sha256:c70e93fba207106cb16bf852e421c37bbded92acd5964390aad07cb50d60f5cf"}, 242 | {file = "lxml-5.4.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:9c886b481aefdf818ad44846145f6eaf373a20d200b5ce1a5c8e1bc2d8745410"}, 243 | {file = "lxml-5.4.0-cp36-cp36m-musllinux_1_2_x86_64.whl", hash = "sha256:fa0e294046de09acd6146be0ed6727d1f42ded4ce3ea1e9a19c11b6774eea27c"}, 244 | {file = "lxml-5.4.0-cp36-cp36m-win32.whl", hash = "sha256:61c7bbf432f09ee44b1ccaa24896d21075e533cd01477966a5ff5a71d88b2f56"}, 245 | {file = "lxml-5.4.0-cp36-cp36m-win_amd64.whl", hash = "sha256:7ce1a171ec325192c6a636b64c94418e71a1964f56d002cc28122fceff0b6121"}, 246 | {file = "lxml-5.4.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:795f61bcaf8770e1b37eec24edf9771b307df3af74d1d6f27d812e15a9ff3872"}, 247 | {file = "lxml-5.4.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:29f451a4b614a7b5b6c2e043d7b64a15bd8304d7e767055e8ab68387a8cacf4e"}, 248 | {file = "lxml-5.4.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:891f7f991a68d20c75cb13c5c9142b2a3f9eb161f1f12a9489c82172d1f133c0"}, 249 | {file = "lxml-5.4.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4aa412a82e460571fad592d0f93ce9935a20090029ba08eca05c614f99b0cc92"}, 250 | {file = "lxml-5.4.0-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:ac7ba71f9561cd7d7b55e1ea5511543c0282e2b6450f122672a2694621d63b7e"}, 251 | {file = "lxml-5.4.0-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:c5d32f5284012deaccd37da1e2cd42f081feaa76981f0eaa474351b68df813c5"}, 252 | {file = "lxml-5.4.0-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:ce31158630a6ac85bddd6b830cffd46085ff90498b397bd0a259f59d27a12188"}, 253 | {file = "lxml-5.4.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:31e63621e073e04697c1b2d23fcb89991790eef370ec37ce4d5d469f40924ed6"}, 254 | {file = "lxml-5.4.0-cp37-cp37m-win32.whl", hash = "sha256:be2ba4c3c5b7900246a8f866580700ef0d538f2ca32535e991027bdaba944063"}, 255 | {file = "lxml-5.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:09846782b1ef650b321484ad429217f5154da4d6e786636c38e434fa32e94e49"}, 256 | {file = "lxml-5.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:eaf24066ad0b30917186420d51e2e3edf4b0e2ea68d8cd885b14dc8afdcf6556"}, 257 | {file = "lxml-5.4.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b31a3a77501d86d8ade128abb01082724c0dfd9524f542f2f07d693c9f1175f"}, 258 | {file = "lxml-5.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e108352e203c7afd0eb91d782582f00a0b16a948d204d4dec8565024fafeea5"}, 259 | {file = "lxml-5.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a11a96c3b3f7551c8a8109aa65e8594e551d5a84c76bf950da33d0fb6dfafab7"}, 260 | {file = "lxml-5.4.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:ca755eebf0d9e62d6cb013f1261e510317a41bf4650f22963474a663fdfe02aa"}, 261 | {file = "lxml-5.4.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:4cd915c0fb1bed47b5e6d6edd424ac25856252f09120e3e8ba5154b6b921860e"}, 262 | {file = "lxml-5.4.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:226046e386556a45ebc787871d6d2467b32c37ce76c2680f5c608e25823ffc84"}, 263 | {file = "lxml-5.4.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:b108134b9667bcd71236c5a02aad5ddd073e372fb5d48ea74853e009fe38acb6"}, 264 | {file = "lxml-5.4.0-cp38-cp38-win32.whl", hash = "sha256:1320091caa89805df7dcb9e908add28166113dcd062590668514dbd510798c88"}, 265 | {file = "lxml-5.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:073eb6dcdf1f587d9b88c8c93528b57eccda40209cf9be549d469b942b41d70b"}, 266 | {file = "lxml-5.4.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:bda3ea44c39eb74e2488297bb39d47186ed01342f0022c8ff407c250ac3f498e"}, 267 | {file = "lxml-5.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9ceaf423b50ecfc23ca00b7f50b64baba85fb3fb91c53e2c9d00bc86150c7e40"}, 268 | {file = "lxml-5.4.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:664cdc733bc87449fe781dbb1f309090966c11cc0c0cd7b84af956a02a8a4729"}, 269 | {file = "lxml-5.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67ed8a40665b84d161bae3181aa2763beea3747f748bca5874b4af4d75998f87"}, 270 | {file = "lxml-5.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b4a3bd174cc9cdaa1afbc4620c049038b441d6ba07629d89a83b408e54c35cd"}, 271 | {file = "lxml-5.4.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:b0989737a3ba6cf2a16efb857fb0dfa20bc5c542737fddb6d893fde48be45433"}, 272 | {file = "lxml-5.4.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:dc0af80267edc68adf85f2a5d9be1cdf062f973db6790c1d065e45025fa26140"}, 273 | {file = "lxml-5.4.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:639978bccb04c42677db43c79bdaa23785dc7f9b83bfd87570da8207872f1ce5"}, 274 | {file = "lxml-5.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5a99d86351f9c15e4a901fc56404b485b1462039db59288b203f8c629260a142"}, 275 | {file = "lxml-5.4.0-cp39-cp39-win32.whl", hash = "sha256:3e6d5557989cdc3ebb5302bbdc42b439733a841891762ded9514e74f60319ad6"}, 276 | {file = "lxml-5.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:a8c9b7f16b63e65bbba889acb436a1034a82d34fa09752d754f88d708eca80e1"}, 277 | {file = "lxml-5.4.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1b717b00a71b901b4667226bba282dd462c42ccf618ade12f9ba3674e1fabc55"}, 278 | {file = "lxml-5.4.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27a9ded0f0b52098ff89dd4c418325b987feed2ea5cc86e8860b0f844285d740"}, 279 | {file = "lxml-5.4.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b7ce10634113651d6f383aa712a194179dcd496bd8c41e191cec2099fa09de5"}, 280 | {file = "lxml-5.4.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:53370c26500d22b45182f98847243efb518d268374a9570409d2e2276232fd37"}, 281 | {file = "lxml-5.4.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c6364038c519dffdbe07e3cf42e6a7f8b90c275d4d1617a69bb59734c1a2d571"}, 282 | {file = "lxml-5.4.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:b12cb6527599808ada9eb2cd6e0e7d3d8f13fe7bbb01c6311255a15ded4c7ab4"}, 283 | {file = "lxml-5.4.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:5f11a1526ebd0dee85e7b1e39e39a0cc0d9d03fb527f56d8457f6df48a10dc0c"}, 284 | {file = "lxml-5.4.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48b4afaf38bf79109bb060d9016fad014a9a48fb244e11b94f74ae366a64d252"}, 285 | {file = "lxml-5.4.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de6f6bb8a7840c7bf216fb83eec4e2f79f7325eca8858167b68708b929ab2172"}, 286 | {file = "lxml-5.4.0-pp37-pypy37_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:5cca36a194a4eb4e2ed6be36923d3cffd03dcdf477515dea687185506583d4c9"}, 287 | {file = "lxml-5.4.0-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b7c86884ad23d61b025989d99bfdd92a7351de956e01c61307cb87035960bcb1"}, 288 | {file = "lxml-5.4.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:53d9469ab5460402c19553b56c3648746774ecd0681b1b27ea74d5d8a3ef5590"}, 289 | {file = "lxml-5.4.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:56dbdbab0551532bb26c19c914848d7251d73edb507c3079d6805fa8bba5b706"}, 290 | {file = "lxml-5.4.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14479c2ad1cb08b62bb941ba8e0e05938524ee3c3114644df905d2331c76cd57"}, 291 | {file = "lxml-5.4.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:32697d2ea994e0db19c1df9e40275ffe84973e4232b5c274f47e7c1ec9763cdd"}, 292 | {file = "lxml-5.4.0-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:24f6df5f24fc3385f622c0c9d63fe34604893bc1a5bdbb2dbf5870f85f9a404a"}, 293 | {file = "lxml-5.4.0-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:151d6c40bc9db11e960619d2bf2ec5829f0aaffb10b41dcf6ad2ce0f3c0b2325"}, 294 | {file = "lxml-5.4.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:4025bf2884ac4370a3243c5aa8d66d3cb9e15d3ddd0af2d796eccc5f0244390e"}, 295 | {file = "lxml-5.4.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:9459e6892f59ecea2e2584ee1058f5d8f629446eab52ba2305ae13a32a059530"}, 296 | {file = "lxml-5.4.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47fb24cc0f052f0576ea382872b3fc7e1f7e3028e53299ea751839418ade92a6"}, 297 | {file = "lxml-5.4.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50441c9de951a153c698b9b99992e806b71c1f36d14b154592580ff4a9d0d877"}, 298 | {file = "lxml-5.4.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:ab339536aa798b1e17750733663d272038bf28069761d5be57cb4a9b0137b4f8"}, 299 | {file = "lxml-5.4.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:9776af1aad5a4b4a1317242ee2bea51da54b2a7b7b48674be736d463c999f37d"}, 300 | {file = "lxml-5.4.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:63e7968ff83da2eb6fdda967483a7a023aa497d85ad8f05c3ad9b1f2e8c84987"}, 301 | {file = "lxml-5.4.0.tar.gz", hash = "sha256:d12832e1dbea4be280b22fd0ea7c9b87f0d8fc51ba06e92dc62d52f804f78ebd"}, 302 | ] 303 | 304 | [package.extras] 305 | cssselect = ["cssselect (>=0.7)"] 306 | html-clean = ["lxml_html_clean"] 307 | html5 = ["html5lib"] 308 | htmlsoup = ["BeautifulSoup4"] 309 | source = ["Cython (>=3.0.11,<3.1.0)"] 310 | 311 | [[package]] 312 | name = "lxml-stubs" 313 | version = "0.5.1" 314 | description = "Type annotations for the lxml package" 315 | optional = false 316 | python-versions = "*" 317 | files = [ 318 | {file = "lxml-stubs-0.5.1.tar.gz", hash = "sha256:e0ec2aa1ce92d91278b719091ce4515c12adc1d564359dfaf81efa7d4feab79d"}, 319 | {file = "lxml_stubs-0.5.1-py3-none-any.whl", hash = "sha256:1f689e5dbc4b9247cb09ae820c7d34daeb1fdbd1db06123814b856dae7787272"}, 320 | ] 321 | 322 | [package.extras] 323 | test = ["coverage[toml] (>=7.2.5)", "mypy (>=1.2.0)", "pytest (>=7.3.0)", "pytest-mypy-plugins (>=1.10.1)"] 324 | 325 | [[package]] 326 | name = "markdown-it-py" 327 | version = "3.0.0" 328 | description = "Python port of markdown-it. Markdown parsing, done right!" 329 | optional = false 330 | python-versions = ">=3.8" 331 | files = [ 332 | {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, 333 | {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, 334 | ] 335 | 336 | [package.dependencies] 337 | mdurl = ">=0.1,<1.0" 338 | 339 | [package.extras] 340 | benchmarking = ["psutil", "pytest", "pytest-benchmark"] 341 | code-style = ["pre-commit (>=3.0,<4.0)"] 342 | compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] 343 | linkify = ["linkify-it-py (>=1,<3)"] 344 | plugins = ["mdit-py-plugins"] 345 | profiling = ["gprof2dot"] 346 | rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] 347 | testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] 348 | 349 | [[package]] 350 | name = "mdurl" 351 | version = "0.1.2" 352 | description = "Markdown URL utilities" 353 | optional = false 354 | python-versions = ">=3.7" 355 | files = [ 356 | {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, 357 | {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, 358 | ] 359 | 360 | [[package]] 361 | name = "ndgr-client" 362 | version = "1.0.1" 363 | description = "NDGRClient: Nicolive NDGR Message Server Client Library" 364 | optional = false 365 | python-versions = ">=3.11,<4.0" 366 | files = [] 367 | develop = false 368 | 369 | [package.dependencies] 370 | beautifulsoup4 = ">=4.12.3" 371 | httpx = ">=0.27.0" 372 | lxml = ">=5.2.2" 373 | lxml-stubs = ">=0.5.1" 374 | protobuf = "<5.28.0" 375 | pydantic = ">=2.8.2" 376 | typer = {version = ">=0.12.3", extras = ["all"]} 377 | typing-extensions = ">=4.12.2" 378 | websockets = ">=12.0" 379 | 380 | [package.source] 381 | type = "git" 382 | url = "https://github.com/tsukumijima/NDGRClient" 383 | reference = "880520f2c98da03450418eeb80d0710b6cf22f94" 384 | resolved_reference = "880520f2c98da03450418eeb80d0710b6cf22f94" 385 | 386 | [[package]] 387 | name = "protobuf" 388 | version = "5.27.5" 389 | description = "" 390 | optional = false 391 | python-versions = ">=3.8" 392 | files = [ 393 | {file = "protobuf-5.27.5-cp310-abi3-win32.whl", hash = "sha256:b46647660bc433a43519af7faabe424bf2feb8db6e2293e6906c7aa3a1abefe2"}, 394 | {file = "protobuf-5.27.5-cp310-abi3-win_amd64.whl", hash = "sha256:5aa37101a985559722e84badf583532b0ec92616a2cc5d3f59f6152f136ca46a"}, 395 | {file = "protobuf-5.27.5-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:83fc15159713bb1de8e24e025d8739c6c9c6856021d2834d6feb0d1d5c6ec3c6"}, 396 | {file = "protobuf-5.27.5-cp38-abi3-manylinux2014_aarch64.whl", hash = "sha256:56cb4f9ade31597d06a5aca264cb5d9bf445dc07758296004ead080ec8e4087c"}, 397 | {file = "protobuf-5.27.5-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:aab519ebdc1bd7469e7df4011545ff4f81decad6d02f0185ddbe6ee496f1d940"}, 398 | {file = "protobuf-5.27.5-cp38-cp38-win32.whl", hash = "sha256:c84672b87840e2250a209481c74301b36677c2a19eabd3cc7a73810207350995"}, 399 | {file = "protobuf-5.27.5-cp38-cp38-win_amd64.whl", hash = "sha256:99c6f0e2406c7b755f73851c63ac79e9087336c36a2cc4a46be82b2742af67c9"}, 400 | {file = "protobuf-5.27.5-cp39-cp39-win32.whl", hash = "sha256:ff4e9db9a21c090f39a6ac91b89262ff1ce49c1fee589ae87c3386f4ad1b2e27"}, 401 | {file = "protobuf-5.27.5-cp39-cp39-win_amd64.whl", hash = "sha256:9dc0a9b61279b04aeff203cf40a3b69bf74e06666ddf264f9860f1e88de01d8e"}, 402 | {file = "protobuf-5.27.5-py3-none-any.whl", hash = "sha256:03a25e0b2b0271bc63fe009d30890ba907fd36dbe2b8e4851da4bb893d251d05"}, 403 | {file = "protobuf-5.27.5.tar.gz", hash = "sha256:7fa81bc550201144a32f4478659da06e0b2ebe4d5303aacce9a202a1c3d5178d"}, 404 | ] 405 | 406 | [[package]] 407 | name = "pydantic" 408 | version = "2.11.4" 409 | description = "Data validation using Python type hints" 410 | optional = false 411 | python-versions = ">=3.9" 412 | files = [ 413 | {file = "pydantic-2.11.4-py3-none-any.whl", hash = "sha256:d9615eaa9ac5a063471da949c8fc16376a84afb5024688b3ff885693506764eb"}, 414 | {file = "pydantic-2.11.4.tar.gz", hash = "sha256:32738d19d63a226a52eed76645a98ee07c1f410ee41d93b4afbfa85ed8111c2d"}, 415 | ] 416 | 417 | [package.dependencies] 418 | annotated-types = ">=0.6.0" 419 | pydantic-core = "2.33.2" 420 | typing-extensions = ">=4.12.2" 421 | typing-inspection = ">=0.4.0" 422 | 423 | [package.extras] 424 | email = ["email-validator (>=2.0.0)"] 425 | timezone = ["tzdata"] 426 | 427 | [[package]] 428 | name = "pydantic-core" 429 | version = "2.33.2" 430 | description = "Core functionality for Pydantic validation and serialization" 431 | optional = false 432 | python-versions = ">=3.9" 433 | files = [ 434 | {file = "pydantic_core-2.33.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2b3d326aaef0c0399d9afffeb6367d5e26ddc24d351dbc9c636840ac355dc5d8"}, 435 | {file = "pydantic_core-2.33.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e5b2671f05ba48b94cb90ce55d8bdcaaedb8ba00cc5359f6810fc918713983d"}, 436 | {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0069c9acc3f3981b9ff4cdfaf088e98d83440a4c7ea1bc07460af3d4dc22e72d"}, 437 | {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d53b22f2032c42eaaf025f7c40c2e3b94568ae077a606f006d206a463bc69572"}, 438 | {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0405262705a123b7ce9f0b92f123334d67b70fd1f20a9372b907ce1080c7ba02"}, 439 | {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4b25d91e288e2c4e0662b8038a28c6a07eaac3e196cfc4ff69de4ea3db992a1b"}, 440 | {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bdfe4b3789761f3bcb4b1ddf33355a71079858958e3a552f16d5af19768fef2"}, 441 | {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:efec8db3266b76ef9607c2c4c419bdb06bf335ae433b80816089ea7585816f6a"}, 442 | {file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:031c57d67ca86902726e0fae2214ce6770bbe2f710dc33063187a68744a5ecac"}, 443 | {file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:f8de619080e944347f5f20de29a975c2d815d9ddd8be9b9b7268e2e3ef68605a"}, 444 | {file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:73662edf539e72a9440129f231ed3757faab89630d291b784ca99237fb94db2b"}, 445 | {file = "pydantic_core-2.33.2-cp310-cp310-win32.whl", hash = "sha256:0a39979dcbb70998b0e505fb1556a1d550a0781463ce84ebf915ba293ccb7e22"}, 446 | {file = "pydantic_core-2.33.2-cp310-cp310-win_amd64.whl", hash = "sha256:b0379a2b24882fef529ec3b4987cb5d003b9cda32256024e6fe1586ac45fc640"}, 447 | {file = "pydantic_core-2.33.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4c5b0a576fb381edd6d27f0a85915c6daf2f8138dc5c267a57c08a62900758c7"}, 448 | {file = "pydantic_core-2.33.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e799c050df38a639db758c617ec771fd8fb7a5f8eaaa4b27b101f266b216a246"}, 449 | {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc46a01bf8d62f227d5ecee74178ffc448ff4e5197c756331f71efcc66dc980f"}, 450 | {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a144d4f717285c6d9234a66778059f33a89096dfb9b39117663fd8413d582dcc"}, 451 | {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73cf6373c21bc80b2e0dc88444f41ae60b2f070ed02095754eb5a01df12256de"}, 452 | {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dc625f4aa79713512d1976fe9f0bc99f706a9dee21dfd1810b4bbbf228d0e8a"}, 453 | {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b21b5549499972441da4758d662aeea93f1923f953e9cbaff14b8b9565aef"}, 454 | {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bdc25f3681f7b78572699569514036afe3c243bc3059d3942624e936ec93450e"}, 455 | {file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fe5b32187cbc0c862ee201ad66c30cf218e5ed468ec8dc1cf49dec66e160cc4d"}, 456 | {file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:bc7aee6f634a6f4a95676fcb5d6559a2c2a390330098dba5e5a5f28a2e4ada30"}, 457 | {file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:235f45e5dbcccf6bd99f9f472858849f73d11120d76ea8707115415f8e5ebebf"}, 458 | {file = "pydantic_core-2.33.2-cp311-cp311-win32.whl", hash = "sha256:6368900c2d3ef09b69cb0b913f9f8263b03786e5b2a387706c5afb66800efd51"}, 459 | {file = "pydantic_core-2.33.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e063337ef9e9820c77acc768546325ebe04ee38b08703244c1309cccc4f1bab"}, 460 | {file = "pydantic_core-2.33.2-cp311-cp311-win_arm64.whl", hash = "sha256:6b99022f1d19bc32a4c2a0d544fc9a76e3be90f0b3f4af413f87d38749300e65"}, 461 | {file = "pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc"}, 462 | {file = "pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7"}, 463 | {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025"}, 464 | {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011"}, 465 | {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f"}, 466 | {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88"}, 467 | {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1"}, 468 | {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b"}, 469 | {file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1"}, 470 | {file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6"}, 471 | {file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea"}, 472 | {file = "pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290"}, 473 | {file = "pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2"}, 474 | {file = "pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab"}, 475 | {file = "pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f"}, 476 | {file = "pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6"}, 477 | {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef"}, 478 | {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a"}, 479 | {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916"}, 480 | {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a"}, 481 | {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d"}, 482 | {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56"}, 483 | {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5"}, 484 | {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e"}, 485 | {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162"}, 486 | {file = "pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849"}, 487 | {file = "pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9"}, 488 | {file = "pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9"}, 489 | {file = "pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac"}, 490 | {file = "pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5"}, 491 | {file = "pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9"}, 492 | {file = "pydantic_core-2.33.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a2b911a5b90e0374d03813674bf0a5fbbb7741570dcd4b4e85a2e48d17def29d"}, 493 | {file = "pydantic_core-2.33.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6fa6dfc3e4d1f734a34710f391ae822e0a8eb8559a85c6979e14e65ee6ba2954"}, 494 | {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c54c939ee22dc8e2d545da79fc5381f1c020d6d3141d3bd747eab59164dc89fb"}, 495 | {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:53a57d2ed685940a504248187d5685e49eb5eef0f696853647bf37c418c538f7"}, 496 | {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09fb9dd6571aacd023fe6aaca316bd01cf60ab27240d7eb39ebd66a3a15293b4"}, 497 | {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0e6116757f7959a712db11f3e9c0a99ade00a5bbedae83cb801985aa154f071b"}, 498 | {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d55ab81c57b8ff8548c3e4947f119551253f4e3787a7bbc0b6b3ca47498a9d3"}, 499 | {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c20c462aa4434b33a2661701b861604913f912254e441ab8d78d30485736115a"}, 500 | {file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:44857c3227d3fb5e753d5fe4a3420d6376fa594b07b621e220cd93703fe21782"}, 501 | {file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:eb9b459ca4df0e5c87deb59d37377461a538852765293f9e6ee834f0435a93b9"}, 502 | {file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9fcd347d2cc5c23b06de6d3b7b8275be558a0c90549495c699e379a80bf8379e"}, 503 | {file = "pydantic_core-2.33.2-cp39-cp39-win32.whl", hash = "sha256:83aa99b1285bc8f038941ddf598501a86f1536789740991d7d8756e34f1e74d9"}, 504 | {file = "pydantic_core-2.33.2-cp39-cp39-win_amd64.whl", hash = "sha256:f481959862f57f29601ccced557cc2e817bce7533ab8e01a797a48b49c9692b3"}, 505 | {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5c4aa4e82353f65e548c476b37e64189783aa5384903bfea4f41580f255fddfa"}, 506 | {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d946c8bf0d5c24bf4fe333af284c59a19358aa3ec18cb3dc4370080da1e8ad29"}, 507 | {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87b31b6846e361ef83fedb187bb5b4372d0da3f7e28d85415efa92d6125d6e6d"}, 508 | {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa9d91b338f2df0508606f7009fde642391425189bba6d8c653afd80fd6bb64e"}, 509 | {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2058a32994f1fde4ca0480ab9d1e75a0e8c87c22b53a3ae66554f9af78f2fe8c"}, 510 | {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:0e03262ab796d986f978f79c943fc5f620381be7287148b8010b4097f79a39ec"}, 511 | {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:1a8695a8d00c73e50bff9dfda4d540b7dee29ff9b8053e38380426a85ef10052"}, 512 | {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:fa754d1850735a0b0e03bcffd9d4b4343eb417e47196e4485d9cca326073a42c"}, 513 | {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a11c8d26a50bfab49002947d3d237abe4d9e4b5bdc8846a63537b6488e197808"}, 514 | {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:dd14041875d09cc0f9308e37a6f8b65f5585cf2598a53aa0123df8b129d481f8"}, 515 | {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d87c561733f66531dced0da6e864f44ebf89a8fba55f31407b00c2f7f9449593"}, 516 | {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f82865531efd18d6e07a04a17331af02cb7a651583c418df8266f17a63c6612"}, 517 | {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bfb5112df54209d820d7bf9317c7a6c9025ea52e49f46b6a2060104bba37de7"}, 518 | {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:64632ff9d614e5eecfb495796ad51b0ed98c453e447a76bcbeeb69615079fc7e"}, 519 | {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f889f7a40498cc077332c7ab6b4608d296d852182211787d4f3ee377aaae66e8"}, 520 | {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:de4b83bb311557e439b9e186f733f6c645b9417c84e2eb8203f3f820a4b988bf"}, 521 | {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:82f68293f055f51b51ea42fafc74b6aad03e70e191799430b90c13d643059ebb"}, 522 | {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:329467cecfb529c925cf2bbd4d60d2c509bc2fb52a20c1045bf09bb70971a9c1"}, 523 | {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:87acbfcf8e90ca885206e98359d7dca4bcbb35abdc0ff66672a293e1d7a19101"}, 524 | {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:7f92c15cd1e97d4b12acd1cc9004fa092578acfa57b67ad5e43a197175d01a64"}, 525 | {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3f26877a748dc4251cfcfda9dfb5f13fcb034f5308388066bcfe9031b63ae7d"}, 526 | {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac89aea9af8cd672fa7b510e7b8c33b0bba9a43186680550ccf23020f32d535"}, 527 | {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:970919794d126ba8645f3837ab6046fb4e72bbc057b3709144066204c19a455d"}, 528 | {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3eb3fe62804e8f859c49ed20a8451342de53ed764150cb14ca71357c765dc2a6"}, 529 | {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:3abcd9392a36025e3bd55f9bd38d908bd17962cc49bc6da8e7e96285336e2bca"}, 530 | {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:3a1c81334778f9e3af2f8aeb7a960736e5cab1dfebfb26aabca09afd2906c039"}, 531 | {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2807668ba86cb38c6817ad9bc66215ab8584d1d304030ce4f0887336f28a5e27"}, 532 | {file = "pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc"}, 533 | ] 534 | 535 | [package.dependencies] 536 | typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" 537 | 538 | [[package]] 539 | name = "pygments" 540 | version = "2.19.1" 541 | description = "Pygments is a syntax highlighting package written in Python." 542 | optional = false 543 | python-versions = ">=3.8" 544 | files = [ 545 | {file = "pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c"}, 546 | {file = "pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f"}, 547 | ] 548 | 549 | [package.extras] 550 | windows-terminal = ["colorama (>=0.4.6)"] 551 | 552 | [[package]] 553 | name = "rich" 554 | version = "14.0.0" 555 | description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" 556 | optional = false 557 | python-versions = ">=3.8.0" 558 | files = [ 559 | {file = "rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0"}, 560 | {file = "rich-14.0.0.tar.gz", hash = "sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725"}, 561 | ] 562 | 563 | [package.dependencies] 564 | markdown-it-py = ">=2.2.0" 565 | pygments = ">=2.13.0,<3.0.0" 566 | 567 | [package.extras] 568 | jupyter = ["ipywidgets (>=7.5.1,<9)"] 569 | 570 | [[package]] 571 | name = "shellingham" 572 | version = "1.5.4" 573 | description = "Tool to Detect Surrounding Shell" 574 | optional = false 575 | python-versions = ">=3.7" 576 | files = [ 577 | {file = "shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686"}, 578 | {file = "shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de"}, 579 | ] 580 | 581 | [[package]] 582 | name = "sniffio" 583 | version = "1.3.1" 584 | description = "Sniff out which async library your code is running under" 585 | optional = false 586 | python-versions = ">=3.7" 587 | files = [ 588 | {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, 589 | {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, 590 | ] 591 | 592 | [[package]] 593 | name = "soupsieve" 594 | version = "2.7" 595 | description = "A modern CSS selector implementation for Beautiful Soup." 596 | optional = false 597 | python-versions = ">=3.8" 598 | files = [ 599 | {file = "soupsieve-2.7-py3-none-any.whl", hash = "sha256:6e60cc5c1ffaf1cebcc12e8188320b72071e922c2e897f737cadce79ad5d30c4"}, 600 | {file = "soupsieve-2.7.tar.gz", hash = "sha256:ad282f9b6926286d2ead4750552c8a6142bc4c783fd66b0293547c8fe6ae126a"}, 601 | ] 602 | 603 | [[package]] 604 | name = "typer" 605 | version = "0.15.3" 606 | description = "Typer, build great CLIs. Easy to code. Based on Python type hints." 607 | optional = false 608 | python-versions = ">=3.7" 609 | files = [ 610 | {file = "typer-0.15.3-py3-none-any.whl", hash = "sha256:c86a65ad77ca531f03de08d1b9cb67cd09ad02ddddf4b34745b5008f43b239bd"}, 611 | {file = "typer-0.15.3.tar.gz", hash = "sha256:818873625d0569653438316567861899f7e9972f2e6e0c16dab608345ced713c"}, 612 | ] 613 | 614 | [package.dependencies] 615 | click = ">=8.0.0" 616 | rich = ">=10.11.0" 617 | shellingham = ">=1.3.0" 618 | typing-extensions = ">=3.7.4.3" 619 | 620 | [[package]] 621 | name = "typing-extensions" 622 | version = "4.13.2" 623 | description = "Backported and Experimental Type Hints for Python 3.8+" 624 | optional = false 625 | python-versions = ">=3.8" 626 | files = [ 627 | {file = "typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c"}, 628 | {file = "typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef"}, 629 | ] 630 | 631 | [[package]] 632 | name = "typing-inspection" 633 | version = "0.4.0" 634 | description = "Runtime typing introspection tools" 635 | optional = false 636 | python-versions = ">=3.9" 637 | files = [ 638 | {file = "typing_inspection-0.4.0-py3-none-any.whl", hash = "sha256:50e72559fcd2a6367a19f7a7e610e6afcb9fac940c650290eed893d61386832f"}, 639 | {file = "typing_inspection-0.4.0.tar.gz", hash = "sha256:9765c87de36671694a67904bf2c96e395be9c6439bb6c87b5142569dcdd65122"}, 640 | ] 641 | 642 | [package.dependencies] 643 | typing-extensions = ">=4.12.0" 644 | 645 | [[package]] 646 | name = "websockets" 647 | version = "15.0.1" 648 | description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" 649 | optional = false 650 | python-versions = ">=3.9" 651 | files = [ 652 | {file = "websockets-15.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d63efaa0cd96cf0c5fe4d581521d9fa87744540d4bc999ae6e08595a1014b45b"}, 653 | {file = "websockets-15.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac60e3b188ec7574cb761b08d50fcedf9d77f1530352db4eef1707fe9dee7205"}, 654 | {file = "websockets-15.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5756779642579d902eed757b21b0164cd6fe338506a8083eb58af5c372e39d9a"}, 655 | {file = "websockets-15.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fdfe3e2a29e4db3659dbd5bbf04560cea53dd9610273917799f1cde46aa725e"}, 656 | {file = "websockets-15.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c2529b320eb9e35af0fa3016c187dffb84a3ecc572bcee7c3ce302bfeba52bf"}, 657 | {file = "websockets-15.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac1e5c9054fe23226fb11e05a6e630837f074174c4c2f0fe442996112a6de4fb"}, 658 | {file = "websockets-15.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5df592cd503496351d6dc14f7cdad49f268d8e618f80dce0cd5a36b93c3fc08d"}, 659 | {file = "websockets-15.0.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0a34631031a8f05657e8e90903e656959234f3a04552259458aac0b0f9ae6fd9"}, 660 | {file = "websockets-15.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3d00075aa65772e7ce9e990cab3ff1de702aa09be3940d1dc88d5abf1ab8a09c"}, 661 | {file = "websockets-15.0.1-cp310-cp310-win32.whl", hash = "sha256:1234d4ef35db82f5446dca8e35a7da7964d02c127b095e172e54397fb6a6c256"}, 662 | {file = "websockets-15.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:39c1fec2c11dc8d89bba6b2bf1556af381611a173ac2b511cf7231622058af41"}, 663 | {file = "websockets-15.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431"}, 664 | {file = "websockets-15.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57"}, 665 | {file = "websockets-15.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905"}, 666 | {file = "websockets-15.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562"}, 667 | {file = "websockets-15.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792"}, 668 | {file = "websockets-15.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413"}, 669 | {file = "websockets-15.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8"}, 670 | {file = "websockets-15.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3"}, 671 | {file = "websockets-15.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf"}, 672 | {file = "websockets-15.0.1-cp311-cp311-win32.whl", hash = "sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85"}, 673 | {file = "websockets-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065"}, 674 | {file = "websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3"}, 675 | {file = "websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665"}, 676 | {file = "websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2"}, 677 | {file = "websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215"}, 678 | {file = "websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5"}, 679 | {file = "websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65"}, 680 | {file = "websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe"}, 681 | {file = "websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4"}, 682 | {file = "websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597"}, 683 | {file = "websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9"}, 684 | {file = "websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7"}, 685 | {file = "websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931"}, 686 | {file = "websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675"}, 687 | {file = "websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151"}, 688 | {file = "websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22"}, 689 | {file = "websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f"}, 690 | {file = "websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8"}, 691 | {file = "websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375"}, 692 | {file = "websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d"}, 693 | {file = "websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4"}, 694 | {file = "websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa"}, 695 | {file = "websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561"}, 696 | {file = "websockets-15.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5f4c04ead5aed67c8a1a20491d54cdfba5884507a48dd798ecaf13c74c4489f5"}, 697 | {file = "websockets-15.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:abdc0c6c8c648b4805c5eacd131910d2a7f6455dfd3becab248ef108e89ab16a"}, 698 | {file = "websockets-15.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a625e06551975f4b7ea7102bc43895b90742746797e2e14b70ed61c43a90f09b"}, 699 | {file = "websockets-15.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d591f8de75824cbb7acad4e05d2d710484f15f29d4a915092675ad3456f11770"}, 700 | {file = "websockets-15.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:47819cea040f31d670cc8d324bb6435c6f133b8c7a19ec3d61634e62f8d8f9eb"}, 701 | {file = "websockets-15.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac017dd64572e5c3bd01939121e4d16cf30e5d7e110a119399cf3133b63ad054"}, 702 | {file = "websockets-15.0.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:4a9fac8e469d04ce6c25bb2610dc535235bd4aa14996b4e6dbebf5e007eba5ee"}, 703 | {file = "websockets-15.0.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:363c6f671b761efcb30608d24925a382497c12c506b51661883c3e22337265ed"}, 704 | {file = "websockets-15.0.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2034693ad3097d5355bfdacfffcbd3ef5694f9718ab7f29c29689a9eae841880"}, 705 | {file = "websockets-15.0.1-cp39-cp39-win32.whl", hash = "sha256:3b1ac0d3e594bf121308112697cf4b32be538fb1444468fb0a6ae4feebc83411"}, 706 | {file = "websockets-15.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:b7643a03db5c95c799b89b31c036d5f27eeb4d259c798e878d6937d71832b1e4"}, 707 | {file = "websockets-15.0.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0c9e74d766f2818bb95f84c25be4dea09841ac0f734d1966f415e4edfc4ef1c3"}, 708 | {file = "websockets-15.0.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1009ee0c7739c08a0cd59de430d6de452a55e42d6b522de7aa15e6f67db0b8e1"}, 709 | {file = "websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76d1f20b1c7a2fa82367e04982e708723ba0e7b8d43aa643d3dcd404d74f1475"}, 710 | {file = "websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f29d80eb9a9263b8d109135351caf568cc3f80b9928bccde535c235de55c22d9"}, 711 | {file = "websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b359ed09954d7c18bbc1680f380c7301f92c60bf924171629c5db97febb12f04"}, 712 | {file = "websockets-15.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:cad21560da69f4ce7658ca2cb83138fb4cf695a2ba3e475e0559e05991aa8122"}, 713 | {file = "websockets-15.0.1-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7f493881579c90fc262d9cdbaa05a6b54b3811c2f300766748db79f098db9940"}, 714 | {file = "websockets-15.0.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:47b099e1f4fbc95b701b6e85768e1fcdaf1630f3cbe4765fa216596f12310e2e"}, 715 | {file = "websockets-15.0.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67f2b6de947f8c757db2db9c71527933ad0019737ec374a8a6be9a956786aaf9"}, 716 | {file = "websockets-15.0.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d08eb4c2b7d6c41da6ca0600c077e93f5adcfd979cd777d747e9ee624556da4b"}, 717 | {file = "websockets-15.0.1-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b826973a4a2ae47ba357e4e82fa44a463b8f168e1ca775ac64521442b19e87f"}, 718 | {file = "websockets-15.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:21c1fa28a6a7e3cbdc171c694398b6df4744613ce9b36b1a498e816787e28123"}, 719 | {file = "websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f"}, 720 | {file = "websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee"}, 721 | ] 722 | 723 | [metadata] 724 | lock-version = "2.0" 725 | python-versions = ">=3.11,<3.12" 726 | content-hash = "51427d93eb8337920e8b9f92512f411799eb93ea10be2811eabd8454403db65b" 727 | -------------------------------------------------------------------------------- /poetry.toml: -------------------------------------------------------------------------------- 1 | [virtualenvs] 2 | in-project = true 3 | 4 | [virtualenvs.options] 5 | always-copy = false 6 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "JKCommentCrawler" 3 | package-mode = false 4 | 5 | [tool.poetry.dependencies] 6 | python = ">=3.11,<3.12" 7 | ndgr-client = { git = "https://github.com/tsukumijima/NDGRClient", rev = "880520f2c98da03450418eeb80d0710b6cf22f94" } 8 | 9 | [tool.poetry.group.dev.dependencies] 10 | 11 | [build-system] 12 | requires = ["poetry-core"] 13 | build-backend = "poetry.core.masonry.api" 14 | --------------------------------------------------------------------------------