├── .gitignore ├── poetry.toml ├── .editorconfig ├── AGENTS.md ├── .vscode └── settings.json ├── app ├── schemas.py ├── constants.py ├── logging.py ├── TwitterAccount.py ├── TwitterScrapeBrowser.py └── TwitterGraphQLAPI.py ├── pyproject.toml ├── main.py ├── static └── zendriver_setup.js └── poetry.lock /.gitignore: -------------------------------------------------------------------------------- 1 | .ruff_cache/ 2 | .venv/ 3 | cookies.txt 4 | __pycache__/ 5 | -------------------------------------------------------------------------------- /poetry.toml: -------------------------------------------------------------------------------- 1 | [virtualenvs] 2 | in-project = true 3 | 4 | [virtualenvs.options] 5 | always-copy = false 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | insert_final_newline = true 8 | indent_size = 4 9 | indent_style = space 10 | trim_trailing_whitespace = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | 15 | [*.vue] 16 | insert_final_newline = false 17 | 18 | [*.yaml] 19 | indent_size = 2 20 | 21 | [config*.yaml] 22 | indent_size = 4 23 | -------------------------------------------------------------------------------- /AGENTS.md: -------------------------------------------------------------------------------- 1 | # AGENTS.md 2 | 3 | ## プロジェクト固有の注意事項 4 | 5 | - サーバー側では poetry を使っているので、python コマンドは必ず全て poetry run 経由で実行します。python を直接実行すると .venv/ 以下のライブラリがインストールされていないために失敗します。 6 | 7 | ## コーディング規約 8 | 9 | ### 全般 10 | - コードをざっくり斜め読みした際の可読性を高めるため、日本語のコメントを多めに記述する 11 | - コードを変更する際、既存のコメントは、変更によりコメント内容がコードの記述と合わなくなった場合を除き、コメント量に関わらずそのまま保持する 12 | - ログメッセージに関しては文字化けを避けるため、必ず英語で記述する 13 | - それ以外のコーディングスタイルは、原則変更箇所周辺のコードスタイルに合わせる 14 | - 通常の Web サービスではないかなり特殊なソフトウェアなので、コンテキストとして分からないことがあれば別途 Readme.md を読むか、私に質問すること 15 | 16 | ### Python コード 17 | - コードの編集後は `poetry run task lint` を実行し、Ruff によるコードリンターと Pyright による型チェッカーを実行すること 18 | - 文字列にはシングルクォートを用いる (Docstring を除く) 19 | - Python 3.11 の機能を使う (3.10 以下での動作は考慮不要) 20 | - ビルトイン型を使用した Type Hint で実装する (from typing import List, Dict などは避ける) 21 | - 変数・インスタンス変数は snake_case で命名する 22 | - 関数・クラスは UpperCamelCase で命名する 23 | - FastAPI で定義するエンドポイントの関数名も UpperCamelCase で命名する必要がある 24 | - FastAPI で定義するエンドポイント名は `UserUpdateAPI` のように、名詞 -> 動詞 -> API の順の命名規則を忠実に守ること 25 | - メソッドは lowerCamelCase で命名する 26 | - クラス内のメソッドとメソッドの間には2行の空白行を挿入する 27 | - 複数行のコレクションには末尾カンマを含める 28 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | // 保存時に Ruff による自動フォーマットを行う 3 | "[python]": { 4 | "editor.codeActionsOnSave": { 5 | "source.fixAll.ruff": "explicit", 6 | "source.organizeImports.ruff": "explicit", 7 | }, 8 | "editor.defaultFormatter": "charliermarsh.ruff", 9 | "editor.formatOnSave": true, 10 | }, 11 | // Pylance の Type Checking を有効化 12 | "python.languageServer": "Pylance", 13 | "python.analysis.typeCheckingMode": "strict", 14 | // Pylance の Type Checking のうち、いくつかのエラー報告を抑制する 15 | "python.analysis.diagnosticSeverityOverrides": { 16 | "reportConstantRedefinition": "none", 17 | "reportDeprecated": "warning", 18 | "reportMissingTypeStubs": "none", 19 | "reportPrivateImportUsage": "none", 20 | "reportShadowedImports": "none", 21 | "reportUnnecessaryComparison": "none", 22 | "reportUnknownArgumentType": "none", 23 | "reportUnknownMemberType": "none", 24 | "reportUnknownVariableType": "none", 25 | "reportUnusedFunction": "none", 26 | }, 27 | } 28 | -------------------------------------------------------------------------------- /app/schemas.py: -------------------------------------------------------------------------------- 1 | # ruff: noqa: RUF012 2 | 3 | # Type Hints を指定できるように 4 | # ref: https://stackoverflow.com/a/33533514/17124142 5 | from __future__ import annotations 6 | 7 | from datetime import datetime 8 | 9 | from pydantic import BaseModel 10 | 11 | 12 | # ***** Twitter 連携 ***** 13 | 14 | 15 | class Tweet(BaseModel): 16 | id: str 17 | created_at: datetime 18 | user: TweetUser 19 | text: str 20 | lang: str 21 | via: str 22 | image_urls: list[str] | None 23 | movie_url: str | None 24 | retweet_count: int 25 | retweeted: bool 26 | favorite_count: int 27 | favorited: bool 28 | retweeted_tweet: Tweet | None 29 | quoted_tweet: Tweet | None 30 | 31 | 32 | class TweetUser(BaseModel): 33 | id: str 34 | name: str 35 | screen_name: str 36 | icon_url: str 37 | 38 | 39 | class TwitterAPIResult(BaseModel): 40 | is_success: bool 41 | detail: str 42 | 43 | 44 | class PostTweetResult(TwitterAPIResult): 45 | tweet_url: str 46 | 47 | 48 | class TimelineTweetsResult(TwitterAPIResult): 49 | next_cursor_id: str 50 | previous_cursor_id: str 51 | tweets: list[Tweet] 52 | -------------------------------------------------------------------------------- /app/constants.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from typing import Any 3 | 4 | 5 | # ベースディレクトリ 6 | BASE_DIR = Path(__file__).resolve().parent.parent 7 | 8 | # スタティックディレクトリ 9 | STATIC_DIR = BASE_DIR / 'static' 10 | 11 | # ロギング設定 12 | LOGGING_CONFIG: dict[str, Any] = { 13 | 'version': 1, 14 | 'disable_existing_loggers': False, 15 | 'formatters': { 16 | # サーバーログ用のログフォーマッター 17 | 'default': { 18 | '()': 'uvicorn.logging.DefaultFormatter', 19 | 'datefmt': '%Y/%m/%d %H:%M:%S', 20 | 'format': '[%(asctime)s] %(levelprefix)s %(message)s', 21 | }, 22 | # サーバーログ (デバッグ) 用のログフォーマッター 23 | 'debug': { 24 | '()': 'uvicorn.logging.DefaultFormatter', 25 | 'datefmt': '%Y/%m/%d %H:%M:%S', 26 | 'format': '[%(asctime)s] %(levelprefix)s %(pathname)s:%(lineno)s:\n' 27 | ' %(message)s', 28 | }, 29 | }, 30 | 'handlers': { 31 | 'default': { 32 | 'formatter': 'default', 33 | 'class': 'logging.StreamHandler', 34 | 'stream': 'ext://sys.stderr', 35 | }, 36 | 'debug': { 37 | 'formatter': 'debug', 38 | 'class': 'logging.StreamHandler', 39 | 'stream': 'ext://sys.stderr', 40 | }, 41 | }, 42 | 'loggers': { 43 | 'uvicorn': {'level': 'INFO', 'handlers': ['default']}, 44 | 'uvicorn.debug': {'level': 'DEBUG', 'handlers': ['debug'], 'propagate': False}, 45 | 'uvicorn.error': {'level': 'INFO'}, 46 | }, 47 | } 48 | -------------------------------------------------------------------------------- /app/logging.py: -------------------------------------------------------------------------------- 1 | import ctypes 2 | import logging 3 | import logging.config 4 | import sys 5 | from typing import Any 6 | 7 | from app.constants import LOGGING_CONFIG 8 | 9 | 10 | # ログの色付き表示に必要な ANSI エスケープシーケンスを Windows でも有効化 11 | # conhost.exe では明示的に SetConsoleMode() で有効にしないとエスケープシーケンスがそのまま表示されてしまう 12 | # Windows Terminal なら何もしなくても色付きで表示される 13 | # Windows 7, 8.1 はエスケープシーケンス非対応だけど、クリティカルな不具合ではないのでご愛嬌… 14 | # ref: https://github.com/tiangolo/fastapi/pull/3753 15 | if sys.platform == 'win32': 16 | kernel32 = ctypes.windll.kernel32 17 | kernel32.SetConsoleMode(kernel32.GetStdHandle(-11), 7) 18 | 19 | # Uvicorn を起動する前に Uvicorn のロガーを使えるようにする 20 | logging.config.dictConfig(LOGGING_CONFIG) 21 | 22 | # ロガーを取得 23 | logger = logging.getLogger('uvicorn') 24 | logger_debug = logging.getLogger('uvicorn.debug') 25 | 26 | 27 | def debug(message: Any, *args: Any, exc_info: BaseException | bool | None = None) -> None: 28 | """ 29 | デバッグログを出力する 30 | 31 | Args: 32 | message (Any): ログメッセージ 33 | """ 34 | logger.setLevel(logging.DEBUG) 35 | logger.debug(message, *args, exc_info=exc_info, stacklevel=2) 36 | logger.setLevel(logging.INFO) 37 | 38 | 39 | def info(message: Any, *args: Any, exc_info: BaseException | bool | None = None) -> None: 40 | """ 41 | 情報ログを出力する 42 | 43 | Args: 44 | message (Any): ログメッセージ 45 | """ 46 | logger.info(message, *args, exc_info=exc_info, stacklevel=2) 47 | 48 | 49 | def warning(message: Any, *args: Any, exc_info: BaseException | bool | None = None) -> None: 50 | """ 51 | 警告ログを出力する 52 | 53 | Args: 54 | message (Any): ログメッセージ 55 | """ 56 | logger.warning(message, *args, exc_info=exc_info, stacklevel=2) 57 | 58 | 59 | def error(message: Any, *args: Any, exc_info: BaseException | bool | None = None) -> None: 60 | """ 61 | エラーログを出力する 62 | 63 | Args: 64 | message (Any): ログメッセージ 65 | """ 66 | logger.error(message, *args, exc_info=exc_info, stacklevel=2) 67 | -------------------------------------------------------------------------------- /app/TwitterAccount.py: -------------------------------------------------------------------------------- 1 | # Type Hints を指定できるように 2 | # ref: https://stackoverflow.com/a/33533514/17124142 3 | from __future__ import annotations 4 | 5 | import json 6 | from typing import Any, cast 7 | 8 | from tortoise import fields 9 | from tortoise.fields import Field as TortoiseField 10 | from tortoise.models import Model as TortoiseModel 11 | 12 | 13 | class User(TortoiseModel): 14 | # データベース上のテーブル名 15 | class Meta(TortoiseModel.Meta): 16 | table: str = 'users' 17 | 18 | # テーブル設計は Notion を参照のこと 19 | id = fields.IntField(pk=True) 20 | name = fields.TextField() 21 | password = fields.TextField() 22 | is_admin = fields.BooleanField() 23 | client_settings = cast( 24 | TortoiseField[dict[str, Any]], 25 | fields.JSONField(default={}, encoder=lambda x: json.dumps(x, ensure_ascii=False)), # type: ignore 26 | ) 27 | niconico_user_id = cast(TortoiseField[int | None], fields.IntField(null=True)) 28 | niconico_user_name = cast(TortoiseField[str | None], fields.TextField(null=True)) 29 | niconico_user_premium = cast(TortoiseField[bool | None], fields.BooleanField(null=True)) 30 | niconico_access_token = cast(TortoiseField[str | None], fields.TextField(null=True)) 31 | niconico_refresh_token = cast(TortoiseField[str | None], fields.TextField(null=True)) 32 | twitter_accounts: fields.ReverseRelation[TwitterAccount] 33 | created_at = fields.DatetimeField(auto_now_add=True) 34 | updated_at = fields.DatetimeField(auto_now=True) 35 | 36 | 37 | class TwitterAccount(TortoiseModel): 38 | # データベース上のテーブル名 39 | class Meta(TortoiseModel.Meta): 40 | table: str = 'twitter_accounts' 41 | 42 | # テーブル設計は Notion を参照のこと 43 | id = fields.IntField(pk=True) 44 | user: fields.ForeignKeyRelation[User] = fields.ForeignKeyField( 45 | 'models.User', related_name='twitter_accounts', on_delete=fields.CASCADE 46 | ) 47 | user_id: int 48 | name = fields.TextField() 49 | screen_name = fields.TextField() 50 | icon_url = fields.TextField() 51 | access_token = fields.TextField() 52 | access_token_secret = fields.TextField() 53 | created_at = fields.DatetimeField(auto_now_add=True) 54 | updated_at = fields.DatetimeField(auto_now=True) 55 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | package-mode = false 3 | 4 | [tool.taskipy.tasks] 5 | lint = "ruff check --fix . ; pyright" 6 | format = "ruff format ." 7 | typecheck = "pyright" 8 | 9 | [tool.poetry.dependencies] 10 | python = ">=3.11,<3.12" 11 | pydantic = "^2.10.3" 12 | pyright = "^1.1.403" 13 | ruff = "^0.9.1" 14 | tortoise-orm = "^0.23.0" 15 | uvicorn = {version = "^0.34.0", extras = ["standard"]} 16 | zendriver = "^0.15.0" 17 | 18 | [tool.poetry.group.dev.dependencies] 19 | 20 | [build-system] 21 | requires = ["poetry-core"] 22 | build-backend = "poetry.core.masonry.api" 23 | 24 | [tool.ruff] 25 | # 1行の長さを最大120文字に設定 26 | line-length = 120 27 | # インデントの幅を4スペースに設定 28 | indent-width = 4 29 | # Python 3.11 を利用する 30 | target-version = "py311" 31 | # data/, thirdparty/ ディレクトリは対象から除外する 32 | extend-exclude = ["data/", "thirdparty/"] 33 | 34 | [tool.ruff.lint] 35 | # flake8, pycodestyle, pyupgrade, isort, Ruff 固有のルールを使う 36 | select = ["F", "E", "W", "UP", "I", "RUF", "TID251"] 37 | ignore = [ 38 | "E501", # 1行の長さを超えている場合の警告を抑制 39 | "E731", # Do not assign a `lambda` expression, use a `def` を抑制 40 | "RUF001", # 全角記号など `ambiguous unicode character` も使いたいため 41 | "RUF002", # 全角記号など `ambiguous unicode character` も使いたいため 42 | "RUF003", # 全角記号など `ambiguous unicode character` も使いたいため 43 | "RUF012", 44 | ] 45 | 46 | [tool.ruff.lint.isort] 47 | # インポートブロックの後に2行空ける 48 | lines-after-imports = 2 49 | 50 | [tool.ruff.format] 51 | # シングルクオートを使う 52 | quote-style = "single" 53 | # インデントにはスペースを使う 54 | indent-style = "space" 55 | 56 | [tool.ruff.lint.flake8-tidy-imports.banned-api] 57 | # Python 3.11 + Pydantic で TypedDict を扱う際は、typing_extensions.TypedDict を使う必要がある 58 | # ref: https://github.com/langchain-ai/langgraph/pull/2910 59 | "typing.TypedDict".msg = "Use typing_extensions.TypedDict instead." 60 | 61 | [tool.pyright] 62 | # Python バージョンを指定 63 | pythonVersion = "3.11" 64 | # TypeCheckingMode を strict に設定(VSCode の設定と同等) 65 | typeCheckingMode = "strict" 66 | # プロジェクトルートを指定 67 | pythonPlatform = "All" 68 | # 除外するパス 69 | exclude = [ 70 | "**/__pycache__", 71 | "**/.venv", 72 | ] 73 | # VSCode の settings.json の diagnosticSeverityOverrides に対応する設定 74 | reportConstantRedefinition = "none" 75 | reportDeprecated = "warning" 76 | reportMissingTypeStubs = "none" 77 | reportPrivateImportUsage = "none" 78 | reportUnnecessaryComparison = "none" 79 | reportUnknownArgumentType = "none" 80 | reportUnknownMemberType = "none" 81 | reportUnknownVariableType = "none" 82 | reportUnusedFunction = "none" 83 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from datetime import datetime 3 | from pathlib import Path 4 | from pprint import pprint 5 | 6 | from app import logging 7 | from app.TwitterAccount import TwitterAccount 8 | from app.TwitterGraphQLAPI import TwitterGraphQLAPI 9 | 10 | 11 | async def main(): 12 | logging.info('Starting test...') 13 | 14 | # cookies.txt を読み込む 15 | cookies_txt_path = Path(__file__).parent / 'cookies.txt' 16 | cookies_txt_content = '' 17 | if cookies_txt_path.exists(): 18 | cookies_txt_content = cookies_txt_path.read_text(encoding='utf-8') 19 | logging.info('Loaded cookies.txt') 20 | else: 21 | logging.info('cookies.txt not found, using empty cookies') 22 | 23 | # TwitterAccount インスタンスを作成 24 | # access_token は "NETSCAPE_COOKIE_FILE" 固定、access_token_secret に cookies.txt の内容を入れる 25 | twitter_account = TwitterAccount() 26 | twitter_account.id = 1 27 | twitter_account.name = 'Demo Account' 28 | twitter_account.screen_name = 'demo_account' 29 | twitter_account.icon_url = 'https://example.com/icon.png' 30 | twitter_account.access_token = 'NETSCAPE_COOKIE_FILE' 31 | twitter_account.access_token_secret = cookies_txt_content 32 | 33 | # TwitterGraphQLAPI インスタンスを取得(シングルトン) 34 | api = TwitterGraphQLAPI(twitter_account) 35 | 36 | # 現在のログイン中ユーザー情報取得 API を呼び出す 37 | logging.info('Calling Viewer API...') 38 | result = await api.fetchLoggedViewer() 39 | if isinstance(result, str): 40 | # エラーメッセージが返された場合 41 | logging.error(f'Viewer API call failed: {result}') 42 | else: 43 | # 成功時はレスポンスデータが返される 44 | logging.info('Viewer API call succeeded.') 45 | logging.info('Result:') 46 | pprint(result) 47 | 48 | # CreateTweet API を呼び出す 49 | logging.info('Calling CreateTweet API...') 50 | result = await api.createTweet( 51 | tweet='Hello, world!' + datetime.now().strftime('%Y-%m-%d %H:%M:%S'), 52 | ) 53 | if isinstance(result, str): 54 | # エラーメッセージが返された場合 55 | logging.error(f'CreateTweet API call failed: {result}') 56 | else: 57 | # 成功時はレスポンスデータが返される 58 | logging.info('CreateTweet API call succeeded.') 59 | logging.info('Result:') 60 | pprint(result) 61 | 62 | # ブラウザが自動的にシャットダウンされるまでしばらく待つ 63 | await asyncio.sleep(35) 64 | 65 | # ブラウザ停止前に x.com 関連の Cookie を取得して cookies.txt を更新 66 | logging.info('Saving cookies to cookies.txt...') 67 | try: 68 | # TwitterAccount.access_token_secret に最新の Cookie がセットされているはず 69 | updated_cookies_content = twitter_account.access_token_secret 70 | cookies_txt_path.write_text(updated_cookies_content, encoding='utf-8') 71 | logging.info('Successfully saved cookies to cookies.txt') 72 | except Exception as ex: 73 | logging.error(f'Error saving cookies: {ex}', exc_info=ex) 74 | 75 | logging.info('Test completed.') 76 | 77 | 78 | if __name__ == '__main__': 79 | asyncio.run(main()) 80 | -------------------------------------------------------------------------------- /static/zendriver_setup.js: -------------------------------------------------------------------------------- 1 | window.__invokeGraphQLAPISetupPromise = (async () => { 2 | 3 | // 以下の実装を強く参考にした (thanks to @fa0311 !!) 4 | // ref: https://gist.github.com/fa0311/f36b00d36d6c4cf9e73c0dd5aefe3516 5 | 6 | // operationInfo を収集する必要がある operationName のセット 7 | const requiredOperationNames = new Set([ 8 | 'Viewer', 9 | 'CreateTweet', 10 | 'CreateRetweet', 11 | 'DeleteRetweet', 12 | 'FavoriteTweet', 13 | 'UnfavoriteTweet', 14 | 'HomeLatestTimeline', 15 | 'SearchTimeline', 16 | ]); 17 | 18 | // GraphQL API の operationInfo を収集し、キー: operationName, 値: operationInfo の Map を作る 19 | const operationInfoMap = await new Promise((resolve) => { 20 | const collectedOperationNames = new Set(); 21 | const operationInfoMap = {}; 22 | // オリジナルの Function.prototype.call を保存 23 | const originalCall = Function.prototype.call; 24 | // クリーンアップが1回だけ実行されるようにするフラグ 25 | let isCleanedUp = false; 26 | // クリーンアップ処理 (Function.prototype.call を元に戻す) 27 | const cleanup = () => { 28 | if (isCleanedUp) return; 29 | isCleanedUp = true; 30 | Function.prototype.call = originalCall; 31 | }; 32 | // タイムアウトタイマーの ID を保存 33 | let timeoutTimerId = null; 34 | // タイムアウト用 Promise 35 | const timeoutPromise = new Promise((_, timeoutReject) => { 36 | timeoutTimerId = setTimeout(() => { 37 | cleanup(); 38 | timeoutReject(new Error('Operation info collection timeout')); 39 | }, 10 * 1000); // 10 秒でタイムアウト 40 | }); 41 | const collectionPromise = new Promise((collectionResolve) => { 42 | // Function.prototype.call を上書きする 43 | Function.prototype.call = function (thisArg, ...args) { 44 | const module = args[0]; 45 | const ret = originalCall.apply(this, [thisArg, ...args]); 46 | try { 47 | const exp = module.exports; 48 | if (exp.operationName) { 49 | operationInfoMap[exp.operationName] = exp; 50 | collectedOperationNames.add(exp.operationName); 51 | // 必要な operationInfo が全て揃ったかチェック 52 | const isAllCollected = Array.from(requiredOperationNames).every( 53 | name => collectedOperationNames.has(name) 54 | ); 55 | if (isAllCollected) { 56 | // タイムアウトタイマーをクリア 57 | if (timeoutTimerId !== null) { 58 | clearTimeout(timeoutTimerId); 59 | timeoutTimerId = null; 60 | } 61 | cleanup(); 62 | collectionResolve(operationInfoMap); 63 | } 64 | } 65 | } catch (_) {} 66 | return ret; 67 | }; 68 | }); 69 | // Promise.race でタイムアウトと収集を競合させる 70 | Promise.race([collectionPromise, timeoutPromise]) 71 | .then((result) => { 72 | resolve(result); 73 | }) 74 | .catch((error) => { 75 | // タイムアウト時は現在の operationInfoMap を返す 76 | resolve(operationInfoMap); 77 | }); 78 | }); 79 | console.log("operationInfoMap:", operationInfoMap); 80 | 81 | // Twitter Web App が内部で使用している API クライアント実装のオブジェクトを収集 82 | const apiClient = await new Promise((resolve, reject) => { 83 | // オリジナルの Function.prototype.apply を保存 84 | const __origApply = Function.prototype.apply; 85 | // クリーンアップが1回だけ実行されるようにするフラグ 86 | let isCleanedUp = false; 87 | // クリーンアップ処理 (Function.prototype.apply を元に戻す) 88 | const cleanup = () => { 89 | if (isCleanedUp) return; 90 | isCleanedUp = true; 91 | Function.prototype.apply = __origApply; 92 | }; 93 | // タイムアウトタイマーの ID を保存 94 | let timeoutTimerId = null; 95 | // タイムアウト用 Promise 96 | const timeoutPromise = new Promise((_, timeoutReject) => { 97 | timeoutTimerId = setTimeout(() => { 98 | cleanup(); 99 | timeoutReject(new Error('API client collection timeout')); 100 | }, 10 * 1000); // 10 秒でタイムアウト 101 | }); 102 | const collectionPromise = new Promise((collectionResolve) => { 103 | // Function.prototype.apply を上書きする 104 | Function.prototype.apply = function (thisArg, argsArray) { 105 | if (thisArg && typeof thisArg === 'object' && thisArg.dispatch === this) { 106 | // タイムアウトタイマーをクリア 107 | if (timeoutTimerId !== null) { 108 | clearTimeout(timeoutTimerId); 109 | timeoutTimerId = null; 110 | } 111 | cleanup(); 112 | collectionResolve(thisArg); 113 | } 114 | return __origApply.call(this, thisArg, argsArray); 115 | }; 116 | }); 117 | // Promise.race でタイムアウトと収集を競合させる 118 | Promise.race([collectionPromise, timeoutPromise]) 119 | .then((result) => { 120 | resolve(result); 121 | }) 122 | .catch((error) => { 123 | // タイムアウト時はエラーを返す(apiClient は必須のため) 124 | reject(error); 125 | }); 126 | }); 127 | console.log("apiClient:", apiClient); 128 | 129 | // API クライアントのラッパーを作成し、これを window オブジェクトに公開する 130 | window.__invokeGraphQLAPI = async (operationName, requestPayload, additionalFlags = null) => { 131 | // オリジナルの XMLHttpRequest を保存 132 | const OriginalXHR = window.XMLHttpRequest; 133 | // HTTP リクエストのフックで取得する API レスポンスを格納するオブジェクト 134 | const responseData = { 135 | parsedResponse: null, 136 | responseText: null, 137 | statusCode: null, 138 | headers: null, 139 | requestError: null, 140 | }; 141 | // HTTP リクエストをフックして API レスポンスを取得する 142 | window.XMLHttpRequest = function() { 143 | const xhr = new OriginalXHR(); 144 | const originalOpen = xhr.open.bind(xhr); 145 | const originalSend = xhr.send.bind(xhr); 146 | xhr.open = function(method, url, ...args) { 147 | // GraphQL API のエンドポイントかどうかを判定 148 | if (url && url.includes('/graphql/')) { 149 | // onreadystatechange をフック 150 | xhr.addEventListener('readystatechange', function() { 151 | if (xhr.readyState === 4) { 152 | responseData.statusCode = xhr.status; 153 | responseData.responseText = xhr.responseText; 154 | responseData.headers = {}; 155 | // レスポンスヘッダーを取得 156 | const headerString = xhr.getAllResponseHeaders(); 157 | if (headerString) { 158 | const headerPairs = headerString.trim().split('\r\n'); 159 | for (const headerPair of headerPairs) { 160 | const [key, value] = headerPair.split(': '); 161 | if (key && value) { 162 | responseData.headers[key.toLowerCase()] = value; 163 | } 164 | } 165 | } 166 | // レスポンスをパース 167 | try { 168 | if (responseData.responseText) { 169 | responseData.parsedResponse = JSON.parse(responseData.responseText); 170 | } 171 | } catch (e) { 172 | // JSON パースに失敗した場合は responseText をそのまま保持 173 | responseData.parsedResponse = null; 174 | } 175 | } 176 | }); 177 | // onerror をフック 178 | xhr.addEventListener('error', function() { 179 | responseData.requestError = 'Request failed'; 180 | }); 181 | // ontimeout をフック 182 | xhr.addEventListener('timeout', function() { 183 | responseData.requestError = 'Request timeout'; 184 | }); 185 | } 186 | return originalOpen(method, url, ...args); 187 | }; 188 | xhr.send = function(...args) { 189 | return originalSend(...args); 190 | }; 191 | return xhr; 192 | }; 193 | // XMLHttpRequest のプロトタイプをコピー 194 | window.XMLHttpRequest.prototype = OriginalXHR.prototype; 195 | try { 196 | // operationName から operationInfo を取得 197 | const operationInfo = operationInfoMap[operationName]; 198 | // HTTP リクエストを実行 199 | // X-Client-Transaction-ID や各ヘッダーの付与はすべて内部で行われる 200 | // XHR フックで生のレスポンスを取得するため、戻り値は使用しない 201 | if (additionalFlags) { 202 | // 第三引数はおそらくサーバーからエラーが返された際に致命的なエラーかをチェックする関数 203 | await apiClient.graphQL(operationInfo, requestPayload, () => false, additionalFlags); 204 | } else { 205 | await apiClient.graphQL(operationInfo, requestPayload); 206 | } 207 | // XMLHttpRequest を元に戻す 208 | window.XMLHttpRequest = OriginalXHR; 209 | // API レスポンスを返す 210 | return { 211 | parsedResponse: responseData.parsedResponse, 212 | responseText: responseData.responseText, 213 | statusCode: responseData.statusCode, 214 | headers: responseData.headers, 215 | }; 216 | } catch (error) { 217 | // XMLHttpRequest を元に戻す 218 | window.XMLHttpRequest = OriginalXHR; 219 | // エラーが発生した場合、取得できたレスポンスがあればそれを含めて返す 220 | return { 221 | parsedResponse: responseData.parsedResponse, 222 | responseText: responseData.responseText, 223 | statusCode: responseData.statusCode, 224 | headers: responseData.headers, 225 | requestError: responseData.requestError, 226 | }; 227 | } 228 | } 229 | 230 | return true; 231 | })(); 232 | -------------------------------------------------------------------------------- /app/TwitterScrapeBrowser.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import json 3 | from typing import Any 4 | 5 | from zendriver import Browser, Tab, cdp 6 | 7 | from app import logging 8 | from app.constants import STATIC_DIR 9 | from app.TwitterAccount import TwitterAccount 10 | 11 | 12 | class TwitterScrapeBrowser: 13 | """ 14 | Twitter のヘッドレスブラウザ操作を隠蔽するクラス 15 | ブラウザのセットアップ処理や、ブラウザインスタンス自身の低レベル操作を隠蔽する 16 | """ 17 | 18 | def __init__(self, twitter_account: TwitterAccount) -> None: 19 | """ 20 | TwitterScrapeBrowser を初期化する 21 | 22 | Args: 23 | twitter_account (TwitterAccount): Twitter アカウントのモデル 24 | """ 25 | 26 | self.twitter_account = twitter_account 27 | 28 | # ZenDriver のブラウザインスタンス 29 | self._browser: Browser | None = None 30 | # 現在アクティブなタブ(ページ)インスタンス 31 | self._page: Tab | None = None 32 | 33 | # セットアップ処理が完了したかどうかのフラグ 34 | self.is_setup_complete = False 35 | # セットアップ・シャットダウン処理の排他制御用ロック 36 | ## setup と shutdown が同時に実行されないようにするため、同じロックを使用する 37 | self._setup_lock = asyncio.Lock() 38 | 39 | async def setup(self) -> None: 40 | """ 41 | ヘッドレスブラウザを起動し、Cookie の供給などのセットアップ処理を行う 42 | 既にセットアップ済みの場合は何も行われない 43 | """ 44 | 45 | # セットアップ処理が複数進行していないことを確認 46 | async with self._setup_lock: 47 | # 既にセットアップ済みの場合は何もしない 48 | if self.is_setup_complete is True: 49 | return 50 | 51 | # セットアップ処理の完了を把握するための Future を作成 52 | setup_complete_future = asyncio.get_running_loop().create_future() 53 | 54 | # ZenDriver でブラウザを起動 55 | logging.info(f'[TwitterScrapeBrowser][@{self.twitter_account.screen_name}] Starting browser...') 56 | self._browser = await Browser.create( 57 | # ユーザーデータディレクトリはあえて設定せず、立ち上げたプロセスが終了したらプロファイルも消えるようにする 58 | # Cookie に関しては別途 DB と同期・永続化されていて、毎回セットアップ時に復元されるため問題はない 59 | user_data_dir=None, 60 | # 今の所ウインドウを表示せずとも問題なく動作しているので、ヘッドレスモードで起動する 61 | headless=False, 62 | # ブラウザは現在の環境にインストールされているものを自動選択させる 63 | browser='auto', 64 | ) 65 | logging.info(f'[TwitterScrapeBrowser][@{self.twitter_account.screen_name}] Browser started.') 66 | 67 | # まず空のタブを開く 68 | self._page = await self._browser.get('about:blank') 69 | logging.debug(f'[TwitterScrapeBrowser][@{self.twitter_account.screen_name}] Blank page opened.') 70 | 71 | # cookies.txt の内容をパースして Cookie を設定 72 | # access_token_secret に cookies.txt の内容が入っている想定 73 | if self.twitter_account.access_token_secret: 74 | cookie_params = self.parseNetscapeCookieFile(self.twitter_account.access_token_secret) 75 | logging.debug( 76 | f'[TwitterScrapeBrowser][@{self.twitter_account.screen_name}] Found {len(cookie_params)} cookies in cookies.txt.' 77 | ) 78 | # 読み込んだ CookieParam のリストを CookieJar に一括で設定 79 | try: 80 | await self._browser.cookies.set_all(cookie_params) 81 | logging.info( 82 | f'[TwitterScrapeBrowser][@{self.twitter_account.screen_name}] Successfully set {len(cookie_params)} cookies.' 83 | ) 84 | except Exception as ex: 85 | logging.error( 86 | f'[TwitterScrapeBrowser][@{self.twitter_account.screen_name}] Error setting cookies: {ex}', 87 | exc_info=ex, 88 | ) 89 | else: 90 | logging.warning( 91 | f'[TwitterScrapeBrowser][@{self.twitter_account.screen_name}] cookies.txt content is empty, skipping Cookie loading.' 92 | ) 93 | 94 | # Debugger を有効化 95 | await self._page.send(cdp.debugger.enable()) 96 | logging.debug(f'[TwitterScrapeBrowser][@{self.twitter_account.screen_name}] DevTools debugger enabled.') 97 | 98 | # zendriver_setup.js の内容を読み込む 99 | setup_js_path = STATIC_DIR / 'zendriver_setup.js' 100 | setup_js_code = setup_js_path.read_text(encoding='utf-8') 101 | 102 | # Debugger.paused イベントをリッスン 103 | async def on_paused(event: cdp.debugger.Paused) -> None: 104 | logging.debug(f'[TwitterScrapeBrowser][@{self.twitter_account.screen_name}] Pause event fired.') 105 | assert self._page is not None 106 | page = self._page 107 | try: 108 | # ブレークポイント停止中に zendriver_setup.js のコードをブラウザタブ側に設置する 109 | ## await_promise は指定しない(デフォルトは False)ので、スクリプトは設置されるが待機しない 110 | ## その後、ブレークポイントから実行を再開すると、設置されたスクリプトが実行される 111 | _, exception = await page.send( 112 | cdp.runtime.evaluate( 113 | expression=setup_js_code, 114 | return_by_value=True, 115 | ) 116 | ) 117 | logging.debug( 118 | f'[TwitterScrapeBrowser][@{self.twitter_account.screen_name}] zendriver_setup.js executed.' 119 | ) 120 | if exception is not None: 121 | # 実行中になんらかの例外が発生した場合 122 | setup_complete_future.set_exception( 123 | Exception(f'Failed to execute zendriver_setup.js: {exception}') 124 | ) 125 | except Exception as ex: 126 | setup_complete_future.set_exception(ex) 127 | finally: 128 | # ブレークポイントから実行を再開 129 | await page.send(cdp.debugger.resume()) 130 | try: 131 | # 再開後に少し待つ (でないと window.__invokeGraphQLAPISetupPromise 自体がまだセットされていない可能性がある) 132 | await asyncio.sleep(1) 133 | logging.info( 134 | f'[TwitterScrapeBrowser][@{self.twitter_account.screen_name}] Waiting for zendriver_setup.js to be resolved...' 135 | ) 136 | # 再開後、window.__invokeGraphQLAPISetupPromise の Promise が解決されるまで待つ 137 | result, exception = await page.send( 138 | cdp.runtime.evaluate( 139 | expression='window.__invokeGraphQLAPISetupPromise', 140 | await_promise=True, 141 | return_by_value=True, 142 | ) 143 | ) 144 | logging.debug( 145 | f'[TwitterScrapeBrowser][@{self.twitter_account.screen_name}] zendriver_setup.js evaluated.' 146 | ) 147 | if exception is not None: 148 | setup_complete_future.set_exception( 149 | Exception(f'Failed to wait for setup promise: {exception}') 150 | ) 151 | else: 152 | # result.value が厳密に True であることを確認(undefined の可能性を排除) 153 | if result.value is True: 154 | logging.debug( 155 | f'[TwitterScrapeBrowser][@{self.twitter_account.screen_name}] zendriver_setup.js resolved.' 156 | ) 157 | setup_complete_future.set_result(True) 158 | else: 159 | setup_complete_future.set_exception( 160 | Exception(f'Setup promise did not return true. Got: {result.value}') 161 | ) 162 | except Exception as ex: 163 | setup_complete_future.set_exception(ex) 164 | 165 | self._page.add_handler(cdp.debugger.Paused, on_paused) 166 | 167 | # x.com の main.js の1行目にブレークポイントを設定 168 | ## ブレークポイントが発火すると on_paused ハンドラーが呼ばれ、zendriver_setup.js が実行される 169 | ## 正規表現はパスが main..js 形式のファイルのみにマッチするように厳密化している 170 | breakpoint_id, _ = await self._page.send( 171 | cdp.debugger.set_breakpoint_by_url( 172 | line_number=0, # 0-based なので 1行目は 0 173 | url_regex=r'^.*?main\.[a-fA-F0-9]+\.js$', # main..js を厳密にマッチさせる正規表現 174 | ) 175 | ) 176 | logging.debug( 177 | f'[TwitterScrapeBrowser][@{self.twitter_account.screen_name}] Breakpoint set. id: {breakpoint_id}' 178 | ) 179 | 180 | # x.com に移動 181 | ## x.com/home だと万が一 Cookie セッションが revoke されている場合にログインモーダルが表示されて 182 | ## セットアップが解決できないっぽいので、ログイン前の画面がそのまま出てくる x.com/ 直下である必要がある 183 | self._page = await self._browser.get('https://x.com/') 184 | await self._page.activate() 185 | 186 | # zendriver_setup.js に記述したセットアップ処理が完了するまで待つ 187 | try: 188 | await asyncio.wait_for(setup_complete_future, timeout=15.0) 189 | logging.info( 190 | f'[TwitterScrapeBrowser][@{self.twitter_account.screen_name}] Setup completed successfully.' 191 | ) 192 | # セットアップ完了後、もうブレークポイントを打つ必要はないのでデバッガを無効化 193 | try: 194 | await self._page.send(cdp.debugger.disable()) 195 | logging.debug( 196 | f'[TwitterScrapeBrowser][@{self.twitter_account.screen_name}] DevTools debugger disabled.' 197 | ) 198 | except Exception as ex: 199 | logging.error( 200 | f'[TwitterScrapeBrowser][@{self.twitter_account.screen_name}] Error disabling debugger: {ex}', 201 | exc_info=ex, 202 | ) 203 | self.is_setup_complete = True 204 | except TimeoutError as ex: 205 | logging.error( 206 | f'[TwitterScrapeBrowser][@{self.twitter_account.screen_name}] Timeout: Breakpoint was not hit or setup did not complete within 15 seconds.' 207 | ) 208 | self.is_setup_complete = False 209 | raise ex 210 | except Exception as ex: 211 | logging.error( 212 | f'[TwitterScrapeBrowser][@{self.twitter_account.screen_name}] Error during setup: {ex}', exc_info=ex 213 | ) 214 | self.is_setup_complete = False 215 | raise ex 216 | 217 | async def invokeGraphQLAPI( 218 | self, 219 | endpoint_name: str, 220 | variables: dict[str, Any], 221 | additional_flags: dict[str, Any] | None = None, 222 | ) -> dict[str, Any]: 223 | """ 224 | ヘッドレスブラウザ越しに、Twitter Web App が持つ内部 GraphQL API クライアントに対して HTTP リクエストの実行を要求する 225 | エラー処理は行わず、生のレスポンスデータを返す(エラー処理は TwitterGraphQLAPI 側で行う) 226 | 227 | Args: 228 | endpoint_name (str): GraphQL API のエンドポイント名 (例: 'CreateTweet') 229 | variables (dict[str, Any]): GraphQL API へのリクエストパラメータ (ペイロードのうち "variables" の部分) 230 | additional_flags (dict[str, Any] | None): 追加のフラグ(オプション) 231 | 232 | Returns: 233 | dict[str, Any]: 生のレスポンスデータ(parsedResponse, statusCode, responseText, headers, requestError を含む) 234 | """ 235 | 236 | # 通常、ブラウザが起動していない時にこのメソッドが呼ばれることはない(呼ばれた場合は何かがバグっている) 237 | if self._browser is None or self._page is None: 238 | raise RuntimeError('Browser or page is not initialized.') 239 | 240 | # JavaScript コードを構築(JSON を文字列化して渡す) 241 | # エラー処理は JavaScript 側で完結し、シリアライズ可能なデータを返す 242 | additional_flags_json = ( 243 | json.dumps(additional_flags, ensure_ascii=False) if additional_flags is not None else 'null' 244 | ) 245 | js_code = f""" 246 | (async () => {{ 247 | const requestPayload = {json.dumps(variables, ensure_ascii=False)}; 248 | const additionalFlags = {additional_flags_json}; 249 | const result = await window.__invokeGraphQLAPI('{endpoint_name}', requestPayload, additionalFlags); 250 | console.log('window.__invokeGraphQLAPI() result:', result); 251 | return result; 252 | }})() 253 | """ 254 | 255 | # Twitter GraphQL API に HTTP リクエストを送信する 256 | result, exception = await self._page.send( 257 | cdp.runtime.evaluate( 258 | expression=js_code, 259 | await_promise=True, 260 | return_by_value=True, 261 | ) 262 | ) 263 | 264 | # 例外が発生した場合はそのまま例外を投げる 265 | if exception is not None: 266 | raise RuntimeError(f'Failed to execute JavaScript: {exception}') 267 | 268 | # 結果が None の場合は例外を投げる 269 | if result.value is None: 270 | raise RuntimeError('Response is None.') 271 | 272 | # JavaScript 側から返された結果を取得 273 | result_value = result.value 274 | if not isinstance(result_value, dict): 275 | raise RuntimeError(f'Response is not a dict. Got: {type(result_value)}') 276 | 277 | # API レスポンスを取得 278 | parsed_response = result_value.get('parsedResponse') 279 | status_code = result_value.get('statusCode') 280 | response_text = result_value.get('responseText') 281 | headers = result_value.get('headers') 282 | request_error = result_value.get('requestError') 283 | 284 | # 生のレスポンスデータを返す(エラー処理は TwitterGraphQLAPI 側で行う) 285 | return { 286 | 'parsedResponse': parsed_response, 287 | 'statusCode': status_code, 288 | 'responseText': response_text, 289 | 'headers': headers, 290 | 'requestError': request_error, 291 | } 292 | 293 | async def shutdown(self) -> None: 294 | """ 295 | 使われなくなったヘッドレスブラウザを安全にシャットダウンする 296 | シャットダウン中は setup() や shutdown() が同時に呼ばれないように、self.setup_lock を使用して排他制御する 297 | """ 298 | 299 | # セットアップ・シャットダウン処理の排他制御 300 | ## シャットダウン中に setup が呼ばれると状態が競合するため、同じロックを使用する 301 | async with self._setup_lock: 302 | if self._browser is None: 303 | logging.warning( 304 | f'[TwitterScrapeBrowser][@{self.twitter_account.screen_name}] Browser is not initialized, skipping shutdown.' 305 | ) 306 | return 307 | 308 | # セットアップ完了フラグをリセット(シャットダウン開始時点でセットアップ状態を無効化) 309 | ## これにより、シャットダウン中に setup が呼ばれた場合でも、シャットダウン完了後に再度セットアップが必要になる 310 | self.is_setup_complete = False 311 | 312 | # ブラウザを停止 313 | logging.info( 314 | f'[TwitterScrapeBrowser][@{self.twitter_account.screen_name}] Waiting for browser to terminate...' 315 | ) 316 | try: 317 | await self._browser.stop() 318 | logging.info(f'[TwitterScrapeBrowser][@{self.twitter_account.screen_name}] Browser terminated.') 319 | except Exception as ex: 320 | logging.error( 321 | f'[TwitterScrapeBrowser][@{self.twitter_account.screen_name}] Error while terminating browser: {ex}', 322 | exc_info=ex, 323 | ) 324 | 325 | self._browser = None 326 | self._page = None 327 | 328 | async def saveTwitterCookiesToNetscapeFormat(self) -> str: 329 | """ 330 | 起動中のヘッドレスブラウザから x.com 関連の Cookie を取得し、Netscape フォーマットの文字列に変換して返す 331 | 332 | Returns: 333 | str: Netscape フォーマットの Cookie ファイルの内容 334 | """ 335 | 336 | # 通常、ブラウザが起動していない時にこのメソッドが呼ばれることはない(呼ばれた場合は何かがバグっている) 337 | if self._browser is None: 338 | raise RuntimeError('Browser is not initialized.') 339 | 340 | # 全ての Cookie を取得 341 | all_cookies = await self._browser.cookies.get_all(requests_cookie_format=False) 342 | # requests_cookie_format=False なので cdp.network.Cookie のリストが返される 343 | # x.com に関連する Cookie をフィルタリング 344 | twitter_cookies: list[cdp.network.Cookie] = [ 345 | c for c in all_cookies if isinstance(c, cdp.network.Cookie) and ('x.com' in c.domain) 346 | ] 347 | 348 | # Netscape フォーマットで文字列を構築 349 | lines: list[str] = [] 350 | # Netscape フォーマットのヘッダーを追加 351 | lines.append('# Netscape HTTP Cookie File') 352 | lines.append('# https://curl.haxx.se/rfc/cookie_spec.html') 353 | lines.append('# This is a generated file! Do not edit.') 354 | lines.append('') 355 | 356 | if not twitter_cookies: 357 | logging.warning( 358 | f'[TwitterScrapeBrowser][@{self.twitter_account.screen_name}] No Twitter-related cookies found, returning empty Netscape format.' 359 | ) 360 | return '\n'.join(lines) 361 | 362 | # 各 Cookie を Netscape フォーマットで追加 363 | for cookie in twitter_cookies: 364 | # domain がドットで始まる場合は flag を TRUE、そうでなければ FALSE 365 | flag = 'TRUE' if cookie.domain.startswith('.') else 'FALSE' 366 | # secure フラグを TRUE/FALSE に変換 367 | secure_str = 'TRUE' if cookie.secure else 'FALSE' 368 | # expires が None の場合は 0(セッション cookie)を設定 369 | expires_value = int(cookie.expires) if cookie.expires is not None else 0 370 | # Netscape フォーマット: domain, flag, path, secure, expiration, name, value 371 | netscape_line = ( 372 | f'{cookie.domain}\t{flag}\t{cookie.path}\t{secure_str}\t{expires_value}\t{cookie.name}\t{cookie.value}' 373 | ) 374 | lines.append(netscape_line) 375 | 376 | return '\n'.join(lines) 377 | 378 | @staticmethod 379 | def parseNetscapeCookieFile(cookies_content: str) -> list[cdp.network.CookieParam]: 380 | """ 381 | Netscape フォーマットの Cookie 文字列をパースして CookieParam に変換する 382 | 383 | Args: 384 | cookies_content (str): Cookie ファイルの内容 385 | 386 | Returns: 387 | list[cdp.network.CookieParam]: CookieParam のリスト 388 | """ 389 | 390 | cookie_params: list[cdp.network.CookieParam] = [] 391 | for line in cookies_content.splitlines(): 392 | line = line.strip() 393 | # コメント行や空行をスキップ 394 | if not line or line.startswith('#'): 395 | continue 396 | # Netscape フォーマット: domain, flag, path, secure, expiration, name, value 397 | parts = line.split('\t') 398 | if len(parts) < 7: 399 | continue 400 | domain = parts[0] 401 | # flag (parts[1]) は使用しない 402 | path = parts[2] 403 | secure = parts[3] == 'TRUE' 404 | expires_str = parts[4] 405 | expires = int(expires_str) if expires_str and expires_str != '0' else None 406 | name = parts[5] 407 | value = parts[6] 408 | 409 | # expires が None の場合は設定しない(セッション Cookie として扱われる) 410 | expires_param = None 411 | if expires is not None: 412 | # TimeSinceEpoch は秒単位の Unix timestamp 413 | expires_param = cdp.network.TimeSinceEpoch(expires) 414 | # domain から URL を構築(ドットで始まる場合は除去) 415 | domain_for_url = domain.lstrip('.') 416 | # secure フラグに応じてプロトコルを選択 417 | protocol = 'https' if secure else 'http' 418 | url = f'{protocol}://{domain_for_url}' 419 | cookie_params.append( 420 | cdp.network.CookieParam( 421 | name=name, 422 | value=value, 423 | url=url, 424 | domain=domain, 425 | path=path, 426 | secure=secure, 427 | expires=expires_param, 428 | ) 429 | ) 430 | return cookie_params 431 | -------------------------------------------------------------------------------- /app/TwitterGraphQLAPI.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import asyncio 4 | import json 5 | import re 6 | import time 7 | from datetime import datetime 8 | from typing import Any, ClassVar, Literal 9 | from zoneinfo import ZoneInfo 10 | 11 | from app import logging, schemas 12 | from app.TwitterAccount import TwitterAccount 13 | from app.TwitterScrapeBrowser import TwitterScrapeBrowser 14 | 15 | 16 | class TwitterGraphQLAPI: 17 | """ 18 | Twitter Web App で利用されている GraphQL API のラッパー 19 | 外部ライブラリを使うよりすべて自前で書いたほうが柔軟に対応でき、凍結リスクを回避できると考えて実装した 20 | 以下に実装されているリクエストペイロードなどは、すべて実装時点の Twitter Web App が実際に送信するリクエストを可能な限り模倣したもの 21 | メソッド名は概ね GraphQL API でのエンドポイント名に対応している 22 | 実際の API リクエストは TwitterScrapeBrowser 経由でヘッドレスブラウザから実行される 23 | """ 24 | 25 | # Twitter API のエラーコードとエラーメッセージの対応表 26 | ## 実際に返ってくる可能性があるものだけ 27 | ## ref: https://developer.twitter.com/ja/docs/basics/response-codes 28 | ERROR_MESSAGES: ClassVar[dict[int, str]] = { 29 | 32: 'Twitter アカウントの認証に失敗しました。もう一度連携し直してください。', 30 | 63: 'Twitter アカウントが凍結またはロックされています。', 31 | 64: 'Twitter アカウントが凍結またはロックされています。', 32 | 88: 'Twitter API エンドポイントのレート制限を超えました。', 33 | 89: 'Twitter アクセストークンの有効期限が切れています。', 34 | 99: 'Twitter OAuth クレデンシャルの認証に失敗しました。', 35 | 131: 'Twitter でサーバーエラーが発生しています。', 36 | 135: 'Twitter アカウントの認証に失敗しました。もう一度連携し直してください。', 37 | 139: 'すでにいいねされています。', 38 | 144: 'ツイートが非公開かすでに削除されています。', 39 | 179: 'フォローしていない非公開アカウントのツイートは表示できません。', 40 | 185: 'ツイート数の上限に達しました。', 41 | 186: 'ツイートが長過ぎます。', 42 | 187: 'ツイートが重複しています。', 43 | 226: 'ツイートが自動化されたスパムと判定されました。', 44 | 261: 'Twitter API アプリケーションが凍結されています。', 45 | 326: 'Twitter アカウントが一時的にロックされています。', 46 | 327: 'すでにリツイートされています。', 47 | 328: 'このツイートではリツイートは許可されていません。', 48 | 416: 'Twitter API アプリケーションが無効化されています。', 49 | } 50 | 51 | # ツイートの最小送信間隔 (秒) 52 | MINIMUM_TWEET_INTERVAL = 20 # 必ずアカウントごとに 20 秒以上間隔を空けてツイートする 53 | 54 | # ブラウザの自動シャットダウンまでの無操作時間 (秒) 55 | BROWSER_IDLE_TIMEOUT = 30 56 | 57 | # Twitter アカウント ID ごとのシングルトンインスタンスを管理する辞書 58 | __instances: ClassVar[dict[int, TwitterGraphQLAPI]] = {} 59 | 60 | def __init__(self, twitter_account: TwitterAccount) -> None: 61 | """ 62 | Twitter GraphQL API クライアントを初期化する 63 | 同じ Twitter アカウント ID のインスタンスが既に存在する場合は、そのインスタンスを返す(シングルトンパターン) 64 | 65 | Args: 66 | twitter_account (TwitterAccount): Twitter アカウントのモデル 67 | """ 68 | 69 | self.twitter_account = twitter_account 70 | 71 | # ZenDriver で自動操作されるヘッドレスブラウザのインスタンス 72 | self._browser = TwitterScrapeBrowser(self.twitter_account) 73 | # 一定期間後にブラウザをシャットダウンするタスク 74 | self._shutdown_task: asyncio.Task[None] | None = None 75 | # shutdown_task へのアクセスを保護するためのロック 76 | self._shutdown_task_lock = asyncio.Lock() 77 | 78 | # GraphQL API の前回呼び出し時刻 79 | self._last_graphql_api_call_time: float = 0.0 80 | 81 | # ツイート送信時の排他制御用のロック・前回ツイート時刻 82 | self._tweet_lock = asyncio.Lock() 83 | self._last_tweet_time: float = 0.0 84 | 85 | def __new__(cls, twitter_account: TwitterAccount) -> TwitterGraphQLAPI: 86 | """ 87 | シングルトンパターンの実装 88 | 同じ Twitter アカウント ID のインスタンスが既に存在する場合は、そのインスタンスを返す 89 | 既存インスタンスが見つかった場合、コンストラクタに渡された twitter_account の情報で既存インスタンスを更新する 90 | 91 | Args: 92 | twitter_account (TwitterAccount): Twitter アカウントのモデル 93 | 94 | Returns: 95 | TwitterGraphQLAPI: Twitter GraphQL API クライアントのインスタンス 96 | """ 97 | 98 | account_id = twitter_account.id 99 | if account_id not in cls.__instances: 100 | instance = super().__new__(cls) 101 | cls.__instances[account_id] = instance 102 | else: 103 | # 既存インスタンスが見つかった場合、twitter_account の情報を更新する 104 | ## DB から取得した新鮮な twitter_account の情報で既存インスタンスを更新することで、認証情報の変更などが反映される 105 | existing_instance = cls.__instances[account_id] 106 | existing_instance.twitter_account = twitter_account 107 | # browser インスタンスが持っている twitter_account も更新する 108 | ## 次回 setup() が呼ばれた際に新しい Cookie が使われるようになる 109 | if existing_instance._browser is not None: 110 | existing_instance._browser.twitter_account = twitter_account 111 | return cls.__instances[account_id] 112 | 113 | @classmethod 114 | async def removeInstance(cls, twitter_account_id: int) -> None: 115 | """ 116 | 指定された Twitter アカウント ID のシングルトンインスタンスを削除する 117 | アカウント削除後のリソースリークを防ぐために使用する 118 | 119 | Args: 120 | twitter_account_id (int): 削除する Twitter アカウントの ID 121 | """ 122 | 123 | if twitter_account_id in cls.__instances: 124 | instance = cls.__instances[twitter_account_id] 125 | # シャットダウンタスクをキャンセル 126 | ## 複数の同時リクエスト完了時の競合状態を防ぐため、ロックで保護する 127 | async with instance._shutdown_task_lock: 128 | if instance._shutdown_task is not None: 129 | if not instance._shutdown_task.done(): 130 | instance._shutdown_task.cancel() 131 | instance._shutdown_task = None 132 | # ブラウザをシャットダウン 133 | ## browser.shutdown() が例外を投げた場合でもレジストリエントリは確実に削除する必要があるため、try/except で囲む 134 | if instance._browser is not None and instance._browser.is_setup_complete is True: 135 | try: 136 | await instance._browser.shutdown() 137 | except Exception as ex: 138 | logging.error( 139 | f'Failed to shutdown browser for Twitter account {twitter_account_id}: {ex}', 140 | exc_info=ex, 141 | ) 142 | # レジストリエントリを削除 143 | del cls.__instances[twitter_account_id] 144 | 145 | async def invokeGraphQLAPI( 146 | self, 147 | endpoint_name: str, 148 | variables: dict[str, Any], 149 | additional_flags: dict[str, Any] | None = None, 150 | error_message_prefix: str = 'Twitter API の操作に失敗しました。', 151 | ) -> dict[str, Any] | str: 152 | """ 153 | Twitter Web App の GraphQL API に HTTP リクエストを送信する 154 | 実際には GraphQL と言いつつペイロードで JSON を渡しているので謎… (本当に GraphQL なのか?) 155 | 実際の API リクエストは TwitterScrapeBrowser 経由でヘッドレスブラウザから実行される 156 | 157 | Args: 158 | endpoint_name (str): GraphQL API のエンドポイント名 (例: 'CreateTweet') 159 | variables (dict[str, Any]): GraphQL API へのリクエストパラメータ (ペイロードのうち "variables" の部分) 160 | additional_flags (dict[str, Any] | None): 追加のフラグ(オプション) 161 | error_message_prefix (str, optional): エラー発生時に付与する prefix (例: 'ツイートの送信に失敗しました。') 162 | 163 | Returns: 164 | dict[str, Any] | str: GraphQL API のレスポンス (失敗時は日本語のエラーメッセージを返す) 165 | """ 166 | 167 | # ヘッドブラウザがまだ起動していない場合、セットアップ処理を実行 168 | if self._browser.is_setup_complete is not True: 169 | await self._browser.setup() 170 | 171 | # TwitterScrapeBrowser 経由で GraphQL API に HTTP リクエストを送信 172 | browser = self._browser 173 | try: 174 | raw_response = await browser.invokeGraphQLAPI( 175 | endpoint_name=endpoint_name, 176 | variables=variables, 177 | additional_flags=additional_flags, 178 | ) 179 | except Exception as ex: 180 | logging.error('[TwitterGraphQLAPI] Failed to connect to Twitter GraphQL API:', exc_info=ex) 181 | return error_message_prefix + 'Twitter API に接続できませんでした。' 182 | finally: 183 | # GraphQL API リクエスト完了後、更新された可能性があるブラウザの Cookie を DB 側に反映 184 | ## こうすることでヘッドレスブラウザと DB 間で Cookie の変更が同期されるはず 185 | ## この処理は API リクエストの成功・失敗に関わらず常に API リクエスト完了後に常に実行すべき 186 | cookies_txt_content = await browser.saveTwitterCookiesToNetscapeFormat() 187 | self.twitter_account.access_token_secret = cookies_txt_content 188 | # await self.twitter_account.save() # これで変更が DB に反映される # このデモでは使わない 189 | 190 | # GraphQL API の前回呼び出し時刻を更新 191 | self._last_graphql_api_call_time = time.time() 192 | 193 | async def OnShutdown() -> None: 194 | # タイムアウト時間に到達するまで待つ 195 | await asyncio.sleep(self.BROWSER_IDLE_TIMEOUT) 196 | # GraphQL API の前回呼び出し時刻を確認し、タイムアウト時間が経過している場合のみ、 197 | # しばらく API 呼び出しが行われていないため、リソース節約のためにブラウザをシャットダウンする 198 | current_time = time.time() 199 | if current_time - self._last_graphql_api_call_time >= self.BROWSER_IDLE_TIMEOUT: 200 | logging.info( 201 | f'[TwitterGraphQLAPI] Shutting down browser after {self.BROWSER_IDLE_TIMEOUT} seconds of inactivity.' 202 | ) 203 | await self._browser.shutdown() 204 | 205 | # 一定時間後にブラウザをシャットダウンするタスクを再スケジュール 206 | ## これも API リクエストの成功・失敗に関わらず常に API リクエスト完了後に常に実行すべき 207 | ## 複数の同時リクエスト完了時の競合状態を防ぐため、ロックで保護する 208 | async with self._shutdown_task_lock: 209 | if self._shutdown_task is not None: 210 | if not self._shutdown_task.done(): 211 | self._shutdown_task.cancel() 212 | self._shutdown_task = None 213 | self._shutdown_task = asyncio.create_task(OnShutdown()) 214 | 215 | # 生のレスポンスデータを取得 216 | parsed_response = raw_response['parsedResponse'] 217 | status_code = raw_response['statusCode'] 218 | response_text = raw_response['responseText'] 219 | headers = raw_response['headers'] 220 | request_error = raw_response['requestError'] 221 | 222 | # リクエストエラーが発生した場合(接続エラー) 223 | if request_error: 224 | logging.error(f'[TwitterGraphQLAPI] Request error: {request_error}') 225 | return error_message_prefix + 'Twitter API に接続できませんでした。' 226 | 227 | # JSON でないレスポンスが返ってきた場合 228 | ## charset=utf-8 が付いている場合もあるので完全一致ではなく部分一致で判定 229 | if headers and isinstance(headers, dict): 230 | content_type = headers.get('content-type', '') 231 | if content_type and 'application/json' not in content_type: 232 | logging.error(f'[TwitterGraphQLAPI] Response is not JSON. (Content-Type: {content_type})') 233 | return ( 234 | error_message_prefix 235 | + f'Twitter API から JSON 以外のレスポンスが返されました。(Content-Type: {content_type})' 236 | ) 237 | 238 | # レスポンスを JSON としてパース 239 | response_json: dict[str, Any] | None = None 240 | if parsed_response is not None: 241 | # JavaScript 側で既にパース済み 242 | response_json = parsed_response 243 | elif response_text: 244 | # JavaScript 側でパースに失敗した場合、Python 側で再試行 245 | try: 246 | response_json = json.loads(response_text) 247 | except Exception as ex: 248 | # 何もレスポンスが返ってきていないが、HTTP ステータスコードが 200 系以外で返ってきている場合 249 | if status_code is not None and not (200 <= status_code < 300): 250 | logging.error(f'[TwitterGraphQLAPI] Failed to invoke GraphQL API. (HTTP Error {status_code})') 251 | logging.error(f'[TwitterGraphQLAPI] Response: {response_text}') 252 | return error_message_prefix + f'Twitter API から HTTP {status_code} エラーが返されました。' 253 | 254 | # HTTP ステータスコードは 200 系だが、何もレスポンスが返ってきていない場合 255 | logging.error('[TwitterGraphQLAPI] Failed to parse response as JSON:', exc_info=ex) 256 | return error_message_prefix + 'Twitter API のレスポンスを JSON としてパースできませんでした。' 257 | 258 | if response_json is None: 259 | logging.error('[TwitterGraphQLAPI] Failed to parse response as JSON.') 260 | return error_message_prefix + 'Twitter API のレスポンスを JSON としてパースできませんでした。' 261 | 262 | # API レスポンスにエラーが含まれていて、かつ data キーが存在しない場合 263 | ## API レスポンスは Twitter の仕様変更で変わりうるので、ここで判定されなかったと言ってエラーでないとは限らない 264 | ## なぜか正常にレスポンスが含まれているのにエラーも返ってくる場合があるので、その場合は(致命的な)エラーではないと判断する 265 | if 'errors' in response_json and 'data' not in response_json: 266 | # Twitter API のエラーコードとエラーメッセージを取得 267 | ## このエラーコードは API v1.1 の頃と変わっていない 268 | response_error_code = response_json['errors'][0]['code'] 269 | response_error_message = response_json['errors'][0]['message'] 270 | 271 | # 想定外のエラーコードが返ってきた場合のエラーメッセージ 272 | alternative_error_message = f'Code: {response_error_code} / Message: {response_error_message}' 273 | logging.error(f'[TwitterGraphQLAPI] Failed to invoke GraphQL API ({alternative_error_message})') 274 | 275 | # エラーコードに対応するエラーメッセージを返し、対応するものがない場合は alternative_error_message を返す 276 | return error_message_prefix + self.ERROR_MESSAGES.get(response_error_code, alternative_error_message) 277 | 278 | # API レスポンスにエラーが含まれていないが、'data' キーが存在しない場合 279 | ## 実装時点の GraphQL API は必ず成功時は 'data' キーの下にレスポンスが格納されるはず 280 | ## もし 'data' キーが存在しない場合は、API 仕様が変更されている可能性がある 281 | if 'data' not in response_json: 282 | logging.error('[TwitterGraphQLAPI] Response does not have "data" key.') 283 | return ( 284 | error_message_prefix 285 | + 'Twitter API のレスポンスに "data" キーが存在しません。開発者に修正を依頼してください。' 286 | ) 287 | 288 | # ここまで来たら (中身のデータ構造はともかく) GraphQL API レスポンスの取得には成功しているはず 289 | return response_json['data'] 290 | 291 | async def fetchLoggedViewer( 292 | self, 293 | ) -> schemas.TweetUser | schemas.TwitterAPIResult: 294 | """ 295 | 現在ログイン中の Twitter アカウントの情報を取得する 296 | 297 | Returns: 298 | schemas.TweetUser | schemas.TwitterAPIResult: Twitter アカウントの情報 (失敗時はエラーメッセージ) 299 | """ 300 | 301 | # Twitter GraphQL API にリクエスト 302 | response = await self.invokeGraphQLAPI( 303 | endpoint_name='Viewer', 304 | variables={ 305 | 'withCommunitiesMemberships': True, 306 | }, 307 | additional_flags={ 308 | 'fieldToggles': { 309 | 'isDelegate': False, 310 | 'withAuxiliaryUserLabels': True, 311 | }, 312 | }, 313 | error_message_prefix='ユーザー情報の取得に失敗しました。', 314 | ) 315 | 316 | # 戻り値が str の場合、ユーザー情報の取得に失敗している (エラーメッセージが返ってくる) 317 | if isinstance(response, str): 318 | logging.error(f'[TwitterGraphQLAPI] Failed to fetch logged viewer: {response}') 319 | return schemas.TwitterAPIResult( 320 | is_success=False, 321 | detail=response, # エラーメッセージをそのまま返す 322 | ) 323 | 324 | # レスポンスからユーザー情報を取得 325 | ## レスポンス構造: data.viewer.user_results.result 326 | try: 327 | viewer = response.get('viewer', {}) 328 | user_results = viewer.get('user_results', {}) 329 | result = user_results.get('result', {}) 330 | 331 | # 必要な情報が存在しない場合はエラーを返す 332 | if not result: 333 | logging.error('[TwitterGraphQLAPI] Failed to fetch logged viewer: user_results.result not found') 334 | return schemas.TwitterAPIResult( 335 | is_success=False, 336 | detail='ユーザー情報の取得に失敗しました。レスポンスにユーザー情報が含まれていません。開発者に修正を依頼してください。', 337 | ) 338 | 339 | # ユーザー ID を取得 340 | user_id = result.get('rest_id') 341 | if not user_id: 342 | logging.error('[TwitterGraphQLAPI] Failed to fetch logged viewer: rest_id not found') 343 | return schemas.TwitterAPIResult( 344 | is_success=False, 345 | detail='ユーザー情報の取得に失敗しました。ユーザー ID を取得できませんでした。開発者に修正を依頼してください。', 346 | ) 347 | 348 | # ユーザー名とスクリーンネームを取得 349 | core = result.get('core', {}) 350 | name = core.get('name', '') 351 | screen_name = core.get('screen_name', '') 352 | if not name or not screen_name: 353 | logging.error('[TwitterGraphQLAPI] Failed to fetch logged viewer: name or screen_name not found') 354 | return schemas.TwitterAPIResult( 355 | is_success=False, 356 | detail='ユーザー情報の取得に失敗しました。ユーザー名またはスクリーンネームを取得できませんでした。開発者に修正を依頼してください。', 357 | ) 358 | 359 | # アイコン URL を取得 360 | avatar = result.get('avatar', {}) 361 | icon_url = avatar.get('image_url', '') 362 | if not icon_url: 363 | logging.error('[TwitterGraphQLAPI] Failed to fetch logged viewer: image_url not found') 364 | return schemas.TwitterAPIResult( 365 | is_success=False, 366 | detail='ユーザー情報の取得に失敗しました。アイコン URL を取得できませんでした。開発者に修正を依頼してください。', 367 | ) 368 | 369 | # (ランダムな文字列)_normal.jpg だと画像サイズが小さいので、(ランダムな文字列).jpg に置換 370 | icon_url = icon_url.replace('_normal', '') 371 | 372 | return schemas.TweetUser( 373 | id=str(user_id), 374 | name=name, 375 | screen_name=screen_name, 376 | icon_url=icon_url, 377 | ) 378 | 379 | except Exception as ex: 380 | # 予期しないエラーが発生した場合 381 | logging.error('[TwitterGraphQLAPI] Failed to fetch logged viewer:', exc_info=ex) 382 | logging.error(f'[TwitterGraphQLAPI] Response: {response}') 383 | return schemas.TwitterAPIResult( 384 | is_success=False, 385 | detail='ユーザー情報の取得に失敗しました。予期しないエラーが発生しました。開発者に修正を依頼してください。', 386 | ) 387 | 388 | async def createTweet( 389 | self, 390 | tweet: str, 391 | media_ids: list[str] = [], 392 | ) -> schemas.PostTweetResult | schemas.TwitterAPIResult: 393 | """ 394 | ツイートを送信する 395 | 396 | Args: 397 | tweet (str): ツイート内容 398 | media_ids (list[str], optional): 添付するメディアの ID のリスト (デフォルトは空リスト) 399 | 400 | Returns: 401 | schemas.PostTweetResult | schemas.TwitterAPIResult: ツイートの送信結果 402 | """ 403 | 404 | # ツイートの最小送信間隔を守るためにロックを取得 405 | async with self._tweet_lock: 406 | # 最後のツイート時刻から最小送信間隔を経過していない場合は待機 407 | current_time = time.time() 408 | wait_time = max(0, self.MINIMUM_TWEET_INTERVAL - (current_time - self._last_tweet_time)) 409 | if wait_time > 0: 410 | await asyncio.sleep(wait_time) 411 | 412 | # 画像の media_id をリストに格納 (画像がない場合は空のリストになる) 413 | media_entities: list[dict[str, Any]] = [] 414 | for media_id in media_ids: 415 | media_entities.append({'media_id': media_id, 'tagged_users': []}) 416 | 417 | # Twitter GraphQL API にリクエスト 418 | response = await self.invokeGraphQLAPI( 419 | endpoint_name='CreateTweet', 420 | variables={ 421 | 'tweet_text': tweet, 422 | 'dark_request': False, 423 | 'media': { 424 | 'media_entities': media_entities, 425 | 'possibly_sensitive': False, 426 | }, 427 | 'semantic_annotation_ids': [], 428 | 'disallowed_reply_options': None, 429 | }, 430 | error_message_prefix='ツイートの送信に失敗しました。', 431 | ) 432 | 433 | # 最後のツイート時刻を更新 434 | self._last_tweet_time = time.time() 435 | 436 | # 戻り値が str の場合、ツイートの送信に失敗している (エラーメッセージが返ってくる) 437 | if isinstance(response, str): 438 | logging.error(f'[TwitterGraphQLAPI] Failed to create tweet: {response}') 439 | return schemas.TwitterAPIResult( 440 | is_success=False, 441 | detail=response, # エラーメッセージをそのまま返す 442 | ) 443 | 444 | # おそらくツイートに成功しているはずなので、可能であれば送信したツイートの ID を取得 445 | tweet_id: str 446 | try: 447 | tweet_id = str(response['create_tweet']['tweet_results']['result']['rest_id']) 448 | except Exception as ex: 449 | # API レスポンスが変わっているなどでツイート ID を取得できなかった 450 | logging.error('[TwitterGraphQLAPI] Failed to get tweet ID:', exc_info=ex) 451 | logging.error(f'[TwitterGraphQLAPI] Response: {response}') 452 | return schemas.PostTweetResult( 453 | is_success=False, 454 | detail='ツイートを送信しましたが、ツイート ID を取得できませんでした。開発者に修正を依頼してください。', 455 | tweet_url='https://x.com/i/status/__error__', 456 | ) 457 | 458 | return schemas.PostTweetResult( 459 | is_success=True, 460 | detail='ツイートを送信しました。', 461 | tweet_url=f'https://x.com/i/status/{tweet_id}', 462 | ) 463 | 464 | async def createRetweet(self, tweet_id: str) -> schemas.TwitterAPIResult: 465 | """ 466 | ツイートをリツイートする 467 | 468 | Args: 469 | tweet_id (str): リツイートするツイートの ID 470 | 471 | Returns: 472 | schemas.TwitterAPIResult: リツイートの結果 473 | """ 474 | 475 | # Twitter GraphQL API にリクエスト 476 | response = await self.invokeGraphQLAPI( 477 | endpoint_name='CreateRetweet', 478 | variables={ 479 | 'tweet_id': tweet_id, 480 | 'dark_request': False, 481 | }, 482 | error_message_prefix='リツイートに失敗しました。', 483 | ) 484 | 485 | # 戻り値が str の場合、リツイートに失敗している (エラーメッセージが返ってくる) 486 | if isinstance(response, str): 487 | logging.error(f'[TwitterGraphQLAPI] Failed to create retweet: {response}') 488 | return schemas.TwitterAPIResult( 489 | is_success=False, 490 | detail=response, # エラーメッセージをそのまま返す 491 | ) 492 | 493 | return schemas.TwitterAPIResult( 494 | is_success=True, 495 | detail='リツイートしました。', 496 | ) 497 | 498 | async def deleteRetweet(self, tweet_id: str) -> schemas.TwitterAPIResult: 499 | """ 500 | ツイートのリツイートを取り消す 501 | 502 | Args: 503 | tweet_id (str): リツイートを取り消すツイートの ID 504 | 505 | Returns: 506 | schemas.TwitterAPIResult: リツイートの取り消し結果 507 | """ 508 | 509 | # Twitter GraphQL API にリクエスト 510 | response = await self.invokeGraphQLAPI( 511 | endpoint_name='DeleteRetweet', 512 | variables={ 513 | 'source_tweet_id': tweet_id, 514 | 'dark_request': False, 515 | }, 516 | error_message_prefix='リツイートの取り消しに失敗しました。', 517 | ) 518 | 519 | # 戻り値が str の場合、リツイートの取り消しに失敗している (エラーメッセージが返ってくる) 520 | if isinstance(response, str): 521 | logging.error(f'[TwitterGraphQLAPI] Failed to delete retweet: {response}') 522 | return schemas.TwitterAPIResult( 523 | is_success=False, 524 | detail=response, # エラーメッセージをそのまま返す 525 | ) 526 | 527 | return schemas.TwitterAPIResult( 528 | is_success=True, 529 | detail='リツイートを取り消ししました。', 530 | ) 531 | 532 | async def favoriteTweet(self, tweet_id: str) -> schemas.TwitterAPIResult: 533 | """ 534 | ツイートをいいねする 535 | 536 | Args: 537 | tweet_id (str): いいねするツイートの ID 538 | 539 | Returns: 540 | schemas.TwitterAPIResult: いいねの結果 541 | """ 542 | 543 | # Twitter GraphQL API にリクエスト 544 | response = await self.invokeGraphQLAPI( 545 | endpoint_name='FavoriteTweet', 546 | variables={ 547 | 'tweet_id': tweet_id, 548 | }, 549 | error_message_prefix='いいねに失敗しました。', 550 | ) 551 | 552 | # 戻り値が str の場合、いいねに失敗している (エラーメッセージが返ってくる) 553 | if isinstance(response, str): 554 | logging.error(f'[TwitterGraphQLAPI] Failed to favorite tweet: {response}') 555 | return schemas.TwitterAPIResult( 556 | is_success=False, 557 | detail=response, # エラーメッセージをそのまま返す 558 | ) 559 | 560 | return schemas.TwitterAPIResult( 561 | is_success=True, 562 | detail='いいねしました。', 563 | ) 564 | 565 | async def unfavoriteTweet(self, tweet_id: str) -> schemas.TwitterAPIResult: 566 | """ 567 | ツイートのいいねを取り消す 568 | 569 | Args: 570 | tweet_id (str): いいねを取り消すツイートの ID 571 | 572 | Returns: 573 | schemas.TwitterAPIResult: いいねの取り消し結果 574 | """ 575 | 576 | # Twitter GraphQL API にリクエスト 577 | response = await self.invokeGraphQLAPI( 578 | endpoint_name='UnfavoriteTweet', 579 | variables={ 580 | 'tweet_id': tweet_id, 581 | }, 582 | error_message_prefix='いいねの取り消しに失敗しました。', 583 | ) 584 | 585 | # 戻り値が str の場合、いいねの取り消しに失敗している (エラーメッセージが返ってくる) 586 | if isinstance(response, str): 587 | logging.error(f'[TwitterGraphQLAPI] Failed to unfavorite tweet: {response}') 588 | return schemas.TwitterAPIResult( 589 | is_success=False, 590 | detail=response, # エラーメッセージをそのまま返す 591 | ) 592 | 593 | return schemas.TwitterAPIResult( 594 | is_success=True, 595 | detail='いいねを取り消しました。', 596 | ) 597 | 598 | def __getCursorIDFromTimelineAPIResponse( 599 | self, response: dict[str, Any], cursor_type: Literal['Top', 'Bottom'] 600 | ) -> str | None: 601 | """ 602 | GraphQL API のうちツイートタイムライン系の API レスポンスから、指定されたタイプに一致するカーソル ID を取得する 603 | 次の API リクエスト時にカーソル ID を指定すると、そのカーソル ID から次のページを取得できる 604 | 605 | Args: 606 | response (dict[str, Any]): ツイートタイムライン系の API レスポンス 607 | cursor_type (Literal['Top', 'Bottom']): カーソル ID タイプ (Top: 現在より最新のツイート, Bottom: 現在より過去のツイート) 608 | 609 | Returns: 610 | str | None: カーソル ID (仕様変更などで取得できなかった場合は None) 611 | """ 612 | 613 | # HomeLatestTimeline からのレスポンス 614 | if 'home' in response: 615 | instructions = response.get('home', {}).get('home_timeline_urt', {}).get('instructions', []) 616 | # SearchTimeline からのレスポンス 617 | elif 'search_by_raw_query' in response: 618 | instructions = ( 619 | response.get('search_by_raw_query', {}) 620 | .get('search_timeline', {}) 621 | .get('timeline', {}) 622 | .get('instructions', []) 623 | ) 624 | # それ以外のレスポンス (通常あり得ないため、ここに到達した場合はレスポンス構造が変わった可能性が高い) 625 | else: 626 | instructions = [] 627 | logging.warning(f'[TwitterGraphQLAPI] Unknown timeline response format: {response}') 628 | 629 | for instruction in instructions: 630 | if instruction.get('type') == 'TimelineAddEntries': 631 | entries = instruction.get('entries', []) 632 | for entry in entries: 633 | content = entry.get('content', {}) 634 | if ( 635 | content.get('entryType') == 'TimelineTimelineCursor' 636 | and content.get('cursorType') == cursor_type 637 | ): 638 | return content.get('value') 639 | # Bottom 指定時、たまに通常 cursorType が Bottom になるところ Gap になっている場合がある 640 | # その場合は Bottom の Cursor として解釈する 641 | elif ( 642 | content.get('entryType') == 'TimelineTimelineCursor' 643 | and content.get('cursorType') == 'Gap' 644 | and cursor_type == 'Bottom' 645 | ): 646 | return content.get('value') 647 | elif instruction.get('type') == 'TimelineReplaceEntry': 648 | entry = instruction.get('entry', {}) 649 | content = entry.get('content', {}) 650 | if content.get('entryType') == 'TimelineTimelineCursor' and content.get('cursorType') == cursor_type: 651 | return content.get('value') 652 | # Bottom 指定時、たまに通常 cursorType が Bottom になるところ Gap になっている場合がある 653 | # その場合は Bottom の Cursor として解釈する 654 | elif ( 655 | content.get('entryType') == 'TimelineTimelineCursor' 656 | and content.get('cursorType') == 'Gap' 657 | and cursor_type == 'Bottom' 658 | ): 659 | return content.get('value') 660 | 661 | return None 662 | 663 | def __getTweetsFromTimelineAPIResponse(self, response: dict[str, Any]) -> list[schemas.Tweet]: 664 | """ 665 | GraphQL API のうちツイートタイムライン系の API レスポンスから、ツイートリストを取得する 666 | 667 | Args: 668 | response (dict[str, Any]): ツイートタイムライン系の API レスポンス 669 | 670 | Returns: 671 | list[schemas.Tweet]: ツイートリスト 672 | """ 673 | 674 | def format_tweet(raw_tweet_object: dict[str, Any]) -> schemas.Tweet: 675 | """API レスポンスから取得したツイート情報を schemas.Tweet に変換する""" 676 | 677 | # もし '__typename' が 'TweetWithVisibilityResults' なら、ツイート情報がさらにネストされているのでそれを取得 678 | if raw_tweet_object['__typename'] == 'TweetWithVisibilityResults': 679 | raw_tweet_object = raw_tweet_object['tweet'] 680 | 681 | # リツイートがある場合は、リツイート元のツイートの情報を取得 682 | retweeted_tweet = None 683 | if 'retweeted_status_result' in raw_tweet_object['legacy']: 684 | retweeted_tweet = format_tweet(raw_tweet_object['legacy']['retweeted_status_result']['result']) 685 | 686 | # 引用リツイートがある場合は、引用リツイート元のツイートの情報を取得 687 | ## なぜかリツイートと異なり legacy 以下ではなく直下に入っている 688 | quoted_tweet = None 689 | if 'quoted_status_result' in raw_tweet_object: 690 | if 'result' not in raw_tweet_object['quoted_status_result']: 691 | # ごく稀に quoted_status_result.result が空のツイート情報が返ってくるので、その場合は警告を出した上で無視する 692 | logging.warning( 693 | f'[TwitterGraphQLAPI] Quoted tweet not found: {raw_tweet_object.get("rest_id", "unknown")}' 694 | ) 695 | else: 696 | quoted_tweet = format_tweet(raw_tweet_object['quoted_status_result']['result']) 697 | 698 | # 画像の URL を取得 699 | image_urls = [] 700 | movie_url = None 701 | if 'extended_entities' in raw_tweet_object['legacy']: 702 | for media in raw_tweet_object['legacy']['extended_entities']['media']: 703 | if media['type'] == 'photo': 704 | image_urls.append(media['media_url_https']) 705 | elif media['type'] in ['video', 'animated_gif']: 706 | # content_type が video/mp4 かつ bitrate が最も高いものを取得 707 | mp4_variants: list[dict[str, Any]] = list( 708 | filter( 709 | lambda variant: variant['content_type'] == 'video/mp4', media['video_info']['variants'] 710 | ) 711 | ) 712 | if len(mp4_variants) > 0: 713 | highest_bitrate_variant: dict[str, Any] = max( 714 | mp4_variants, 715 | key=lambda variant: int(variant['bitrate']) if 'bitrate' in variant else 0, # type: ignore 716 | ) 717 | movie_url = ( 718 | str(highest_bitrate_variant['url']) if 'url' in highest_bitrate_variant else None 719 | ) 720 | 721 | # t.co の URL を展開した URL に置換 722 | expanded_text = raw_tweet_object['legacy']['full_text'] 723 | if 'entities' in raw_tweet_object['legacy'] and 'urls' in raw_tweet_object['legacy']['entities']: 724 | for url_entity in raw_tweet_object['legacy']['entities']['urls']: 725 | if 'expanded_url' in url_entity: # 展開後の URL が存在する場合のみ (稀に存在しない場合がある) 726 | expanded_text = expanded_text.replace(url_entity['url'], url_entity['expanded_url']) 727 | 728 | # 残った t.co の URL を削除 729 | if len(image_urls) > 0 or movie_url: 730 | expanded_text = re.sub(r'\s*https://t\.co/\w+$', '', expanded_text) 731 | 732 | return schemas.Tweet( 733 | id=raw_tweet_object['legacy']['id_str'], 734 | created_at=datetime.strptime( 735 | raw_tweet_object['legacy']['created_at'], '%a %b %d %H:%M:%S %z %Y' 736 | ).astimezone(ZoneInfo('Asia/Tokyo')), 737 | user=schemas.TweetUser( 738 | id=raw_tweet_object['core']['user_results']['result']['rest_id'], 739 | name=raw_tweet_object['core']['user_results']['result']['core']['name'], 740 | screen_name=raw_tweet_object['core']['user_results']['result']['core']['screen_name'], 741 | # (ランダムな文字列)_normal.jpg だと画像サイズが小さいので、(ランダムな文字列).jpg に置換 742 | icon_url=raw_tweet_object['core']['user_results']['result']['avatar']['image_url'].replace( 743 | '_normal', '' 744 | ), 745 | ), 746 | text=expanded_text, 747 | lang=raw_tweet_object['legacy']['lang'], 748 | via=re.sub(r'<.+?>', '', raw_tweet_object['source']), 749 | image_urls=image_urls if len(image_urls) > 0 else None, 750 | movie_url=movie_url, 751 | retweet_count=raw_tweet_object['legacy']['retweet_count'], 752 | favorite_count=raw_tweet_object['legacy']['favorite_count'], 753 | retweeted=raw_tweet_object['legacy']['retweeted'], 754 | favorited=raw_tweet_object['legacy']['favorited'], 755 | retweeted_tweet=retweeted_tweet, 756 | quoted_tweet=quoted_tweet, 757 | ) 758 | 759 | # HomeLatestTimeline からのレスポンス 760 | if 'home' in response: 761 | instructions = response.get('home', {}).get('home_timeline_urt', {}).get('instructions', []) 762 | # SearchTimeline からのレスポンス 763 | elif 'search_by_raw_query' in response: 764 | instructions = ( 765 | response.get('search_by_raw_query', {}) 766 | .get('search_timeline', {}) 767 | .get('timeline', {}) 768 | .get('instructions', []) 769 | ) 770 | # それ以外のレスポンス (通常あり得ないため、ここに到達した場合はレスポンス構造が変わった可能性が高い) 771 | else: 772 | instructions = [] 773 | logging.warning(f'[TwitterGraphQLAPI] Unknown timeline response format: {response}') 774 | 775 | tweets: list[schemas.Tweet] = [] 776 | for instruction in instructions: 777 | if instruction.get('type') == 'TimelineAddEntries': 778 | entries = instruction.get('entries', []) 779 | for entry in entries: 780 | # entryId が promoted- から始まるツイートは広告ツイートなので除外 781 | if entry.get('entryId', '').startswith('promoted-'): 782 | continue 783 | content = entry.get('content', {}) 784 | if ( 785 | content.get('entryType') == 'TimelineTimelineItem' 786 | and content.get('itemContent', {}).get('itemType') == 'TimelineTweet' 787 | ): 788 | tweet_results = content.get('itemContent', {}).get('tweet_results', {}).get('result') 789 | if tweet_results and tweet_results.get('__typename') in ['Tweet', 'TweetWithVisibilityResults']: 790 | tweets.append(format_tweet(tweet_results)) 791 | 792 | return tweets 793 | 794 | async def homeLatestTimeline( 795 | self, 796 | cursor_id: str | None = None, 797 | count: int = 20, 798 | ) -> schemas.TimelineTweetsResult | schemas.TwitterAPIResult: 799 | """ 800 | タイムラインの最新ツイートを取得する 801 | 一応 API 上は取得するツイート数を指定できることになっているが、検索と異なり実際に返ってくるツイート数は保証されてないっぽい (100 件返ってくることもある) 802 | 803 | Args: 804 | cursor_id (str | None, optional): 次のページを取得するためのカーソル ID (デフォルトは None) 805 | count (int, optional): 取得するツイート数 (デフォルトは 20) 806 | 807 | Returns: 808 | schemas.TimelineTweets | schemas.TwitterAPIResult: 検索結果 809 | """ 810 | 811 | # variables の挿入順序を Twitter Web App に厳密に合わせるためにこのような実装としている 812 | variables: dict[str, Any] = {} 813 | variables['count'] = count 814 | if cursor_id is not None: 815 | variables['cursor'] = cursor_id 816 | variables['includePromotedContent'] = True 817 | variables['latestControlAvailable'] = True 818 | if cursor_id is None: 819 | variables['requestContext'] = 'launch' 820 | ## おそらく実際に表示されたツイートの ID を入れるキーだが、取得できないので空リストを入れておく 821 | variables['seenTweetIds'] = [] 822 | 823 | # Twitter GraphQL API にリクエスト 824 | response = await self.invokeGraphQLAPI( 825 | endpoint_name='HomeLatestTimeline', 826 | variables=variables, 827 | error_message_prefix='タイムラインの取得に失敗しました。', 828 | ) 829 | 830 | # 戻り値が str の場合、タイムラインの取得に失敗している (エラーメッセージが返ってくる) 831 | if isinstance(response, str): 832 | logging.error(f'[TwitterGraphQLAPI] Failed to fetch timeline: {response}') 833 | return schemas.TwitterAPIResult( 834 | is_success=False, 835 | detail=response, # エラーメッセージをそのまま返す 836 | ) 837 | 838 | # まずはカーソル ID を取得 839 | ## カーソル ID が取得できなかった場合は仕様変更があったとみなし、エラーを返す 840 | next_cursor_id = self.__getCursorIDFromTimelineAPIResponse( 841 | response, 'Top' 842 | ) # 現在よりも新しいツイートを取得するためのカーソル ID 843 | previous_cursor_id = self.__getCursorIDFromTimelineAPIResponse( 844 | response, 'Bottom' 845 | ) # 現在よりも過去のツイートを取得するためのカーソル ID 846 | if next_cursor_id is None or previous_cursor_id is None: 847 | logging.error('[TwitterGraphQLAPI] Failed to fetch timeline: Cursor ID not found') 848 | return schemas.TwitterAPIResult( 849 | is_success=False, 850 | detail='タイムラインの取得に失敗しました。カーソル ID を取得できませんでした。開発者に修正を依頼してください。', 851 | ) 852 | 853 | # ツイートリストを取得 854 | ## 取得できなかった場合、あるいは単純に一致する結果がない場合は空のリストになる 855 | tweets = self.__getTweetsFromTimelineAPIResponse(response) 856 | 857 | return schemas.TimelineTweetsResult( 858 | is_success=True, 859 | detail='タイムラインを取得しました。', 860 | next_cursor_id=next_cursor_id, 861 | previous_cursor_id=previous_cursor_id, 862 | tweets=tweets, 863 | ) 864 | 865 | async def searchTimeline( 866 | self, 867 | search_type: Literal['Top', 'Latest'], 868 | query: str, 869 | cursor_id: str | None = None, 870 | count: int = 20, 871 | ) -> schemas.TimelineTweetsResult | schemas.TwitterAPIResult: 872 | """ 873 | ツイートを検索する 874 | 875 | Args: 876 | search_type (Literal['Top', 'Latest']): 検索タイプ (Top: トップツイート, Latest: 最新ツイート) 877 | query (str): 検索クエリ 878 | cursor_id (str | None, optional): 次のページを取得するためのカーソル ID (デフォルトは None) 879 | count (int, optional): 取得するツイート数 (デフォルトは 20) 880 | 881 | Returns: 882 | schemas.TimelineTweets | schemas.TwitterAPIResult: 検索結果 883 | """ 884 | 885 | # variables の挿入順序を Twitter Web App に厳密に合わせるためにこのような実装としている 886 | variables: dict[str, Any] = {} 887 | variables['rawQuery'] = query.strip() + ' exclude:replies lang:ja' 888 | variables['count'] = count 889 | if cursor_id is not None: 890 | variables['cursor'] = cursor_id 891 | ## Twitter Web App で検索すると typed_query になることが多いのでそれに合わせる 892 | variables['querySource'] = 'typed_query' 893 | ## 検索タイプに Top か Latest を指定する 894 | variables['product'] = search_type 895 | ## Twitter Web App の挙動に合わせて設定 896 | variables['withGrokTranslatedBio'] = False 897 | 898 | # Twitter GraphQL API にリクエスト 899 | response = await self.invokeGraphQLAPI( 900 | endpoint_name='SearchTimeline', 901 | variables=variables, 902 | error_message_prefix='ツイートの検索に失敗しました。', 903 | ) 904 | 905 | # 戻り値が str の場合、ツイートの検索に失敗している (エラーメッセージが返ってくる) 906 | if isinstance(response, str): 907 | logging.error(f'[TwitterGraphQLAPI] Failed to search tweets: {response}') 908 | return schemas.TwitterAPIResult( 909 | is_success=False, 910 | detail=response, # エラーメッセージをそのまま返す 911 | ) 912 | 913 | # まずはカーソル ID を取得 914 | ## カーソル ID が取得できなかった場合は仕様変更があったとみなし、エラーを返す 915 | next_cursor_id = self.__getCursorIDFromTimelineAPIResponse( 916 | response, 'Top' 917 | ) # 現在よりも新しいツイートを取得するためのカーソル ID 918 | previous_cursor_id = self.__getCursorIDFromTimelineAPIResponse( 919 | response, 'Bottom' 920 | ) # 現在よりも過去のツイートを取得するためのカーソル ID 921 | if next_cursor_id is None or previous_cursor_id is None: 922 | logging.error('[TwitterGraphQLAPI] Failed to search tweets: Cursor ID not found') 923 | return schemas.TwitterAPIResult( 924 | is_success=False, 925 | detail='ツイートの検索に失敗しました。カーソル ID を取得できませんでした。開発者に修正を依頼してください。', 926 | ) 927 | 928 | # ツイートリストを取得 929 | ## 取得できなかった場合、あるいは単純に一致する結果がない場合は空のリストになる 930 | tweets = self.__getTweetsFromTimelineAPIResponse(response) 931 | 932 | return schemas.TimelineTweetsResult( 933 | is_success=True, 934 | detail='ツイートを検索しました。', 935 | next_cursor_id=next_cursor_id, 936 | previous_cursor_id=previous_cursor_id, 937 | tweets=tweets, 938 | ) 939 | -------------------------------------------------------------------------------- /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 = "aiosqlite" 5 | version = "0.20.0" 6 | description = "asyncio bridge to the standard sqlite3 module" 7 | optional = false 8 | python-versions = ">=3.8" 9 | files = [ 10 | {file = "aiosqlite-0.20.0-py3-none-any.whl", hash = "sha256:36a1deaca0cac40ebe32aac9977a6e2bbc7f5189f23f4a54d5908986729e5bd6"}, 11 | {file = "aiosqlite-0.20.0.tar.gz", hash = "sha256:6d35c8c256637f4672f843c31021464090805bf925385ac39473fb16eaaca3d7"}, 12 | ] 13 | 14 | [package.dependencies] 15 | typing_extensions = ">=4.0" 16 | 17 | [package.extras] 18 | dev = ["attribution (==1.7.0)", "black (==24.2.0)", "coverage[toml] (==7.4.1)", "flake8 (==7.0.0)", "flake8-bugbear (==24.2.6)", "flit (==3.9.0)", "mypy (==1.8.0)", "ufmt (==2.3.0)", "usort (==1.0.8.post1)"] 19 | docs = ["sphinx (==7.2.6)", "sphinx-mdinclude (==0.5.3)"] 20 | 21 | [[package]] 22 | name = "annotated-types" 23 | version = "0.7.0" 24 | description = "Reusable constraint types to use with typing.Annotated" 25 | optional = false 26 | python-versions = ">=3.8" 27 | files = [ 28 | {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, 29 | {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, 30 | ] 31 | 32 | [[package]] 33 | name = "anyio" 34 | version = "4.11.0" 35 | description = "High-level concurrency and networking framework on top of asyncio or Trio" 36 | optional = false 37 | python-versions = ">=3.9" 38 | files = [ 39 | {file = "anyio-4.11.0-py3-none-any.whl", hash = "sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc"}, 40 | {file = "anyio-4.11.0.tar.gz", hash = "sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4"}, 41 | ] 42 | 43 | [package.dependencies] 44 | idna = ">=2.8" 45 | sniffio = ">=1.1" 46 | typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""} 47 | 48 | [package.extras] 49 | trio = ["trio (>=0.31.0)"] 50 | 51 | [[package]] 52 | name = "asyncio-atexit" 53 | version = "1.0.1" 54 | description = "Like atexit, but for asyncio" 55 | optional = false 56 | python-versions = ">=3.6" 57 | files = [ 58 | {file = "asyncio-atexit-1.0.1.tar.gz", hash = "sha256:1d0c71544b8ee2c484d322844ee72c0875dde6f250c0ed5b6993592ab9f7d436"}, 59 | {file = "asyncio_atexit-1.0.1-py3-none-any.whl", hash = "sha256:d93d5f7d5633a534abd521ce2896ed0fbe8de170bb1e65ec871d1c20eac9d376"}, 60 | ] 61 | 62 | [package.extras] 63 | test = ["pytest", "uvloop"] 64 | 65 | [[package]] 66 | name = "click" 67 | version = "8.3.0" 68 | description = "Composable command line interface toolkit" 69 | optional = false 70 | python-versions = ">=3.10" 71 | files = [ 72 | {file = "click-8.3.0-py3-none-any.whl", hash = "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc"}, 73 | {file = "click-8.3.0.tar.gz", hash = "sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4"}, 74 | ] 75 | 76 | [package.dependencies] 77 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 78 | 79 | [[package]] 80 | name = "colorama" 81 | version = "0.4.6" 82 | description = "Cross-platform colored terminal text." 83 | optional = false 84 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 85 | files = [ 86 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 87 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 88 | ] 89 | 90 | [[package]] 91 | name = "deprecated" 92 | version = "1.3.1" 93 | description = "Python @deprecated decorator to deprecate old python classes, functions or methods." 94 | optional = false 95 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" 96 | files = [ 97 | {file = "deprecated-1.3.1-py2.py3-none-any.whl", hash = "sha256:597bfef186b6f60181535a29fbe44865ce137a5079f295b479886c82729d5f3f"}, 98 | {file = "deprecated-1.3.1.tar.gz", hash = "sha256:b1b50e0ff0c1fddaa5708a2c6b0a6588bb09b892825ab2b214ac9ea9d92a5223"}, 99 | ] 100 | 101 | [package.dependencies] 102 | wrapt = ">=1.10,<3" 103 | 104 | [package.extras] 105 | dev = ["PyTest", "PyTest-Cov", "bump2version (<1)", "setuptools", "tox"] 106 | 107 | [[package]] 108 | name = "emoji" 109 | version = "2.15.0" 110 | description = "Emoji for Python" 111 | optional = false 112 | python-versions = ">=3.8" 113 | files = [ 114 | {file = "emoji-2.15.0-py3-none-any.whl", hash = "sha256:205296793d66a89d88af4688fa57fd6496732eb48917a87175a023c8138995eb"}, 115 | {file = "emoji-2.15.0.tar.gz", hash = "sha256:eae4ab7d86456a70a00a985125a03263a5eac54cd55e51d7e184b1ed3b6757e4"}, 116 | ] 117 | 118 | [package.extras] 119 | dev = ["coverage", "pytest (>=7.4.4)"] 120 | 121 | [[package]] 122 | name = "grapheme" 123 | version = "0.6.0" 124 | description = "Unicode grapheme helpers" 125 | optional = false 126 | python-versions = "*" 127 | files = [ 128 | {file = "grapheme-0.6.0.tar.gz", hash = "sha256:44c2b9f21bbe77cfb05835fec230bd435954275267fea1858013b102f8603cca"}, 129 | ] 130 | 131 | [package.extras] 132 | test = ["pytest", "sphinx", "sphinx-autobuild", "twine", "wheel"] 133 | 134 | [[package]] 135 | name = "h11" 136 | version = "0.16.0" 137 | description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" 138 | optional = false 139 | python-versions = ">=3.8" 140 | files = [ 141 | {file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"}, 142 | {file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"}, 143 | ] 144 | 145 | [[package]] 146 | name = "httptools" 147 | version = "0.7.1" 148 | description = "A collection of framework independent HTTP protocol utils." 149 | optional = false 150 | python-versions = ">=3.9" 151 | files = [ 152 | {file = "httptools-0.7.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:11d01b0ff1fe02c4c32d60af61a4d613b74fad069e47e06e9067758c01e9ac78"}, 153 | {file = "httptools-0.7.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:84d86c1e5afdc479a6fdabf570be0d3eb791df0ae727e8dbc0259ed1249998d4"}, 154 | {file = "httptools-0.7.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c8c751014e13d88d2be5f5f14fc8b89612fcfa92a9cc480f2bc1598357a23a05"}, 155 | {file = "httptools-0.7.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:654968cb6b6c77e37b832a9be3d3ecabb243bbe7a0b8f65fbc5b6b04c8fcabed"}, 156 | {file = "httptools-0.7.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b580968316348b474b020edf3988eecd5d6eec4634ee6561e72ae3a2a0e00a8a"}, 157 | {file = "httptools-0.7.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d496e2f5245319da9d764296e86c5bb6fcf0cf7a8806d3d000717a889c8c0b7b"}, 158 | {file = "httptools-0.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:cbf8317bfccf0fed3b5680c559d3459cccf1abe9039bfa159e62e391c7270568"}, 159 | {file = "httptools-0.7.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:474d3b7ab469fefcca3697a10d11a32ee2b9573250206ba1e50d5980910da657"}, 160 | {file = "httptools-0.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3c3b7366bb6c7b96bd72d0dbe7f7d5eead261361f013be5f6d9590465ea1c70"}, 161 | {file = "httptools-0.7.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:379b479408b8747f47f3b253326183d7c009a3936518cdb70db58cffd369d9df"}, 162 | {file = "httptools-0.7.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cad6b591a682dcc6cf1397c3900527f9affef1e55a06c4547264796bbd17cf5e"}, 163 | {file = "httptools-0.7.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:eb844698d11433d2139bbeeb56499102143beb582bd6c194e3ba69c22f25c274"}, 164 | {file = "httptools-0.7.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f65744d7a8bdb4bda5e1fa23e4ba16832860606fcc09d674d56e425e991539ec"}, 165 | {file = "httptools-0.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:135fbe974b3718eada677229312e97f3b31f8a9c8ffa3ae6f565bf808d5b6bcb"}, 166 | {file = "httptools-0.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:38e0c83a2ea9746ebbd643bdfb521b9aa4a91703e2cd705c20443405d2fd16a5"}, 167 | {file = "httptools-0.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f25bbaf1235e27704f1a7b86cd3304eabc04f569c828101d94a0e605ef7205a5"}, 168 | {file = "httptools-0.7.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c15f37ef679ab9ecc06bfc4e6e8628c32a8e4b305459de7cf6785acd57e4d03"}, 169 | {file = "httptools-0.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7fe6e96090df46b36ccfaf746f03034e5ab723162bc51b0a4cf58305324036f2"}, 170 | {file = "httptools-0.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f72fdbae2dbc6e68b8239defb48e6a5937b12218e6ffc2c7846cc37befa84362"}, 171 | {file = "httptools-0.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e99c7b90a29fd82fea9ef57943d501a16f3404d7b9ee81799d41639bdaae412c"}, 172 | {file = "httptools-0.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:3e14f530fefa7499334a79b0cf7e7cd2992870eb893526fb097d51b4f2d0f321"}, 173 | {file = "httptools-0.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6babce6cfa2a99545c60bfef8bee0cc0545413cb0018f617c8059a30ad985de3"}, 174 | {file = "httptools-0.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:601b7628de7504077dd3dcb3791c6b8694bbd967148a6d1f01806509254fb1ca"}, 175 | {file = "httptools-0.7.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:04c6c0e6c5fb0739c5b8a9eb046d298650a0ff38cf42537fc372b28dc7e4472c"}, 176 | {file = "httptools-0.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69d4f9705c405ae3ee83d6a12283dc9feba8cc6aaec671b412917e644ab4fa66"}, 177 | {file = "httptools-0.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:44c8f4347d4b31269c8a9205d8a5ee2df5322b09bbbd30f8f862185bb6b05346"}, 178 | {file = "httptools-0.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:465275d76db4d554918aba40bf1cbebe324670f3dfc979eaffaa5d108e2ed650"}, 179 | {file = "httptools-0.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:322d00c2068d125bd570f7bf78b2d367dad02b919d8581d7476d8b75b294e3e6"}, 180 | {file = "httptools-0.7.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:c08fe65728b8d70b6923ce31e3956f859d5e1e8548e6f22ec520a962c6757270"}, 181 | {file = "httptools-0.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7aea2e3c3953521c3c51106ee11487a910d45586e351202474d45472db7d72d3"}, 182 | {file = "httptools-0.7.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0e68b8582f4ea9166be62926077a3334064d422cf08ab87d8b74664f8e9058e1"}, 183 | {file = "httptools-0.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df091cf961a3be783d6aebae963cc9b71e00d57fa6f149025075217bc6a55a7b"}, 184 | {file = "httptools-0.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f084813239e1eb403ddacd06a30de3d3e09a9b76e7894dcda2b22f8a726e9c60"}, 185 | {file = "httptools-0.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7347714368fb2b335e9063bc2b96f2f87a9ceffcd9758ac295f8bbcd3ffbc0ca"}, 186 | {file = "httptools-0.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:cfabda2a5bb85aa2a904ce06d974a3f30fb36cc63d7feaddec05d2050acede96"}, 187 | {file = "httptools-0.7.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:ac50afa68945df63ec7a2707c506bd02239272288add34539a2ef527254626a4"}, 188 | {file = "httptools-0.7.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:de987bb4e7ac95b99b805b99e0aae0ad51ae61df4263459d36e07cf4052d8b3a"}, 189 | {file = "httptools-0.7.1-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d169162803a24425eb5e4d51d79cbf429fd7a491b9e570a55f495ea55b26f0bf"}, 190 | {file = "httptools-0.7.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49794f9250188a57fa73c706b46cb21a313edb00d337ca4ce1a011fe3c760b28"}, 191 | {file = "httptools-0.7.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:aeefa0648362bb97a7d6b5ff770bfb774930a327d7f65f8208394856862de517"}, 192 | {file = "httptools-0.7.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:0d92b10dbf0b3da4823cde6a96d18e6ae358a9daa741c71448975f6a2c339cad"}, 193 | {file = "httptools-0.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:5ddbd045cfcb073db2449563dd479057f2c2b681ebc232380e63ef15edc9c023"}, 194 | {file = "httptools-0.7.1.tar.gz", hash = "sha256:abd72556974f8e7c74a259655924a717a2365b236c882c3f6f8a45fe94703ac9"}, 195 | ] 196 | 197 | [[package]] 198 | name = "idna" 199 | version = "3.11" 200 | description = "Internationalized Domain Names in Applications (IDNA)" 201 | optional = false 202 | python-versions = ">=3.8" 203 | files = [ 204 | {file = "idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea"}, 205 | {file = "idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902"}, 206 | ] 207 | 208 | [package.extras] 209 | all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] 210 | 211 | [[package]] 212 | name = "iso8601" 213 | version = "2.1.0" 214 | description = "Simple module to parse ISO 8601 dates" 215 | optional = false 216 | python-versions = ">=3.7,<4.0" 217 | files = [ 218 | {file = "iso8601-2.1.0-py3-none-any.whl", hash = "sha256:aac4145c4dcb66ad8b648a02830f5e2ff6c24af20f4f482689be402db2429242"}, 219 | {file = "iso8601-2.1.0.tar.gz", hash = "sha256:6b1d3829ee8921c4301998c909f7829fa9ed3cbdac0d3b16af2d743aed1ba8df"}, 220 | ] 221 | 222 | [[package]] 223 | name = "mss" 224 | version = "10.1.0" 225 | description = "An ultra fast cross-platform multiple screenshots module in pure python using ctypes." 226 | optional = false 227 | python-versions = ">=3.9" 228 | files = [ 229 | {file = "mss-10.1.0-py3-none-any.whl", hash = "sha256:9179c110cadfef5dc6dc4a041a0cd161c74c379218648e6640b48c6b5cfe8918"}, 230 | {file = "mss-10.1.0.tar.gz", hash = "sha256:7182baf7ee16ca569e2804028b6ab9bcbf6be5c46fc2880840f33b513b9cb4f8"}, 231 | ] 232 | 233 | [package.extras] 234 | dev = ["build (==1.3.0)", "mypy (==1.17.1)", "ruff (==0.12.9)", "twine (==6.1.0)"] 235 | docs = ["shibuya (==2025.7.24)", "sphinx (==8.2.3)", "sphinx-copybutton (==0.5.2)", "sphinx-new-tab-link (==0.8.0)"] 236 | tests = ["numpy (==2.2.4)", "pillow (==11.3.0)", "pytest (==8.4.1)", "pytest-cov (==6.2.1)", "pytest-rerunfailures (==15.1)", "pyvirtualdisplay (==3.0)"] 237 | 238 | [[package]] 239 | name = "nodeenv" 240 | version = "1.9.1" 241 | description = "Node.js virtual environment builder" 242 | optional = false 243 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 244 | files = [ 245 | {file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"}, 246 | {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, 247 | ] 248 | 249 | [[package]] 250 | name = "pydantic" 251 | version = "2.12.4" 252 | description = "Data validation using Python type hints" 253 | optional = false 254 | python-versions = ">=3.9" 255 | files = [ 256 | {file = "pydantic-2.12.4-py3-none-any.whl", hash = "sha256:92d3d202a745d46f9be6df459ac5a064fdaa3c1c4cd8adcfa332ccf3c05f871e"}, 257 | {file = "pydantic-2.12.4.tar.gz", hash = "sha256:0f8cb9555000a4b5b617f66bfd2566264c4984b27589d3b845685983e8ea85ac"}, 258 | ] 259 | 260 | [package.dependencies] 261 | annotated-types = ">=0.6.0" 262 | pydantic-core = "2.41.5" 263 | typing-extensions = ">=4.14.1" 264 | typing-inspection = ">=0.4.2" 265 | 266 | [package.extras] 267 | email = ["email-validator (>=2.0.0)"] 268 | timezone = ["tzdata"] 269 | 270 | [[package]] 271 | name = "pydantic-core" 272 | version = "2.41.5" 273 | description = "Core functionality for Pydantic validation and serialization" 274 | optional = false 275 | python-versions = ">=3.9" 276 | files = [ 277 | {file = "pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146"}, 278 | {file = "pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2"}, 279 | {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97"}, 280 | {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9"}, 281 | {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52"}, 282 | {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941"}, 283 | {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a"}, 284 | {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c"}, 285 | {file = "pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2"}, 286 | {file = "pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556"}, 287 | {file = "pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49"}, 288 | {file = "pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba"}, 289 | {file = "pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9"}, 290 | {file = "pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6"}, 291 | {file = "pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b"}, 292 | {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a"}, 293 | {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8"}, 294 | {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e"}, 295 | {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1"}, 296 | {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b"}, 297 | {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b"}, 298 | {file = "pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284"}, 299 | {file = "pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594"}, 300 | {file = "pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e"}, 301 | {file = "pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b"}, 302 | {file = "pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe"}, 303 | {file = "pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f"}, 304 | {file = "pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7"}, 305 | {file = "pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0"}, 306 | {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69"}, 307 | {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75"}, 308 | {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05"}, 309 | {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc"}, 310 | {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c"}, 311 | {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5"}, 312 | {file = "pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c"}, 313 | {file = "pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294"}, 314 | {file = "pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1"}, 315 | {file = "pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d"}, 316 | {file = "pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815"}, 317 | {file = "pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3"}, 318 | {file = "pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9"}, 319 | {file = "pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34"}, 320 | {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0"}, 321 | {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33"}, 322 | {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e"}, 323 | {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2"}, 324 | {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586"}, 325 | {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d"}, 326 | {file = "pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740"}, 327 | {file = "pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e"}, 328 | {file = "pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858"}, 329 | {file = "pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36"}, 330 | {file = "pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11"}, 331 | {file = "pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd"}, 332 | {file = "pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a"}, 333 | {file = "pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14"}, 334 | {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1"}, 335 | {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66"}, 336 | {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869"}, 337 | {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2"}, 338 | {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375"}, 339 | {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553"}, 340 | {file = "pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90"}, 341 | {file = "pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07"}, 342 | {file = "pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb"}, 343 | {file = "pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23"}, 344 | {file = "pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf"}, 345 | {file = "pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0"}, 346 | {file = "pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a"}, 347 | {file = "pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3"}, 348 | {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c"}, 349 | {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612"}, 350 | {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d"}, 351 | {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9"}, 352 | {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660"}, 353 | {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9"}, 354 | {file = "pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3"}, 355 | {file = "pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf"}, 356 | {file = "pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470"}, 357 | {file = "pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa"}, 358 | {file = "pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c"}, 359 | {file = "pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008"}, 360 | {file = "pydantic_core-2.41.5-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:8bfeaf8735be79f225f3fefab7f941c712aaca36f1128c9d7e2352ee1aa87bdf"}, 361 | {file = "pydantic_core-2.41.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:346285d28e4c8017da95144c7f3acd42740d637ff41946af5ce6e5e420502dd5"}, 362 | {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a75dafbf87d6276ddc5b2bf6fae5254e3d0876b626eb24969a574fff9149ee5d"}, 363 | {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7b93a4d08587e2b7e7882de461e82b6ed76d9026ce91ca7915e740ecc7855f60"}, 364 | {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e8465ab91a4bd96d36dde3263f06caa6a8a6019e4113f24dc753d79a8b3a3f82"}, 365 | {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:299e0a22e7ae2b85c1a57f104538b2656e8ab1873511fd718a1c1c6f149b77b5"}, 366 | {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:707625ef0983fcfb461acfaf14de2067c5942c6bb0f3b4c99158bed6fedd3cf3"}, 367 | {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f41eb9797986d6ebac5e8edff36d5cef9de40def462311b3eb3eeded1431e425"}, 368 | {file = "pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0384e2e1021894b1ff5a786dbf94771e2986ebe2869533874d7e43bc79c6f504"}, 369 | {file = "pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:f0cd744688278965817fd0839c4a4116add48d23890d468bc436f78beb28abf5"}, 370 | {file = "pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:753e230374206729bf0a807954bcc6c150d3743928a73faffee51ac6557a03c3"}, 371 | {file = "pydantic_core-2.41.5-cp39-cp39-win32.whl", hash = "sha256:873e0d5b4fb9b89ef7c2d2a963ea7d02879d9da0da8d9d4933dee8ee86a8b460"}, 372 | {file = "pydantic_core-2.41.5-cp39-cp39-win_amd64.whl", hash = "sha256:e4f4a984405e91527a0d62649ee21138f8e3d0ef103be488c1dc11a80d7f184b"}, 373 | {file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034"}, 374 | {file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c"}, 375 | {file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2"}, 376 | {file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad"}, 377 | {file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd"}, 378 | {file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc"}, 379 | {file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56"}, 380 | {file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b"}, 381 | {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8"}, 382 | {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a"}, 383 | {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b"}, 384 | {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2"}, 385 | {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093"}, 386 | {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a"}, 387 | {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963"}, 388 | {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a"}, 389 | {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26"}, 390 | {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808"}, 391 | {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc"}, 392 | {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1"}, 393 | {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84"}, 394 | {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770"}, 395 | {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f"}, 396 | {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51"}, 397 | {file = "pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e"}, 398 | ] 399 | 400 | [package.dependencies] 401 | typing-extensions = ">=4.14.1" 402 | 403 | [[package]] 404 | name = "pypika-tortoise" 405 | version = "0.3.2" 406 | description = "Forked from pypika and streamline just for tortoise-orm" 407 | optional = false 408 | python-versions = "<4.0,>=3.8" 409 | files = [ 410 | {file = "pypika_tortoise-0.3.2-py3-none-any.whl", hash = "sha256:c5c52bc4473fe6f3db36cf659340750246ec5dd0f980d04ae7811430e299c3a2"}, 411 | {file = "pypika_tortoise-0.3.2.tar.gz", hash = "sha256:f5d508e2ef00255e52ec6ac79ef889e10dbab328f218c55cd134c4d02ff9f6f4"}, 412 | ] 413 | 414 | [[package]] 415 | name = "pyright" 416 | version = "1.1.407" 417 | description = "Command line wrapper for pyright" 418 | optional = false 419 | python-versions = ">=3.7" 420 | files = [ 421 | {file = "pyright-1.1.407-py3-none-any.whl", hash = "sha256:6dd419f54fcc13f03b52285796d65e639786373f433e243f8b94cf93a7444d21"}, 422 | {file = "pyright-1.1.407.tar.gz", hash = "sha256:099674dba5c10489832d4a4b2d302636152a9a42d317986c38474c76fe562262"}, 423 | ] 424 | 425 | [package.dependencies] 426 | nodeenv = ">=1.6.0" 427 | typing-extensions = ">=4.1" 428 | 429 | [package.extras] 430 | all = ["nodejs-wheel-binaries", "twine (>=3.4.1)"] 431 | dev = ["twine (>=3.4.1)"] 432 | nodejs = ["nodejs-wheel-binaries"] 433 | 434 | [[package]] 435 | name = "python-dotenv" 436 | version = "1.2.1" 437 | description = "Read key-value pairs from a .env file and set them as environment variables" 438 | optional = false 439 | python-versions = ">=3.9" 440 | files = [ 441 | {file = "python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61"}, 442 | {file = "python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6"}, 443 | ] 444 | 445 | [package.extras] 446 | cli = ["click (>=5.0)"] 447 | 448 | [[package]] 449 | name = "pytz" 450 | version = "2025.2" 451 | description = "World timezone definitions, modern and historical" 452 | optional = false 453 | python-versions = "*" 454 | files = [ 455 | {file = "pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00"}, 456 | {file = "pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3"}, 457 | ] 458 | 459 | [[package]] 460 | name = "pyyaml" 461 | version = "6.0.3" 462 | description = "YAML parser and emitter for Python" 463 | optional = false 464 | python-versions = ">=3.8" 465 | files = [ 466 | {file = "PyYAML-6.0.3-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f"}, 467 | {file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4"}, 468 | {file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:efd7b85f94a6f21e4932043973a7ba2613b059c4a000551892ac9f1d11f5baf3"}, 469 | {file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22ba7cfcad58ef3ecddc7ed1db3409af68d023b7f940da23c6c2a1890976eda6"}, 470 | {file = "PyYAML-6.0.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:6344df0d5755a2c9a276d4473ae6b90647e216ab4757f8426893b5dd2ac3f369"}, 471 | {file = "PyYAML-6.0.3-cp38-cp38-win32.whl", hash = "sha256:3ff07ec89bae51176c0549bc4c63aa6202991da2d9a6129d7aef7f1407d3f295"}, 472 | {file = "PyYAML-6.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:5cf4e27da7e3fbed4d6c3d8e797387aaad68102272f8f9752883bc32d61cb87b"}, 473 | {file = "pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b"}, 474 | {file = "pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956"}, 475 | {file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8"}, 476 | {file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198"}, 477 | {file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b"}, 478 | {file = "pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0"}, 479 | {file = "pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69"}, 480 | {file = "pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e"}, 481 | {file = "pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c"}, 482 | {file = "pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e"}, 483 | {file = "pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824"}, 484 | {file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c"}, 485 | {file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00"}, 486 | {file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d"}, 487 | {file = "pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a"}, 488 | {file = "pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4"}, 489 | {file = "pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b"}, 490 | {file = "pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf"}, 491 | {file = "pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196"}, 492 | {file = "pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0"}, 493 | {file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28"}, 494 | {file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c"}, 495 | {file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc"}, 496 | {file = "pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e"}, 497 | {file = "pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea"}, 498 | {file = "pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5"}, 499 | {file = "pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b"}, 500 | {file = "pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd"}, 501 | {file = "pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8"}, 502 | {file = "pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1"}, 503 | {file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c"}, 504 | {file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5"}, 505 | {file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6"}, 506 | {file = "pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6"}, 507 | {file = "pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be"}, 508 | {file = "pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26"}, 509 | {file = "pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c"}, 510 | {file = "pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb"}, 511 | {file = "pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac"}, 512 | {file = "pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310"}, 513 | {file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7"}, 514 | {file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788"}, 515 | {file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5"}, 516 | {file = "pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764"}, 517 | {file = "pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35"}, 518 | {file = "pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac"}, 519 | {file = "pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3"}, 520 | {file = "pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3"}, 521 | {file = "pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba"}, 522 | {file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c"}, 523 | {file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702"}, 524 | {file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c"}, 525 | {file = "pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065"}, 526 | {file = "pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65"}, 527 | {file = "pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9"}, 528 | {file = "pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b"}, 529 | {file = "pyyaml-6.0.3-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da"}, 530 | {file = "pyyaml-6.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917"}, 531 | {file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9"}, 532 | {file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5"}, 533 | {file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a"}, 534 | {file = "pyyaml-6.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926"}, 535 | {file = "pyyaml-6.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7"}, 536 | {file = "pyyaml-6.0.3-cp39-cp39-win32.whl", hash = "sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0"}, 537 | {file = "pyyaml-6.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007"}, 538 | {file = "pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f"}, 539 | ] 540 | 541 | [[package]] 542 | name = "ruff" 543 | version = "0.9.10" 544 | description = "An extremely fast Python linter and code formatter, written in Rust." 545 | optional = false 546 | python-versions = ">=3.7" 547 | files = [ 548 | {file = "ruff-0.9.10-py3-none-linux_armv6l.whl", hash = "sha256:eb4d25532cfd9fe461acc83498361ec2e2252795b4f40b17e80692814329e42d"}, 549 | {file = "ruff-0.9.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:188a6638dab1aa9bb6228a7302387b2c9954e455fb25d6b4470cb0641d16759d"}, 550 | {file = "ruff-0.9.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5284dcac6b9dbc2fcb71fdfc26a217b2ca4ede6ccd57476f52a587451ebe450d"}, 551 | {file = "ruff-0.9.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47678f39fa2a3da62724851107f438c8229a3470f533894b5568a39b40029c0c"}, 552 | {file = "ruff-0.9.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:99713a6e2766b7a17147b309e8c915b32b07a25c9efd12ada79f217c9c778b3e"}, 553 | {file = "ruff-0.9.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:524ee184d92f7c7304aa568e2db20f50c32d1d0caa235d8ddf10497566ea1a12"}, 554 | {file = "ruff-0.9.10-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:df92aeac30af821f9acf819fc01b4afc3dfb829d2782884f8739fb52a8119a16"}, 555 | {file = "ruff-0.9.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de42e4edc296f520bb84954eb992a07a0ec5a02fecb834498415908469854a52"}, 556 | {file = "ruff-0.9.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d257f95b65806104b6b1ffca0ea53f4ef98454036df65b1eda3693534813ecd1"}, 557 | {file = "ruff-0.9.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b60dec7201c0b10d6d11be00e8f2dbb6f40ef1828ee75ed739923799513db24c"}, 558 | {file = "ruff-0.9.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:d838b60007da7a39c046fcdd317293d10b845001f38bcb55ba766c3875b01e43"}, 559 | {file = "ruff-0.9.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:ccaf903108b899beb8e09a63ffae5869057ab649c1e9231c05ae354ebc62066c"}, 560 | {file = "ruff-0.9.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:f9567d135265d46e59d62dc60c0bfad10e9a6822e231f5b24032dba5a55be6b5"}, 561 | {file = "ruff-0.9.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5f202f0d93738c28a89f8ed9eaba01b7be339e5d8d642c994347eaa81c6d75b8"}, 562 | {file = "ruff-0.9.10-py3-none-win32.whl", hash = "sha256:bfb834e87c916521ce46b1788fbb8484966e5113c02df216680102e9eb960029"}, 563 | {file = "ruff-0.9.10-py3-none-win_amd64.whl", hash = "sha256:f2160eeef3031bf4b17df74e307d4c5fb689a6f3a26a2de3f7ef4044e3c484f1"}, 564 | {file = "ruff-0.9.10-py3-none-win_arm64.whl", hash = "sha256:5fd804c0327a5e5ea26615550e706942f348b197d5475ff34c19733aee4b2e69"}, 565 | {file = "ruff-0.9.10.tar.gz", hash = "sha256:9bacb735d7bada9cfb0f2c227d3658fc443d90a727b47f206fb33f52f3c0eac7"}, 566 | ] 567 | 568 | [[package]] 569 | name = "sniffio" 570 | version = "1.3.1" 571 | description = "Sniff out which async library your code is running under" 572 | optional = false 573 | python-versions = ">=3.7" 574 | files = [ 575 | {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, 576 | {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, 577 | ] 578 | 579 | [[package]] 580 | name = "tortoise-orm" 581 | version = "0.23.0" 582 | description = "Easy async ORM for python, built with relations in mind" 583 | optional = false 584 | python-versions = "<4.0,>=3.8" 585 | files = [ 586 | {file = "tortoise_orm-0.23.0-py3-none-any.whl", hash = "sha256:deaabed1619ea8aab6213508dff025571a701b7f34ee534473d7bb7661aa9f4f"}, 587 | {file = "tortoise_orm-0.23.0.tar.gz", hash = "sha256:f25d431ef4fb521a84edad582f4b9c53dccc5abf6cfbc6f228cbece5a13952fa"}, 588 | ] 589 | 590 | [package.dependencies] 591 | aiosqlite = ">=0.16.0,<0.21.0" 592 | iso8601 = ">=2.1.0,<3.0.0" 593 | pypika-tortoise = ">=0.3.2,<0.4.0" 594 | pytz = "*" 595 | 596 | [package.extras] 597 | accel = ["ciso8601", "orjson", "uvloop"] 598 | aiomysql = ["aiomysql"] 599 | asyncmy = ["asyncmy (>=0.2.8,<0.3.0)"] 600 | asyncodbc = ["asyncodbc (>=0.1.1,<0.2.0)"] 601 | asyncpg = ["asyncpg"] 602 | psycopg = ["psycopg[binary,pool] (>=3.0.12,<4.0.0)"] 603 | 604 | [[package]] 605 | name = "typing-extensions" 606 | version = "4.15.0" 607 | description = "Backported and Experimental Type Hints for Python 3.9+" 608 | optional = false 609 | python-versions = ">=3.9" 610 | files = [ 611 | {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, 612 | {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, 613 | ] 614 | 615 | [[package]] 616 | name = "typing-inspection" 617 | version = "0.4.2" 618 | description = "Runtime typing introspection tools" 619 | optional = false 620 | python-versions = ">=3.9" 621 | files = [ 622 | {file = "typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7"}, 623 | {file = "typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464"}, 624 | ] 625 | 626 | [package.dependencies] 627 | typing-extensions = ">=4.12.0" 628 | 629 | [[package]] 630 | name = "uvicorn" 631 | version = "0.34.3" 632 | description = "The lightning-fast ASGI server." 633 | optional = false 634 | python-versions = ">=3.9" 635 | files = [ 636 | {file = "uvicorn-0.34.3-py3-none-any.whl", hash = "sha256:16246631db62bdfbf069b0645177d6e8a77ba950cfedbfd093acef9444e4d885"}, 637 | {file = "uvicorn-0.34.3.tar.gz", hash = "sha256:35919a9a979d7a59334b6b10e05d77c1d0d574c50e0fc98b8b1a0f165708b55a"}, 638 | ] 639 | 640 | [package.dependencies] 641 | click = ">=7.0" 642 | colorama = {version = ">=0.4", optional = true, markers = "sys_platform == \"win32\" and extra == \"standard\""} 643 | h11 = ">=0.8" 644 | httptools = {version = ">=0.6.3", optional = true, markers = "extra == \"standard\""} 645 | python-dotenv = {version = ">=0.13", optional = true, markers = "extra == \"standard\""} 646 | pyyaml = {version = ">=5.1", optional = true, markers = "extra == \"standard\""} 647 | uvloop = {version = ">=0.15.1", optional = true, markers = "(sys_platform != \"win32\" and sys_platform != \"cygwin\") and platform_python_implementation != \"PyPy\" and extra == \"standard\""} 648 | watchfiles = {version = ">=0.13", optional = true, markers = "extra == \"standard\""} 649 | websockets = {version = ">=10.4", optional = true, markers = "extra == \"standard\""} 650 | 651 | [package.extras] 652 | standard = ["colorama (>=0.4)", "httptools (>=0.6.3)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.4)"] 653 | 654 | [[package]] 655 | name = "uvloop" 656 | version = "0.22.1" 657 | description = "Fast implementation of asyncio event loop on top of libuv" 658 | optional = false 659 | python-versions = ">=3.8.1" 660 | files = [ 661 | {file = "uvloop-0.22.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ef6f0d4cc8a9fa1f6a910230cd53545d9a14479311e87e3cb225495952eb672c"}, 662 | {file = "uvloop-0.22.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7cd375a12b71d33d46af85a3343b35d98e8116134ba404bd657b3b1d15988792"}, 663 | {file = "uvloop-0.22.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac33ed96229b7790eb729702751c0e93ac5bc3bcf52ae9eccbff30da09194b86"}, 664 | {file = "uvloop-0.22.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:481c990a7abe2c6f4fc3d98781cc9426ebd7f03a9aaa7eb03d3bfc68ac2a46bd"}, 665 | {file = "uvloop-0.22.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a592b043a47ad17911add5fbd087c76716d7c9ccc1d64ec9249ceafd735f03c2"}, 666 | {file = "uvloop-0.22.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1489cf791aa7b6e8c8be1c5a080bae3a672791fcb4e9e12249b05862a2ca9cec"}, 667 | {file = "uvloop-0.22.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c60ebcd36f7b240b30788554b6f0782454826a0ed765d8430652621b5de674b9"}, 668 | {file = "uvloop-0.22.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b7f102bf3cb1995cfeaee9321105e8f5da76fdb104cdad8986f85461a1b7b77"}, 669 | {file = "uvloop-0.22.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53c85520781d84a4b8b230e24a5af5b0778efdb39142b424990ff1ef7c48ba21"}, 670 | {file = "uvloop-0.22.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:56a2d1fae65fd82197cb8c53c367310b3eabe1bbb9fb5a04d28e3e3520e4f702"}, 671 | {file = "uvloop-0.22.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:40631b049d5972c6755b06d0bfe8233b1bd9a8a6392d9d1c45c10b6f9e9b2733"}, 672 | {file = "uvloop-0.22.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:535cc37b3a04f6cd2c1ef65fa1d370c9a35b6695df735fcff5427323f2cd5473"}, 673 | {file = "uvloop-0.22.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fe94b4564e865d968414598eea1a6de60adba0c040ba4ed05ac1300de402cd42"}, 674 | {file = "uvloop-0.22.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:51eb9bd88391483410daad430813d982010f9c9c89512321f5b60e2cddbdddd6"}, 675 | {file = "uvloop-0.22.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:700e674a166ca5778255e0e1dc4e9d79ab2acc57b9171b79e65feba7184b3370"}, 676 | {file = "uvloop-0.22.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b5b1ac819a3f946d3b2ee07f09149578ae76066d70b44df3fa990add49a82e4"}, 677 | {file = "uvloop-0.22.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e047cc068570bac9866237739607d1313b9253c3051ad84738cbb095be0537b2"}, 678 | {file = "uvloop-0.22.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:512fec6815e2dd45161054592441ef76c830eddaad55c8aa30952e6fe1ed07c0"}, 679 | {file = "uvloop-0.22.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:561577354eb94200d75aca23fbde86ee11be36b00e52a4eaf8f50fb0c86b7705"}, 680 | {file = "uvloop-0.22.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cdf5192ab3e674ca26da2eada35b288d2fa49fdd0f357a19f0e7c4e7d5077c8"}, 681 | {file = "uvloop-0.22.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e2ea3d6190a2968f4a14a23019d3b16870dd2190cd69c8180f7c632d21de68d"}, 682 | {file = "uvloop-0.22.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0530a5fbad9c9e4ee3f2b33b148c6a64d47bbad8000ea63704fa8260f4cf728e"}, 683 | {file = "uvloop-0.22.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bc5ef13bbc10b5335792360623cc378d52d7e62c2de64660616478c32cd0598e"}, 684 | {file = "uvloop-0.22.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1f38ec5e3f18c8a10ded09742f7fb8de0108796eb673f30ce7762ce1b8550cad"}, 685 | {file = "uvloop-0.22.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3879b88423ec7e97cd4eba2a443aa26ed4e59b45e6b76aabf13fe2f27023a142"}, 686 | {file = "uvloop-0.22.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4baa86acedf1d62115c1dc6ad1e17134476688f08c6efd8a2ab076e815665c74"}, 687 | {file = "uvloop-0.22.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:297c27d8003520596236bdb2335e6b3f649480bd09e00d1e3a99144b691d2a35"}, 688 | {file = "uvloop-0.22.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1955d5a1dd43198244d47664a5858082a3239766a839b2102a269aaff7a4e25"}, 689 | {file = "uvloop-0.22.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b31dc2fccbd42adc73bc4e7cdbae4fc5086cf378979e53ca5d0301838c5682c6"}, 690 | {file = "uvloop-0.22.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:93f617675b2d03af4e72a5333ef89450dfaa5321303ede6e67ba9c9d26878079"}, 691 | {file = "uvloop-0.22.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:37554f70528f60cad66945b885eb01f1bb514f132d92b6eeed1c90fd54ed6289"}, 692 | {file = "uvloop-0.22.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b76324e2dc033a0b2f435f33eb88ff9913c156ef78e153fb210e03c13da746b3"}, 693 | {file = "uvloop-0.22.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:badb4d8e58ee08dad957002027830d5c3b06aea446a6a3744483c2b3b745345c"}, 694 | {file = "uvloop-0.22.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b91328c72635f6f9e0282e4a57da7470c7350ab1c9f48546c0f2866205349d21"}, 695 | {file = "uvloop-0.22.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:daf620c2995d193449393d6c62131b3fbd40a63bf7b307a1527856ace637fe88"}, 696 | {file = "uvloop-0.22.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6cde23eeda1a25c75b2e07d39970f3374105d5eafbaab2a4482be82f272d5a5e"}, 697 | {file = "uvloop-0.22.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:80eee091fe128e425177fbd82f8635769e2f32ec9daf6468286ec57ec0313efa"}, 698 | {file = "uvloop-0.22.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:017bd46f9e7b78e81606329d07141d3da446f8798c6baeec124260e22c262772"}, 699 | {file = "uvloop-0.22.1-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c3e5c6727a57cb6558592a95019e504f605d1c54eb86463ee9f7a2dbd411c820"}, 700 | {file = "uvloop-0.22.1-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:57df59d8b48feb0e613d9b1f5e57b7532e97cbaf0d61f7aa9aa32221e84bc4b6"}, 701 | {file = "uvloop-0.22.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:55502bc2c653ed2e9692e8c55cb95b397d33f9f2911e929dc97c4d6b26d04242"}, 702 | {file = "uvloop-0.22.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:4a968a72422a097b09042d5fa2c5c590251ad484acf910a651b4b620acd7f193"}, 703 | {file = "uvloop-0.22.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b45649628d816c030dba3c80f8e2689bab1c89518ed10d426036cdc47874dfc4"}, 704 | {file = "uvloop-0.22.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ea721dd3203b809039fcc2983f14608dae82b212288b346e0bfe46ec2fab0b7c"}, 705 | {file = "uvloop-0.22.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ae676de143db2b2f60a9696d7eca5bb9d0dd6cc3ac3dad59a8ae7e95f9e1b54"}, 706 | {file = "uvloop-0.22.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:17d4e97258b0172dfa107b89aa1eeba3016f4b1974ce85ca3ef6a66b35cbf659"}, 707 | {file = "uvloop-0.22.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:05e4b5f86e621cf3927631789999e697e58f0d2d32675b67d9ca9eb0bca55743"}, 708 | {file = "uvloop-0.22.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:286322a90bea1f9422a470d5d2ad82d38080be0a29c4dd9b3e6384320a4d11e7"}, 709 | {file = "uvloop-0.22.1.tar.gz", hash = "sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f"}, 710 | ] 711 | 712 | [package.extras] 713 | dev = ["Cython (>=3.0,<4.0)", "setuptools (>=60)"] 714 | docs = ["Sphinx (>=4.1.2,<4.2.0)", "sphinx_rtd_theme (>=0.5.2,<0.6.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)"] 715 | test = ["aiohttp (>=3.10.5)", "flake8 (>=6.1,<7.0)", "mypy (>=0.800)", "psutil", "pyOpenSSL (>=25.3.0,<25.4.0)", "pycodestyle (>=2.11.0,<2.12.0)"] 716 | 717 | [[package]] 718 | name = "watchfiles" 719 | version = "1.1.1" 720 | description = "Simple, modern and high performance file watching and code reload in python." 721 | optional = false 722 | python-versions = ">=3.9" 723 | files = [ 724 | {file = "watchfiles-1.1.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:eef58232d32daf2ac67f42dea51a2c80f0d03379075d44a587051e63cc2e368c"}, 725 | {file = "watchfiles-1.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:03fa0f5237118a0c5e496185cafa92878568b652a2e9a9382a5151b1a0380a43"}, 726 | {file = "watchfiles-1.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8ca65483439f9c791897f7db49202301deb6e15fe9f8fe2fed555bf986d10c31"}, 727 | {file = "watchfiles-1.1.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f0ab1c1af0cb38e3f598244c17919fb1a84d1629cc08355b0074b6d7f53138ac"}, 728 | {file = "watchfiles-1.1.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bc570d6c01c206c46deb6e935a260be44f186a2f05179f52f7fcd2be086a94d"}, 729 | {file = "watchfiles-1.1.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e84087b432b6ac94778de547e08611266f1f8ffad28c0ee4c82e028b0fc5966d"}, 730 | {file = "watchfiles-1.1.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:620bae625f4cb18427b1bb1a2d9426dc0dd5a5ba74c7c2cdb9de405f7b129863"}, 731 | {file = "watchfiles-1.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:544364b2b51a9b0c7000a4b4b02f90e9423d97fbbf7e06689236443ebcad81ab"}, 732 | {file = "watchfiles-1.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:bbe1ef33d45bc71cf21364df962af171f96ecaeca06bd9e3d0b583efb12aec82"}, 733 | {file = "watchfiles-1.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1a0bb430adb19ef49389e1ad368450193a90038b5b752f4ac089ec6942c4dff4"}, 734 | {file = "watchfiles-1.1.1-cp310-cp310-win32.whl", hash = "sha256:3f6d37644155fb5beca5378feb8c1708d5783145f2a0f1c4d5a061a210254844"}, 735 | {file = "watchfiles-1.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:a36d8efe0f290835fd0f33da35042a1bb5dc0e83cbc092dcf69bce442579e88e"}, 736 | {file = "watchfiles-1.1.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:f57b396167a2565a4e8b5e56a5a1c537571733992b226f4f1197d79e94cf0ae5"}, 737 | {file = "watchfiles-1.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:421e29339983e1bebc281fab40d812742268ad057db4aee8c4d2bce0af43b741"}, 738 | {file = "watchfiles-1.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e43d39a741e972bab5d8100b5cdacf69db64e34eb19b6e9af162bccf63c5cc6"}, 739 | {file = "watchfiles-1.1.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f537afb3276d12814082a2e9b242bdcf416c2e8fd9f799a737990a1dbe906e5b"}, 740 | {file = "watchfiles-1.1.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b2cd9e04277e756a2e2d2543d65d1e2166d6fd4c9b183f8808634fda23f17b14"}, 741 | {file = "watchfiles-1.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f3f58818dc0b07f7d9aa7fe9eb1037aecb9700e63e1f6acfed13e9fef648f5d"}, 742 | {file = "watchfiles-1.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9bb9f66367023ae783551042d31b1d7fd422e8289eedd91f26754a66f44d5cff"}, 743 | {file = "watchfiles-1.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aebfd0861a83e6c3d1110b78ad54704486555246e542be3e2bb94195eabb2606"}, 744 | {file = "watchfiles-1.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5fac835b4ab3c6487b5dbad78c4b3724e26bcc468e886f8ba8cc4306f68f6701"}, 745 | {file = "watchfiles-1.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:399600947b170270e80134ac854e21b3ccdefa11a9529a3decc1327088180f10"}, 746 | {file = "watchfiles-1.1.1-cp311-cp311-win32.whl", hash = "sha256:de6da501c883f58ad50db3a32ad397b09ad29865b5f26f64c24d3e3281685849"}, 747 | {file = "watchfiles-1.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:35c53bd62a0b885bf653ebf6b700d1bf05debb78ad9292cf2a942b23513dc4c4"}, 748 | {file = "watchfiles-1.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:57ca5281a8b5e27593cb7d82c2ac927ad88a96ed406aa446f6344e4328208e9e"}, 749 | {file = "watchfiles-1.1.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:8c89f9f2f740a6b7dcc753140dd5e1ab9215966f7a3530d0c0705c83b401bd7d"}, 750 | {file = "watchfiles-1.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bd404be08018c37350f0d6e34676bd1e2889990117a2b90070b3007f172d0610"}, 751 | {file = "watchfiles-1.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8526e8f916bb5b9a0a777c8317c23ce65de259422bba5b31325a6fa6029d33af"}, 752 | {file = "watchfiles-1.1.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2edc3553362b1c38d9f06242416a5d8e9fe235c204a4072e988ce2e5bb1f69f6"}, 753 | {file = "watchfiles-1.1.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30f7da3fb3f2844259cba4720c3fc7138eb0f7b659c38f3bfa65084c7fc7abce"}, 754 | {file = "watchfiles-1.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8979280bdafff686ba5e4d8f97840f929a87ed9cdf133cbbd42f7766774d2aa"}, 755 | {file = "watchfiles-1.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dcc5c24523771db3a294c77d94771abcfcb82a0e0ee8efd910c37c59ec1b31bb"}, 756 | {file = "watchfiles-1.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db5d7ae38ff20153d542460752ff397fcf5c96090c1230803713cf3147a6803"}, 757 | {file = "watchfiles-1.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:28475ddbde92df1874b6c5c8aaeb24ad5be47a11f87cde5a28ef3835932e3e94"}, 758 | {file = "watchfiles-1.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:36193ed342f5b9842edd3532729a2ad55c4160ffcfa3700e0d54be496b70dd43"}, 759 | {file = "watchfiles-1.1.1-cp312-cp312-win32.whl", hash = "sha256:859e43a1951717cc8de7f4c77674a6d389b106361585951d9e69572823f311d9"}, 760 | {file = "watchfiles-1.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:91d4c9a823a8c987cce8fa2690923b069966dabb196dd8d137ea2cede885fde9"}, 761 | {file = "watchfiles-1.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:a625815d4a2bdca61953dbba5a39d60164451ef34c88d751f6c368c3ea73d404"}, 762 | {file = "watchfiles-1.1.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:130e4876309e8686a5e37dba7d5e9bc77e6ed908266996ca26572437a5271e18"}, 763 | {file = "watchfiles-1.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5f3bde70f157f84ece3765b42b4a52c6ac1a50334903c6eaf765362f6ccca88a"}, 764 | {file = "watchfiles-1.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14e0b1fe858430fc0251737ef3824c54027bedb8c37c38114488b8e131cf8219"}, 765 | {file = "watchfiles-1.1.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f27db948078f3823a6bb3b465180db8ebecf26dd5dae6f6180bd87383b6b4428"}, 766 | {file = "watchfiles-1.1.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:059098c3a429f62fc98e8ec62b982230ef2c8df68c79e826e37b895bc359a9c0"}, 767 | {file = "watchfiles-1.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfb5862016acc9b869bb57284e6cb35fdf8e22fe59f7548858e2f971d045f150"}, 768 | {file = "watchfiles-1.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:319b27255aacd9923b8a276bb14d21a5f7ff82564c744235fc5eae58d95422ae"}, 769 | {file = "watchfiles-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c755367e51db90e75b19454b680903631d41f9e3607fbd941d296a020c2d752d"}, 770 | {file = "watchfiles-1.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c22c776292a23bfc7237a98f791b9ad3144b02116ff10d820829ce62dff46d0b"}, 771 | {file = "watchfiles-1.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:3a476189be23c3686bc2f4321dd501cb329c0a0469e77b7b534ee10129ae6374"}, 772 | {file = "watchfiles-1.1.1-cp313-cp313-win32.whl", hash = "sha256:bf0a91bfb5574a2f7fc223cf95eeea79abfefa404bf1ea5e339c0c1560ae99a0"}, 773 | {file = "watchfiles-1.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:52e06553899e11e8074503c8e716d574adeeb7e68913115c4b3653c53f9bae42"}, 774 | {file = "watchfiles-1.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:ac3cc5759570cd02662b15fbcd9d917f7ecd47efe0d6b40474eafd246f91ea18"}, 775 | {file = "watchfiles-1.1.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:563b116874a9a7ce6f96f87cd0b94f7faf92d08d0021e837796f0a14318ef8da"}, 776 | {file = "watchfiles-1.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3ad9fe1dae4ab4212d8c91e80b832425e24f421703b5a42ef2e4a1e215aff051"}, 777 | {file = "watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce70f96a46b894b36eba678f153f052967a0d06d5b5a19b336ab0dbbd029f73e"}, 778 | {file = "watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cb467c999c2eff23a6417e58d75e5828716f42ed8289fe6b77a7e5a91036ca70"}, 779 | {file = "watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:836398932192dae4146c8f6f737d74baeac8b70ce14831a239bdb1ca882fc261"}, 780 | {file = "watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:743185e7372b7bc7c389e1badcc606931a827112fbbd37f14c537320fca08620"}, 781 | {file = "watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:afaeff7696e0ad9f02cbb8f56365ff4686ab205fcf9c4c5b6fdfaaa16549dd04"}, 782 | {file = "watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7eb7da0eb23aa2ba036d4f616d46906013a68caf61b7fdbe42fc8b25132e77"}, 783 | {file = "watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:831a62658609f0e5c64178211c942ace999517f5770fe9436be4c2faeba0c0ef"}, 784 | {file = "watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f9a2ae5c91cecc9edd47e041a930490c31c3afb1f5e6d71de3dc671bfaca02bf"}, 785 | {file = "watchfiles-1.1.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:d1715143123baeeaeadec0528bb7441103979a1d5f6fd0e1f915383fea7ea6d5"}, 786 | {file = "watchfiles-1.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:39574d6370c4579d7f5d0ad940ce5b20db0e4117444e39b6d8f99db5676c52fd"}, 787 | {file = "watchfiles-1.1.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7365b92c2e69ee952902e8f70f3ba6360d0d596d9299d55d7d386df84b6941fb"}, 788 | {file = "watchfiles-1.1.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bfff9740c69c0e4ed32416f013f3c45e2ae42ccedd1167ef2d805c000b6c71a5"}, 789 | {file = "watchfiles-1.1.1-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b27cf2eb1dda37b2089e3907d8ea92922b673c0c427886d4edc6b94d8dfe5db3"}, 790 | {file = "watchfiles-1.1.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:526e86aced14a65a5b0ec50827c745597c782ff46b571dbfe46192ab9e0b3c33"}, 791 | {file = "watchfiles-1.1.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04e78dd0b6352db95507fd8cb46f39d185cf8c74e4cf1e4fbad1d3df96faf510"}, 792 | {file = "watchfiles-1.1.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c85794a4cfa094714fb9c08d4a218375b2b95b8ed1666e8677c349906246c05"}, 793 | {file = "watchfiles-1.1.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:74d5012b7630714b66be7b7b7a78855ef7ad58e8650c73afc4c076a1f480a8d6"}, 794 | {file = "watchfiles-1.1.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:8fbe85cb3201c7d380d3d0b90e63d520f15d6afe217165d7f98c9c649654db81"}, 795 | {file = "watchfiles-1.1.1-cp314-cp314-win32.whl", hash = "sha256:3fa0b59c92278b5a7800d3ee7733da9d096d4aabcfabb9a928918bd276ef9b9b"}, 796 | {file = "watchfiles-1.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:c2047d0b6cea13b3316bdbafbfa0c4228ae593d995030fda39089d36e64fc03a"}, 797 | {file = "watchfiles-1.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:842178b126593addc05acf6fce960d28bc5fae7afbaa2c6c1b3a7b9460e5be02"}, 798 | {file = "watchfiles-1.1.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:88863fbbc1a7312972f1c511f202eb30866370ebb8493aef2812b9ff28156a21"}, 799 | {file = "watchfiles-1.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:55c7475190662e202c08c6c0f4d9e345a29367438cf8e8037f3155e10a88d5a5"}, 800 | {file = "watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f53fa183d53a1d7a8852277c92b967ae99c2d4dcee2bfacff8868e6e30b15f7"}, 801 | {file = "watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6aae418a8b323732fa89721d86f39ec8f092fc2af67f4217a2b07fd3e93c6101"}, 802 | {file = "watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f096076119da54a6080e8920cbdaac3dbee667eb91dcc5e5b78840b87415bd44"}, 803 | {file = "watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00485f441d183717038ed2e887a7c868154f216877653121068107b227a2f64c"}, 804 | {file = "watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a55f3e9e493158d7bfdb60a1165035f1cf7d320914e7b7ea83fe22c6023b58fc"}, 805 | {file = "watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c91ed27800188c2ae96d16e3149f199d62f86c7af5f5f4d2c61a3ed8cd3666c"}, 806 | {file = "watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:311ff15a0bae3714ffb603e6ba6dbfba4065ab60865d15a6ec544133bdb21099"}, 807 | {file = "watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a916a2932da8f8ab582f242c065f5c81bed3462849ca79ee357dd9551b0e9b01"}, 808 | {file = "watchfiles-1.1.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:c882d69f6903ef6092bedfb7be973d9319940d56b8427ab9187d1ecd73438a70"}, 809 | {file = "watchfiles-1.1.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d6ff426a7cb54f310d51bfe83fe9f2bbe40d540c741dc974ebc30e6aa238f52e"}, 810 | {file = "watchfiles-1.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79ff6c6eadf2e3fc0d7786331362e6ef1e51125892c75f1004bd6b52155fb956"}, 811 | {file = "watchfiles-1.1.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c1f5210f1b8fc91ead1283c6fd89f70e76fb07283ec738056cf34d51e9c1d62c"}, 812 | {file = "watchfiles-1.1.1-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b9c4702f29ca48e023ffd9b7ff6b822acdf47cb1ff44cb490a3f1d5ec8987e9c"}, 813 | {file = "watchfiles-1.1.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:acb08650863767cbc58bca4813b92df4d6c648459dcaa3d4155681962b2aa2d3"}, 814 | {file = "watchfiles-1.1.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:08af70fd77eee58549cd69c25055dc344f918d992ff626068242259f98d598a2"}, 815 | {file = "watchfiles-1.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c3631058c37e4a0ec440bf583bc53cdbd13e5661bb6f465bc1d88ee9a0a4d02"}, 816 | {file = "watchfiles-1.1.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:cf57a27fb986c6243d2ee78392c503826056ffe0287e8794503b10fb51b881be"}, 817 | {file = "watchfiles-1.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d7e7067c98040d646982daa1f37a33d3544138ea155536c2e0e63e07ff8a7e0f"}, 818 | {file = "watchfiles-1.1.1-cp39-cp39-win32.whl", hash = "sha256:6c9c9262f454d1c4d8aaa7050121eb4f3aea197360553699520767daebf2180b"}, 819 | {file = "watchfiles-1.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:74472234c8370669850e1c312490f6026d132ca2d396abfad8830b4f1c096957"}, 820 | {file = "watchfiles-1.1.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:17ef139237dfced9da49fb7f2232c86ca9421f666d78c264c7ffca6601d154c3"}, 821 | {file = "watchfiles-1.1.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:672b8adf25b1a0d35c96b5888b7b18699d27d4194bac8beeae75be4b7a3fc9b2"}, 822 | {file = "watchfiles-1.1.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77a13aea58bc2b90173bc69f2a90de8e282648939a00a602e1dc4ee23e26b66d"}, 823 | {file = "watchfiles-1.1.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b495de0bb386df6a12b18335a0285dda90260f51bdb505503c02bcd1ce27a8b"}, 824 | {file = "watchfiles-1.1.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:db476ab59b6765134de1d4fe96a1a9c96ddf091683599be0f26147ea1b2e4b88"}, 825 | {file = "watchfiles-1.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:89eef07eee5e9d1fda06e38822ad167a044153457e6fd997f8a858ab7564a336"}, 826 | {file = "watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce19e06cbda693e9e7686358af9cd6f5d61312ab8b00488bc36f5aabbaf77e24"}, 827 | {file = "watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e6f39af2eab0118338902798b5aa6664f46ff66bc0280de76fca67a7f262a49"}, 828 | {file = "watchfiles-1.1.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:cdab464fee731e0884c35ae3588514a9bcf718d0e2c82169c1c4a85cc19c3c7f"}, 829 | {file = "watchfiles-1.1.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:3dbd8cbadd46984f802f6d479b7e3afa86c42d13e8f0f322d669d79722c8ec34"}, 830 | {file = "watchfiles-1.1.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5524298e3827105b61951a29c3512deb9578586abf3a7c5da4a8069df247cccc"}, 831 | {file = "watchfiles-1.1.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b943d3668d61cfa528eb949577479d3b077fd25fb83c641235437bc0b5bc60e"}, 832 | {file = "watchfiles-1.1.1.tar.gz", hash = "sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2"}, 833 | ] 834 | 835 | [package.dependencies] 836 | anyio = ">=3.0.0" 837 | 838 | [[package]] 839 | name = "websockets" 840 | version = "15.0.1" 841 | description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" 842 | optional = false 843 | python-versions = ">=3.9" 844 | files = [ 845 | {file = "websockets-15.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d63efaa0cd96cf0c5fe4d581521d9fa87744540d4bc999ae6e08595a1014b45b"}, 846 | {file = "websockets-15.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac60e3b188ec7574cb761b08d50fcedf9d77f1530352db4eef1707fe9dee7205"}, 847 | {file = "websockets-15.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5756779642579d902eed757b21b0164cd6fe338506a8083eb58af5c372e39d9a"}, 848 | {file = "websockets-15.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fdfe3e2a29e4db3659dbd5bbf04560cea53dd9610273917799f1cde46aa725e"}, 849 | {file = "websockets-15.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c2529b320eb9e35af0fa3016c187dffb84a3ecc572bcee7c3ce302bfeba52bf"}, 850 | {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"}, 851 | {file = "websockets-15.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5df592cd503496351d6dc14f7cdad49f268d8e618f80dce0cd5a36b93c3fc08d"}, 852 | {file = "websockets-15.0.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0a34631031a8f05657e8e90903e656959234f3a04552259458aac0b0f9ae6fd9"}, 853 | {file = "websockets-15.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3d00075aa65772e7ce9e990cab3ff1de702aa09be3940d1dc88d5abf1ab8a09c"}, 854 | {file = "websockets-15.0.1-cp310-cp310-win32.whl", hash = "sha256:1234d4ef35db82f5446dca8e35a7da7964d02c127b095e172e54397fb6a6c256"}, 855 | {file = "websockets-15.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:39c1fec2c11dc8d89bba6b2bf1556af381611a173ac2b511cf7231622058af41"}, 856 | {file = "websockets-15.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431"}, 857 | {file = "websockets-15.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57"}, 858 | {file = "websockets-15.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905"}, 859 | {file = "websockets-15.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562"}, 860 | {file = "websockets-15.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792"}, 861 | {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"}, 862 | {file = "websockets-15.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8"}, 863 | {file = "websockets-15.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3"}, 864 | {file = "websockets-15.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf"}, 865 | {file = "websockets-15.0.1-cp311-cp311-win32.whl", hash = "sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85"}, 866 | {file = "websockets-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065"}, 867 | {file = "websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3"}, 868 | {file = "websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665"}, 869 | {file = "websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2"}, 870 | {file = "websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215"}, 871 | {file = "websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5"}, 872 | {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"}, 873 | {file = "websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe"}, 874 | {file = "websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4"}, 875 | {file = "websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597"}, 876 | {file = "websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9"}, 877 | {file = "websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7"}, 878 | {file = "websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931"}, 879 | {file = "websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675"}, 880 | {file = "websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151"}, 881 | {file = "websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22"}, 882 | {file = "websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f"}, 883 | {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"}, 884 | {file = "websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375"}, 885 | {file = "websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d"}, 886 | {file = "websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4"}, 887 | {file = "websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa"}, 888 | {file = "websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561"}, 889 | {file = "websockets-15.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5f4c04ead5aed67c8a1a20491d54cdfba5884507a48dd798ecaf13c74c4489f5"}, 890 | {file = "websockets-15.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:abdc0c6c8c648b4805c5eacd131910d2a7f6455dfd3becab248ef108e89ab16a"}, 891 | {file = "websockets-15.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a625e06551975f4b7ea7102bc43895b90742746797e2e14b70ed61c43a90f09b"}, 892 | {file = "websockets-15.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d591f8de75824cbb7acad4e05d2d710484f15f29d4a915092675ad3456f11770"}, 893 | {file = "websockets-15.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:47819cea040f31d670cc8d324bb6435c6f133b8c7a19ec3d61634e62f8d8f9eb"}, 894 | {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"}, 895 | {file = "websockets-15.0.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:4a9fac8e469d04ce6c25bb2610dc535235bd4aa14996b4e6dbebf5e007eba5ee"}, 896 | {file = "websockets-15.0.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:363c6f671b761efcb30608d24925a382497c12c506b51661883c3e22337265ed"}, 897 | {file = "websockets-15.0.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2034693ad3097d5355bfdacfffcbd3ef5694f9718ab7f29c29689a9eae841880"}, 898 | {file = "websockets-15.0.1-cp39-cp39-win32.whl", hash = "sha256:3b1ac0d3e594bf121308112697cf4b32be538fb1444468fb0a6ae4feebc83411"}, 899 | {file = "websockets-15.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:b7643a03db5c95c799b89b31c036d5f27eeb4d259c798e878d6937d71832b1e4"}, 900 | {file = "websockets-15.0.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0c9e74d766f2818bb95f84c25be4dea09841ac0f734d1966f415e4edfc4ef1c3"}, 901 | {file = "websockets-15.0.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1009ee0c7739c08a0cd59de430d6de452a55e42d6b522de7aa15e6f67db0b8e1"}, 902 | {file = "websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76d1f20b1c7a2fa82367e04982e708723ba0e7b8d43aa643d3dcd404d74f1475"}, 903 | {file = "websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f29d80eb9a9263b8d109135351caf568cc3f80b9928bccde535c235de55c22d9"}, 904 | {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"}, 905 | {file = "websockets-15.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:cad21560da69f4ce7658ca2cb83138fb4cf695a2ba3e475e0559e05991aa8122"}, 906 | {file = "websockets-15.0.1-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7f493881579c90fc262d9cdbaa05a6b54b3811c2f300766748db79f098db9940"}, 907 | {file = "websockets-15.0.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:47b099e1f4fbc95b701b6e85768e1fcdaf1630f3cbe4765fa216596f12310e2e"}, 908 | {file = "websockets-15.0.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67f2b6de947f8c757db2db9c71527933ad0019737ec374a8a6be9a956786aaf9"}, 909 | {file = "websockets-15.0.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d08eb4c2b7d6c41da6ca0600c077e93f5adcfd979cd777d747e9ee624556da4b"}, 910 | {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"}, 911 | {file = "websockets-15.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:21c1fa28a6a7e3cbdc171c694398b6df4744613ce9b36b1a498e816787e28123"}, 912 | {file = "websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f"}, 913 | {file = "websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee"}, 914 | ] 915 | 916 | [[package]] 917 | name = "wrapt" 918 | version = "2.0.1" 919 | description = "Module for decorators, wrappers and monkey patching." 920 | optional = false 921 | python-versions = ">=3.8" 922 | files = [ 923 | {file = "wrapt-2.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:64b103acdaa53b7caf409e8d45d39a8442fe6dcfec6ba3f3d141e0cc2b5b4dbd"}, 924 | {file = "wrapt-2.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:91bcc576260a274b169c3098e9a3519fb01f2989f6d3d386ef9cbf8653de1374"}, 925 | {file = "wrapt-2.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ab594f346517010050126fcd822697b25a7031d815bb4fbc238ccbe568216489"}, 926 | {file = "wrapt-2.0.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:36982b26f190f4d737f04a492a68accbfc6fa042c3f42326fdfbb6c5b7a20a31"}, 927 | {file = "wrapt-2.0.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:23097ed8bc4c93b7bf36fa2113c6c733c976316ce0ee2c816f64ca06102034ef"}, 928 | {file = "wrapt-2.0.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8bacfe6e001749a3b64db47bcf0341da757c95959f592823a93931a422395013"}, 929 | {file = "wrapt-2.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8ec3303e8a81932171f455f792f8df500fc1a09f20069e5c16bd7049ab4e8e38"}, 930 | {file = "wrapt-2.0.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:3f373a4ab5dbc528a94334f9fe444395b23c2f5332adab9ff4ea82f5a9e33bc1"}, 931 | {file = "wrapt-2.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f49027b0b9503bf6c8cdc297ca55006b80c2f5dd36cecc72c6835ab6e10e8a25"}, 932 | {file = "wrapt-2.0.1-cp310-cp310-win32.whl", hash = "sha256:8330b42d769965e96e01fa14034b28a2a7600fbf7e8f0cc90ebb36d492c993e4"}, 933 | {file = "wrapt-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:1218573502a8235bb8a7ecaed12736213b22dcde9feab115fa2989d42b5ded45"}, 934 | {file = "wrapt-2.0.1-cp310-cp310-win_arm64.whl", hash = "sha256:eda8e4ecd662d48c28bb86be9e837c13e45c58b8300e43ba3c9b4fa9900302f7"}, 935 | {file = "wrapt-2.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0e17283f533a0d24d6e5429a7d11f250a58d28b4ae5186f8f47853e3e70d2590"}, 936 | {file = "wrapt-2.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:85df8d92158cb8f3965aecc27cf821461bb5f40b450b03facc5d9f0d4d6ddec6"}, 937 | {file = "wrapt-2.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c1be685ac7700c966b8610ccc63c3187a72e33cab53526a27b2a285a662cd4f7"}, 938 | {file = "wrapt-2.0.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:df0b6d3b95932809c5b3fecc18fda0f1e07452d05e2662a0b35548985f256e28"}, 939 | {file = "wrapt-2.0.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4da7384b0e5d4cae05c97cd6f94faaf78cc8b0f791fc63af43436d98c4ab37bb"}, 940 | {file = "wrapt-2.0.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ec65a78fbd9d6f083a15d7613b2800d5663dbb6bb96003899c834beaa68b242c"}, 941 | {file = "wrapt-2.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7de3cc939be0e1174969f943f3b44e0d79b6f9a82198133a5b7fc6cc92882f16"}, 942 | {file = "wrapt-2.0.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:fb1a5b72cbd751813adc02ef01ada0b0d05d3dcbc32976ce189a1279d80ad4a2"}, 943 | {file = "wrapt-2.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3fa272ca34332581e00bf7773e993d4f632594eb2d1b0b162a9038df0fd971dd"}, 944 | {file = "wrapt-2.0.1-cp311-cp311-win32.whl", hash = "sha256:fc007fdf480c77301ab1afdbb6ab22a5deee8885f3b1ed7afcb7e5e84a0e27be"}, 945 | {file = "wrapt-2.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:47434236c396d04875180171ee1f3815ca1eada05e24a1ee99546320d54d1d1b"}, 946 | {file = "wrapt-2.0.1-cp311-cp311-win_arm64.whl", hash = "sha256:837e31620e06b16030b1d126ed78e9383815cbac914693f54926d816d35d8edf"}, 947 | {file = "wrapt-2.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1fdbb34da15450f2b1d735a0e969c24bdb8d8924892380126e2a293d9902078c"}, 948 | {file = "wrapt-2.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3d32794fe940b7000f0519904e247f902f0149edbe6316c710a8562fb6738841"}, 949 | {file = "wrapt-2.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:386fb54d9cd903ee0012c09291336469eb7b244f7183d40dc3e86a16a4bace62"}, 950 | {file = "wrapt-2.0.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7b219cb2182f230676308cdcacd428fa837987b89e4b7c5c9025088b8a6c9faf"}, 951 | {file = "wrapt-2.0.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:641e94e789b5f6b4822bb8d8ebbdfc10f4e4eae7756d648b717d980f657a9eb9"}, 952 | {file = "wrapt-2.0.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fe21b118b9f58859b5ebaa4b130dee18669df4bd111daad082b7beb8799ad16b"}, 953 | {file = "wrapt-2.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:17fb85fa4abc26a5184d93b3efd2dcc14deb4b09edcdb3535a536ad34f0b4dba"}, 954 | {file = "wrapt-2.0.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:b89ef9223d665ab255ae42cc282d27d69704d94be0deffc8b9d919179a609684"}, 955 | {file = "wrapt-2.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a453257f19c31b31ba593c30d997d6e5be39e3b5ad9148c2af5a7314061c63eb"}, 956 | {file = "wrapt-2.0.1-cp312-cp312-win32.whl", hash = "sha256:3e271346f01e9c8b1130a6a3b0e11908049fe5be2d365a5f402778049147e7e9"}, 957 | {file = "wrapt-2.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:2da620b31a90cdefa9cd0c2b661882329e2e19d1d7b9b920189956b76c564d75"}, 958 | {file = "wrapt-2.0.1-cp312-cp312-win_arm64.whl", hash = "sha256:aea9c7224c302bc8bfc892b908537f56c430802560e827b75ecbde81b604598b"}, 959 | {file = "wrapt-2.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:47b0f8bafe90f7736151f61482c583c86b0693d80f075a58701dd1549b0010a9"}, 960 | {file = "wrapt-2.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:cbeb0971e13b4bd81d34169ed57a6dda017328d1a22b62fda45e1d21dd06148f"}, 961 | {file = "wrapt-2.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb7cffe572ad0a141a7886a1d2efa5bef0bf7fe021deeea76b3ab334d2c38218"}, 962 | {file = "wrapt-2.0.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c8d60527d1ecfc131426b10d93ab5d53e08a09c5fa0175f6b21b3252080c70a9"}, 963 | {file = "wrapt-2.0.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c654eafb01afac55246053d67a4b9a984a3567c3808bb7df2f8de1c1caba2e1c"}, 964 | {file = "wrapt-2.0.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:98d873ed6c8b4ee2418f7afce666751854d6d03e3c0ec2a399bb039cd2ae89db"}, 965 | {file = "wrapt-2.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c9e850f5b7fc67af856ff054c71690d54fa940c3ef74209ad9f935b4f66a0233"}, 966 | {file = "wrapt-2.0.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e505629359cb5f751e16e30cf3f91a1d3ddb4552480c205947da415d597f7ac2"}, 967 | {file = "wrapt-2.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2879af909312d0baf35f08edeea918ee3af7ab57c37fe47cb6a373c9f2749c7b"}, 968 | {file = "wrapt-2.0.1-cp313-cp313-win32.whl", hash = "sha256:d67956c676be5a24102c7407a71f4126d30de2a569a1c7871c9f3cabc94225d7"}, 969 | {file = "wrapt-2.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:9ca66b38dd642bf90c59b6738af8070747b610115a39af2498535f62b5cdc1c3"}, 970 | {file = "wrapt-2.0.1-cp313-cp313-win_arm64.whl", hash = "sha256:5a4939eae35db6b6cec8e7aa0e833dcca0acad8231672c26c2a9ab7a0f8ac9c8"}, 971 | {file = "wrapt-2.0.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a52f93d95c8d38fed0669da2ebdb0b0376e895d84596a976c15a9eb45e3eccb3"}, 972 | {file = "wrapt-2.0.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4e54bbf554ee29fcceee24fa41c4d091398b911da6e7f5d7bffda963c9aed2e1"}, 973 | {file = "wrapt-2.0.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:908f8c6c71557f4deaa280f55d0728c3bca0960e8c3dd5ceeeafb3c19942719d"}, 974 | {file = "wrapt-2.0.1-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e2f84e9af2060e3904a32cea9bb6db23ce3f91cfd90c6b426757cf7cc01c45c7"}, 975 | {file = "wrapt-2.0.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e3612dc06b436968dfb9142c62e5dfa9eb5924f91120b3c8ff501ad878f90eb3"}, 976 | {file = "wrapt-2.0.1-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6d2d947d266d99a1477cd005b23cbd09465276e302515e122df56bb9511aca1b"}, 977 | {file = "wrapt-2.0.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:7d539241e87b650cbc4c3ac9f32c8d1ac8a54e510f6dca3f6ab60dcfd48c9b10"}, 978 | {file = "wrapt-2.0.1-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:4811e15d88ee62dbf5c77f2c3ff3932b1e3ac92323ba3912f51fc4016ce81ecf"}, 979 | {file = "wrapt-2.0.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c1c91405fcf1d501fa5d55df21e58ea49e6b879ae829f1039faaf7e5e509b41e"}, 980 | {file = "wrapt-2.0.1-cp313-cp313t-win32.whl", hash = "sha256:e76e3f91f864e89db8b8d2a8311d57df93f01ad6bb1e9b9976d1f2e83e18315c"}, 981 | {file = "wrapt-2.0.1-cp313-cp313t-win_amd64.whl", hash = "sha256:83ce30937f0ba0d28818807b303a412440c4b63e39d3d8fc036a94764b728c92"}, 982 | {file = "wrapt-2.0.1-cp313-cp313t-win_arm64.whl", hash = "sha256:4b55cacc57e1dc2d0991dbe74c6419ffd415fb66474a02335cb10efd1aa3f84f"}, 983 | {file = "wrapt-2.0.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:5e53b428f65ece6d9dad23cb87e64506392b720a0b45076c05354d27a13351a1"}, 984 | {file = "wrapt-2.0.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ad3ee9d0f254851c71780966eb417ef8e72117155cff04821ab9b60549694a55"}, 985 | {file = "wrapt-2.0.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d7b822c61ed04ee6ad64bc90d13368ad6eb094db54883b5dde2182f67a7f22c0"}, 986 | {file = "wrapt-2.0.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7164a55f5e83a9a0b031d3ffab4d4e36bbec42e7025db560f225489fa929e509"}, 987 | {file = "wrapt-2.0.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e60690ba71a57424c8d9ff28f8d006b7ad7772c22a4af432188572cd7fa004a1"}, 988 | {file = "wrapt-2.0.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3cd1a4bd9a7a619922a8557e1318232e7269b5fb69d4ba97b04d20450a6bf970"}, 989 | {file = "wrapt-2.0.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b4c2e3d777e38e913b8ce3a6257af72fb608f86a1df471cb1d4339755d0a807c"}, 990 | {file = "wrapt-2.0.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3d366aa598d69416b5afedf1faa539fac40c1d80a42f6b236c88c73a3c8f2d41"}, 991 | {file = "wrapt-2.0.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c235095d6d090aa903f1db61f892fffb779c1eaeb2a50e566b52001f7a0f66ed"}, 992 | {file = "wrapt-2.0.1-cp314-cp314-win32.whl", hash = "sha256:bfb5539005259f8127ea9c885bdc231978c06b7a980e63a8a61c8c4c979719d0"}, 993 | {file = "wrapt-2.0.1-cp314-cp314-win_amd64.whl", hash = "sha256:4ae879acc449caa9ed43fc36ba08392b9412ee67941748d31d94e3cedb36628c"}, 994 | {file = "wrapt-2.0.1-cp314-cp314-win_arm64.whl", hash = "sha256:8639b843c9efd84675f1e100ed9e99538ebea7297b62c4b45a7042edb84db03e"}, 995 | {file = "wrapt-2.0.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:9219a1d946a9b32bb23ccae66bdb61e35c62773ce7ca6509ceea70f344656b7b"}, 996 | {file = "wrapt-2.0.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:fa4184e74197af3adad3c889a1af95b53bb0466bced92ea99a0c014e48323eec"}, 997 | {file = "wrapt-2.0.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c5ef2f2b8a53b7caee2f797ef166a390fef73979b15778a4a153e4b5fedce8fa"}, 998 | {file = "wrapt-2.0.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e042d653a4745be832d5aa190ff80ee4f02c34b21f4b785745eceacd0907b815"}, 999 | {file = "wrapt-2.0.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2afa23318136709c4b23d87d543b425c399887b4057936cd20386d5b1422b6fa"}, 1000 | {file = "wrapt-2.0.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6c72328f668cf4c503ffcf9434c2b71fdd624345ced7941bc6693e61bbe36bef"}, 1001 | {file = "wrapt-2.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3793ac154afb0e5b45d1233cb94d354ef7a983708cc3bb12563853b1d8d53747"}, 1002 | {file = "wrapt-2.0.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:fec0d993ecba3991645b4857837277469c8cc4c554a7e24d064d1ca291cfb81f"}, 1003 | {file = "wrapt-2.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:949520bccc1fa227274da7d03bf238be15389cd94e32e4297b92337df9b7a349"}, 1004 | {file = "wrapt-2.0.1-cp314-cp314t-win32.whl", hash = "sha256:be9e84e91d6497ba62594158d3d31ec0486c60055c49179edc51ee43d095f79c"}, 1005 | {file = "wrapt-2.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:61c4956171c7434634401db448371277d07032a81cc21c599c22953374781395"}, 1006 | {file = "wrapt-2.0.1-cp314-cp314t-win_arm64.whl", hash = "sha256:35cdbd478607036fee40273be8ed54a451f5f23121bd9d4be515158f9498f7ad"}, 1007 | {file = "wrapt-2.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:90897ea1cf0679763b62e79657958cd54eae5659f6360fc7d2ccc6f906342183"}, 1008 | {file = "wrapt-2.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:50844efc8cdf63b2d90cd3d62d4947a28311e6266ce5235a219d21b195b4ec2c"}, 1009 | {file = "wrapt-2.0.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:49989061a9977a8cbd6d20f2efa813f24bf657c6990a42967019ce779a878dbf"}, 1010 | {file = "wrapt-2.0.1-cp38-cp38-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:09c7476ab884b74dce081ad9bfd07fe5822d8600abade571cb1f66d5fc915af6"}, 1011 | {file = "wrapt-2.0.1-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1a8a09a004ef100e614beec82862d11fc17d601092c3599afd22b1f36e4137e"}, 1012 | {file = "wrapt-2.0.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:89a82053b193837bf93c0f8a57ded6e4b6d88033a499dadff5067e912c2a41e9"}, 1013 | {file = "wrapt-2.0.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:f26f8e2ca19564e2e1fdbb6a0e47f36e0efbab1acc31e15471fad88f828c75f6"}, 1014 | {file = "wrapt-2.0.1-cp38-cp38-win32.whl", hash = "sha256:115cae4beed3542e37866469a8a1f2b9ec549b4463572b000611e9946b86e6f6"}, 1015 | {file = "wrapt-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:c4012a2bd37059d04f8209916aa771dfb564cccb86079072bdcd48a308b6a5c5"}, 1016 | {file = "wrapt-2.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:68424221a2dc00d634b54f92441914929c5ffb1c30b3b837343978343a3512a3"}, 1017 | {file = "wrapt-2.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6bd1a18f5a797fe740cb3d7a0e853a8ce6461cc62023b630caec80171a6b8097"}, 1018 | {file = "wrapt-2.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fb3a86e703868561c5cad155a15c36c716e1ab513b7065bd2ac8ed353c503333"}, 1019 | {file = "wrapt-2.0.1-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5dc1b852337c6792aa111ca8becff5bacf576bf4a0255b0f05eb749da6a1643e"}, 1020 | {file = "wrapt-2.0.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c046781d422f0830de6329fa4b16796096f28a92c8aef3850674442cdcb87b7f"}, 1021 | {file = "wrapt-2.0.1-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f73f9f7a0ebd0db139253d27e5fc8d2866ceaeef19c30ab5d69dcbe35e1a6981"}, 1022 | {file = "wrapt-2.0.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:b667189cf8efe008f55bbda321890bef628a67ab4147ebf90d182f2dadc78790"}, 1023 | {file = "wrapt-2.0.1-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:a9a83618c4f0757557c077ef71d708ddd9847ed66b7cc63416632af70d3e2308"}, 1024 | {file = "wrapt-2.0.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1e9b121e9aeb15df416c2c960b8255a49d44b4038016ee17af03975992d03931"}, 1025 | {file = "wrapt-2.0.1-cp39-cp39-win32.whl", hash = "sha256:1f186e26ea0a55f809f232e92cc8556a0977e00183c3ebda039a807a42be1494"}, 1026 | {file = "wrapt-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:bf4cb76f36be5de950ce13e22e7fdf462b35b04665a12b64f3ac5c1bbbcf3728"}, 1027 | {file = "wrapt-2.0.1-cp39-cp39-win_arm64.whl", hash = "sha256:d6cc985b9c8b235bd933990cdbf0f891f8e010b65a3911f7a55179cd7b0fc57b"}, 1028 | {file = "wrapt-2.0.1-py3-none-any.whl", hash = "sha256:4d2ce1bf1a48c5277d7969259232b57645aae5686dba1eaeade39442277afbca"}, 1029 | {file = "wrapt-2.0.1.tar.gz", hash = "sha256:9c9c635e78497cacb81e84f8b11b23e0aacac7a136e73b8e5b2109a1d9fc468f"}, 1030 | ] 1031 | 1032 | [package.extras] 1033 | dev = ["pytest", "setuptools"] 1034 | 1035 | [[package]] 1036 | name = "zendriver" 1037 | version = "0.15.0" 1038 | description = "A blazing fast, async-first, undetectable webscraping/web automation framework" 1039 | optional = false 1040 | python-versions = ">=3.10" 1041 | files = [ 1042 | {file = "zendriver-0.15.0-py3-none-any.whl", hash = "sha256:88eb19cb86b138ffff745f54bbf84e37f668088ac2e88c07fef29e8b958be3b7"}, 1043 | {file = "zendriver-0.15.0.tar.gz", hash = "sha256:38327109ec92fefdf16b80d241b0328aa8ed78492752940a27cde19daa6b15c3"}, 1044 | ] 1045 | 1046 | [package.dependencies] 1047 | asyncio-atexit = ">=1.0.1" 1048 | deprecated = ">=1.2.14" 1049 | emoji = ">=2.14.1" 1050 | grapheme = ">=0.6.0" 1051 | mss = ">=9.0.2" 1052 | websockets = ">=14.0" 1053 | 1054 | [metadata] 1055 | lock-version = "2.0" 1056 | python-versions = ">=3.11,<3.12" 1057 | content-hash = "45509d195f993dc4edb2dd37edf365939919839fbdc04506ef2176e1ac82c67b" 1058 | --------------------------------------------------------------------------------