├── docs ├── images │ ├── seedrcc.png │ └── seedrpy.png ├── sync_client.md ├── async_client.md ├── exceptions.md ├── models.md └── index.md ├── seedrcc ├── __init__.py ├── _constants.py ├── _utils.py ├── token.py ├── exceptions.py ├── _request_models.py ├── _base.py ├── models.py ├── client.py └── async_client.py ├── .readthedocs.yaml ├── LICENSE ├── .github └── workflows │ └── python-publish.yml ├── pyproject.toml ├── mkdocs.yml ├── .gitignore ├── README.md └── uv.lock /docs/images/seedrcc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hemantapkh/seedrcc/HEAD/docs/images/seedrcc.png -------------------------------------------------------------------------------- /docs/images/seedrpy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hemantapkh/seedrcc/HEAD/docs/images/seedrpy.png -------------------------------------------------------------------------------- /docs/sync_client.md: -------------------------------------------------------------------------------- 1 | # Synchronous Client 2 | 3 | This page contains the API reference for the synchronous `Seedr` client. 4 | 5 | ::: seedrcc.client.Seedr 6 | options: 7 | show_root_heading: true 8 | show_source: true 9 | members_order: source 10 | -------------------------------------------------------------------------------- /seedrcc/__init__.py: -------------------------------------------------------------------------------- 1 | from . import exceptions, models 2 | from .async_client import AsyncSeedr 3 | from .client import Seedr 4 | from .token import Token 5 | 6 | __all__ = [ 7 | "Seedr", 8 | "AsyncSeedr", 9 | "Token", 10 | "models", 11 | "exceptions", 12 | ] 13 | -------------------------------------------------------------------------------- /docs/async_client.md: -------------------------------------------------------------------------------- 1 | # Asynchronous Client 2 | 3 | This page contains the API reference for the asynchronous `AsyncSeedr` client. 4 | 5 | ::: seedrcc.async_client.AsyncSeedr 6 | options: 7 | show_root_heading: true 8 | show_source: true 9 | members_order: source 10 | -------------------------------------------------------------------------------- /seedrcc/_constants.py: -------------------------------------------------------------------------------- 1 | # Base URLs 2 | BASE_API_URL = "https://www.seedr.cc/api" 3 | OAUTH_URL = "https://www.seedr.cc/oauth_test" 4 | 5 | # API Endpoints 6 | RESOURCE_URL = f"{OAUTH_URL}/resource.php" 7 | TOKEN_URL = f"{OAUTH_URL}/token.php" 8 | DEVICE_CODE_URL = f"{BASE_API_URL}/device/code" 9 | DEVICE_AUTHORIZE_URL = f"{BASE_API_URL}/device/authorize" 10 | 11 | # Client IDs 12 | DEVICE_CLIENT_ID = "seedr_xbmc" 13 | PSWRD_CLIENT_ID = "seedr_chrome" 14 | -------------------------------------------------------------------------------- /seedrcc/_utils.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from typing import Any, Optional 3 | 4 | 5 | def parse_datetime(dt_str: Optional[Any]) -> Optional[datetime]: 6 | """ 7 | A centralized helper to parse datetime strings or timestamps from the API. 8 | Returns None if the input is invalid or None. 9 | """ 10 | if not dt_str: 11 | return None 12 | try: 13 | if isinstance(dt_str, (int, float)): 14 | return datetime.fromtimestamp(dt_str) 15 | return datetime.strptime(str(dt_str), "%Y-%m-%d %H:%M:%S") 16 | except (ValueError, TypeError): 17 | return None 18 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yaml 2 | # Read the Docs configuration file for MkDocs projects 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | version: 2 6 | 7 | # Specify os and python version 8 | build: 9 | os: "ubuntu-24.04" 10 | tools: 11 | python: "3.12" 12 | jobs: 13 | create_environment: 14 | - asdf plugin add uv 15 | - asdf install uv latest 16 | - asdf global uv latest 17 | - UV_PROJECT_ENVIRONMENT=$READTHEDOCS_VIRTUALENV_PATH uv sync --all-extras --group docs 18 | install: 19 | - "true" 20 | 21 | mkdocs: 22 | configuration: mkdocs.yml 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Hemanta Pokharel 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will upload a Python Package using Twine when a release is created 2 | # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries 3 | 4 | # This workflow uses actions that are not certified by GitHub. 5 | # They are provided by a third-party and are governed by 6 | # separate terms of service, privacy policy, and support 7 | # documentation. 8 | 9 | name: Upload Python Package 10 | 11 | on: 12 | release: 13 | types: [published] 14 | 15 | permissions: 16 | contents: read 17 | 18 | jobs: 19 | deploy: 20 | 21 | runs-on: ubuntu-latest 22 | 23 | steps: 24 | - uses: actions/checkout@v3 25 | - name: Set up Python 26 | uses: actions/setup-python@v3 27 | with: 28 | python-version: '3.x' 29 | - name: Install dependencies 30 | run: | 31 | python -m pip install --upgrade pip 32 | pip install build 33 | - name: Build package 34 | run: python -m build 35 | - name: Publish package 36 | uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 37 | with: 38 | user: __token__ 39 | password: ${{ secrets.PYPI_API_TOKEN }} 40 | -------------------------------------------------------------------------------- /docs/exceptions.md: -------------------------------------------------------------------------------- 1 | # Exceptions 2 | 3 | This page contains the reference for all custom exceptions. 4 | 5 | ## Handling Errors 6 | 7 | All exceptions raised by `seedrcc` inherit from the base exception `seedrcc.exceptions.SeedrError`. This allows you to catch all errors from the library with a single `try...except` block, while still being able to handle specific error types differently. 8 | 9 | Here is a example of how you can handle various exceptions: 10 | 11 | ```python 12 | import seedrcc 13 | 14 | try: 15 | # This operation might fail for various reasons 16 | client.add_torrent("some-magnet-link") 17 | 18 | except seedrcc.exceptions.APIError as e: 19 | # Handle specific API errors (e.g., invalid magnet) 20 | print(f"An API error occurred: {e}") 21 | print(f"Error Type: {e.error_type}, Code: {e.code}") 22 | 23 | except seedrcc.exceptions.AuthenticationError as e: 24 | # Handle authentication failures (e.g., invalid token) 25 | print(f"An authentication error occurred: {e}") 26 | 27 | except seedrcc.exceptions.SeedrError as e: 28 | # Catch any other library-specific errors 29 | print(f"An unexpected error occurred with seedrcc: {e}") 30 | ``` 31 | 32 | ## Exception Reference 33 | 34 | ::: seedrcc.exceptions 35 | options: 36 | show_root_heading: true 37 | show_source: true 38 | show_bases: true 39 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "seedrcc" 3 | version = "2.0.2" 4 | authors = [{ name = "Hemanta Pokharel", email = "yo@hemantapkh.com" }] 5 | description = "A comprehensive Python API wrapper for seedr.cc" 6 | readme = "README.md" 7 | requires-python = ">=3.9" 8 | license = { file = "LICENSE" } 9 | classifiers = [ 10 | "Development Status :: 5 - Production/Stable", 11 | "Intended Audience :: Developers", 12 | "License :: OSI Approved :: MIT License", 13 | "Operating System :: OS Independent", 14 | "Programming Language :: Python :: 3.9", 15 | "Programming Language :: Python :: 3.10", 16 | "Programming Language :: Python :: 3.11", 17 | "Programming Language :: Python :: 3.12", 18 | "Programming Language :: Python :: 3.13", 19 | "Topic :: Internet", 20 | "Topic :: Software Development :: Libraries :: Python Modules", 21 | "Topic :: Utilities", 22 | "Typing :: Typed", 23 | ] 24 | dependencies = ["anyio", "httpx"] 25 | 26 | [project.urls] 27 | "Homepage" = "https://github.com/hemantapkh/seedrcc" 28 | "Documentation" = "https://seedrcc.readthedocs.io/en/latest/" 29 | "Bug Tracker" = "https://github.com/hemantapkh/seedrcc/issues" 30 | 31 | [build-system] 32 | requires = ["hatchling"] 33 | build-backend = "hatchling.build" 34 | 35 | [tool.ruff] 36 | line-length = 120 37 | 38 | [dependency-groups] 39 | docs = [ 40 | "griffe-fieldz>=0.3.0", 41 | "mkdocs>=1.6.1", 42 | "mkdocs-material>=9.6.17", 43 | "mkdocstrings[python]>=0.30.0", 44 | ] 45 | -------------------------------------------------------------------------------- /docs/models.md: -------------------------------------------------------------------------------- 1 | # Data Models 2 | 3 | This library uses data models to represent both the authentication token and the data returned from the Seedr API. 4 | 5 | ## The Token Object 6 | 7 | The `Token` object is a central piece of the client, holding the authentication credentials for your session. It's designed to be a simple, immutable data container that can be easily serialized and deserialized. 8 | 9 | ::: seedrcc.token.Token 10 | options: 11 | show_root_heading: true 12 | show_source: false 13 | show_bases: false 14 | filters: ["!^_"] 15 | 16 | ## API Response Models 17 | 18 | All data returned from the Seedr API is parsed into clean, easy-to-use data models. These models provide type-hinted attributes for all documented API fields, making it easy to work with the responses in a structured and predictable way. 19 | 20 | ### Accessing Raw Data 21 | 22 | All API response models provide a `.get_raw()` method to access the original, unmodified dictionary from the server. 23 | 24 | ```python 25 | # This example assumes you have a 'client' instance from a previous example 26 | settings = client.get_settings() 27 | 28 | # Access a typed attribute 29 | print(settings.account.username) 30 | 31 | # Access the raw, underlying dictionary 32 | raw_data = settings.get_raw() 33 | print(raw_data["account"]["username"]) 34 | ``` 35 | 36 | ### Models Reference 37 | 38 | ::: seedrcc.models 39 | options: 40 | show_root_heading: true 41 | show_source: false 42 | show_bases: false 43 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: seedrcc 2 | repo_url: https://github.com/hemantapkh/seedrcc 3 | repo_name: hemantapkh/seedrcc 4 | 5 | theme: 6 | name: material 7 | language: en 8 | logo: images/seedrpy.png 9 | favicon: images/seedrpy.png 10 | icon: 11 | repo: fontawesome/brands/github 12 | features: 13 | - navigation.tabs 14 | - navigation.tabs.sticky 15 | - navigation.footer 16 | - navigation.top 17 | - search.suggest 18 | - search.highlight 19 | - content.code.copy 20 | - content.code.annotate 21 | palette: 22 | # Palette toggle for light mode 23 | - media: "(prefers-color-scheme: light)" 24 | scheme: default 25 | primary: green 26 | accent: amber 27 | toggle: 28 | icon: material/brightness-7 29 | name: Switch to dark mode 30 | 31 | # Palette toggle for dark mode 32 | - media: "(prefers-color-scheme: dark)" 33 | scheme: slate 34 | primary: green 35 | accent: amber 36 | toggle: 37 | icon: material/brightness-4 38 | name: Switch to light mode 39 | 40 | plugins: 41 | - search 42 | - mkdocstrings: 43 | handlers: 44 | python: 45 | options: 46 | show_root_heading: true 47 | show_source: true 48 | members_order: source 49 | extensions: 50 | - griffe_fieldz: 51 | include_inherited: false 52 | include_private: false 53 | add_fields_to: docstring-attributes 54 | remove_fields_from_members: false 55 | 56 | nav: 57 | - "Home": "index.md" 58 | - "API Reference": 59 | - "Synchronous Client": "sync_client.md" 60 | - "Asynchronous Client": "async_client.md" 61 | - "Data Models": "models.md" 62 | - "Exceptions": "exceptions.md" 63 | 64 | markdown_extensions: 65 | - pymdownx.highlight 66 | - pymdownx.superfences 67 | - admonition 68 | - pymdownx.details 69 | - toc: 70 | permalink: true -------------------------------------------------------------------------------- /seedrcc/token.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import json 3 | from dataclasses import asdict, dataclass 4 | from typing import Any, Dict, Optional 5 | 6 | from .exceptions import TokenError 7 | 8 | 9 | @dataclass(frozen=True) 10 | class Token: 11 | """ 12 | Represents the authentication tokens for a Seedr session. 13 | """ 14 | 15 | access_token: str 16 | refresh_token: Optional[str] = None 17 | device_code: Optional[str] = None 18 | 19 | def to_dict(self) -> Dict[str, Any]: 20 | """ 21 | Returns the token data as a dictionary, excluding any fields that are None. 22 | """ 23 | return asdict(self, dict_factory=lambda x: {k: v for (k, v) in x if v is not None}) 24 | 25 | def to_json(self) -> str: 26 | """ 27 | Returns the token data as a JSON string. 28 | """ 29 | return json.dumps(self.to_dict()) 30 | 31 | def to_base64(self) -> str: 32 | """ 33 | Returns the token data as a Base64-encoded JSON string. 34 | """ 35 | json_str = self.to_json() 36 | return base64.b64encode(json_str.encode("utf-8")).decode("utf-8") 37 | 38 | def __iter__(self): 39 | """Allows the object to be converted to a dict using dict().""" 40 | yield from self.to_dict().items() 41 | 42 | def __str__(self) -> str: 43 | """ 44 | Returns the JSON representation of the token. 45 | """ 46 | return self.to_json() 47 | 48 | def __repr__(self) -> str: 49 | """ 50 | Provides a safe, masked representation of the Token that avoids leaking secrets. 51 | """ 52 | 53 | def _mask(value: Optional[str]) -> str: 54 | if value is None: 55 | return "None" 56 | return f"{value[:5]}****" 57 | 58 | parts = [ 59 | f"access_token={_mask(self.access_token)}", 60 | f"refresh_token={_mask(self.refresh_token)}", 61 | f"device_code={_mask(self.device_code)}", 62 | ] 63 | return f"Token({', '.join(parts)})" 64 | 65 | @classmethod 66 | def from_dict(cls, data: Dict[str, Any]) -> "Token": 67 | """ 68 | Creates a Token object from a dictionary. 69 | """ 70 | try: 71 | return cls(**data) 72 | except TypeError as e: 73 | raise TokenError(f"Failed to create Token from dictionary: {e}") from e 74 | 75 | @classmethod 76 | def from_json(cls, json_str: str) -> "Token": 77 | """ 78 | Creates a Token object from a JSON string. 79 | """ 80 | try: 81 | data = json.loads(json_str) 82 | return cls.from_dict(data) 83 | except json.JSONDecodeError as e: 84 | raise TokenError(f"Failed to decode JSON: {e}") from e 85 | 86 | @classmethod 87 | def from_base64(cls, b64_str: str) -> "Token": 88 | """ 89 | Creates a Token object from a Base64-encoded JSON string. 90 | """ 91 | try: 92 | json_str = base64.b64decode(b64_str).decode("utf-8") 93 | return cls.from_json(json_str) 94 | except (ValueError, TypeError) as e: 95 | raise TokenError(f"Failed to decode Base64 string: {e}") from e 96 | -------------------------------------------------------------------------------- /seedrcc/exceptions.py: -------------------------------------------------------------------------------- 1 | import json 2 | from typing import Optional 3 | 4 | import httpx 5 | 6 | 7 | class SeedrError(Exception): 8 | """Base exception for all seedrcc errors.""" 9 | 10 | 11 | class APIError(SeedrError): 12 | """ 13 | Raised when the API returns an error. 14 | 15 | Attributes: 16 | response (Optional[httpx.Response]): The full HTTP response object. 17 | code (Optional[int]): The custom error code from the API response body. 18 | error_type (Optional[str]): The type of error from the API response body (e.g., 'parsing_error'). 19 | """ 20 | 21 | def __init__( 22 | self, default_message: str = "An API error occurred.", response: Optional[httpx.Response] = None 23 | ) -> None: 24 | self.response = response 25 | self.code: Optional[int] = None 26 | self.error_type: Optional[str] = None 27 | 28 | if response: 29 | try: 30 | data = response.json() 31 | if isinstance(data, dict): 32 | self.code = data.get("code") 33 | self.error_type = data.get("result") 34 | 35 | except json.JSONDecodeError: 36 | pass 37 | 38 | super().__init__(default_message) 39 | 40 | 41 | class ServerError(SeedrError): 42 | """Raised for 5xx server-side errors.""" 43 | 44 | def __init__( 45 | self, default_message: str = "A server error occurred.", response: Optional[httpx.Response] = None 46 | ) -> None: 47 | self.response = response 48 | if response: 49 | message = f"{response.status_code} {response.reason_phrase}" 50 | else: 51 | message = default_message 52 | super().__init__(message) 53 | 54 | 55 | class AuthenticationError(SeedrError): 56 | """ 57 | Raised when authentication or re-authentication fails. 58 | 59 | Attributes: 60 | response (Optional[httpx.Response]): The full HTTP response object from the failed auth attempt. 61 | error_type (Optional[str]): The error type from the API response body (e.g., 'invalid_grant'). 62 | """ 63 | 64 | def __init__( 65 | self, default_message: str = "An authentication error occurred.", response: Optional[httpx.Response] = None 66 | ) -> None: 67 | self.response = response 68 | self.error_type: Optional[str] = None 69 | message = default_message 70 | 71 | # Attempt to parse a more specific error message from the response 72 | if response: 73 | try: 74 | data = response.json() 75 | if isinstance(data, dict): 76 | # Use 'error_description' as the main message if available 77 | if "error_description" in data: 78 | message = data["error_description"] 79 | self.error_type = data.get("error") 80 | except json.JSONDecodeError: 81 | pass 82 | 83 | super().__init__(message) 84 | 85 | 86 | class NetworkError(SeedrError): 87 | """Raised for network-level errors, such as timeouts or connection problems.""" 88 | 89 | pass 90 | 91 | 92 | class TokenError(SeedrError): 93 | """Raised for errors related to token serialization or deserialization.""" 94 | 95 | pass 96 | -------------------------------------------------------------------------------- /seedrcc/_request_models.py: -------------------------------------------------------------------------------- 1 | from dataclasses import asdict, dataclass 2 | from typing import Any, Dict, Literal, Optional 3 | 4 | from . import _constants 5 | 6 | 7 | @dataclass 8 | class BaseModel: 9 | """Base model for request payloads and parameters.""" 10 | 11 | def to_dict(self) -> Dict[str, Any]: 12 | """Converts the dataclass instance to a dictionary.""" 13 | return asdict(self) 14 | 15 | 16 | @dataclass 17 | class PasswordLoginPayload(BaseModel): 18 | """Payload for password-based authentication.""" 19 | 20 | username: str 21 | password: str 22 | grant_type: str = "password" 23 | client_id: str = _constants.PSWRD_CLIENT_ID 24 | type: str = "login" 25 | 26 | 27 | @dataclass 28 | class RefreshTokenPayload(BaseModel): 29 | """Payload for refreshing an access token.""" 30 | 31 | refresh_token: str 32 | grant_type: str = "refresh_token" 33 | client_id: str = _constants.PSWRD_CLIENT_ID 34 | 35 | 36 | @dataclass 37 | class GetDeviceCodeParams(BaseModel): 38 | """Parameters for fetching device code for device code authentication.""" 39 | 40 | client_id: str = _constants.DEVICE_CLIENT_ID 41 | 42 | 43 | @dataclass 44 | class DeviceCodeAuthParams(BaseModel): 45 | """Parameters for device code authorization.""" 46 | 47 | device_code: str 48 | client_id: str = _constants.DEVICE_CLIENT_ID 49 | 50 | 51 | @dataclass 52 | class AddTorrentPayload(BaseModel): 53 | """Payload for adding a new torrent.""" 54 | 55 | folder_id: str = "-1" 56 | torrent_magnet: Optional[str] = None 57 | wishlist_id: Optional[str] = None 58 | 59 | 60 | @dataclass 61 | class ScanPagePayload(BaseModel): 62 | """Payload for scanning a page.""" 63 | 64 | url: str 65 | 66 | 67 | @dataclass 68 | class CreateArchivePayload(BaseModel): 69 | """Payload for creating an archive from a folder.""" 70 | 71 | folder_id: str 72 | 73 | def to_dict(self) -> Dict[str, Any]: 74 | """Overrides base to_dict to format the archive_arr correctly.""" 75 | return {"archive_arr": f'[{{"type":"folder","id":{self.folder_id}}}]'} 76 | 77 | 78 | @dataclass 79 | class FetchFilePayload(BaseModel): 80 | """Payload for fetching a file's details.""" 81 | 82 | folder_file_id: str 83 | 84 | 85 | @dataclass 86 | class ListContentsPayload(BaseModel): 87 | """Payload for listing contents of a folder.""" 88 | 89 | content_type: str = "folder" 90 | content_id: str = "0" 91 | 92 | 93 | @dataclass 94 | class RenameFilePayload(BaseModel): 95 | """Payload for renaming a file.""" 96 | 97 | rename_to: str 98 | file_id: str 99 | 100 | 101 | @dataclass 102 | class RenameFolderPayload(BaseModel): 103 | """Payload for renaming a folder.""" 104 | 105 | rename_to: str 106 | folder_id: str 107 | 108 | 109 | @dataclass 110 | class DeleteItemPayload(BaseModel): 111 | """Payload for deleting an item (file, folder, or torrent).""" 112 | 113 | item_type: Literal["file", "folder", "torrent"] 114 | item_id: str 115 | 116 | def to_dict(self) -> Dict[str, Any]: 117 | """Overrides base to_dict to format the delete_arr correctly.""" 118 | return {"delete_arr": f'[{{"type":"{self.item_type}","id":{self.item_id}}}]'} 119 | 120 | 121 | @dataclass 122 | class RemoveWishlistPayload(BaseModel): 123 | """Payload for removing a wishlist item.""" 124 | 125 | id: str 126 | 127 | 128 | @dataclass 129 | class AddFolderPayload(BaseModel): 130 | """Payload for adding a new folder.""" 131 | 132 | name: str 133 | 134 | 135 | @dataclass 136 | class SearchFilesPayload(BaseModel): 137 | """Payload for searching files.""" 138 | 139 | search_query: str 140 | 141 | 142 | @dataclass 143 | class ChangeNamePayload(BaseModel): 144 | """Payload for changing the account name.""" 145 | 146 | fullname: str 147 | password: str 148 | setting: str = "fullname" 149 | 150 | 151 | @dataclass 152 | class ChangePasswordPayload(BaseModel): 153 | """Payload for changing the account password.""" 154 | 155 | password: str 156 | new_password: str 157 | new_password_repeat: str 158 | setting: str = "password" 159 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # UV 98 | # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | #uv.lock 102 | 103 | # poetry 104 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 105 | # This is especially recommended for binary packages to ensure reproducibility, and is more 106 | # commonly ignored for libraries. 107 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 108 | #poetry.lock 109 | 110 | # pdm 111 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 112 | #pdm.lock 113 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 114 | # in version control. 115 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control 116 | .pdm.toml 117 | .pdm-python 118 | .pdm-build/ 119 | 120 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 121 | __pypackages__/ 122 | 123 | # Celery stuff 124 | celerybeat-schedule 125 | celerybeat.pid 126 | 127 | # SageMath parsed files 128 | *.sage.py 129 | 130 | # Environments 131 | .env 132 | .venv 133 | env/ 134 | venv/ 135 | ENV/ 136 | env.bak/ 137 | venv.bak/ 138 | 139 | # Spyder project settings 140 | .spyderproject 141 | .spyproject 142 | 143 | # Rope project settings 144 | .ropeproject 145 | 146 | # mkdocs documentation 147 | /site 148 | 149 | # mypy 150 | .mypy_cache/ 151 | .dmypy.json 152 | dmypy.json 153 | 154 | # Pyre type checker 155 | .pyre/ 156 | 157 | # pytype static type analyzer 158 | .pytype/ 159 | 160 | # Cython debug symbols 161 | cython_debug/ 162 | 163 | # PyCharm 164 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 165 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 166 | # and can be added to the global gitignore or merged into this file. For a more nuclear 167 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 168 | #.idea/ 169 | 170 | -------------------------------------------------------------------------------- /seedrcc/_base.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from typing import Any, Callable, Dict, List, Optional 3 | 4 | from . import models 5 | from .token import Token 6 | 7 | 8 | class BaseClient(ABC): 9 | """ 10 | Abstract Base Client defining the contract for all API clients. 11 | """ 12 | 13 | _on_token_refresh: Optional[Callable[[Token], None]] 14 | _token: Token 15 | 16 | def __init__( 17 | self, 18 | token: Token, 19 | on_token_refresh: Optional[Callable[[Token], None]] = None, 20 | ) -> None: 21 | self._token = token 22 | self._on_token_refresh = on_token_refresh 23 | 24 | @property 25 | def token(self) -> Token: 26 | """Get the current authentication token used by the client.""" 27 | return self._token 28 | 29 | @staticmethod 30 | @abstractmethod 31 | def get_device_code() -> "models.DeviceCode": 32 | """Get the device and user code required for authorization.""" 33 | pass 34 | 35 | @abstractmethod 36 | def refresh_token(self) -> "models.RefreshTokenResult": 37 | """Manually refresh the access token.""" 38 | pass 39 | 40 | @abstractmethod 41 | def get_settings(self) -> "models.UserSettings": 42 | """Get the user settings.""" 43 | pass 44 | 45 | @abstractmethod 46 | def get_memory_bandwidth(self) -> "models.MemoryBandwidth": 47 | """Get the memory and bandwidth usage.""" 48 | pass 49 | 50 | @abstractmethod 51 | def list_contents(self, folder_id: str = "0") -> "models.ListContentsResult": 52 | """List the contents of a folder.""" 53 | pass 54 | 55 | @abstractmethod 56 | def add_torrent( 57 | self, 58 | magnet_link: Optional[str] = None, 59 | torrent_file: Optional[str] = None, 60 | wishlist_id: Optional[str] = None, 61 | folder_id: str = "-1", 62 | ) -> "models.AddTorrentResult": 63 | """Add a torrent to the seedr account for downloading.""" 64 | pass 65 | 66 | @abstractmethod 67 | def scan_page(self, url: str) -> "models.ScanPageResult": 68 | """Scan a page for torrents and magnet links.""" 69 | pass 70 | 71 | @abstractmethod 72 | def fetch_file(self, file_id: str) -> "models.FetchFileResult": 73 | """Create a link of a file.""" 74 | pass 75 | 76 | @abstractmethod 77 | def create_archive(self, folder_id: str) -> "models.CreateArchiveResult": 78 | """Create an archive link of a folder.""" 79 | pass 80 | 81 | @abstractmethod 82 | def search_files(self, query: str) -> "models.Folder": 83 | """Search for files.""" 84 | pass 85 | 86 | @abstractmethod 87 | def add_folder(self, name: str) -> "models.APIResult": 88 | """Add a folder.""" 89 | pass 90 | 91 | @abstractmethod 92 | def rename_file(self, file_id: str, rename_to: str) -> "models.APIResult": 93 | """Rename a file.""" 94 | pass 95 | 96 | @abstractmethod 97 | def rename_folder(self, folder_id: str, rename_to: str) -> "models.APIResult": 98 | """Rename a folder.""" 99 | pass 100 | 101 | @abstractmethod 102 | def delete_file(self, file_id: str) -> "models.APIResult": 103 | """Delete a file.""" 104 | pass 105 | 106 | @abstractmethod 107 | def delete_folder(self, folder_id: str) -> "models.APIResult": 108 | """Delete a folder.""" 109 | pass 110 | 111 | @abstractmethod 112 | def delete_torrent(self, torrent_id: str) -> "models.APIResult": 113 | """Delete an active downloading torrent.""" 114 | pass 115 | 116 | @abstractmethod 117 | def delete_wishlist(self, wishlist_id: str) -> "models.APIResult": 118 | """Delete an item from the wishlist.""" 119 | pass 120 | 121 | @abstractmethod 122 | def get_devices(self) -> List["models.Device"]: 123 | """Get the devices connected to the seedr account.""" 124 | pass 125 | 126 | @abstractmethod 127 | def change_name(self, name: str, password: str) -> "models.APIResult": 128 | """Change the name of the account.""" 129 | pass 130 | 131 | @abstractmethod 132 | def change_password(self, old_password: str, new_password: str) -> "models.APIResult": 133 | """Change the password of the account.""" 134 | pass 135 | 136 | @abstractmethod 137 | def close(self) -> None: 138 | """Close the underlying HTTP client.""" 139 | pass 140 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | seedrcc logo 3 |

4 | 5 |

seedrcc

6 | 7 |

8 | A comprehensive Python API wrapper for seedr.cc. 9 |

10 | 11 |

12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | Stars 20 | 21 | 22 | Issues 23 | 24 |

25 | 26 | --- 27 | 28 | **seedrcc** provides a clean Python interface for the Seedr API, with support for both synchronous and asynchronous operations. 29 | 30 |
31 | Table of Contents 32 | 33 | - [✨ Features](#-features) 34 | - [📦 Installation](#-installation) 35 | - [🚀 Usage](#-usage) 36 | - [🗺️ How I Found the Endpoints](#️-how-i-found-the-endpoints) 37 | - [📚 Documentation](#-documentation) 38 | - [🙌 Contributing](#-contributing) 39 | - [📄 License](#-license) 40 | 41 |
42 | 43 | ## ✨ Features 44 | 45 | - **Complete API Coverage:** All major Seedr API endpoints are supported. 46 | - **Works for All Users:** Fully functional for both free and premium Seedr accounts. 47 | - **Sync & Async:** Includes `seedrcc.Seedr` for synchronous operations and `seedrcc.AsyncSeedr` for asynchronous ones. 48 | - **Robust Authentication:** Handles all authentication flows, including automatic token refreshes. 49 | - **Fully Typed:** Provides type hints for all methods and models to improve code quality and clarity. 50 | - **Custom Exceptions:** Provides specific exceptions for API, network, and authentication errors. 51 | - **Dataclass Models:** API responses are parsed into clean, easy-to-use dataclasses. 52 | 53 | ## 📦 Installation 54 | 55 | Install from PyPI: 56 | 57 | ```bash 58 | pip install seedrcc 59 | ``` 60 | 61 | Or, install the latest version directly from GitHub: 62 | 63 | ```bash 64 | pip install git+https://github.com/hemantapkh/seedrcc.git 65 | ``` 66 | 67 | ## 🚀 Usage 68 | 69 | ### Synchronous Example (with Device Authentication) 70 | 71 | ```python 72 | from seedrcc import Seedr 73 | 74 | # 1. Get the device and user codes from the API. 75 | codes = Seedr.get_device_code() 76 | 77 | # 2. Open the verification URL (https://seedr.cc/devices) and enter the user code. 78 | print(f"Please go to {codes.verification_url} and enter the code: {codes.user_code}") 79 | input("Press Enter after authorizing the device.") 80 | 81 | # 3. Create the client using the device_code. 82 | with Seedr.from_device_code(codes.device_code) as client: 83 | settings = client.get_settings() 84 | print(f"Success! Hello, {settings.account.username}") 85 | ``` 86 | 87 | ### Asynchronous Example (with Password Authentication) 88 | 89 | ```python 90 | import asyncio 91 | from seedrcc import AsyncSeedr 92 | 93 | async def main(): 94 | # Authenticate using your username and password. 95 | async with AsyncSeedr.from_password("your_email@example.com", "your_password") as client: 96 | # Get your account settings. 97 | settings = await client.get_settings() 98 | print(f"Hello, {settings.account.username}!") 99 | 100 | if __name__ == "__main__": 101 | asyncio.run(main()) 102 | ``` 103 | 104 | 105 | ## 🗺️ How I Found the Endpoints 106 | 107 | While Seedr.cc offers a premium [API](https://www.seedr.cc/docs/api/rest/v1/), it is not available to free users. This library was built by studying the network requests from the official **[Kodi](https://github.com/DannyZB/seedr_kodi)** and **[Chrome](https://github.com/DannyZB/seedr_chrome)** extensions. 108 | 109 | Further analysis of the main Seedr website's network traffic revealed a very similar API pattern, which made it possible to implement the full feature set. Because the library uses the same API as the official tools, it works reliably for all users. 110 | 111 | ## 📚 Documentation 112 | 113 | For a complete guide to every available method, data model, and advanced features like saving sessions, please see the **[Full Documentation](https://seedrcc.readthedocs.io/)**. 114 | 115 | ## 🙌 Contributing 116 | 117 | Contributions are welcome! If you'd like to help, please feel free to fork the repository, create a feature branch, and open a pull request. 118 | 119 | ## 📄 License 120 | 121 | This project is distributed under the MIT License. See `LICENSE` for more information. 122 | 123 | --- 124 | Author/Maintainer: [Hemanta Pokharel](https://hemantapkh.com) ([GitHub](https://github.com/hemantapkh)) 125 | 126 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 |

2 | seedrcc logo 3 |

4 | 5 |

seedrcc

6 | 7 |

8 | A comprehensive Python API wrapper for seedr.cc. 9 |

10 | 11 |

12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | Stars 20 | 21 | 22 | Issues 23 | 24 |

25 | 26 | --- 27 | 28 | **seedrcc** provides a clean Python interface for the Seedr API, with support for both synchronous and asynchronous operations. 29 | 30 | ## Features 31 | 32 | - **Complete API Coverage:** All major Seedr API endpoints are supported. 33 | - **Works for All Users:** Fully functional for both free and premium Seedr accounts. 34 | - **Sync & Async:** Includes `seedrcc.Seedr` for synchronous operations and `seedrcc.AsyncSeedr` for asynchronous ones. 35 | - **Robust Authentication:** Handles all authentication flows, including automatic token refreshes. 36 | - **Fully Typed:** Provides type hints for all methods and models to improve code quality and clarity. 37 | - **Custom Exceptions:** Provides specific exceptions for API, network, and authentication errors. 38 | - **Dataclass Models:** API responses are parsed into clean, easy-to-use dataclasses. 39 | 40 | ## Installation 41 | 42 | Install the library from PyPI using `pip` or your favorite package manager. 43 | 44 | ```bash 45 | pip install seedrcc 46 | ``` 47 | 48 | Or, install the latest version directly from GitHub: 49 | 50 | ```bash 51 | pip install git+https://github.com/hemantapkh/seedrcc.git 52 | ``` 53 | 54 | ## Basic Usage 55 | 56 | ### Synchronous 57 | 58 | ```python 59 | from seedrcc import Seedr 60 | 61 | # Authenticate using your username and password 62 | with Seedr.from_password("your_email@example.com", "your_password") as client: 63 | # Get your account settings 64 | settings = client.get_settings() 65 | print(f"Hello, {settings.account.username}!") 66 | ``` 67 | 68 | ### Asynchronous 69 | 70 | ```python 71 | import asyncio 72 | from seedrcc import AsyncSeedr 73 | 74 | async def main(): 75 | # Authenticate using your username and password 76 | async with AsyncSeedr.from_password("your_email@example.com", "your_password") as client: 77 | # Get your account settings 78 | settings = await client.get_settings() 79 | print(f"Hello, {settings.account.username}!") 80 | 81 | if __name__ == "__main__": 82 | asyncio.run(main()) 83 | ``` 84 | 85 | ## Authentication Methods 86 | 87 | ### Device Code Flow (Recommended) 88 | 89 | This is the recommended authentication method as it provides a long-term session. The process involves three steps: 90 | 91 | 1. **Get Device Code:** Use `Seedr.get_device_code()` to get the device and user codes. 92 | 2. **Authorize:** Open the `verification_url` ([https://www.seedr.cc/devices](https://www.seedr.cc/devices)) in a browser and enter the `user_code`. 93 | 3. **Create Client:** Once authorized, create the client using the `device_code`. 94 | 95 | ```python 96 | from seedrcc import Seedr 97 | 98 | # 1. Get the device and user codes from the API. 99 | codes = Seedr.get_device_code() 100 | 101 | # 2. Display the authorization details for the user to act on. 102 | print(f"Please open this URL in your browser: {codes.verification_url}") 103 | print(f"And enter this code: {codes.user_code}") 104 | input("Press Enter after authorizing.") 105 | 106 | # 3. Create the client using the device_code from the initial request. 107 | with Seedr.from_device_code(codes.device_code) as client: 108 | settings = client.get_settings() 109 | print(f"Success! Hello, {settings.account.username}") 110 | ``` 111 | 112 | ### Password Flow 113 | 114 | You can also authenticate directly with your username and password, as shown in the Basic Usage examples. 115 | 116 | ## Saving and Reusing Your Session 117 | 118 | To avoid logging in every time, you can save the `Token` object after your first authentication and reuse it. 119 | 120 | The `Token` object has several methods to convert it for storage or use: 121 | 122 | - [`token.to_json()`][seedrcc.token.Token.to_json]: Converts the token to a JSON string, perfect for saving in text files. 123 | - [`token.to_base64()`][seedrcc.token.Token.to_base64]: Converts the token to a simple Base64 string, great for databases or environment variables. 124 | - [`token.to_dict()`][seedrcc.token.Token.to_dict]: Converts the token to a Python dictionary for in-memory use. 125 | 126 | You can then use the corresponding [`Token.from_...()`][seedrcc.token.Token.from_dict] method to load it back. 127 | 128 | ```python 129 | from seedrcc import Seedr, Token 130 | 131 | # Assume 'client' is an authenticated client from a previous session 132 | # client = Seedr.from_password(...) or Seedr.from_device_code(...) 133 | 134 | # 1. Get the token and convert it to a JSON string. 135 | token = client.token 136 | json_string = token.to_json() 137 | 138 | # You would typically save this string to a file or database. 139 | 140 | # --- In a new session --- 141 | 142 | # 2. Load the token from the saved string. 143 | reloaded_token = Token.from_json(json_string) 144 | 145 | # 3. Initialize the client directly with the reloaded token. 146 | with Seedr(token=reloaded_token) as new_client: 147 | settings = new_client.get_settings() 148 | print(f"Successfully re-authenticated as {settings.account.username}") 149 | ``` 150 | 151 | ## Handling Token Refreshes 152 | 153 | The client automatically handles token refreshes. However, if you load an old token when your program starts, the client must perform a refresh on the first API call, which adds a delay. 154 | 155 | To avoid this, you can provide an `on_token_refresh` callback. This function is called whenever a refresh happens, giving you the new `Token` object so you can save it. By saving the new token, your program will start with a fresh token on its next run. 156 | 157 | Your callback function will receive the new `Token` object as its first argument. 158 | 159 | When using the `AsyncSeedr` client, you can provide an `async` function for the callback. If a regular synchronous function is provided instead, it will be safely executed in a separate thread. 160 | 161 | **Callback with a single argument:** 162 | 163 | ```python 164 | from seedrcc import Seedr, Token 165 | 166 | def save_token(token: Token): 167 | # This function will be called whenever the token is refreshed. 168 | # You should save the new token data to your database or file. 169 | print(f"New token received: {token.access_token}") 170 | with open("token.json", "w") as f: 171 | f.write(token.to_json()) 172 | 173 | # When creating the client, pass the callback function. 174 | client = Seedr.from_password("user", "pass", on_token_refresh=save_token) 175 | ``` 176 | 177 | **Callback with multiple arguments:** 178 | 179 | If you need to pass additional arguments to your callback (like a user ID), you can use a `lambda` for a synchronous callback. For an `async` callback, the recommended approach is to use `functools.partial`. 180 | 181 | ```python 182 | import functools 183 | from seedrcc import AsyncSeedr, Seedr, Token 184 | 185 | # Synchronous example with lambda 186 | def save_token_for_user(token: Token, user_id: int): 187 | print(f"Saving new token for user {user_id}") 188 | # ... save to database ... 189 | 190 | user_id = 123 191 | client = Seedr.from_password( 192 | "user", "pass", 193 | on_token_refresh=lambda token: save_token_for_user(token, user_id) 194 | ) 195 | 196 | # Asynchronous example with functools.partial 197 | async def save_token_for_user_async(token: Token, user_id: int): 198 | print(f"Saving new token for user {user_id}") 199 | # ... save to database ... 200 | 201 | user_id = 456 202 | async_callback = functools.partial(save_token_for_user_async, user_id=user_id) 203 | async_client = await AsyncSeedr.from_password( 204 | "user", "pass", on_token_refresh=async_callback 205 | ) 206 | ``` 207 | 208 | ## Next Steps 209 | 210 | Ready to explore the full API? The reference documentation provides a complete guide to every available method, from adding torrents and managing files to checking your account details. 211 | 212 | - **API Reference:** 213 | - [Synchronous Client](sync_client.md) 214 | - [Asynchronous Client](async_client.md) 215 | - [Data Models](models.md) 216 | - [Exceptions](exceptions.md) 217 | - **License:** This project is licensed under the MIT License. See the `LICENSE` file for details. 218 | - **Contributing:** Contributions are welcome! Please see the project on [GitHub](https://github.com/hemantapkh/seedrcc). 219 | -------------------------------------------------------------------------------- /seedrcc/models.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass, field, fields 2 | from datetime import datetime 3 | from typing import Any, Dict, List, Optional 4 | 5 | from ._utils import parse_datetime 6 | 7 | __all__ = [ 8 | "File", 9 | "Torrent", 10 | "Folder", 11 | "ListContentsResult", 12 | "UserSettings", 13 | "AccountInfo", 14 | "AccountSettings", 15 | "FetchFileResult", 16 | "DeviceCode", 17 | "CreateArchiveResult", 18 | "Device", 19 | "MemoryBandwidth", 20 | "AddTorrentResult", 21 | "APIResult", 22 | "RefreshTokenResult", 23 | "ScannedTorrent", 24 | "ScanPageResult", 25 | ] 26 | 27 | 28 | @dataclass(frozen=True) 29 | class _BaseModel: 30 | """Base model with a raw data field. Internal use only.""" 31 | 32 | _raw: Dict[str, Any] = field(repr=False, compare=False, init=False) 33 | 34 | def get_raw(self) -> Dict[str, Any]: 35 | """Returns the raw, unmodified dictionary from the API response.""" 36 | return self._raw 37 | 38 | @classmethod 39 | def from_dict(cls, data: dict): 40 | """Creates a model instance from a dictionary, ignoring unknown fields.""" 41 | model_fields = {f.name for f in fields(cls)} 42 | filtered_data = {k: v for k, v in data.items() if k in model_fields} 43 | instance = cls(**filtered_data) 44 | object.__setattr__(instance, "_raw", data) 45 | return instance 46 | 47 | 48 | @dataclass(frozen=True) 49 | class Torrent(_BaseModel): 50 | """Represents a torrent in the user's account.""" 51 | 52 | id: int 53 | name: str 54 | size: int 55 | hash: str 56 | progress: str 57 | last_update: Optional[datetime] 58 | folder: str = "" 59 | download_rate: int = 0 60 | upload_rate: int = 0 61 | torrent_quality: Optional[int] = None 62 | connected_to: int = 0 63 | downloading_from: int = 0 64 | uploading_to: int = 0 65 | seeders: int = 0 66 | leechers: int = 0 67 | warnings: Optional[str] = None 68 | stopped: int = 0 69 | progress_url: Optional[str] = None 70 | 71 | @classmethod 72 | def from_dict(cls, data: dict) -> "Torrent": 73 | instance = cls( 74 | id=data.get("id", 0), 75 | name=data.get("name", ""), 76 | size=data.get("size", 0), 77 | hash=data.get("hash", ""), 78 | progress=data.get("progress", ""), 79 | last_update=parse_datetime(data.get("last_update")), 80 | folder=data.get("folder", ""), 81 | download_rate=data.get("download_rate", 0), 82 | upload_rate=data.get("upload_rate", 0), 83 | torrent_quality=data.get("torrent_quality"), 84 | connected_to=data.get("connected_to", 0), 85 | downloading_from=data.get("downloading_from", 0), 86 | uploading_to=data.get("uploading_to", 0), 87 | seeders=data.get("seeders", 0), 88 | leechers=data.get("leechers", 0), 89 | warnings=data.get("warnings"), 90 | stopped=data.get("stopped", 0), 91 | progress_url=data.get("progress_url"), 92 | ) 93 | object.__setattr__(instance, "_raw", data) 94 | return instance 95 | 96 | 97 | @dataclass(frozen=True) 98 | class File(_BaseModel): 99 | """Represents a file within Seedr.""" 100 | 101 | file_id: int 102 | name: str 103 | size: int 104 | folder_id: int 105 | folder_file_id: int 106 | hash: str 107 | last_update: Optional[datetime] = None 108 | play_audio: bool = False 109 | play_video: bool = False 110 | video_progress: Optional[str] = None 111 | is_lost: int = 0 112 | thumb: Optional[str] = None 113 | 114 | @classmethod 115 | def from_dict(cls, data: dict) -> "File": 116 | instance = cls( 117 | file_id=data.get("file_id", 0), 118 | name=data.get("name", ""), 119 | size=data.get("size", 0), 120 | folder_id=data.get("folder_id", 0), 121 | folder_file_id=data.get("folder_file_id", 0), 122 | hash=data.get("hash", ""), 123 | last_update=parse_datetime(data.get("last_update")), 124 | play_audio=data.get("play_audio", False), 125 | play_video=data.get("play_video", False), 126 | video_progress=data.get("video_progress"), 127 | is_lost=data.get("is_lost", 0), 128 | thumb=data.get("thumb"), 129 | ) 130 | object.__setattr__(instance, "_raw", data) 131 | return instance 132 | 133 | 134 | @dataclass(frozen=True) 135 | class Folder(_BaseModel): 136 | """Represents a folder, which can contain files, torrents, and other folders.""" 137 | 138 | id: int 139 | name: str 140 | fullname: str 141 | size: int 142 | last_update: Optional[datetime] 143 | is_shared: bool 144 | play_audio: bool 145 | play_video: bool 146 | folders: List["Folder"] = field(default_factory=list) 147 | files: List[File] = field(default_factory=list) 148 | torrents: List["Torrent"] = field(default_factory=list) 149 | parent: Optional[int] = None 150 | timestamp: Optional[datetime] = None 151 | indexes: List[Any] = field(default_factory=list) 152 | 153 | @classmethod 154 | def from_dict(cls, data: dict) -> "Folder": 155 | instance = cls( 156 | id=data.get("id") or data.get("folder_id") or 0, 157 | name=data.get("name", ""), 158 | fullname=data.get("fullname", data.get("name", "")), 159 | size=data.get("size", 0), 160 | last_update=parse_datetime(data.get("last_update") or data.get("timestamp")), 161 | is_shared=data.get("is_shared", False), 162 | play_audio=data.get("play_audio", False), 163 | play_video=data.get("play_video", False), 164 | folders=[Folder.from_dict(f) for f in data.get("folders", [])], 165 | files=[File.from_dict(f) for f in data.get("files", [])], 166 | torrents=[Torrent.from_dict(t) for t in data.get("torrents", [])], 167 | parent=data.get("parent"), 168 | timestamp=parse_datetime(data.get("timestamp")), 169 | indexes=data.get("indexes", []), 170 | ) 171 | object.__setattr__(instance, "_raw", data) 172 | return instance 173 | 174 | 175 | @dataclass(frozen=True) 176 | class AccountSettings(_BaseModel): 177 | """Represents the nested 'settings' object in the user settings response.""" 178 | 179 | allow_remote_access: bool 180 | site_language: str 181 | subtitles_language: str 182 | email_announcements: bool 183 | email_newsletter: bool 184 | 185 | 186 | @dataclass(frozen=True) 187 | class AccountInfo(_BaseModel): 188 | """Represents the nested 'account' object in the user settings response.""" 189 | 190 | username: str 191 | user_id: int 192 | premium: int 193 | package_id: int 194 | package_name: str 195 | space_used: int 196 | space_max: int 197 | bandwidth_used: int 198 | email: str 199 | wishlist: list 200 | invites: int 201 | invites_accepted: int 202 | max_invites: int 203 | 204 | 205 | @dataclass(frozen=True) 206 | class UserSettings(_BaseModel): 207 | """Represents the complete response from the get_settings endpoint.""" 208 | 209 | result: bool 210 | code: int 211 | settings: AccountSettings 212 | account: AccountInfo 213 | country: str 214 | 215 | @classmethod 216 | def from_dict(cls, data: dict) -> "UserSettings": 217 | instance = cls( 218 | result=data.get("result", False), 219 | code=data.get("code", 0), 220 | settings=AccountSettings.from_dict(data.get("settings", {})), 221 | account=AccountInfo.from_dict(data.get("account", {})), 222 | country=data.get("country", ""), 223 | ) 224 | object.__setattr__(instance, "_raw", data) 225 | return instance 226 | 227 | 228 | @dataclass(frozen=True) 229 | class MemoryBandwidth(_BaseModel): 230 | """Represents the user's memory and bandwidth usage details.""" 231 | 232 | bandwidth_used: int 233 | bandwidth_max: int 234 | space_used: int 235 | space_max: int 236 | is_premium: int 237 | 238 | 239 | @dataclass(frozen=True) 240 | class Device(_BaseModel): 241 | """Represents a device connected to the user's account.""" 242 | 243 | client_id: str 244 | client_name: str 245 | device_code: str 246 | tk: str 247 | 248 | 249 | @dataclass(frozen=True) 250 | class DeviceCode(_BaseModel): 251 | """Represents the codes used in the device authentication flow.""" 252 | 253 | expires_in: int 254 | interval: int 255 | device_code: str 256 | user_code: str 257 | verification_url: str 258 | 259 | 260 | @dataclass(frozen=True) 261 | class ScannedTorrent(_BaseModel): 262 | """Represents a torrent found by the scan_page method.""" 263 | 264 | id: int 265 | hash: str 266 | size: int 267 | title: str 268 | magnet: str 269 | last_use: Optional[datetime] 270 | pct: float 271 | filenames: List[str] = field(default_factory=list) 272 | filesizes: List[int] = field(default_factory=list) 273 | 274 | @classmethod 275 | def from_dict(cls, data: dict) -> "ScannedTorrent": 276 | instance = cls( 277 | id=data.get("id", 0), 278 | hash=data.get("hash", ""), 279 | size=data.get("size", 0), 280 | title=data.get("title", ""), 281 | magnet=data.get("magnet", ""), 282 | last_use=parse_datetime(data.get("last_use")), 283 | pct=data.get("pct", 0.0), 284 | filenames=data.get("filenames", []), 285 | filesizes=data.get("filesizes", []), 286 | ) 287 | object.__setattr__(instance, "_raw", data) 288 | return instance 289 | 290 | 291 | @dataclass(frozen=True) 292 | class ListContentsResult(Folder): 293 | """Represents the result of listing folder contents, including account metadata.""" 294 | 295 | space_used: int = 0 296 | space_max: int = 0 297 | saw_walkthrough: int = 0 298 | type: str = "" 299 | t: List[Optional[datetime]] = field(default_factory=list) 300 | 301 | @classmethod 302 | def from_dict(cls, data: dict) -> "ListContentsResult": 303 | folder = Folder.from_dict(data) 304 | instance = cls( 305 | id=folder.id, 306 | name=folder.name, 307 | fullname=folder.fullname, 308 | size=folder.size, 309 | last_update=folder.last_update, 310 | is_shared=folder.is_shared, 311 | play_audio=folder.play_audio, 312 | play_video=folder.play_video, 313 | folders=folder.folders, 314 | files=folder.files, 315 | torrents=folder.torrents, 316 | parent=folder.parent, 317 | timestamp=folder.timestamp, 318 | indexes=folder.indexes, 319 | space_used=data.get("space_used", 0), 320 | space_max=data.get("space_max", 0), 321 | saw_walkthrough=data.get("saw_walkthrough", 0), 322 | type=data.get("type", ""), 323 | t=[parse_datetime(ts) for ts in data.get("t", [])], 324 | ) 325 | object.__setattr__(instance, "_raw", data) 326 | return instance 327 | 328 | 329 | @dataclass(frozen=True) 330 | class AddTorrentResult(_BaseModel): 331 | """Represents the result of adding a torrent.""" 332 | 333 | result: bool 334 | user_torrent_id: int 335 | title: str 336 | torrent_hash: str 337 | code: Optional[int] = None 338 | 339 | 340 | @dataclass(frozen=True) 341 | class CreateArchiveResult(_BaseModel): 342 | """Represents the result of a request to create an archive.""" 343 | 344 | result: bool 345 | archive_id: int 346 | archive_url: str 347 | code: Optional[int] = None 348 | 349 | 350 | @dataclass(frozen=True) 351 | class FetchFileResult(_BaseModel): 352 | """Represents the result of a request to fetch a file, including the download URL.""" 353 | 354 | result: bool 355 | url: str 356 | name: str 357 | 358 | 359 | @dataclass(frozen=True) 360 | class RefreshTokenResult(_BaseModel): 361 | """Represents the response from a token refresh.""" 362 | 363 | access_token: str 364 | expires_in: int 365 | token_type: str 366 | scope: Optional[str] = None 367 | 368 | 369 | @dataclass(frozen=True) 370 | class ScanPageResult(_BaseModel): 371 | """Represents the full result of a scan_page request.""" 372 | 373 | result: bool 374 | torrents: List[ScannedTorrent] 375 | 376 | @classmethod 377 | def from_dict(cls, data: dict) -> "ScanPageResult": 378 | instance = cls( 379 | result=data.get("result", False), torrents=[ScannedTorrent.from_dict(t) for t in data.get("torrents", [])] 380 | ) 381 | object.__setattr__(instance, "_raw", data) 382 | return instance 383 | 384 | 385 | @dataclass(frozen=True) 386 | class APIResult(_BaseModel): 387 | """Represents a generic API result for operations that return a simple success/failure.""" 388 | 389 | result: bool 390 | code: Optional[int] = None 391 | -------------------------------------------------------------------------------- /seedrcc/client.py: -------------------------------------------------------------------------------- 1 | import json 2 | from typing import Any, Callable, Dict, List, Literal, Optional, Type 3 | 4 | import httpx 5 | 6 | from . import _constants, _request_models, models 7 | from ._base import BaseClient 8 | from .exceptions import APIError, AuthenticationError, NetworkError, ServerError 9 | from .token import Token 10 | 11 | 12 | class Seedr(BaseClient): 13 | """Synchronous client for interacting with the Seedr API. 14 | 15 | Example: 16 | ```python 17 | from seedrcc import Seedr, Token 18 | 19 | # Load a previously saved token from a JSON string 20 | token_string = '{"access_token": "...", "refresh_token": "..."}' 21 | token = Token.from_json(token_string) 22 | 23 | # Initialize the client and make a request 24 | with Seedr(token=token) as client: 25 | settings = client.get_settings() 26 | print(f"Hello, {settings.account.username}") 27 | ``` 28 | """ 29 | 30 | _client: httpx.Client 31 | _manages_client_lifecycle: bool 32 | 33 | def __init__( 34 | self, 35 | token: Token, 36 | on_token_refresh: Optional[Callable[[Token], None]] = None, 37 | httpx_client: Optional[httpx.Client] = None, 38 | timeout: float = 30.0, 39 | proxy: Optional[Dict[str, str]] = None, 40 | **httpx_kwargs: Any, 41 | ) -> None: 42 | """Initializes the synchronous client with an existing token. 43 | 44 | Args: 45 | token: An authenticated `Token` object. 46 | on_token_refresh: An optional callback function that is called with the new 47 | `Token` object when the session is refreshed. 48 | httpx_client: An optional, pre-configured `httpx.Client` instance. 49 | timeout: The timeout for network requests in seconds. 50 | proxy: An optional dictionary of proxy to use for requests. 51 | **httpx_kwargs: Optional keyword arguments to pass to the `httpx.Client` constructor. 52 | These are ignored if `httpx_client` is provided. 53 | """ 54 | super().__init__(token, on_token_refresh) 55 | if httpx_client is not None: 56 | self._client = httpx_client 57 | self._manages_client_lifecycle = False 58 | else: 59 | httpx_kwargs.setdefault("timeout", timeout) 60 | httpx_kwargs.setdefault("proxy", proxy) 61 | self._client = httpx.Client(**httpx_kwargs) 62 | self._manages_client_lifecycle = True 63 | 64 | @property 65 | def token(self) -> Token: 66 | return super().token 67 | 68 | @staticmethod 69 | def get_device_code() -> models.DeviceCode: 70 | """ 71 | Gets the device and user codes required for authorization. 72 | 73 | This is the first step in the device authentication flow. 74 | 75 | Returns: 76 | A `DeviceCode` object containing the codes needed for the next step. 77 | 78 | Example: 79 | ```python 80 | from seedrcc import Seedr 81 | 82 | codes = Seedr.get_device_code() 83 | print(f"Go to {codes.verification_url} and enter {codes.user_code}") 84 | ``` 85 | """ 86 | params = _request_models.GetDeviceCodeParams() 87 | with httpx.Client() as client: 88 | response = Seedr._make_http_request(client, "get", _constants.DEVICE_CODE_URL, params=params.to_dict()) 89 | 90 | if not response.is_success: 91 | raise APIError("Failed to get device code.", response=response) 92 | 93 | try: 94 | response_data = response.json() 95 | except json.JSONDecodeError as e: 96 | raise APIError("Invalid JSON response from API.", response=None) from e 97 | return models.DeviceCode.from_dict(response_data) 98 | 99 | @classmethod 100 | def from_password( 101 | cls: Type["Seedr"], 102 | username: str, 103 | password: str, 104 | on_token_refresh: Optional[Callable[[Token], None]] = None, 105 | httpx_client: Optional[httpx.Client] = None, 106 | timeout: float = 30.0, 107 | proxy: Optional[Dict[str, str]] = None, 108 | **httpx_kwargs: Any, 109 | ) -> "Seedr": 110 | """ 111 | Creates a new client by authenticating with a username and password. 112 | 113 | Args: 114 | username: The user's Seedr username (email). 115 | password: The user's Seedr password. 116 | on_token_refresh: A callback function that is called with the new 117 | Token object when the session is refreshed. 118 | httpx_client: An optional, pre-configured `httpx.Client` instance. 119 | timeout: The timeout for network requests in seconds. 120 | proxy: A dictionary of proxy to use for requests. 121 | **httpx_kwargs: Optional keyword arguments to pass to the `httpx.Client` constructor. 122 | These are ignored if `httpx_client` is provided. 123 | 124 | Returns: 125 | An initialized `Seedr` client instance. 126 | 127 | Example: 128 | ```python 129 | client = Seedr.from_password("your_email@example.com", "your_password") 130 | ``` 131 | """ 132 | 133 | def auth_callable(client: httpx.Client) -> Dict[str, Any]: 134 | """Prepare and execute the authentication request.""" 135 | payload = _request_models.PasswordLoginPayload(username=username, password=password) 136 | return cls._authenticate_and_get_token_data( 137 | client, 138 | "post", 139 | _constants.TOKEN_URL, 140 | data=payload.to_dict(), 141 | ) 142 | 143 | return cls._initialize_client( 144 | auth_callable, 145 | lambda r: {}, 146 | on_token_refresh, 147 | httpx_client, 148 | timeout=timeout, 149 | proxy=proxy, 150 | **httpx_kwargs, 151 | ) 152 | 153 | @classmethod 154 | def from_device_code( 155 | cls: Type["Seedr"], 156 | device_code: str, 157 | on_token_refresh: Optional[Callable[[Token], None]] = None, 158 | httpx_client: Optional[httpx.Client] = None, 159 | timeout: float = 30.0, 160 | proxy: Optional[Dict[str, str]] = None, 161 | **httpx_kwargs: Any, 162 | ) -> "Seedr": 163 | """ 164 | Creates a new client by authorizing with a device code. 165 | 166 | This is the second step in the device authentication flow, after getting the 167 | codes from `Seedr.get_device_code()`. 168 | 169 | Args: 170 | device_code: The device code obtained from `get_device_code()`. 171 | on_token_refresh: A callback function that is called with the new 172 | Token object when the session is refreshed. 173 | httpx_client: An optional, pre-configured `httpx.Client` instance. 174 | timeout: The timeout for network requests in seconds. 175 | proxy: A dictionary of proxy to use for requests. 176 | **httpx_kwargs: Optional keyword arguments to pass to the `httpx.Client` constructor. 177 | These are ignored if `httpx_client` is provided. 178 | 179 | Returns: 180 | An initialized `Seedr` client instance. 181 | 182 | Example: 183 | ```python 184 | client = Seedr.from_device_code("your_device_code") 185 | ``` 186 | """ 187 | 188 | def auth_callable(client: httpx.Client) -> Dict[str, Any]: 189 | """Prepare and execute the device authorization request.""" 190 | params = _request_models.DeviceCodeAuthParams(device_code=device_code) 191 | return cls._authenticate_and_get_token_data( 192 | client, 193 | "get", 194 | _constants.DEVICE_AUTHORIZE_URL, 195 | params=params.to_dict(), 196 | ) 197 | 198 | return cls._initialize_client( 199 | auth_callable, 200 | lambda r: {"device_code": device_code}, 201 | on_token_refresh, 202 | httpx_client, 203 | timeout=timeout, 204 | proxy=proxy, 205 | **httpx_kwargs, 206 | ) 207 | 208 | @classmethod 209 | def from_refresh_token( 210 | cls: Type["Seedr"], 211 | refresh_token: str, 212 | on_token_refresh: Optional[Callable[[Token], None]] = None, 213 | httpx_client: Optional[httpx.Client] = None, 214 | timeout: float = 30.0, 215 | proxy: Optional[Dict[str, str]] = None, 216 | **httpx_kwargs: Any, 217 | ) -> "Seedr": 218 | """ 219 | Creates a new client by using an existing refresh token. 220 | 221 | Args: 222 | refresh_token: A valid refresh token. 223 | on_token_refresh: A callback function that is called with the new 224 | Token object when the session is refreshed. 225 | httpx_client: An optional, pre-configured `httpx.Client` instance. 226 | timeout: The timeout for network requests in seconds. 227 | proxy: A dictionary of proxy to use for requests. 228 | **httpx_kwargs: Optional keyword arguments to pass to the `httpx.Client` constructor. 229 | These are ignored if `httpx_client` is provided. 230 | 231 | Returns: 232 | An initialized `Seedr` client instance. 233 | 234 | Example: 235 | ```python 236 | client = Seedr.from_refresh_token("your_refresh_token") 237 | ``` 238 | """ 239 | 240 | def auth_callable(client: httpx.Client) -> Dict[str, Any]: 241 | """Prepare and execute the token refresh request.""" 242 | payload = _request_models.RefreshTokenPayload(refresh_token=refresh_token) 243 | return cls._authenticate_and_get_token_data( 244 | client, 245 | "post", 246 | _constants.TOKEN_URL, 247 | data=payload.to_dict(), 248 | ) 249 | 250 | return cls._initialize_client( 251 | auth_callable, 252 | lambda r: {"refresh_token": refresh_token}, 253 | on_token_refresh, 254 | httpx_client, 255 | timeout=timeout, 256 | proxy=proxy, 257 | **httpx_kwargs, 258 | ) 259 | 260 | def refresh_token(self) -> models.RefreshTokenResult: 261 | """ 262 | Manually refreshes the access token. 263 | 264 | This is useful if you want to proactively manage the token's lifecycle 265 | instead of waiting for an automatic refresh on an API call. 266 | 267 | Returns: 268 | The result of the token refresh operation. 269 | 270 | Example: 271 | ```python 272 | try: 273 | result = client.refresh_token() 274 | print(f"Token successfully refreshed. New token expires in {result.expires_in} seconds.") 275 | except AuthenticationError as e: 276 | print(f"Failed to refresh token: {e}") 277 | ``` 278 | """ 279 | return self._refresh_access_token() 280 | 281 | def get_settings(self) -> models.UserSettings: 282 | """ 283 | Get the user settings. 284 | 285 | Returns: 286 | An object containing the user's account settings. 287 | 288 | Example: 289 | ```python 290 | settings = client.get_settings() 291 | print(settings.account.username) 292 | ``` 293 | """ 294 | response_data = self._api_request("get", "get_settings") 295 | return models.UserSettings.from_dict(response_data) 296 | 297 | def get_memory_bandwidth(self) -> models.MemoryBandwidth: 298 | """ 299 | Get the memory and bandwidth usage. 300 | 301 | Returns: 302 | An object containing memory and bandwidth details. 303 | 304 | Example: 305 | ```python 306 | usage = client.get_memory_bandwidth() 307 | print(f"Space used: {usage.space_used}/{usage.space_max}") 308 | ``` 309 | """ 310 | response_data = self._api_request("get", "get_memory_bandwidth") 311 | return models.MemoryBandwidth.from_dict(response_data) 312 | 313 | def list_contents(self, folder_id: str = "0") -> models.ListContentsResult: 314 | """ 315 | List the contents of a folder. 316 | 317 | Args: 318 | folder_id (str, optional): The folder id to list the contents of. Defaults to root folder. 319 | 320 | Returns: 321 | An object containing the contents of the folder. 322 | 323 | Example: 324 | ```python 325 | response = client.list_contents() 326 | print(response) 327 | ``` 328 | """ 329 | payload = _request_models.ListContentsPayload(content_id=folder_id) 330 | response_data = self._api_request("post", "list_contents", data=payload.to_dict()) 331 | return models.ListContentsResult.from_dict(response_data) 332 | 333 | def add_torrent( 334 | self, 335 | magnet_link: Optional[str] = None, 336 | torrent_file: Optional[str] = None, 337 | wishlist_id: Optional[str] = None, 338 | folder_id: str = "-1", 339 | ) -> models.AddTorrentResult: 340 | """ 341 | Add a torrent to the seedr account for downloading. 342 | 343 | Args: 344 | magnet_link (str, optional): The magnet link of the torrent. 345 | torrent_file (str, optional): Remote or local path of the torrent file. 346 | wishlist_id (str, optional): The ID of a wishlist item to add. 347 | folder_id (str, optional): The folder ID to add the torrent to. Defaults to root ('-1'). 348 | 349 | Returns: 350 | An object containing the result of the add torrent operation. 351 | 352 | Example: 353 | ```python 354 | # Add by magnet link 355 | result = client.add_torrent(magnet_link="magnet:?xt=urn:btih:...") 356 | print(result.title) 357 | 358 | # Add from a local .torrent file 359 | result = client.add_torrent(torrent_file="/path/to/your/file.torrent") 360 | print(result.title) 361 | ``` 362 | """ 363 | payload = _request_models.AddTorrentPayload( 364 | torrent_magnet=magnet_link, 365 | wishlist_id=wishlist_id, 366 | folder_id=folder_id, 367 | ) 368 | files = {} 369 | if torrent_file: 370 | files = self._read_torrent_file(torrent_file) 371 | 372 | response_data = self._api_request("post", "add_torrent", data=payload.to_dict(), files=files) 373 | return models.AddTorrentResult.from_dict(response_data) 374 | 375 | def scan_page(self, url: str) -> models.ScanPageResult: 376 | """ 377 | Scan a page for torrents and magnet links. 378 | 379 | Args: 380 | url (str): The URL of the page to scan. 381 | 382 | Returns: 383 | An object containing the list of torrents found on the page. 384 | 385 | Example: 386 | ```python 387 | result = client.scan_page(url='some_torrent_page_url') 388 | for torrent in result.torrents: 389 | print(torrent.title) 390 | ``` 391 | """ 392 | payload = _request_models.ScanPagePayload(url=url) 393 | response_data = self._api_request("post", "scan_page", data=payload.to_dict()) 394 | return models.ScanPageResult.from_dict(response_data) 395 | 396 | def fetch_file(self, file_id: str) -> models.FetchFileResult: 397 | """ 398 | Create a link of a file. 399 | 400 | Args: 401 | file_id (str): The file id to fetch. This is the `folder_file_id` from the `list_contents` method. 402 | 403 | Returns: 404 | An object containing the file details and download URL. 405 | 406 | Example: 407 | ```python 408 | result = client.fetch_file(file_id='12345') 409 | print(f"Download URL: {result.url}") 410 | ``` 411 | """ 412 | payload = _request_models.FetchFilePayload(folder_file_id=file_id) 413 | response_data = self._api_request("post", "fetch_file", data=payload.to_dict()) 414 | return models.FetchFileResult.from_dict(response_data) 415 | 416 | def create_archive(self, folder_id: str) -> models.CreateArchiveResult: 417 | """ 418 | Create an archive link of a folder. 419 | 420 | Args: 421 | folder_id (str): The folder id to create the archive of. 422 | 423 | Returns: 424 | An object containing the result of the archive creation. 425 | 426 | Example: 427 | ```python 428 | result = client.create_archive(folder_id='12345') 429 | print(f"Archive URL: {result.archive_url}") 430 | ``` 431 | """ 432 | payload = _request_models.CreateArchivePayload(folder_id=folder_id) 433 | response_data = self._api_request("post", "create_empty_archive", data=payload.to_dict()) 434 | return models.CreateArchiveResult.from_dict(response_data) 435 | 436 | def search_files(self, query: str) -> models.Folder: 437 | """ 438 | Search for files. 439 | 440 | Args: 441 | query (str): The query to search for. 442 | 443 | Returns: 444 | An object containing the search results. 445 | 446 | Example: 447 | ```python 448 | results = client.search_files(query='harry potter') 449 | for f in results.folders: 450 | print(f"Found folder: {f.name}") 451 | ``` 452 | """ 453 | payload = _request_models.SearchFilesPayload(search_query=query) 454 | response_data = self._api_request("post", "search_files", data=payload.to_dict()) 455 | return models.Folder.from_dict(response_data) 456 | 457 | def add_folder(self, name: str) -> models.APIResult: 458 | """ 459 | Add a folder. 460 | 461 | Args: 462 | name (str): Folder name to add. 463 | 464 | Returns: 465 | An object indicating the result of the operation. 466 | 467 | Example: 468 | ```python 469 | result = client.add_folder(name='New Folder') 470 | if result.result: 471 | print("Folder created successfully.") 472 | ``` 473 | """ 474 | payload = _request_models.AddFolderPayload(name=name) 475 | response_data = self._api_request("post", "add_folder", data=payload.to_dict()) 476 | return models.APIResult.from_dict(response_data) 477 | 478 | def rename_file(self, file_id: str, rename_to: str) -> models.APIResult: 479 | """ 480 | Rename a file. 481 | 482 | Args: 483 | file_id (str): The file id to rename. 484 | rename_to (str): The new name of the file. 485 | 486 | Returns: 487 | An object indicating the result of the operation. 488 | 489 | Example: 490 | ```python 491 | result = client.rename_file(file_id='12345', rename_to='newName') 492 | if result.result: 493 | print("File renamed successfully.") 494 | ``` 495 | """ 496 | payload = _request_models.RenameFilePayload(rename_to=rename_to, file_id=file_id) 497 | response_data = self._api_request("post", "rename", data=payload.to_dict()) 498 | return models.APIResult.from_dict(response_data) 499 | 500 | def rename_folder(self, folder_id: str, rename_to: str) -> models.APIResult: 501 | """ 502 | Rename a folder. 503 | 504 | Args: 505 | folder_id (str): The folder id to rename. 506 | rename_to (str): The new name of the folder. 507 | 508 | Returns: 509 | An object indicating the result of the operation. 510 | 511 | Example: 512 | ```python 513 | result = client.rename_folder(folder_id='12345', rename_to='newName') 514 | if result.result: 515 | print("Folder renamed successfully.") 516 | ``` 517 | """ 518 | payload = _request_models.RenameFolderPayload(rename_to=rename_to, folder_id=folder_id) 519 | response_data = self._api_request("post", "rename", data=payload.to_dict()) 520 | return models.APIResult.from_dict(response_data) 521 | 522 | def delete_file(self, file_id: str) -> models.APIResult: 523 | """ 524 | Delete a file. 525 | 526 | Args: 527 | file_id (str): The file id to delete. 528 | 529 | Returns: 530 | An object indicating the result of the operation. 531 | 532 | Example: 533 | ```python 534 | response = client.delete_file(file_id='12345') 535 | print(response) 536 | ``` 537 | """ 538 | return self._delete_api_item("file", file_id) 539 | 540 | def delete_folder(self, folder_id: str) -> models.APIResult: 541 | """ 542 | Delete a folder. 543 | 544 | Args: 545 | folder_id (str): The folder id to delete. 546 | 547 | Returns: 548 | An object indicating the result of the operation. 549 | 550 | Example: 551 | ```python 552 | response = client.delete_folder(folder_id='12345') 553 | print(response) 554 | ``` 555 | """ 556 | return self._delete_api_item("folder", folder_id) 557 | 558 | def delete_torrent(self, torrent_id: str) -> models.APIResult: 559 | """ 560 | Delete an active downloading torrent. 561 | 562 | Args: 563 | torrent_id (str): The torrent id to delete. 564 | 565 | Returns: 566 | An object indicating the result of the operation. 567 | 568 | Example: 569 | ```python 570 | response = client.delete_torrent(torrent_id='12345') 571 | print(response) 572 | ``` 573 | """ 574 | return self._delete_api_item("torrent", torrent_id) 575 | 576 | def delete_wishlist(self, wishlist_id: str) -> models.APIResult: 577 | """ 578 | Delete an item from the wishlist. 579 | 580 | Args: 581 | wishlist_id (str): The wishlistId of item to delete. 582 | 583 | Returns: 584 | An object indicating the result of the operation. 585 | 586 | Example: 587 | ```python 588 | result = client.delete_wishlist(wishlist_id='12345') 589 | ``` 590 | """ 591 | payload = _request_models.RemoveWishlistPayload(id=wishlist_id) 592 | response_data = self._api_request("post", "remove_wishlist", data=payload.to_dict()) 593 | return models.APIResult.from_dict(response_data) 594 | 595 | def get_devices(self) -> List[models.Device]: 596 | """ 597 | Get the devices connected to the seedr account. 598 | 599 | Returns: 600 | A list of devices connected to the account. 601 | 602 | Example: 603 | ```python 604 | devices = client.get_devices() 605 | for device in devices: 606 | print(device.client_name) 607 | ``` 608 | """ 609 | response_data = self._api_request("get", "get_devices") 610 | devices_data = response_data.get("devices", []) 611 | return [models.Device.from_dict(d) for d in devices_data] 612 | 613 | def change_name(self, name: str, password: str) -> models.APIResult: 614 | """ 615 | Change the name of the account. 616 | 617 | Args: 618 | name (str): The new name of the account. 619 | password (str): The password of the account. 620 | 621 | Returns: 622 | An object indicating the result of the operation. 623 | 624 | Example: 625 | ```python 626 | result = client.change_name(name='New Name', password='password') 627 | ``` 628 | """ 629 | payload = _request_models.ChangeNamePayload(fullname=name, password=password) 630 | response_data = self._api_request("post", "user_account_modify", data=payload.to_dict()) 631 | return models.APIResult.from_dict(response_data) 632 | 633 | def change_password(self, old_password: str, new_password: str) -> models.APIResult: 634 | """ 635 | Change the password of the account. 636 | 637 | Args: 638 | old_password (str): The old password of the account. 639 | new_password (str): The new password of the account. 640 | 641 | Returns: 642 | An object indicating the result of the operation. 643 | 644 | Example: 645 | ```python 646 | result = client.change_password(old_password='old', new_password='new') 647 | ``` 648 | """ 649 | payload = _request_models.ChangePasswordPayload( 650 | password=old_password, 651 | new_password=new_password, 652 | new_password_repeat=new_password, 653 | ) 654 | response_data = self._api_request("post", "user_account_modify", data=payload.to_dict()) 655 | return models.APIResult.from_dict(response_data) 656 | 657 | def _api_request( 658 | self, http_method: str, func: str, files: Optional[Dict[str, Any]] = None, **kwargs: Any 659 | ) -> Dict[str, Any]: 660 | """Handles the core logic for making authenticated API requests, including token refreshes.""" 661 | url = kwargs.pop("url", _constants.RESOURCE_URL) 662 | params = kwargs.pop("params", {}) 663 | if "access_token" not in params: 664 | params["access_token"] = self._token.access_token 665 | if func: 666 | params["func"] = func 667 | 668 | response = self._make_http_request(self._client, http_method, url, params=params, files=files, **kwargs) 669 | try: 670 | data = response.json() 671 | except json.JSONDecodeError as e: 672 | raise APIError("Invalid JSON response from API.", response=None) from e 673 | 674 | if isinstance(data, dict) and data.get("error") == "expired_token": 675 | self._refresh_access_token() 676 | params["access_token"] = self._token.access_token 677 | response = self._make_http_request(self._client, http_method, url, params=params, files=files, **kwargs) 678 | try: 679 | data = response.json() 680 | except json.JSONDecodeError as e: 681 | raise APIError("Invalid JSON response from API.", response=None) from e 682 | 683 | if response.is_client_error: 684 | if response.status_code == 401: 685 | raise AuthenticationError("Authentication failed.", response=response) 686 | raise APIError("API request failed.", response=response) 687 | 688 | if isinstance(data, dict) and data.get("result", True) is not True: 689 | raise APIError("API operation failed.", response=response) 690 | 691 | return data 692 | 693 | def _refresh_access_token(self) -> models.RefreshTokenResult: 694 | """Refreshes the access token using the refresh token or device code.""" 695 | if self._token.refresh_token: 696 | payload = _request_models.RefreshTokenPayload(refresh_token=self._token.refresh_token) 697 | response = self._make_http_request(self._client, "post", _constants.TOKEN_URL, data=payload.to_dict()) 698 | elif self._token.device_code: 699 | params = _request_models.DeviceCodeAuthParams(device_code=self._token.device_code) 700 | response = self._make_http_request( 701 | self._client, "get", _constants.DEVICE_AUTHORIZE_URL, params=params.to_dict() 702 | ) 703 | else: 704 | raise AuthenticationError("No refresh token or device code available to refresh the session.") 705 | 706 | if not response.is_success: 707 | raise AuthenticationError("Failed to refresh token.", response=response) 708 | 709 | try: 710 | response_data = response.json() 711 | except json.JSONDecodeError as e: 712 | raise APIError("Invalid JSON response from API.", response=None) from e 713 | 714 | if "access_token" not in response_data: 715 | raise AuthenticationError( 716 | "Token refresh failed. The response did not contain a new access token.", 717 | response=response, 718 | ) 719 | 720 | self._token = Token( 721 | access_token=response_data["access_token"], 722 | refresh_token=self._token.refresh_token, 723 | device_code=self._token.device_code, 724 | ) 725 | if self._on_token_refresh: 726 | self._on_token_refresh(self._token) 727 | 728 | return models.RefreshTokenResult.from_dict(response_data) 729 | 730 | def _read_torrent_file(self, torrent_file: str) -> Dict[str, Any]: 731 | """Reads a torrent file from a local path or a remote URL into memory.""" 732 | if torrent_file.startswith(("http://", "https://")): 733 | file_content = httpx.get(torrent_file).content 734 | return {"torrent_file": file_content} 735 | else: 736 | with open(torrent_file, "rb") as f: 737 | return {"torrent_file": f.read()} 738 | 739 | def _delete_api_item(self, item_type: Literal["file", "folder", "torrent"], item_id: str) -> models.APIResult: 740 | """Constructs and sends a request to delete a specific item (file, folder, etc.).""" 741 | payload = _request_models.DeleteItemPayload(item_type=item_type, item_id=item_id) 742 | response_data = self._api_request("post", "delete", data=payload.to_dict()) 743 | return models.APIResult.from_dict(response_data) 744 | 745 | @classmethod 746 | def _initialize_client( 747 | cls: Type["Seedr"], 748 | auth_callable: Callable[[httpx.Client], Dict[str, Any]], 749 | token_callable: Callable[[Dict[str, Any]], Dict[str, Any]], 750 | on_token_refresh: Optional[Callable[[Token], None]], 751 | httpx_client: Optional[httpx.Client], 752 | timeout: float = 30.0, 753 | proxy: Optional[Dict[str, str]] = None, 754 | **httpx_kwargs: Any, 755 | ) -> "Seedr": 756 | """A factory helper that orchestrates the authentication process and constructs the client.""" 757 | httpx_kwargs.setdefault("timeout", timeout) 758 | httpx_kwargs.setdefault("proxy", proxy) 759 | client = httpx_client or httpx.Client(**httpx_kwargs) 760 | success = False 761 | try: 762 | response_data = auth_callable(client) 763 | token_extras = token_callable(response_data) 764 | token = Token( 765 | access_token=response_data["access_token"], 766 | refresh_token=response_data.get("refresh_token"), 767 | **token_extras, 768 | ) 769 | instance = cls(token, on_token_refresh=on_token_refresh, httpx_client=client, **httpx_kwargs) 770 | success = True 771 | return instance 772 | finally: 773 | if httpx_client is None and not success: 774 | client.close() 775 | 776 | @classmethod 777 | def _authenticate_and_get_token_data( 778 | cls: Type["Seedr"], 779 | client: httpx.Client, 780 | method: str, 781 | url: str, 782 | **httpx_kwargs: Any, 783 | ) -> Dict[str, Any]: 784 | """Handles the common logic for making an authentication request.""" 785 | response = cls._make_http_request(client, method, url, **httpx_kwargs) 786 | 787 | if not response.is_success: 788 | raise AuthenticationError("Authentication failed.", response=response) 789 | 790 | try: 791 | data = response.json() 792 | if isinstance(data, dict) and data.get("error") in ["authorization_pending"]: 793 | raise AuthenticationError( 794 | "Authentication failed.", 795 | response=response, 796 | ) 797 | return data 798 | except json.JSONDecodeError as e: 799 | raise APIError("Invalid JSON response from API.", response=None) from e 800 | 801 | @staticmethod 802 | def _make_http_request( 803 | client: httpx.Client, 804 | method: str, 805 | url: str, 806 | **kwargs: Any, 807 | ) -> httpx.Response: 808 | """Performs the raw HTTP request, handles network/server errors, and returns the response.""" 809 | try: 810 | response = client.request(method, url, **kwargs) 811 | 812 | if response.is_server_error: 813 | raise ServerError(response=response) 814 | 815 | return response 816 | except httpx.RequestError as e: 817 | raise NetworkError(str(e)) from e 818 | 819 | def close(self) -> None: 820 | """Closes the underlying httpx client if it was created by this instance.""" 821 | if self._manages_client_lifecycle: 822 | self._client.close() 823 | 824 | def __enter__(self) -> "Seedr": 825 | return self 826 | 827 | def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: 828 | self.close() 829 | -------------------------------------------------------------------------------- /seedrcc/async_client.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | import json 3 | from typing import Any, Callable, Coroutine, Dict, List, Literal, Optional, Type 4 | 5 | import anyio 6 | import httpx 7 | 8 | from . import _constants, _request_models, models 9 | from ._base import BaseClient 10 | from .exceptions import APIError, AuthenticationError, NetworkError, ServerError 11 | from .token import Token 12 | 13 | 14 | class AsyncSeedr(BaseClient): 15 | """Asynchronous client for interacting with the Seedr API. 16 | 17 | Example: 18 | ```python 19 | import asyncio 20 | from seedrcc import AsyncSeedr, Token 21 | 22 | async def main(): 23 | # Load a previously saved token from a Base64 string 24 | b64_string = "eydhY2Nlc3NfdG9rZW4nOiAnbmV2ZXIgZ29ubmEgZ2l2ZSB5b3UgdXAnfQ==" 25 | token = Token.from_base64(b64_string) 26 | 27 | # Initialize the client and make a request 28 | async with AsyncSeedr(token=token) as client: 29 | settings = await client.get_settings() 30 | print(f"Hello, {settings.account.username}") 31 | 32 | if __name__ == "__main__": 33 | asyncio.run(main()) 34 | ``` 35 | """ 36 | 37 | _client: httpx.AsyncClient 38 | _manages_client_lifecycle: bool 39 | 40 | def __init__( 41 | self, 42 | token: Token, 43 | on_token_refresh: Optional[Callable[[Token], None]] = None, 44 | httpx_client: Optional[httpx.AsyncClient] = None, 45 | timeout: float = 30.0, 46 | proxy: Optional[Dict[str, str]] = None, 47 | **httpx_kwargs: Any, 48 | ) -> None: 49 | """Initializes the asynchronous client with an existing token. 50 | 51 | Args: 52 | token: An authenticated `Token` object. 53 | on_token_refresh: An optional callback function that is called with the new 54 | `Token` object when the session is refreshed. 55 | httpx_client: An optional, pre-configured `httpx.AsyncClient` instance. 56 | timeout: The timeout for network requests in seconds. 57 | proxy: An optional dictionary of proxy to use for requests. 58 | **httpx_kwargs: Optional keyword arguments to pass to the `httpx.AsyncClient` constructor. 59 | These are ignored if `httpx_client` is provided. 60 | """ 61 | super().__init__(token, on_token_refresh) 62 | if httpx_client is not None: 63 | self._client = httpx_client 64 | self._manages_client_lifecycle = False 65 | else: 66 | httpx_kwargs.setdefault("timeout", timeout) 67 | httpx_kwargs.setdefault("proxy", proxy) 68 | self._client = httpx.AsyncClient(**httpx_kwargs) 69 | self._manages_client_lifecycle = True 70 | 71 | @property 72 | def token(self) -> Token: 73 | return super().token 74 | 75 | @staticmethod 76 | async def get_device_code() -> models.DeviceCode: 77 | """ 78 | Gets the device and user codes required for authorization. 79 | 80 | This is the first step in the device authentication flow. 81 | 82 | Returns: 83 | A `DeviceCode` object containing the codes needed for the next step. 84 | 85 | Example: 86 | ```python 87 | from seedrcc import AsyncSeedr 88 | 89 | codes = await AsyncSeedr.get_device_code() 90 | print(f"Go to {codes.verification_url} and enter {codes.user_code}") 91 | ``` 92 | """ 93 | params = _request_models.GetDeviceCodeParams() 94 | async with httpx.AsyncClient() as client: 95 | response = await AsyncSeedr._make_http_request( 96 | client, "get", _constants.DEVICE_CODE_URL, params=params.to_dict() 97 | ) 98 | 99 | if not response.is_success: 100 | raise APIError("Failed to get device code.", response=response) 101 | 102 | try: 103 | response_data = response.json() 104 | except json.JSONDecodeError as e: 105 | raise APIError("Invalid JSON response from API.", response=None) from e 106 | return models.DeviceCode.from_dict(response_data) 107 | 108 | @classmethod 109 | async def from_password( 110 | cls: Type["AsyncSeedr"], 111 | username: str, 112 | password: str, 113 | on_token_refresh: Optional[Callable[[Token], None]] = None, 114 | httpx_client: Optional[httpx.AsyncClient] = None, 115 | timeout: float = 30.0, 116 | proxy: Optional[Dict[str, str]] = None, 117 | **httpx_kwargs: Any, 118 | ) -> "AsyncSeedr": 119 | """ 120 | Creates a new client by authenticating with a username and password. 121 | 122 | Args: 123 | username: The user's Seedr username (email). 124 | password: The user's Seedr password. 125 | on_token_refresh: A callback function that is called with the new 126 | Token object when the session is refreshed. 127 | httpx_client: An optional, pre-configured `httpx.AsyncClient` instance. 128 | timeout: The timeout for network requests in seconds. 129 | proxy: A dictionary of proxy to use for requests. 130 | **httpx_kwargs: Optional keyword arguments to pass to the `httpx.AsyncClient` constructor. 131 | These are ignored if `httpx_client` is provided. 132 | 133 | Returns: 134 | An initialized `AsyncSeedr` client instance. 135 | 136 | Example: 137 | ```python 138 | client = await AsyncSeedr.from_password("your_email@example.com", "your_password") 139 | ``` 140 | """ 141 | 142 | async def auth_callable(client: httpx.AsyncClient) -> Dict[str, Any]: 143 | """Prepare and execute the authentication request.""" 144 | payload = _request_models.PasswordLoginPayload(username=username, password=password) 145 | return await cls._authenticate_and_get_token_data( 146 | client, 147 | "post", 148 | _constants.TOKEN_URL, 149 | data=payload.to_dict(), 150 | ) 151 | 152 | return await cls._initialize_client( 153 | auth_callable, 154 | lambda r: {}, 155 | on_token_refresh, 156 | httpx_client, 157 | timeout=timeout, 158 | proxy=proxy, 159 | **httpx_kwargs, 160 | ) 161 | 162 | @classmethod 163 | async def from_device_code( 164 | cls: Type["AsyncSeedr"], 165 | device_code: str, 166 | on_token_refresh: Optional[Callable[[Token], None]] = None, 167 | httpx_client: Optional[httpx.AsyncClient] = None, 168 | timeout: float = 30.0, 169 | proxy: Optional[Dict[str, str]] = None, 170 | **httpx_kwargs: Any, 171 | ) -> "AsyncSeedr": 172 | """ 173 | Creates a new client by authorizing with a device code. 174 | 175 | This is the second step in the device authentication flow, after getting the 176 | codes from `AsyncSeedr.get_device_code()`. 177 | 178 | Args: 179 | device_code: The device code obtained from `get_device_code()`. 180 | on_token_refresh: A callback function that is called with the new 181 | Token object when the session is refreshed. 182 | httpx_client: An optional, pre-configured `httpx.AsyncClient` instance. 183 | timeout: The timeout for network requests in seconds. 184 | proxy: A dictionary of proxy to use for requests. 185 | **httpx_kwargs: Optional keyword arguments to pass to the `httpx.AsyncClient` constructor. 186 | These are ignored if `httpx_client` is provided. 187 | 188 | Returns: 189 | An initialized `AsyncSeedr` client instance. 190 | 191 | Example: 192 | ```python 193 | client = await AsyncSeedr.from_device_code("your_device_code") 194 | ``` 195 | """ 196 | 197 | async def auth_callable(client: httpx.AsyncClient) -> Dict[str, Any]: 198 | """Prepare and execute the device authorization request.""" 199 | params = _request_models.DeviceCodeAuthParams(device_code=device_code) 200 | return await cls._authenticate_and_get_token_data( 201 | client, 202 | "get", 203 | _constants.DEVICE_AUTHORIZE_URL, 204 | params=params.to_dict(), 205 | ) 206 | 207 | return await cls._initialize_client( 208 | auth_callable, 209 | lambda r: {"device_code": device_code}, 210 | on_token_refresh, 211 | httpx_client, 212 | timeout=timeout, 213 | proxy=proxy, 214 | **httpx_kwargs, 215 | ) 216 | 217 | @classmethod 218 | async def from_refresh_token( 219 | cls: Type["AsyncSeedr"], 220 | refresh_token: str, 221 | on_token_refresh: Optional[Callable[[Token], None]] = None, 222 | httpx_client: Optional[httpx.AsyncClient] = None, 223 | timeout: float = 30.0, 224 | proxy: Optional[Dict[str, str]] = None, 225 | **httpx_kwargs: Any, 226 | ) -> "AsyncSeedr": 227 | """ 228 | Creates a new client by using an existing refresh token. 229 | 230 | Args: 231 | refresh_token: A valid refresh token. 232 | on_token_refresh: A callback function that is called with the new 233 | Token object when the session is refreshed. 234 | httpx_client: An optional, pre-configured `httpx.AsyncClient` instance. 235 | timeout: The timeout for network requests in seconds. 236 | proxy: A dictionary of proxy to use for requests. 237 | **httpx_kwargs: Optional keyword arguments to pass to the `httpx.AsyncClient` constructor. 238 | These are ignored if `httpx_client` is provided. 239 | 240 | Returns: 241 | An initialized `AsyncSeedr` client instance. 242 | 243 | Example: 244 | ```python 245 | client = await AsyncSeedr.from_refresh_token("your_refresh_token") 246 | ``` 247 | """ 248 | 249 | async def auth_callable(client: httpx.AsyncClient) -> Dict[str, Any]: 250 | """Prepare and execute the token refresh request.""" 251 | payload = _request_models.RefreshTokenPayload(refresh_token=refresh_token) 252 | return await cls._authenticate_and_get_token_data( 253 | client, 254 | "post", 255 | _constants.TOKEN_URL, 256 | data=payload.to_dict(), 257 | ) 258 | 259 | return await cls._initialize_client( 260 | auth_callable, 261 | lambda r: {"refresh_token": refresh_token}, 262 | on_token_refresh, 263 | httpx_client, 264 | timeout=timeout, 265 | proxy=proxy, 266 | **httpx_kwargs, 267 | ) 268 | 269 | async def refresh_token(self) -> models.RefreshTokenResult: 270 | """ 271 | Manually refreshes the access token. 272 | 273 | This is useful if you want to proactively manage the token's lifecycle 274 | instead of waiting for an automatic refresh on an API call. 275 | 276 | Returns: 277 | The result of the token refresh operation. 278 | 279 | Example: 280 | ```python 281 | try: 282 | result = await client.refresh_token() 283 | print(f"Token successfully refreshed. New token expires in {result.expires_in} seconds.") 284 | except AuthenticationError as e: 285 | print(f"Failed to refresh token: {e}") 286 | ``` 287 | """ 288 | return await self._refresh_access_token() 289 | 290 | async def get_settings(self) -> models.UserSettings: 291 | """ 292 | Get the user settings. 293 | 294 | Returns: 295 | An object containing the user's account settings. 296 | 297 | Example: 298 | ```python 299 | settings = await client.get_settings() 300 | print(settings.account.username) 301 | ``` 302 | """ 303 | response_data = await self._api_request("get", "get_settings") 304 | return models.UserSettings.from_dict(response_data) 305 | 306 | async def get_memory_bandwidth(self) -> models.MemoryBandwidth: 307 | """ 308 | Get the memory and bandwidth usage. 309 | 310 | Returns: 311 | An object containing memory and bandwidth details. 312 | 313 | Example: 314 | ```python 315 | usage = await client.get_memory_bandwidth() 316 | print(f"Space used: {usage.space_used}/{usage.space_max}") 317 | ``` 318 | """ 319 | response_data = await self._api_request("get", "get_memory_bandwidth") 320 | return models.MemoryBandwidth.from_dict(response_data) 321 | 322 | async def list_contents(self, folder_id: str = "0") -> models.ListContentsResult: 323 | """ 324 | List the contents of a folder. 325 | 326 | Args: 327 | folder_id (str, optional): The folder id to list the contents of. Defaults to root folder. 328 | 329 | Returns: 330 | An object containing the contents of the folder. 331 | 332 | Example: 333 | ```python 334 | response = await client.list_contents() 335 | print(response) 336 | ``` 337 | """ 338 | payload = _request_models.ListContentsPayload(content_id=folder_id) 339 | response_data = await self._api_request("post", "list_contents", data=payload.to_dict()) 340 | return models.ListContentsResult.from_dict(response_data) 341 | 342 | async def add_torrent( 343 | self, 344 | magnet_link: Optional[str] = None, 345 | torrent_file: Optional[str] = None, 346 | wishlist_id: Optional[str] = None, 347 | folder_id: str = "-1", 348 | ) -> models.AddTorrentResult: 349 | """ 350 | Add a torrent to the seedr account for downloading. 351 | 352 | Args: 353 | magnet_link (str, optional): The magnet link of the torrent. 354 | torrent_file (str, optional): Remote or local path of the torrent file. 355 | wishlist_id (str, optional): The ID of a wishlist item to add. 356 | folder_id (str, optional): The folder ID to add the torrent to. Defaults to root ('-1'). 357 | 358 | Returns: 359 | An object containing the result of the add torrent operation. 360 | 361 | Example: 362 | ```python 363 | # Add by magnet link 364 | result = await client.add_torrent(magnet_link="magnet:?xt=urn:btih:...") 365 | print(result.title) 366 | 367 | # Add from a local .torrent file 368 | result = await client.add_torrent(torrent_file="/path/to/your/file.torrent") 369 | print(result.title) 370 | ``` 371 | """ 372 | payload = _request_models.AddTorrentPayload( 373 | torrent_magnet=magnet_link, 374 | wishlist_id=wishlist_id, 375 | folder_id=folder_id, 376 | ) 377 | files = {} 378 | if torrent_file: 379 | files = await self._read_torrent_file_async(torrent_file) 380 | 381 | response_data = await self._api_request("post", "add_torrent", data=payload.to_dict(), files=files) 382 | return models.AddTorrentResult.from_dict(response_data) 383 | 384 | async def scan_page(self, url: str) -> models.ScanPageResult: 385 | """ 386 | Scan a page for torrents and magnet links. 387 | 388 | Args: 389 | url (str): The URL of the page to scan. 390 | 391 | Returns: 392 | An object containing the list of torrents found on the page. 393 | 394 | Example: 395 | ```python 396 | result = await client.scan_page(url='some_torrent_page_url') 397 | for torrent in result.torrents: 398 | print(torrent.title) 399 | ``` 400 | """ 401 | payload = _request_models.ScanPagePayload(url=url) 402 | response_data = await self._api_request("post", "scan_page", data=payload.to_dict()) 403 | return models.ScanPageResult.from_dict(response_data) 404 | 405 | async def fetch_file(self, file_id: str) -> models.FetchFileResult: 406 | """ 407 | Create a link of a file. 408 | 409 | Args: 410 | file_id (str): The file id to fetch. This is the `folder_file_id` from the `list_contents` method. 411 | 412 | Returns: 413 | An object containing the file details and download URL. 414 | 415 | Example: 416 | ```python 417 | result = await client.fetch_file(file_id='12345') 418 | print(f"Download URL: {result.url}") 419 | ``` 420 | """ 421 | payload = _request_models.FetchFilePayload(folder_file_id=file_id) 422 | response_data = await self._api_request("post", "fetch_file", data=payload.to_dict()) 423 | return models.FetchFileResult.from_dict(response_data) 424 | 425 | async def create_archive(self, folder_id: str) -> models.CreateArchiveResult: 426 | """ 427 | Create an archive link of a folder. 428 | 429 | Args: 430 | folder_id (str): The folder id to create the archive of. 431 | 432 | Returns: 433 | An object containing the result of the archive creation. 434 | 435 | Example: 436 | ```python 437 | result = await client.create_archive(folder_id='12345') 438 | print(f"Archive URL: {result.archive_url}") 439 | ``` 440 | """ 441 | payload = _request_models.CreateArchivePayload(folder_id=folder_id) 442 | response_data = await self._api_request("post", "create_empty_archive", data=payload.to_dict()) 443 | return models.CreateArchiveResult.from_dict(response_data) 444 | 445 | async def search_files(self, query: str) -> models.Folder: 446 | """ 447 | Search for files. 448 | 449 | Args: 450 | query (str): The query to search for. 451 | 452 | Returns: 453 | An object containing the search results. 454 | 455 | Example: 456 | ```python 457 | results = await client.search_files(query='harry potter') 458 | for f in results.folders: 459 | print(f"Found folder: {f.name}") 460 | ``` 461 | """ 462 | payload = _request_models.SearchFilesPayload(search_query=query) 463 | response_data = await self._api_request("post", "search_files", data=payload.to_dict()) 464 | return models.Folder.from_dict(response_data) 465 | 466 | async def add_folder(self, name: str) -> models.APIResult: 467 | """ 468 | Add a folder. 469 | 470 | Args: 471 | name (str): Folder name to add. 472 | 473 | Returns: 474 | An object indicating the result of the operation. 475 | 476 | Example: 477 | ```python 478 | result = await client.add_folder(name='New Folder') 479 | if result.result: 480 | print("Folder created successfully.") 481 | ``` 482 | """ 483 | payload = _request_models.AddFolderPayload(name=name) 484 | response_data = await self._api_request("post", "add_folder", data=payload.to_dict()) 485 | return models.APIResult.from_dict(response_data) 486 | 487 | async def rename_file(self, file_id: str, rename_to: str) -> models.APIResult: 488 | """ 489 | Rename a file. 490 | 491 | Args: 492 | file_id (str): The file id to rename. 493 | rename_to (str): The new name of the file. 494 | 495 | Returns: 496 | An object indicating the result of the operation. 497 | 498 | Example: 499 | ```python 500 | result = await client.rename_file(file_id='12345', rename_to='newName') 501 | if result.result: 502 | print("File renamed successfully.") 503 | ``` 504 | """ 505 | payload = _request_models.RenameFilePayload(rename_to=rename_to, file_id=file_id) 506 | response_data = await self._api_request("post", "rename", data=payload.to_dict()) 507 | return models.APIResult.from_dict(response_data) 508 | 509 | async def rename_folder(self, folder_id: str, rename_to: str) -> models.APIResult: 510 | """ 511 | Rename a folder. 512 | 513 | Args: 514 | folder_id (str): The folder id to rename. 515 | rename_to (str): The new name of the folder. 516 | 517 | Returns: 518 | An object indicating the result of the operation. 519 | 520 | Example: 521 | ```python 522 | result = await client.rename_folder(folder_id='12345', rename_to='newName') 523 | if result.result: 524 | print("Folder renamed successfully.") 525 | ``` 526 | """ 527 | payload = _request_models.RenameFolderPayload(rename_to=rename_to, folder_id=folder_id) 528 | response_data = await self._api_request("post", "rename", data=payload.to_dict()) 529 | return models.APIResult.from_dict(response_data) 530 | 531 | async def delete_file(self, file_id: str) -> models.APIResult: 532 | """ 533 | Delete a file. 534 | 535 | Args: 536 | file_id (str): The file id to delete. 537 | 538 | Returns: 539 | An object indicating the result of the operation. 540 | 541 | Example: 542 | ```python 543 | response = await client.delete_file(file_id='12345') 544 | print(response) 545 | ``` 546 | """ 547 | return await self._delete_api_item("file", file_id) 548 | 549 | async def delete_folder(self, folder_id: str) -> models.APIResult: 550 | """ 551 | Delete a folder. 552 | 553 | Args: 554 | folder_id (str): The folder id to delete. 555 | 556 | Returns: 557 | An object indicating the result of the operation. 558 | 559 | Example: 560 | ```python 561 | response = await client.delete_folder(folder_id='12345') 562 | print(response) 563 | ``` 564 | """ 565 | return await self._delete_api_item("folder", folder_id) 566 | 567 | async def delete_torrent(self, torrent_id: str) -> models.APIResult: 568 | """ 569 | Delete an active downloading torrent. 570 | 571 | Args: 572 | torrent_id (str): The torrent id to delete. 573 | 574 | Returns: 575 | An object indicating the result of the operation. 576 | 577 | Example: 578 | ```python 579 | response = await client.delete_torrent(torrent_id='12345') 580 | print(response) 581 | ``` 582 | """ 583 | return await self._delete_api_item("torrent", torrent_id) 584 | 585 | async def delete_wishlist(self, wishlist_id: str) -> models.APIResult: 586 | """ 587 | Delete an item from the wishlist. 588 | 589 | Args: 590 | wishlist_id (str): The wishlistId of item to delete. 591 | 592 | Returns: 593 | An object indicating the result of the operation. 594 | 595 | Example: 596 | ```python 597 | result = await client.delete_wishlist(wishlist_id='12345') 598 | ``` 599 | """ 600 | payload = _request_models.RemoveWishlistPayload(id=wishlist_id) 601 | response_data = await self._api_request("post", "remove_wishlist", data=payload.to_dict()) 602 | return models.APIResult.from_dict(response_data) 603 | 604 | async def get_devices(self) -> List[models.Device]: 605 | """ 606 | Get the devices connected to the seedr account. 607 | 608 | Returns: 609 | A list of devices connected to the account. 610 | 611 | Example: 612 | ```python 613 | devices = await client.get_devices() 614 | for device in devices: 615 | print(device.client_name) 616 | ``` 617 | """ 618 | response_data = await self._api_request("get", "get_devices") 619 | devices_data = response_data.get("devices", []) 620 | return [models.Device.from_dict(d) for d in devices_data] 621 | 622 | async def change_name(self, name: str, password: str) -> models.APIResult: 623 | """ 624 | Change the name of the account. 625 | 626 | Args: 627 | name (str): The new name of the account. 628 | password (str): The password of the account. 629 | 630 | Returns: 631 | An object indicating the result of the operation. 632 | 633 | Example: 634 | ```python 635 | result = await client.change_name(name='New Name', password='password') 636 | ``` 637 | """ 638 | payload = _request_models.ChangeNamePayload(fullname=name, password=password) 639 | response_data = await self._api_request("post", "user_account_modify", data=payload.to_dict()) 640 | return models.APIResult.from_dict(response_data) 641 | 642 | async def change_password(self, old_password: str, new_password: str) -> models.APIResult: 643 | """ 644 | Change the password of the account. 645 | 646 | Args: 647 | old_password (str): The old password of the account. 648 | new_password (str): The new password of the account. 649 | 650 | Returns: 651 | An object indicating the result of the operation. 652 | 653 | Example: 654 | ```python 655 | result = await client.change_password(old_password='old', new_password='new') 656 | ``` 657 | """ 658 | payload = _request_models.ChangePasswordPayload( 659 | password=old_password, 660 | new_password=new_password, 661 | new_password_repeat=new_password, 662 | ) 663 | response_data = await self._api_request("post", "user_account_modify", data=payload.to_dict()) 664 | return models.APIResult.from_dict(response_data) 665 | 666 | async def _api_request( 667 | self, http_method: str, func: str, files: Optional[Dict[str, Any]] = None, **kwargs: Any 668 | ) -> Dict[str, Any]: 669 | """Handles the core logic for making authenticated API requests, including token refreshes.""" 670 | url = kwargs.pop("url", _constants.RESOURCE_URL) 671 | params = kwargs.pop("params", {}) 672 | if "access_token" not in params: 673 | params["access_token"] = self._token.access_token 674 | if func: 675 | params["func"] = func 676 | 677 | response = await self._make_http_request(self._client, http_method, url, params=params, files=files, **kwargs) 678 | try: 679 | data = response.json() 680 | except json.JSONDecodeError as e: 681 | raise APIError("Invalid JSON response from API.", response=None) from e 682 | 683 | if isinstance(data, dict) and data.get("error") == "expired_token": 684 | await self._refresh_access_token() 685 | params["access_token"] = self._token.access_token 686 | response = await self._make_http_request( 687 | self._client, http_method, url, params=params, files=files, **kwargs 688 | ) 689 | try: 690 | data = response.json() 691 | except json.JSONDecodeError as e: 692 | raise APIError("Invalid JSON response from API.", response=None) from e 693 | 694 | if response.is_client_error: 695 | if response.status_code == 401: 696 | raise AuthenticationError("Authentication failed.", response=response) 697 | raise APIError("API request failed.", response=response) 698 | 699 | if isinstance(data, dict) and data.get("result", True) is not True: 700 | raise APIError("API operation failed.", response=response) 701 | 702 | return data 703 | 704 | async def _refresh_access_token(self) -> models.RefreshTokenResult: 705 | """Refreshes the access token using the refresh token or device code.""" 706 | if self._token.refresh_token: 707 | payload = _request_models.RefreshTokenPayload(refresh_token=self._token.refresh_token) 708 | response = await self._make_http_request(self._client, "post", _constants.TOKEN_URL, data=payload.to_dict()) 709 | elif self._token.device_code: 710 | params = _request_models.DeviceCodeAuthParams(device_code=self._token.device_code) 711 | response = await self._make_http_request( 712 | self._client, "get", _constants.DEVICE_AUTHORIZE_URL, params=params.to_dict() 713 | ) 714 | else: 715 | raise AuthenticationError("No refresh token or device code available to refresh the session.") 716 | 717 | if not response.is_success: 718 | raise AuthenticationError("Failed to refresh token.", response=response) 719 | 720 | try: 721 | response_data = response.json() 722 | except json.JSONDecodeError as e: 723 | raise APIError("Invalid JSON response from API.", response=None) from e 724 | 725 | if "access_token" not in response_data: 726 | raise AuthenticationError( 727 | "Token refresh failed. The response did not contain a new access token.", 728 | response=response, 729 | ) 730 | 731 | self._token = Token( 732 | access_token=response_data["access_token"], 733 | refresh_token=self._token.refresh_token, 734 | device_code=self._token.device_code, 735 | ) 736 | if self._on_token_refresh: 737 | if inspect.iscoroutinefunction(self._on_token_refresh): 738 | await self._on_token_refresh(self._token) 739 | else: 740 | await anyio.to_thread.run_sync(self._on_token_refresh, self._token) 741 | 742 | return models.RefreshTokenResult.from_dict(response_data) 743 | 744 | async def _read_torrent_file_async(self, torrent_file: str) -> Dict[str, Any]: 745 | """Asynchronously reads a torrent file from a local path or a remote URL into memory.""" 746 | if torrent_file.startswith(("http://", "https://")): 747 | async with httpx.AsyncClient() as client: 748 | response = await client.get(torrent_file) 749 | response.raise_for_status() 750 | return {"torrent_file": response.content} 751 | else: 752 | path = anyio.Path(torrent_file) 753 | content = await path.read_bytes() 754 | return {"torrent_file": content} 755 | 756 | async def _delete_api_item(self, item_type: Literal["file", "folder", "torrent"], item_id: str) -> models.APIResult: 757 | """Constructs and sends a request to delete a specific item (file, folder, etc.).""" 758 | payload = _request_models.DeleteItemPayload(item_type=item_type, item_id=item_id) 759 | response_data = await self._api_request("post", "delete", data=payload.to_dict()) 760 | return models.APIResult.from_dict(response_data) 761 | 762 | @classmethod 763 | async def _initialize_client( 764 | cls: Type["AsyncSeedr"], 765 | auth_callable: Callable[[httpx.AsyncClient], Coroutine[Any, Any, Dict[str, Any]]], 766 | token_callable: Callable[[Dict[str, Any]], Dict[str, Any]], 767 | on_token_refresh: Optional[Callable[[Token], None]], 768 | httpx_client: Optional[httpx.AsyncClient], 769 | timeout: float = 30.0, 770 | proxy: Optional[Dict[str, str]] = None, 771 | **httpx_kwargs: Any, 772 | ) -> "AsyncSeedr": 773 | """A factory helper that orchestrates the authentication process and constructs the client.""" 774 | httpx_kwargs.setdefault("timeout", timeout) 775 | httpx_kwargs.setdefault("proxy", proxy) 776 | client = httpx_client or httpx.AsyncClient(**httpx_kwargs) 777 | success = False 778 | try: 779 | response_data = await auth_callable(client) 780 | token_extras = token_callable(response_data) 781 | token = Token( 782 | access_token=response_data["access_token"], 783 | refresh_token=response_data.get("refresh_token"), 784 | **token_extras, 785 | ) 786 | instance = cls(token, on_token_refresh=on_token_refresh, httpx_client=client, **httpx_kwargs) 787 | success = True 788 | return instance 789 | finally: 790 | if httpx_client is None and not success: 791 | await client.aclose() 792 | 793 | @classmethod 794 | async def _authenticate_and_get_token_data( 795 | cls: Type["AsyncSeedr"], 796 | client: httpx.AsyncClient, 797 | method: str, 798 | url: str, 799 | **httpx_kwargs: Any, 800 | ) -> Dict[str, Any]: 801 | """Handles the common logic for making an asynchronous authentication request.""" 802 | response = await cls._make_http_request(client, method, url, **httpx_kwargs) 803 | 804 | if not response.is_success: 805 | raise AuthenticationError("Authentication failed.", response=response) 806 | 807 | try: 808 | data = response.json() 809 | if isinstance(data, dict) and data.get("error") in ["authorization_pending"]: 810 | raise AuthenticationError( 811 | "Authentication failed.", 812 | response=response, 813 | ) 814 | return data 815 | except json.JSONDecodeError as e: 816 | raise APIError("Invalid JSON response from API.", response=None) from e 817 | 818 | @staticmethod 819 | async def _make_http_request( 820 | client: httpx.AsyncClient, 821 | method: str, 822 | url: str, 823 | **kwargs: Any, 824 | ) -> httpx.Response: 825 | """Performs the raw HTTP request, handles network/server errors, and returns the response.""" 826 | try: 827 | response = await client.request(method, url, **kwargs) 828 | 829 | if response.is_server_error: 830 | raise ServerError(response=response) 831 | 832 | return response 833 | except httpx.RequestError as e: 834 | raise NetworkError(str(e)) from e 835 | 836 | async def close(self) -> None: 837 | """Closes the underlying httpx client if it was created by this instance.""" 838 | if self._manages_client_lifecycle: 839 | await self._client.aclose() 840 | 841 | async def __aenter__(self) -> "AsyncSeedr": 842 | return self 843 | 844 | async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: 845 | await self.close() 846 | -------------------------------------------------------------------------------- /uv.lock: -------------------------------------------------------------------------------- 1 | version = 1 2 | revision = 3 3 | requires-python = ">=3.9" 4 | resolution-markers = [ 5 | "python_full_version >= '3.10'", 6 | "python_full_version < '3.10'", 7 | ] 8 | 9 | [[package]] 10 | name = "anyio" 11 | version = "4.10.0" 12 | source = { registry = "https://pypi.org/simple" } 13 | dependencies = [ 14 | { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, 15 | { name = "idna" }, 16 | { name = "sniffio" }, 17 | { name = "typing-extensions", marker = "python_full_version < '3.13'" }, 18 | ] 19 | sdist = { url = "https://files.pythonhosted.org/packages/f1/b4/636b3b65173d3ce9a38ef5f0522789614e590dab6a8d505340a4efe4c567/anyio-4.10.0.tar.gz", hash = "sha256:3f3fae35c96039744587aa5b8371e7e8e603c0702999535961dd336026973ba6", size = 213252, upload-time = "2025-08-04T08:54:26.451Z" } 20 | wheels = [ 21 | { url = "https://files.pythonhosted.org/packages/6f/12/e5e0282d673bb9746bacfb6e2dba8719989d3660cdb2ea79aee9a9651afb/anyio-4.10.0-py3-none-any.whl", hash = "sha256:60e474ac86736bbfd6f210f7a61218939c318f43f9972497381f1c5e930ed3d1", size = 107213, upload-time = "2025-08-04T08:54:24.882Z" }, 22 | ] 23 | 24 | [[package]] 25 | name = "babel" 26 | version = "2.17.0" 27 | source = { registry = "https://pypi.org/simple" } 28 | sdist = { url = "https://files.pythonhosted.org/packages/7d/6b/d52e42361e1aa00709585ecc30b3f9684b3ab62530771402248b1b1d6240/babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d", size = 9951852, upload-time = "2025-02-01T15:17:41.026Z" } 29 | wheels = [ 30 | { url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537, upload-time = "2025-02-01T15:17:37.39Z" }, 31 | ] 32 | 33 | [[package]] 34 | name = "backrefs" 35 | version = "5.9" 36 | source = { registry = "https://pypi.org/simple" } 37 | sdist = { url = "https://files.pythonhosted.org/packages/eb/a7/312f673df6a79003279e1f55619abbe7daebbb87c17c976ddc0345c04c7b/backrefs-5.9.tar.gz", hash = "sha256:808548cb708d66b82ee231f962cb36faaf4f2baab032f2fbb783e9c2fdddaa59", size = 5765857, upload-time = "2025-06-22T19:34:13.97Z" } 38 | wheels = [ 39 | { url = "https://files.pythonhosted.org/packages/19/4d/798dc1f30468134906575156c089c492cf79b5a5fd373f07fe26c4d046bf/backrefs-5.9-py310-none-any.whl", hash = "sha256:db8e8ba0e9de81fcd635f440deab5ae5f2591b54ac1ebe0550a2ca063488cd9f", size = 380267, upload-time = "2025-06-22T19:34:05.252Z" }, 40 | { url = "https://files.pythonhosted.org/packages/55/07/f0b3375bf0d06014e9787797e6b7cc02b38ac9ff9726ccfe834d94e9991e/backrefs-5.9-py311-none-any.whl", hash = "sha256:6907635edebbe9b2dc3de3a2befff44d74f30a4562adbb8b36f21252ea19c5cf", size = 392072, upload-time = "2025-06-22T19:34:06.743Z" }, 41 | { url = "https://files.pythonhosted.org/packages/9d/12/4f345407259dd60a0997107758ba3f221cf89a9b5a0f8ed5b961aef97253/backrefs-5.9-py312-none-any.whl", hash = "sha256:7fdf9771f63e6028d7fee7e0c497c81abda597ea45d6b8f89e8ad76994f5befa", size = 397947, upload-time = "2025-06-22T19:34:08.172Z" }, 42 | { url = "https://files.pythonhosted.org/packages/10/bf/fa31834dc27a7f05e5290eae47c82690edc3a7b37d58f7fb35a1bdbf355b/backrefs-5.9-py313-none-any.whl", hash = "sha256:cc37b19fa219e93ff825ed1fed8879e47b4d89aa7a1884860e2db64ccd7c676b", size = 399843, upload-time = "2025-06-22T19:34:09.68Z" }, 43 | { url = "https://files.pythonhosted.org/packages/fc/24/b29af34b2c9c41645a9f4ff117bae860291780d73880f449e0b5d948c070/backrefs-5.9-py314-none-any.whl", hash = "sha256:df5e169836cc8acb5e440ebae9aad4bf9d15e226d3bad049cf3f6a5c20cc8dc9", size = 411762, upload-time = "2025-06-22T19:34:11.037Z" }, 44 | { url = "https://files.pythonhosted.org/packages/41/ff/392bff89415399a979be4a65357a41d92729ae8580a66073d8ec8d810f98/backrefs-5.9-py39-none-any.whl", hash = "sha256:f48ee18f6252b8f5777a22a00a09a85de0ca931658f1dd96d4406a34f3748c60", size = 380265, upload-time = "2025-06-22T19:34:12.405Z" }, 45 | ] 46 | 47 | [[package]] 48 | name = "certifi" 49 | version = "2025.8.3" 50 | source = { registry = "https://pypi.org/simple" } 51 | sdist = { url = "https://files.pythonhosted.org/packages/dc/67/960ebe6bf230a96cda2e0abcf73af550ec4f090005363542f0765df162e0/certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407", size = 162386, upload-time = "2025-08-03T03:07:47.08Z" } 52 | wheels = [ 53 | { url = "https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5", size = 161216, upload-time = "2025-08-03T03:07:45.777Z" }, 54 | ] 55 | 56 | [[package]] 57 | name = "charset-normalizer" 58 | version = "3.4.3" 59 | source = { registry = "https://pypi.org/simple" } 60 | sdist = { url = "https://files.pythonhosted.org/packages/83/2d/5fd176ceb9b2fc619e63405525573493ca23441330fcdaee6bef9460e924/charset_normalizer-3.4.3.tar.gz", hash = "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14", size = 122371, upload-time = "2025-08-09T07:57:28.46Z" } 61 | wheels = [ 62 | { url = "https://files.pythonhosted.org/packages/d6/98/f3b8013223728a99b908c9344da3aa04ee6e3fa235f19409033eda92fb78/charset_normalizer-3.4.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fb7f67a1bfa6e40b438170ebdc8158b78dc465a5a67b6dde178a46987b244a72", size = 207695, upload-time = "2025-08-09T07:55:36.452Z" }, 63 | { url = "https://files.pythonhosted.org/packages/21/40/5188be1e3118c82dcb7c2a5ba101b783822cfb413a0268ed3be0468532de/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc9370a2da1ac13f0153780040f465839e6cccb4a1e44810124b4e22483c93fe", size = 147153, upload-time = "2025-08-09T07:55:38.467Z" }, 64 | { url = "https://files.pythonhosted.org/packages/37/60/5d0d74bc1e1380f0b72c327948d9c2aca14b46a9efd87604e724260f384c/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:07a0eae9e2787b586e129fdcbe1af6997f8d0e5abaa0bc98c0e20e124d67e601", size = 160428, upload-time = "2025-08-09T07:55:40.072Z" }, 65 | { url = "https://files.pythonhosted.org/packages/85/9a/d891f63722d9158688de58d050c59dc3da560ea7f04f4c53e769de5140f5/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:74d77e25adda8581ffc1c720f1c81ca082921329452eba58b16233ab1842141c", size = 157627, upload-time = "2025-08-09T07:55:41.706Z" }, 66 | { url = "https://files.pythonhosted.org/packages/65/1a/7425c952944a6521a9cfa7e675343f83fd82085b8af2b1373a2409c683dc/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0e909868420b7049dafd3a31d45125b31143eec59235311fc4c57ea26a4acd2", size = 152388, upload-time = "2025-08-09T07:55:43.262Z" }, 67 | { url = "https://files.pythonhosted.org/packages/f0/c9/a2c9c2a355a8594ce2446085e2ec97fd44d323c684ff32042e2a6b718e1d/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c6f162aabe9a91a309510d74eeb6507fab5fff92337a15acbe77753d88d9dcf0", size = 150077, upload-time = "2025-08-09T07:55:44.903Z" }, 68 | { url = "https://files.pythonhosted.org/packages/3b/38/20a1f44e4851aa1c9105d6e7110c9d020e093dfa5836d712a5f074a12bf7/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4ca4c094de7771a98d7fbd67d9e5dbf1eb73efa4f744a730437d8a3a5cf994f0", size = 161631, upload-time = "2025-08-09T07:55:46.346Z" }, 69 | { url = "https://files.pythonhosted.org/packages/a4/fa/384d2c0f57edad03d7bec3ebefb462090d8905b4ff5a2d2525f3bb711fac/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:02425242e96bcf29a49711b0ca9f37e451da7c70562bc10e8ed992a5a7a25cc0", size = 159210, upload-time = "2025-08-09T07:55:47.539Z" }, 70 | { url = "https://files.pythonhosted.org/packages/33/9e/eca49d35867ca2db336b6ca27617deed4653b97ebf45dfc21311ce473c37/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:78deba4d8f9590fe4dae384aeff04082510a709957e968753ff3c48399f6f92a", size = 153739, upload-time = "2025-08-09T07:55:48.744Z" }, 71 | { url = "https://files.pythonhosted.org/packages/2a/91/26c3036e62dfe8de8061182d33be5025e2424002125c9500faff74a6735e/charset_normalizer-3.4.3-cp310-cp310-win32.whl", hash = "sha256:d79c198e27580c8e958906f803e63cddb77653731be08851c7df0b1a14a8fc0f", size = 99825, upload-time = "2025-08-09T07:55:50.305Z" }, 72 | { url = "https://files.pythonhosted.org/packages/e2/c6/f05db471f81af1fa01839d44ae2a8bfeec8d2a8b4590f16c4e7393afd323/charset_normalizer-3.4.3-cp310-cp310-win_amd64.whl", hash = "sha256:c6e490913a46fa054e03699c70019ab869e990270597018cef1d8562132c2669", size = 107452, upload-time = "2025-08-09T07:55:51.461Z" }, 73 | { url = "https://files.pythonhosted.org/packages/7f/b5/991245018615474a60965a7c9cd2b4efbaabd16d582a5547c47ee1c7730b/charset_normalizer-3.4.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b256ee2e749283ef3ddcff51a675ff43798d92d746d1a6e4631bf8c707d22d0b", size = 204483, upload-time = "2025-08-09T07:55:53.12Z" }, 74 | { url = "https://files.pythonhosted.org/packages/c7/2a/ae245c41c06299ec18262825c1569c5d3298fc920e4ddf56ab011b417efd/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13faeacfe61784e2559e690fc53fa4c5ae97c6fcedb8eb6fb8d0a15b475d2c64", size = 145520, upload-time = "2025-08-09T07:55:54.712Z" }, 75 | { url = "https://files.pythonhosted.org/packages/3a/a4/b3b6c76e7a635748c4421d2b92c7b8f90a432f98bda5082049af37ffc8e3/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:00237675befef519d9af72169d8604a067d92755e84fe76492fef5441db05b91", size = 158876, upload-time = "2025-08-09T07:55:56.024Z" }, 76 | { url = "https://files.pythonhosted.org/packages/e2/e6/63bb0e10f90a8243c5def74b5b105b3bbbfb3e7bb753915fe333fb0c11ea/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:585f3b2a80fbd26b048a0be90c5aae8f06605d3c92615911c3a2b03a8a3b796f", size = 156083, upload-time = "2025-08-09T07:55:57.582Z" }, 77 | { url = "https://files.pythonhosted.org/packages/87/df/b7737ff046c974b183ea9aa111b74185ac8c3a326c6262d413bd5a1b8c69/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e78314bdc32fa80696f72fa16dc61168fda4d6a0c014e0380f9d02f0e5d8a07", size = 150295, upload-time = "2025-08-09T07:55:59.147Z" }, 78 | { url = "https://files.pythonhosted.org/packages/61/f1/190d9977e0084d3f1dc169acd060d479bbbc71b90bf3e7bf7b9927dec3eb/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:96b2b3d1a83ad55310de8c7b4a2d04d9277d5591f40761274856635acc5fcb30", size = 148379, upload-time = "2025-08-09T07:56:00.364Z" }, 79 | { url = "https://files.pythonhosted.org/packages/4c/92/27dbe365d34c68cfe0ca76f1edd70e8705d82b378cb54ebbaeabc2e3029d/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:939578d9d8fd4299220161fdd76e86c6a251987476f5243e8864a7844476ba14", size = 160018, upload-time = "2025-08-09T07:56:01.678Z" }, 80 | { url = "https://files.pythonhosted.org/packages/99/04/baae2a1ea1893a01635d475b9261c889a18fd48393634b6270827869fa34/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:fd10de089bcdcd1be95a2f73dbe6254798ec1bda9f450d5828c96f93e2536b9c", size = 157430, upload-time = "2025-08-09T07:56:02.87Z" }, 81 | { url = "https://files.pythonhosted.org/packages/2f/36/77da9c6a328c54d17b960c89eccacfab8271fdaaa228305330915b88afa9/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1e8ac75d72fa3775e0b7cb7e4629cec13b7514d928d15ef8ea06bca03ef01cae", size = 151600, upload-time = "2025-08-09T07:56:04.089Z" }, 82 | { url = "https://files.pythonhosted.org/packages/64/d4/9eb4ff2c167edbbf08cdd28e19078bf195762e9bd63371689cab5ecd3d0d/charset_normalizer-3.4.3-cp311-cp311-win32.whl", hash = "sha256:6cf8fd4c04756b6b60146d98cd8a77d0cdae0e1ca20329da2ac85eed779b6849", size = 99616, upload-time = "2025-08-09T07:56:05.658Z" }, 83 | { url = "https://files.pythonhosted.org/packages/f4/9c/996a4a028222e7761a96634d1820de8a744ff4327a00ada9c8942033089b/charset_normalizer-3.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:31a9a6f775f9bcd865d88ee350f0ffb0e25936a7f930ca98995c05abf1faf21c", size = 107108, upload-time = "2025-08-09T07:56:07.176Z" }, 84 | { url = "https://files.pythonhosted.org/packages/e9/5e/14c94999e418d9b87682734589404a25854d5f5d0408df68bc15b6ff54bb/charset_normalizer-3.4.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e28e334d3ff134e88989d90ba04b47d84382a828c061d0d1027b1b12a62b39b1", size = 205655, upload-time = "2025-08-09T07:56:08.475Z" }, 85 | { url = "https://files.pythonhosted.org/packages/7d/a8/c6ec5d389672521f644505a257f50544c074cf5fc292d5390331cd6fc9c3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0cacf8f7297b0c4fcb74227692ca46b4a5852f8f4f24b3c766dd94a1075c4884", size = 146223, upload-time = "2025-08-09T07:56:09.708Z" }, 86 | { url = "https://files.pythonhosted.org/packages/fc/eb/a2ffb08547f4e1e5415fb69eb7db25932c52a52bed371429648db4d84fb1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c6fd51128a41297f5409deab284fecbe5305ebd7e5a1f959bee1c054622b7018", size = 159366, upload-time = "2025-08-09T07:56:11.326Z" }, 87 | { url = "https://files.pythonhosted.org/packages/82/10/0fd19f20c624b278dddaf83b8464dcddc2456cb4b02bb902a6da126b87a1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cfb2aad70f2c6debfbcb717f23b7eb55febc0bb23dcffc0f076009da10c6392", size = 157104, upload-time = "2025-08-09T07:56:13.014Z" }, 88 | { url = "https://files.pythonhosted.org/packages/16/ab/0233c3231af734f5dfcf0844aa9582d5a1466c985bbed6cedab85af9bfe3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1606f4a55c0fd363d754049cdf400175ee96c992b1f8018b993941f221221c5f", size = 151830, upload-time = "2025-08-09T07:56:14.428Z" }, 89 | { url = "https://files.pythonhosted.org/packages/ae/02/e29e22b4e02839a0e4a06557b1999d0a47db3567e82989b5bb21f3fbbd9f/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:027b776c26d38b7f15b26a5da1044f376455fb3766df8fc38563b4efbc515154", size = 148854, upload-time = "2025-08-09T07:56:16.051Z" }, 90 | { url = "https://files.pythonhosted.org/packages/05/6b/e2539a0a4be302b481e8cafb5af8792da8093b486885a1ae4d15d452bcec/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:42e5088973e56e31e4fa58eb6bd709e42fc03799c11c42929592889a2e54c491", size = 160670, upload-time = "2025-08-09T07:56:17.314Z" }, 91 | { url = "https://files.pythonhosted.org/packages/31/e7/883ee5676a2ef217a40ce0bffcc3d0dfbf9e64cbcfbdf822c52981c3304b/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cc34f233c9e71701040d772aa7490318673aa7164a0efe3172b2981218c26d93", size = 158501, upload-time = "2025-08-09T07:56:18.641Z" }, 92 | { url = "https://files.pythonhosted.org/packages/c1/35/6525b21aa0db614cf8b5792d232021dca3df7f90a1944db934efa5d20bb1/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:320e8e66157cc4e247d9ddca8e21f427efc7a04bbd0ac8a9faf56583fa543f9f", size = 153173, upload-time = "2025-08-09T07:56:20.289Z" }, 93 | { url = "https://files.pythonhosted.org/packages/50/ee/f4704bad8201de513fdc8aac1cabc87e38c5818c93857140e06e772b5892/charset_normalizer-3.4.3-cp312-cp312-win32.whl", hash = "sha256:fb6fecfd65564f208cbf0fba07f107fb661bcd1a7c389edbced3f7a493f70e37", size = 99822, upload-time = "2025-08-09T07:56:21.551Z" }, 94 | { url = "https://files.pythonhosted.org/packages/39/f5/3b3836ca6064d0992c58c7561c6b6eee1b3892e9665d650c803bd5614522/charset_normalizer-3.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:86df271bf921c2ee3818f0522e9a5b8092ca2ad8b065ece5d7d9d0e9f4849bcc", size = 107543, upload-time = "2025-08-09T07:56:23.115Z" }, 95 | { url = "https://files.pythonhosted.org/packages/65/ca/2135ac97709b400c7654b4b764daf5c5567c2da45a30cdd20f9eefe2d658/charset_normalizer-3.4.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:14c2a87c65b351109f6abfc424cab3927b3bdece6f706e4d12faaf3d52ee5efe", size = 205326, upload-time = "2025-08-09T07:56:24.721Z" }, 96 | { url = "https://files.pythonhosted.org/packages/71/11/98a04c3c97dd34e49c7d247083af03645ca3730809a5509443f3c37f7c99/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41d1fc408ff5fdfb910200ec0e74abc40387bccb3252f3f27c0676731df2b2c8", size = 146008, upload-time = "2025-08-09T07:56:26.004Z" }, 97 | { url = "https://files.pythonhosted.org/packages/60/f5/4659a4cb3c4ec146bec80c32d8bb16033752574c20b1252ee842a95d1a1e/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1bb60174149316da1c35fa5233681f7c0f9f514509b8e399ab70fea5f17e45c9", size = 159196, upload-time = "2025-08-09T07:56:27.25Z" }, 98 | { url = "https://files.pythonhosted.org/packages/86/9e/f552f7a00611f168b9a5865a1414179b2c6de8235a4fa40189f6f79a1753/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30d006f98569de3459c2fc1f2acde170b7b2bd265dc1943e87e1a4efe1b67c31", size = 156819, upload-time = "2025-08-09T07:56:28.515Z" }, 99 | { url = "https://files.pythonhosted.org/packages/7e/95/42aa2156235cbc8fa61208aded06ef46111c4d3f0de233107b3f38631803/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:416175faf02e4b0810f1f38bcb54682878a4af94059a1cd63b8747244420801f", size = 151350, upload-time = "2025-08-09T07:56:29.716Z" }, 100 | { url = "https://files.pythonhosted.org/packages/c2/a9/3865b02c56f300a6f94fc631ef54f0a8a29da74fb45a773dfd3dcd380af7/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6aab0f181c486f973bc7262a97f5aca3ee7e1437011ef0c2ec04b5a11d16c927", size = 148644, upload-time = "2025-08-09T07:56:30.984Z" }, 101 | { url = "https://files.pythonhosted.org/packages/77/d9/cbcf1a2a5c7d7856f11e7ac2d782aec12bdfea60d104e60e0aa1c97849dc/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabf8315679312cfa71302f9bd509ded4f2f263fb5b765cf1433b39106c3cc9", size = 160468, upload-time = "2025-08-09T07:56:32.252Z" }, 102 | { url = "https://files.pythonhosted.org/packages/f6/42/6f45efee8697b89fda4d50580f292b8f7f9306cb2971d4b53f8914e4d890/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:bd28b817ea8c70215401f657edef3a8aa83c29d447fb0b622c35403780ba11d5", size = 158187, upload-time = "2025-08-09T07:56:33.481Z" }, 103 | { url = "https://files.pythonhosted.org/packages/70/99/f1c3bdcfaa9c45b3ce96f70b14f070411366fa19549c1d4832c935d8e2c3/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:18343b2d246dc6761a249ba1fb13f9ee9a2bcd95decc767319506056ea4ad4dc", size = 152699, upload-time = "2025-08-09T07:56:34.739Z" }, 104 | { url = "https://files.pythonhosted.org/packages/a3/ad/b0081f2f99a4b194bcbb1934ef3b12aa4d9702ced80a37026b7607c72e58/charset_normalizer-3.4.3-cp313-cp313-win32.whl", hash = "sha256:6fb70de56f1859a3f71261cbe41005f56a7842cc348d3aeb26237560bfa5e0ce", size = 99580, upload-time = "2025-08-09T07:56:35.981Z" }, 105 | { url = "https://files.pythonhosted.org/packages/9a/8f/ae790790c7b64f925e5c953b924aaa42a243fb778fed9e41f147b2a5715a/charset_normalizer-3.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:cf1ebb7d78e1ad8ec2a8c4732c7be2e736f6e5123a4146c5b89c9d1f585f8cef", size = 107366, upload-time = "2025-08-09T07:56:37.339Z" }, 106 | { url = "https://files.pythonhosted.org/packages/8e/91/b5a06ad970ddc7a0e513112d40113e834638f4ca1120eb727a249fb2715e/charset_normalizer-3.4.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3cd35b7e8aedeb9e34c41385fda4f73ba609e561faedfae0a9e75e44ac558a15", size = 204342, upload-time = "2025-08-09T07:56:38.687Z" }, 107 | { url = "https://files.pythonhosted.org/packages/ce/ec/1edc30a377f0a02689342f214455c3f6c2fbedd896a1d2f856c002fc3062/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b89bc04de1d83006373429975f8ef9e7932534b8cc9ca582e4db7d20d91816db", size = 145995, upload-time = "2025-08-09T07:56:40.048Z" }, 108 | { url = "https://files.pythonhosted.org/packages/17/e5/5e67ab85e6d22b04641acb5399c8684f4d37caf7558a53859f0283a650e9/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2001a39612b241dae17b4687898843f254f8748b796a2e16f1051a17078d991d", size = 158640, upload-time = "2025-08-09T07:56:41.311Z" }, 109 | { url = "https://files.pythonhosted.org/packages/f1/e5/38421987f6c697ee3722981289d554957c4be652f963d71c5e46a262e135/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8dcfc373f888e4fb39a7bc57e93e3b845e7f462dacc008d9749568b1c4ece096", size = 156636, upload-time = "2025-08-09T07:56:43.195Z" }, 110 | { url = "https://files.pythonhosted.org/packages/a0/e4/5a075de8daa3ec0745a9a3b54467e0c2967daaaf2cec04c845f73493e9a1/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18b97b8404387b96cdbd30ad660f6407799126d26a39ca65729162fd810a99aa", size = 150939, upload-time = "2025-08-09T07:56:44.819Z" }, 111 | { url = "https://files.pythonhosted.org/packages/02/f7/3611b32318b30974131db62b4043f335861d4d9b49adc6d57c1149cc49d4/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ccf600859c183d70eb47e05a44cd80a4ce77394d1ac0f79dbd2dd90a69a3a049", size = 148580, upload-time = "2025-08-09T07:56:46.684Z" }, 112 | { url = "https://files.pythonhosted.org/packages/7e/61/19b36f4bd67f2793ab6a99b979b4e4f3d8fc754cbdffb805335df4337126/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:53cd68b185d98dde4ad8990e56a58dea83a4162161b1ea9272e5c9182ce415e0", size = 159870, upload-time = "2025-08-09T07:56:47.941Z" }, 113 | { url = "https://files.pythonhosted.org/packages/06/57/84722eefdd338c04cf3030ada66889298eaedf3e7a30a624201e0cbe424a/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:30a96e1e1f865f78b030d65241c1ee850cdf422d869e9028e2fc1d5e4db73b92", size = 157797, upload-time = "2025-08-09T07:56:49.756Z" }, 114 | { url = "https://files.pythonhosted.org/packages/72/2a/aff5dd112b2f14bcc3462c312dce5445806bfc8ab3a7328555da95330e4b/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d716a916938e03231e86e43782ca7878fb602a125a91e7acb8b5112e2e96ac16", size = 152224, upload-time = "2025-08-09T07:56:51.369Z" }, 115 | { url = "https://files.pythonhosted.org/packages/b7/8c/9839225320046ed279c6e839d51f028342eb77c91c89b8ef2549f951f3ec/charset_normalizer-3.4.3-cp314-cp314-win32.whl", hash = "sha256:c6dbd0ccdda3a2ba7c2ecd9d77b37f3b5831687d8dc1b6ca5f56a4880cc7b7ce", size = 100086, upload-time = "2025-08-09T07:56:52.722Z" }, 116 | { url = "https://files.pythonhosted.org/packages/ee/7a/36fbcf646e41f710ce0a563c1c9a343c6edf9be80786edeb15b6f62e17db/charset_normalizer-3.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:73dc19b562516fc9bcf6e5d6e596df0b4eb98d87e4f79f3ae71840e6ed21361c", size = 107400, upload-time = "2025-08-09T07:56:55.172Z" }, 117 | { url = "https://files.pythonhosted.org/packages/c2/ca/9a0983dd5c8e9733565cf3db4df2b0a2e9a82659fd8aa2a868ac6e4a991f/charset_normalizer-3.4.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:70bfc5f2c318afece2f5838ea5e4c3febada0be750fcf4775641052bbba14d05", size = 207520, upload-time = "2025-08-09T07:57:11.026Z" }, 118 | { url = "https://files.pythonhosted.org/packages/39/c6/99271dc37243a4f925b09090493fb96c9333d7992c6187f5cfe5312008d2/charset_normalizer-3.4.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:23b6b24d74478dc833444cbd927c338349d6ae852ba53a0d02a2de1fce45b96e", size = 147307, upload-time = "2025-08-09T07:57:12.4Z" }, 119 | { url = "https://files.pythonhosted.org/packages/e4/69/132eab043356bba06eb333cc2cc60c6340857d0a2e4ca6dc2b51312886b3/charset_normalizer-3.4.3-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:34a7f768e3f985abdb42841e20e17b330ad3aaf4bb7e7aeeb73db2e70f077b99", size = 160448, upload-time = "2025-08-09T07:57:13.712Z" }, 120 | { url = "https://files.pythonhosted.org/packages/04/9a/914d294daa4809c57667b77470533e65def9c0be1ef8b4c1183a99170e9d/charset_normalizer-3.4.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fb731e5deb0c7ef82d698b0f4c5bb724633ee2a489401594c5c88b02e6cb15f7", size = 157758, upload-time = "2025-08-09T07:57:14.979Z" }, 121 | { url = "https://files.pythonhosted.org/packages/b0/a8/6f5bcf1bcf63cb45625f7c5cadca026121ff8a6c8a3256d8d8cd59302663/charset_normalizer-3.4.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:257f26fed7d7ff59921b78244f3cd93ed2af1800ff048c33f624c87475819dd7", size = 152487, upload-time = "2025-08-09T07:57:16.332Z" }, 122 | { url = "https://files.pythonhosted.org/packages/c4/72/d3d0e9592f4e504f9dea08b8db270821c909558c353dc3b457ed2509f2fb/charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1ef99f0456d3d46a50945c98de1774da86f8e992ab5c77865ea8b8195341fc19", size = 150054, upload-time = "2025-08-09T07:57:17.576Z" }, 123 | { url = "https://files.pythonhosted.org/packages/20/30/5f64fe3981677fe63fa987b80e6c01042eb5ff653ff7cec1b7bd9268e54e/charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:2c322db9c8c89009a990ef07c3bcc9f011a3269bc06782f916cd3d9eed7c9312", size = 161703, upload-time = "2025-08-09T07:57:20.012Z" }, 124 | { url = "https://files.pythonhosted.org/packages/e1/ef/dd08b2cac9284fd59e70f7d97382c33a3d0a926e45b15fc21b3308324ffd/charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:511729f456829ef86ac41ca78c63a5cb55240ed23b4b737faca0eb1abb1c41bc", size = 159096, upload-time = "2025-08-09T07:57:21.329Z" }, 125 | { url = "https://files.pythonhosted.org/packages/45/8c/dcef87cfc2b3f002a6478f38906f9040302c68aebe21468090e39cde1445/charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:88ab34806dea0671532d3f82d82b85e8fc23d7b2dd12fa837978dad9bb392a34", size = 153852, upload-time = "2025-08-09T07:57:22.608Z" }, 126 | { url = "https://files.pythonhosted.org/packages/63/86/9cbd533bd37883d467fcd1bd491b3547a3532d0fbb46de2b99feeebf185e/charset_normalizer-3.4.3-cp39-cp39-win32.whl", hash = "sha256:16a8770207946ac75703458e2c743631c79c59c5890c80011d536248f8eaa432", size = 99840, upload-time = "2025-08-09T07:57:23.883Z" }, 127 | { url = "https://files.pythonhosted.org/packages/ce/d6/7e805c8e5c46ff9729c49950acc4ee0aeb55efb8b3a56687658ad10c3216/charset_normalizer-3.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:d22dbedd33326a4a5190dd4fe9e9e693ef12160c77382d9e87919bce54f3d4ca", size = 107438, upload-time = "2025-08-09T07:57:25.287Z" }, 128 | { url = "https://files.pythonhosted.org/packages/8a/1f/f041989e93b001bc4e44bb1669ccdcf54d3f00e628229a85b08d330615c5/charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a", size = 53175, upload-time = "2025-08-09T07:57:26.864Z" }, 129 | ] 130 | 131 | [[package]] 132 | name = "click" 133 | version = "8.1.8" 134 | source = { registry = "https://pypi.org/simple" } 135 | resolution-markers = [ 136 | "python_full_version < '3.10'", 137 | ] 138 | dependencies = [ 139 | { name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" }, 140 | ] 141 | sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593, upload-time = "2024-12-21T18:38:44.339Z" } 142 | wheels = [ 143 | { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188, upload-time = "2024-12-21T18:38:41.666Z" }, 144 | ] 145 | 146 | [[package]] 147 | name = "click" 148 | version = "8.2.1" 149 | source = { registry = "https://pypi.org/simple" } 150 | resolution-markers = [ 151 | "python_full_version >= '3.10'", 152 | ] 153 | dependencies = [ 154 | { name = "colorama", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" }, 155 | ] 156 | sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" } 157 | wheels = [ 158 | { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" }, 159 | ] 160 | 161 | [[package]] 162 | name = "colorama" 163 | version = "0.4.6" 164 | source = { registry = "https://pypi.org/simple" } 165 | sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } 166 | wheels = [ 167 | { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, 168 | ] 169 | 170 | [[package]] 171 | name = "exceptiongroup" 172 | version = "1.3.0" 173 | source = { registry = "https://pypi.org/simple" } 174 | dependencies = [ 175 | { name = "typing-extensions", marker = "python_full_version < '3.13'" }, 176 | ] 177 | sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" } 178 | wheels = [ 179 | { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" }, 180 | ] 181 | 182 | [[package]] 183 | name = "fieldz" 184 | version = "0.1.2" 185 | source = { registry = "https://pypi.org/simple" } 186 | dependencies = [ 187 | { name = "typing-extensions" }, 188 | ] 189 | sdist = { url = "https://files.pythonhosted.org/packages/90/62/698c5cc2e7d4c8c89e63033e2e9d3c74902a1bf28782712eacb0653097ce/fieldz-0.1.2.tar.gz", hash = "sha256:0448ed5dacb13eaa49da0db786e87fae298fbd2652d26c510e5d7aea6b6bebf4", size = 17277, upload-time = "2025-06-30T18:06:40.881Z" } 190 | wheels = [ 191 | { url = "https://files.pythonhosted.org/packages/6d/8c/8958392cade27a272daf45d09a08473073dedeccad94b097dfeb898d969f/fieldz-0.1.2-py3-none-any.whl", hash = "sha256:e25884d2821a2d5638ef8d4d8bce5d1039359cfcb46d0f93df8cb1f7c2eb3a2e", size = 17878, upload-time = "2025-06-30T18:06:39.322Z" }, 192 | ] 193 | 194 | [[package]] 195 | name = "ghp-import" 196 | version = "2.1.0" 197 | source = { registry = "https://pypi.org/simple" } 198 | dependencies = [ 199 | { name = "python-dateutil" }, 200 | ] 201 | sdist = { url = "https://files.pythonhosted.org/packages/d9/29/d40217cbe2f6b1359e00c6c307bb3fc876ba74068cbab3dde77f03ca0dc4/ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343", size = 10943, upload-time = "2022-05-02T15:47:16.11Z" } 202 | wheels = [ 203 | { url = "https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", size = 11034, upload-time = "2022-05-02T15:47:14.552Z" }, 204 | ] 205 | 206 | [[package]] 207 | name = "griffe" 208 | version = "1.12.1" 209 | source = { registry = "https://pypi.org/simple" } 210 | dependencies = [ 211 | { name = "colorama" }, 212 | ] 213 | sdist = { url = "https://files.pythonhosted.org/packages/81/ca/29f36e00c74844ae50d139cf5a8b1751887b2f4d5023af65d460268ad7aa/griffe-1.12.1.tar.gz", hash = "sha256:29f5a6114c0aeda7d9c86a570f736883f8a2c5b38b57323d56b3d1c000565567", size = 411863, upload-time = "2025-08-14T21:08:15.38Z" } 214 | wheels = [ 215 | { url = "https://files.pythonhosted.org/packages/13/f2/4fab6c3e5bcaf38a44cc8a974d2752eaad4c129e45d6533d926a30edd133/griffe-1.12.1-py3-none-any.whl", hash = "sha256:2d7c12334de00089c31905424a00abcfd931b45b8b516967f224133903d302cc", size = 138940, upload-time = "2025-08-14T21:08:13.382Z" }, 216 | ] 217 | 218 | [[package]] 219 | name = "griffe-fieldz" 220 | version = "0.3.0" 221 | source = { registry = "https://pypi.org/simple" } 222 | dependencies = [ 223 | { name = "fieldz" }, 224 | { name = "griffe" }, 225 | ] 226 | sdist = { url = "https://files.pythonhosted.org/packages/c5/6a/94754bf39fd63ba424c667b2abf0ade78e3878e223591d1fb9c3e8a77bce/griffe_fieldz-0.3.0.tar.gz", hash = "sha256:42e7707dac51d38e26fb7f3f7f51429da9b47e98060bfeb81a4287456d5b8a89", size = 10149, upload-time = "2025-07-30T21:43:10.042Z" } 227 | wheels = [ 228 | { url = "https://files.pythonhosted.org/packages/4d/33/cc527c11132a6274724a04938d50e1ff2b54a5f5943cd0480427571e1adb/griffe_fieldz-0.3.0-py3-none-any.whl", hash = "sha256:52e02fdcbdf6dea3c8c95756d1e0b30861569f871d19437fda702776fde4e64d", size = 6577, upload-time = "2025-07-30T21:43:09.073Z" }, 229 | ] 230 | 231 | [[package]] 232 | name = "h11" 233 | version = "0.16.0" 234 | source = { registry = "https://pypi.org/simple" } 235 | sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } 236 | wheels = [ 237 | { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, 238 | ] 239 | 240 | [[package]] 241 | name = "httpcore" 242 | version = "1.0.9" 243 | source = { registry = "https://pypi.org/simple" } 244 | dependencies = [ 245 | { name = "certifi" }, 246 | { name = "h11" }, 247 | ] 248 | sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } 249 | wheels = [ 250 | { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, 251 | ] 252 | 253 | [[package]] 254 | name = "httpx" 255 | version = "0.28.1" 256 | source = { registry = "https://pypi.org/simple" } 257 | dependencies = [ 258 | { name = "anyio" }, 259 | { name = "certifi" }, 260 | { name = "httpcore" }, 261 | { name = "idna" }, 262 | ] 263 | sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } 264 | wheels = [ 265 | { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, 266 | ] 267 | 268 | [[package]] 269 | name = "idna" 270 | version = "3.10" 271 | source = { registry = "https://pypi.org/simple" } 272 | sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } 273 | wheels = [ 274 | { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, 275 | ] 276 | 277 | [[package]] 278 | name = "importlib-metadata" 279 | version = "8.7.0" 280 | source = { registry = "https://pypi.org/simple" } 281 | dependencies = [ 282 | { name = "zipp", marker = "python_full_version < '3.10'" }, 283 | ] 284 | sdist = { url = "https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", size = 56641, upload-time = "2025-04-27T15:29:01.736Z" } 285 | wheels = [ 286 | { url = "https://files.pythonhosted.org/packages/20/b0/36bd937216ec521246249be3bf9855081de4c5e06a0c9b4219dbeda50373/importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd", size = 27656, upload-time = "2025-04-27T15:29:00.214Z" }, 287 | ] 288 | 289 | [[package]] 290 | name = "jinja2" 291 | version = "3.1.6" 292 | source = { registry = "https://pypi.org/simple" } 293 | dependencies = [ 294 | { name = "markupsafe" }, 295 | ] 296 | sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } 297 | wheels = [ 298 | { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, 299 | ] 300 | 301 | [[package]] 302 | name = "markdown" 303 | version = "3.8.2" 304 | source = { registry = "https://pypi.org/simple" } 305 | dependencies = [ 306 | { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, 307 | ] 308 | sdist = { url = "https://files.pythonhosted.org/packages/d7/c2/4ab49206c17f75cb08d6311171f2d65798988db4360c4d1485bd0eedd67c/markdown-3.8.2.tar.gz", hash = "sha256:247b9a70dd12e27f67431ce62523e675b866d254f900c4fe75ce3dda62237c45", size = 362071, upload-time = "2025-06-19T17:12:44.483Z" } 309 | wheels = [ 310 | { url = "https://files.pythonhosted.org/packages/96/2b/34cc11786bc00d0f04d0f5fdc3a2b1ae0b6239eef72d3d345805f9ad92a1/markdown-3.8.2-py3-none-any.whl", hash = "sha256:5c83764dbd4e00bdd94d85a19b8d55ccca20fe35b2e678a1422b380324dd5f24", size = 106827, upload-time = "2025-06-19T17:12:42.994Z" }, 311 | ] 312 | 313 | [[package]] 314 | name = "markupsafe" 315 | version = "3.0.2" 316 | source = { registry = "https://pypi.org/simple" } 317 | sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537, upload-time = "2024-10-18T15:21:54.129Z" } 318 | wheels = [ 319 | { url = "https://files.pythonhosted.org/packages/04/90/d08277ce111dd22f77149fd1a5d4653eeb3b3eaacbdfcbae5afb2600eebd/MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8", size = 14357, upload-time = "2024-10-18T15:20:51.44Z" }, 320 | { url = "https://files.pythonhosted.org/packages/04/e1/6e2194baeae0bca1fae6629dc0cbbb968d4d941469cbab11a3872edff374/MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158", size = 12393, upload-time = "2024-10-18T15:20:52.426Z" }, 321 | { url = "https://files.pythonhosted.org/packages/1d/69/35fa85a8ece0a437493dc61ce0bb6d459dcba482c34197e3efc829aa357f/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579", size = 21732, upload-time = "2024-10-18T15:20:53.578Z" }, 322 | { url = "https://files.pythonhosted.org/packages/22/35/137da042dfb4720b638d2937c38a9c2df83fe32d20e8c8f3185dbfef05f7/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d", size = 20866, upload-time = "2024-10-18T15:20:55.06Z" }, 323 | { url = "https://files.pythonhosted.org/packages/29/28/6d029a903727a1b62edb51863232152fd335d602def598dade38996887f0/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb", size = 20964, upload-time = "2024-10-18T15:20:55.906Z" }, 324 | { url = "https://files.pythonhosted.org/packages/cc/cd/07438f95f83e8bc028279909d9c9bd39e24149b0d60053a97b2bc4f8aa51/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b", size = 21977, upload-time = "2024-10-18T15:20:57.189Z" }, 325 | { url = "https://files.pythonhosted.org/packages/29/01/84b57395b4cc062f9c4c55ce0df7d3108ca32397299d9df00fedd9117d3d/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c", size = 21366, upload-time = "2024-10-18T15:20:58.235Z" }, 326 | { url = "https://files.pythonhosted.org/packages/bd/6e/61ebf08d8940553afff20d1fb1ba7294b6f8d279df9fd0c0db911b4bbcfd/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171", size = 21091, upload-time = "2024-10-18T15:20:59.235Z" }, 327 | { url = "https://files.pythonhosted.org/packages/11/23/ffbf53694e8c94ebd1e7e491de185124277964344733c45481f32ede2499/MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50", size = 15065, upload-time = "2024-10-18T15:21:00.307Z" }, 328 | { url = "https://files.pythonhosted.org/packages/44/06/e7175d06dd6e9172d4a69a72592cb3f7a996a9c396eee29082826449bbc3/MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a", size = 15514, upload-time = "2024-10-18T15:21:01.122Z" }, 329 | { url = "https://files.pythonhosted.org/packages/6b/28/bbf83e3f76936960b850435576dd5e67034e200469571be53f69174a2dfd/MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", size = 14353, upload-time = "2024-10-18T15:21:02.187Z" }, 330 | { url = "https://files.pythonhosted.org/packages/6c/30/316d194b093cde57d448a4c3209f22e3046c5bb2fb0820b118292b334be7/MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", size = 12392, upload-time = "2024-10-18T15:21:02.941Z" }, 331 | { url = "https://files.pythonhosted.org/packages/f2/96/9cdafba8445d3a53cae530aaf83c38ec64c4d5427d975c974084af5bc5d2/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", size = 23984, upload-time = "2024-10-18T15:21:03.953Z" }, 332 | { url = "https://files.pythonhosted.org/packages/f1/a4/aefb044a2cd8d7334c8a47d3fb2c9f328ac48cb349468cc31c20b539305f/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", size = 23120, upload-time = "2024-10-18T15:21:06.495Z" }, 333 | { url = "https://files.pythonhosted.org/packages/8d/21/5e4851379f88f3fad1de30361db501300d4f07bcad047d3cb0449fc51f8c/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", size = 23032, upload-time = "2024-10-18T15:21:07.295Z" }, 334 | { url = "https://files.pythonhosted.org/packages/00/7b/e92c64e079b2d0d7ddf69899c98842f3f9a60a1ae72657c89ce2655c999d/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", size = 24057, upload-time = "2024-10-18T15:21:08.073Z" }, 335 | { url = "https://files.pythonhosted.org/packages/f9/ac/46f960ca323037caa0a10662ef97d0a4728e890334fc156b9f9e52bcc4ca/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", size = 23359, upload-time = "2024-10-18T15:21:09.318Z" }, 336 | { url = "https://files.pythonhosted.org/packages/69/84/83439e16197337b8b14b6a5b9c2105fff81d42c2a7c5b58ac7b62ee2c3b1/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", size = 23306, upload-time = "2024-10-18T15:21:10.185Z" }, 337 | { url = "https://files.pythonhosted.org/packages/9a/34/a15aa69f01e2181ed8d2b685c0d2f6655d5cca2c4db0ddea775e631918cd/MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", size = 15094, upload-time = "2024-10-18T15:21:11.005Z" }, 338 | { url = "https://files.pythonhosted.org/packages/da/b8/3a3bd761922d416f3dc5d00bfbed11f66b1ab89a0c2b6e887240a30b0f6b/MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", size = 15521, upload-time = "2024-10-18T15:21:12.911Z" }, 339 | { url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274, upload-time = "2024-10-18T15:21:13.777Z" }, 340 | { url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348, upload-time = "2024-10-18T15:21:14.822Z" }, 341 | { url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149, upload-time = "2024-10-18T15:21:15.642Z" }, 342 | { url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118, upload-time = "2024-10-18T15:21:17.133Z" }, 343 | { url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993, upload-time = "2024-10-18T15:21:18.064Z" }, 344 | { url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178, upload-time = "2024-10-18T15:21:18.859Z" }, 345 | { url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319, upload-time = "2024-10-18T15:21:19.671Z" }, 346 | { url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352, upload-time = "2024-10-18T15:21:20.971Z" }, 347 | { url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097, upload-time = "2024-10-18T15:21:22.646Z" }, 348 | { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601, upload-time = "2024-10-18T15:21:23.499Z" }, 349 | { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274, upload-time = "2024-10-18T15:21:24.577Z" }, 350 | { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352, upload-time = "2024-10-18T15:21:25.382Z" }, 351 | { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122, upload-time = "2024-10-18T15:21:26.199Z" }, 352 | { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085, upload-time = "2024-10-18T15:21:27.029Z" }, 353 | { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978, upload-time = "2024-10-18T15:21:27.846Z" }, 354 | { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208, upload-time = "2024-10-18T15:21:28.744Z" }, 355 | { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357, upload-time = "2024-10-18T15:21:29.545Z" }, 356 | { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344, upload-time = "2024-10-18T15:21:30.366Z" }, 357 | { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101, upload-time = "2024-10-18T15:21:31.207Z" }, 358 | { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603, upload-time = "2024-10-18T15:21:32.032Z" }, 359 | { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510, upload-time = "2024-10-18T15:21:33.625Z" }, 360 | { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486, upload-time = "2024-10-18T15:21:34.611Z" }, 361 | { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480, upload-time = "2024-10-18T15:21:35.398Z" }, 362 | { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914, upload-time = "2024-10-18T15:21:36.231Z" }, 363 | { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796, upload-time = "2024-10-18T15:21:37.073Z" }, 364 | { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473, upload-time = "2024-10-18T15:21:37.932Z" }, 365 | { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114, upload-time = "2024-10-18T15:21:39.799Z" }, 366 | { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098, upload-time = "2024-10-18T15:21:40.813Z" }, 367 | { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208, upload-time = "2024-10-18T15:21:41.814Z" }, 368 | { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739, upload-time = "2024-10-18T15:21:42.784Z" }, 369 | { url = "https://files.pythonhosted.org/packages/a7/ea/9b1530c3fdeeca613faeb0fb5cbcf2389d816072fab72a71b45749ef6062/MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a", size = 14344, upload-time = "2024-10-18T15:21:43.721Z" }, 370 | { url = "https://files.pythonhosted.org/packages/4b/c2/fbdbfe48848e7112ab05e627e718e854d20192b674952d9042ebd8c9e5de/MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff", size = 12389, upload-time = "2024-10-18T15:21:44.666Z" }, 371 | { url = "https://files.pythonhosted.org/packages/f0/25/7a7c6e4dbd4f867d95d94ca15449e91e52856f6ed1905d58ef1de5e211d0/MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13", size = 21607, upload-time = "2024-10-18T15:21:45.452Z" }, 372 | { url = "https://files.pythonhosted.org/packages/53/8f/f339c98a178f3c1e545622206b40986a4c3307fe39f70ccd3d9df9a9e425/MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144", size = 20728, upload-time = "2024-10-18T15:21:46.295Z" }, 373 | { url = "https://files.pythonhosted.org/packages/1a/03/8496a1a78308456dbd50b23a385c69b41f2e9661c67ea1329849a598a8f9/MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29", size = 20826, upload-time = "2024-10-18T15:21:47.134Z" }, 374 | { url = "https://files.pythonhosted.org/packages/e6/cf/0a490a4bd363048c3022f2f475c8c05582179bb179defcee4766fb3dcc18/MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0", size = 21843, upload-time = "2024-10-18T15:21:48.334Z" }, 375 | { url = "https://files.pythonhosted.org/packages/19/a3/34187a78613920dfd3cdf68ef6ce5e99c4f3417f035694074beb8848cd77/MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0", size = 21219, upload-time = "2024-10-18T15:21:49.587Z" }, 376 | { url = "https://files.pythonhosted.org/packages/17/d8/5811082f85bb88410ad7e452263af048d685669bbbfb7b595e8689152498/MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178", size = 20946, upload-time = "2024-10-18T15:21:50.441Z" }, 377 | { url = "https://files.pythonhosted.org/packages/7c/31/bd635fb5989440d9365c5e3c47556cfea121c7803f5034ac843e8f37c2f2/MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f", size = 15063, upload-time = "2024-10-18T15:21:51.385Z" }, 378 | { url = "https://files.pythonhosted.org/packages/b3/73/085399401383ce949f727afec55ec3abd76648d04b9f22e1c0e99cb4bec3/MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a", size = 15506, upload-time = "2024-10-18T15:21:52.974Z" }, 379 | ] 380 | 381 | [[package]] 382 | name = "mergedeep" 383 | version = "1.3.4" 384 | source = { registry = "https://pypi.org/simple" } 385 | sdist = { url = "https://files.pythonhosted.org/packages/3a/41/580bb4006e3ed0361b8151a01d324fb03f420815446c7def45d02f74c270/mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8", size = 4661, upload-time = "2021-02-05T18:55:30.623Z" } 386 | wheels = [ 387 | { url = "https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307", size = 6354, upload-time = "2021-02-05T18:55:29.583Z" }, 388 | ] 389 | 390 | [[package]] 391 | name = "mkdocs" 392 | version = "1.6.1" 393 | source = { registry = "https://pypi.org/simple" } 394 | dependencies = [ 395 | { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, 396 | { name = "click", version = "8.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, 397 | { name = "colorama", marker = "sys_platform == 'win32'" }, 398 | { name = "ghp-import" }, 399 | { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, 400 | { name = "jinja2" }, 401 | { name = "markdown" }, 402 | { name = "markupsafe" }, 403 | { name = "mergedeep" }, 404 | { name = "mkdocs-get-deps" }, 405 | { name = "packaging" }, 406 | { name = "pathspec" }, 407 | { name = "pyyaml" }, 408 | { name = "pyyaml-env-tag" }, 409 | { name = "watchdog" }, 410 | ] 411 | sdist = { url = "https://files.pythonhosted.org/packages/bc/c6/bbd4f061bd16b378247f12953ffcb04786a618ce5e904b8c5a01a0309061/mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2", size = 3889159, upload-time = "2024-08-30T12:24:06.899Z" } 412 | wheels = [ 413 | { url = "https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e", size = 3864451, upload-time = "2024-08-30T12:24:05.054Z" }, 414 | ] 415 | 416 | [[package]] 417 | name = "mkdocs-autorefs" 418 | version = "1.4.2" 419 | source = { registry = "https://pypi.org/simple" } 420 | dependencies = [ 421 | { name = "markdown" }, 422 | { name = "markupsafe" }, 423 | { name = "mkdocs" }, 424 | ] 425 | sdist = { url = "https://files.pythonhosted.org/packages/47/0c/c9826f35b99c67fa3a7cddfa094c1a6c43fafde558c309c6e4403e5b37dc/mkdocs_autorefs-1.4.2.tar.gz", hash = "sha256:e2ebe1abd2b67d597ed19378c0fff84d73d1dbce411fce7a7cc6f161888b6749", size = 54961, upload-time = "2025-05-20T13:09:09.886Z" } 426 | wheels = [ 427 | { url = "https://files.pythonhosted.org/packages/87/dc/fc063b78f4b769d1956319351704e23ebeba1e9e1d6a41b4b602325fd7e4/mkdocs_autorefs-1.4.2-py3-none-any.whl", hash = "sha256:83d6d777b66ec3c372a1aad4ae0cf77c243ba5bcda5bf0c6b8a2c5e7a3d89f13", size = 24969, upload-time = "2025-05-20T13:09:08.237Z" }, 428 | ] 429 | 430 | [[package]] 431 | name = "mkdocs-get-deps" 432 | version = "0.2.0" 433 | source = { registry = "https://pypi.org/simple" } 434 | dependencies = [ 435 | { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, 436 | { name = "mergedeep" }, 437 | { name = "platformdirs" }, 438 | { name = "pyyaml" }, 439 | ] 440 | sdist = { url = "https://files.pythonhosted.org/packages/98/f5/ed29cd50067784976f25ed0ed6fcd3c2ce9eb90650aa3b2796ddf7b6870b/mkdocs_get_deps-0.2.0.tar.gz", hash = "sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c", size = 10239, upload-time = "2023-11-20T17:51:09.981Z" } 441 | wheels = [ 442 | { url = "https://files.pythonhosted.org/packages/9f/d4/029f984e8d3f3b6b726bd33cafc473b75e9e44c0f7e80a5b29abc466bdea/mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134", size = 9521, upload-time = "2023-11-20T17:51:08.587Z" }, 443 | ] 444 | 445 | [[package]] 446 | name = "mkdocs-material" 447 | version = "9.6.17" 448 | source = { registry = "https://pypi.org/simple" } 449 | dependencies = [ 450 | { name = "babel" }, 451 | { name = "backrefs" }, 452 | { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, 453 | { name = "click", version = "8.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, 454 | { name = "colorama" }, 455 | { name = "jinja2" }, 456 | { name = "markdown" }, 457 | { name = "mkdocs" }, 458 | { name = "mkdocs-material-extensions" }, 459 | { name = "paginate" }, 460 | { name = "pygments" }, 461 | { name = "pymdown-extensions" }, 462 | { name = "requests" }, 463 | ] 464 | sdist = { url = "https://files.pythonhosted.org/packages/47/02/51115cdda743e1551c5c13bdfaaf8c46b959acc57ba914d8ec479dd2fe1f/mkdocs_material-9.6.17.tar.gz", hash = "sha256:48ae7aec72a3f9f501a70be3fbd329c96ff5f5a385b67a1563e5ed5ce064affe", size = 4032898, upload-time = "2025-08-15T16:09:21.412Z" } 465 | wheels = [ 466 | { url = "https://files.pythonhosted.org/packages/3c/7c/0f0d44c92c8f3068930da495b752244bd59fd87b5b0f9571fa2d2a93aee7/mkdocs_material-9.6.17-py3-none-any.whl", hash = "sha256:221dd8b37a63f52e580bcab4a7e0290e4a6f59bd66190be9c3d40767e05f9417", size = 9229230, upload-time = "2025-08-15T16:09:18.301Z" }, 467 | ] 468 | 469 | [[package]] 470 | name = "mkdocs-material-extensions" 471 | version = "1.3.1" 472 | source = { registry = "https://pypi.org/simple" } 473 | sdist = { url = "https://files.pythonhosted.org/packages/79/9b/9b4c96d6593b2a541e1cb8b34899a6d021d208bb357042823d4d2cabdbe7/mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443", size = 11847, upload-time = "2023-11-22T19:09:45.208Z" } 474 | wheels = [ 475 | { url = "https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31", size = 8728, upload-time = "2023-11-22T19:09:43.465Z" }, 476 | ] 477 | 478 | [[package]] 479 | name = "mkdocstrings" 480 | version = "0.30.0" 481 | source = { registry = "https://pypi.org/simple" } 482 | dependencies = [ 483 | { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, 484 | { name = "jinja2" }, 485 | { name = "markdown" }, 486 | { name = "markupsafe" }, 487 | { name = "mkdocs" }, 488 | { name = "mkdocs-autorefs" }, 489 | { name = "pymdown-extensions" }, 490 | ] 491 | sdist = { url = "https://files.pythonhosted.org/packages/e2/0a/7e4776217d4802009c8238c75c5345e23014a4706a8414a62c0498858183/mkdocstrings-0.30.0.tar.gz", hash = "sha256:5d8019b9c31ddacd780b6784ffcdd6f21c408f34c0bd1103b5351d609d5b4444", size = 106597, upload-time = "2025-07-22T23:48:45.998Z" } 492 | wheels = [ 493 | { url = "https://files.pythonhosted.org/packages/de/b4/3c5eac68f31e124a55d255d318c7445840fa1be55e013f507556d6481913/mkdocstrings-0.30.0-py3-none-any.whl", hash = "sha256:ae9e4a0d8c1789697ac776f2e034e2ddd71054ae1cf2c2bb1433ccfd07c226f2", size = 36579, upload-time = "2025-07-22T23:48:44.152Z" }, 494 | ] 495 | 496 | [package.optional-dependencies] 497 | python = [ 498 | { name = "mkdocstrings-python" }, 499 | ] 500 | 501 | [[package]] 502 | name = "mkdocstrings-python" 503 | version = "1.17.0" 504 | source = { registry = "https://pypi.org/simple" } 505 | dependencies = [ 506 | { name = "griffe" }, 507 | { name = "mkdocs-autorefs" }, 508 | { name = "mkdocstrings" }, 509 | { name = "typing-extensions", marker = "python_full_version < '3.11'" }, 510 | ] 511 | sdist = { url = "https://files.pythonhosted.org/packages/39/7c/6dfd8ad59c0eebae167168528ed6cad00116f58ef2327686149f7b25d175/mkdocstrings_python-1.17.0.tar.gz", hash = "sha256:c6295962b60542a9c7468a3b515ce8524616ca9f8c1a38c790db4286340ba501", size = 200408, upload-time = "2025-08-14T21:18:14.568Z" } 512 | wheels = [ 513 | { url = "https://files.pythonhosted.org/packages/bd/ac/b1fcc937f4ecd372f3e857162dea67c45c1e2eedbac80447be516e3372bb/mkdocstrings_python-1.17.0-py3-none-any.whl", hash = "sha256:49903fa355dfecc5ad0b891e78ff5d25d30ffd00846952801bbe8331e123d4b0", size = 124778, upload-time = "2025-08-14T21:18:12.821Z" }, 514 | ] 515 | 516 | [[package]] 517 | name = "packaging" 518 | version = "25.0" 519 | source = { registry = "https://pypi.org/simple" } 520 | sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } 521 | wheels = [ 522 | { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, 523 | ] 524 | 525 | [[package]] 526 | name = "paginate" 527 | version = "0.5.7" 528 | source = { registry = "https://pypi.org/simple" } 529 | sdist = { url = "https://files.pythonhosted.org/packages/ec/46/68dde5b6bc00c1296ec6466ab27dddede6aec9af1b99090e1107091b3b84/paginate-0.5.7.tar.gz", hash = "sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945", size = 19252, upload-time = "2024-08-25T14:17:24.139Z" } 530 | wheels = [ 531 | { url = "https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591", size = 13746, upload-time = "2024-08-25T14:17:22.55Z" }, 532 | ] 533 | 534 | [[package]] 535 | name = "pathspec" 536 | version = "0.12.1" 537 | source = { registry = "https://pypi.org/simple" } 538 | sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" } 539 | wheels = [ 540 | { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" }, 541 | ] 542 | 543 | [[package]] 544 | name = "platformdirs" 545 | version = "4.3.8" 546 | source = { registry = "https://pypi.org/simple" } 547 | sdist = { url = "https://files.pythonhosted.org/packages/fe/8b/3c73abc9c759ecd3f1f7ceff6685840859e8070c4d947c93fae71f6a0bf2/platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc", size = 21362, upload-time = "2025-05-07T22:47:42.121Z" } 548 | wheels = [ 549 | { url = "https://files.pythonhosted.org/packages/fe/39/979e8e21520d4e47a0bbe349e2713c0aac6f3d853d0e5b34d76206c439aa/platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4", size = 18567, upload-time = "2025-05-07T22:47:40.376Z" }, 550 | ] 551 | 552 | [[package]] 553 | name = "pygments" 554 | version = "2.19.2" 555 | source = { registry = "https://pypi.org/simple" } 556 | sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } 557 | wheels = [ 558 | { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, 559 | ] 560 | 561 | [[package]] 562 | name = "pymdown-extensions" 563 | version = "10.16.1" 564 | source = { registry = "https://pypi.org/simple" } 565 | dependencies = [ 566 | { name = "markdown" }, 567 | { name = "pyyaml" }, 568 | ] 569 | sdist = { url = "https://files.pythonhosted.org/packages/55/b3/6d2b3f149bc5413b0a29761c2c5832d8ce904a1d7f621e86616d96f505cc/pymdown_extensions-10.16.1.tar.gz", hash = "sha256:aace82bcccba3efc03e25d584e6a22d27a8e17caa3f4dd9f207e49b787aa9a91", size = 853277, upload-time = "2025-07-28T16:19:34.167Z" } 570 | wheels = [ 571 | { url = "https://files.pythonhosted.org/packages/e4/06/43084e6cbd4b3bc0e80f6be743b2e79fbc6eed8de9ad8c629939fa55d972/pymdown_extensions-10.16.1-py3-none-any.whl", hash = "sha256:d6ba157a6c03146a7fb122b2b9a121300056384eafeec9c9f9e584adfdb2a32d", size = 266178, upload-time = "2025-07-28T16:19:31.401Z" }, 572 | ] 573 | 574 | [[package]] 575 | name = "python-dateutil" 576 | version = "2.9.0.post0" 577 | source = { registry = "https://pypi.org/simple" } 578 | dependencies = [ 579 | { name = "six" }, 580 | ] 581 | sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } 582 | wheels = [ 583 | { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, 584 | ] 585 | 586 | [[package]] 587 | name = "pyyaml" 588 | version = "6.0.2" 589 | source = { registry = "https://pypi.org/simple" } 590 | sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" } 591 | wheels = [ 592 | { url = "https://files.pythonhosted.org/packages/9b/95/a3fac87cb7158e231b5a6012e438c647e1a87f09f8e0d123acec8ab8bf71/PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", size = 184199, upload-time = "2024-08-06T20:31:40.178Z" }, 593 | { url = "https://files.pythonhosted.org/packages/c7/7a/68bd47624dab8fd4afbfd3c48e3b79efe09098ae941de5b58abcbadff5cb/PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", size = 171758, upload-time = "2024-08-06T20:31:42.173Z" }, 594 | { url = "https://files.pythonhosted.org/packages/49/ee/14c54df452143b9ee9f0f29074d7ca5516a36edb0b4cc40c3f280131656f/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", size = 718463, upload-time = "2024-08-06T20:31:44.263Z" }, 595 | { url = "https://files.pythonhosted.org/packages/4d/61/de363a97476e766574650d742205be468921a7b532aa2499fcd886b62530/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", size = 719280, upload-time = "2024-08-06T20:31:50.199Z" }, 596 | { url = "https://files.pythonhosted.org/packages/6b/4e/1523cb902fd98355e2e9ea5e5eb237cbc5f3ad5f3075fa65087aa0ecb669/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", size = 751239, upload-time = "2024-08-06T20:31:52.292Z" }, 597 | { url = "https://files.pythonhosted.org/packages/b7/33/5504b3a9a4464893c32f118a9cc045190a91637b119a9c881da1cf6b7a72/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", size = 695802, upload-time = "2024-08-06T20:31:53.836Z" }, 598 | { url = "https://files.pythonhosted.org/packages/5c/20/8347dcabd41ef3a3cdc4f7b7a2aff3d06598c8779faa189cdbf878b626a4/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", size = 720527, upload-time = "2024-08-06T20:31:55.565Z" }, 599 | { url = "https://files.pythonhosted.org/packages/be/aa/5afe99233fb360d0ff37377145a949ae258aaab831bde4792b32650a4378/PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", size = 144052, upload-time = "2024-08-06T20:31:56.914Z" }, 600 | { url = "https://files.pythonhosted.org/packages/b5/84/0fa4b06f6d6c958d207620fc60005e241ecedceee58931bb20138e1e5776/PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", size = 161774, upload-time = "2024-08-06T20:31:58.304Z" }, 601 | { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612, upload-time = "2024-08-06T20:32:03.408Z" }, 602 | { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040, upload-time = "2024-08-06T20:32:04.926Z" }, 603 | { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829, upload-time = "2024-08-06T20:32:06.459Z" }, 604 | { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167, upload-time = "2024-08-06T20:32:08.338Z" }, 605 | { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952, upload-time = "2024-08-06T20:32:14.124Z" }, 606 | { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301, upload-time = "2024-08-06T20:32:16.17Z" }, 607 | { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638, upload-time = "2024-08-06T20:32:18.555Z" }, 608 | { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850, upload-time = "2024-08-06T20:32:19.889Z" }, 609 | { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980, upload-time = "2024-08-06T20:32:21.273Z" }, 610 | { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873, upload-time = "2024-08-06T20:32:25.131Z" }, 611 | { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302, upload-time = "2024-08-06T20:32:26.511Z" }, 612 | { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154, upload-time = "2024-08-06T20:32:28.363Z" }, 613 | { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223, upload-time = "2024-08-06T20:32:30.058Z" }, 614 | { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542, upload-time = "2024-08-06T20:32:31.881Z" }, 615 | { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164, upload-time = "2024-08-06T20:32:37.083Z" }, 616 | { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611, upload-time = "2024-08-06T20:32:38.898Z" }, 617 | { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591, upload-time = "2024-08-06T20:32:40.241Z" }, 618 | { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338, upload-time = "2024-08-06T20:32:41.93Z" }, 619 | { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309, upload-time = "2024-08-06T20:32:43.4Z" }, 620 | { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679, upload-time = "2024-08-06T20:32:44.801Z" }, 621 | { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428, upload-time = "2024-08-06T20:32:46.432Z" }, 622 | { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361, upload-time = "2024-08-06T20:32:51.188Z" }, 623 | { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523, upload-time = "2024-08-06T20:32:53.019Z" }, 624 | { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660, upload-time = "2024-08-06T20:32:54.708Z" }, 625 | { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597, upload-time = "2024-08-06T20:32:56.985Z" }, 626 | { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527, upload-time = "2024-08-06T20:33:03.001Z" }, 627 | { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" }, 628 | { url = "https://files.pythonhosted.org/packages/65/d8/b7a1db13636d7fb7d4ff431593c510c8b8fca920ade06ca8ef20015493c5/PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d", size = 184777, upload-time = "2024-08-06T20:33:25.896Z" }, 629 | { url = "https://files.pythonhosted.org/packages/0a/02/6ec546cd45143fdf9840b2c6be8d875116a64076218b61d68e12548e5839/PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f", size = 172318, upload-time = "2024-08-06T20:33:27.212Z" }, 630 | { url = "https://files.pythonhosted.org/packages/0e/9a/8cc68be846c972bda34f6c2a93abb644fb2476f4dcc924d52175786932c9/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290", size = 720891, upload-time = "2024-08-06T20:33:28.974Z" }, 631 | { url = "https://files.pythonhosted.org/packages/e9/6c/6e1b7f40181bc4805e2e07f4abc10a88ce4648e7e95ff1abe4ae4014a9b2/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12", size = 722614, upload-time = "2024-08-06T20:33:34.157Z" }, 632 | { url = "https://files.pythonhosted.org/packages/3d/32/e7bd8535d22ea2874cef6a81021ba019474ace0d13a4819c2a4bce79bd6a/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19", size = 737360, upload-time = "2024-08-06T20:33:35.84Z" }, 633 | { url = "https://files.pythonhosted.org/packages/d7/12/7322c1e30b9be969670b672573d45479edef72c9a0deac3bb2868f5d7469/PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e", size = 699006, upload-time = "2024-08-06T20:33:37.501Z" }, 634 | { url = "https://files.pythonhosted.org/packages/82/72/04fcad41ca56491995076630c3ec1e834be241664c0c09a64c9a2589b507/PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725", size = 723577, upload-time = "2024-08-06T20:33:39.389Z" }, 635 | { url = "https://files.pythonhosted.org/packages/ed/5e/46168b1f2757f1fcd442bc3029cd8767d88a98c9c05770d8b420948743bb/PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631", size = 144593, upload-time = "2024-08-06T20:33:46.63Z" }, 636 | { url = "https://files.pythonhosted.org/packages/19/87/5124b1c1f2412bb95c59ec481eaf936cd32f0fe2a7b16b97b81c4c017a6a/PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8", size = 162312, upload-time = "2024-08-06T20:33:49.073Z" }, 637 | ] 638 | 639 | [[package]] 640 | name = "pyyaml-env-tag" 641 | version = "1.1" 642 | source = { registry = "https://pypi.org/simple" } 643 | dependencies = [ 644 | { name = "pyyaml" }, 645 | ] 646 | sdist = { url = "https://files.pythonhosted.org/packages/eb/2e/79c822141bfd05a853236b504869ebc6b70159afc570e1d5a20641782eaa/pyyaml_env_tag-1.1.tar.gz", hash = "sha256:2eb38b75a2d21ee0475d6d97ec19c63287a7e140231e4214969d0eac923cd7ff", size = 5737, upload-time = "2025-05-13T15:24:01.64Z" } 647 | wheels = [ 648 | { url = "https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl", hash = "sha256:17109e1a528561e32f026364712fee1264bc2ea6715120891174ed1b980d2e04", size = 4722, upload-time = "2025-05-13T15:23:59.629Z" }, 649 | ] 650 | 651 | [[package]] 652 | name = "requests" 653 | version = "2.32.4" 654 | source = { registry = "https://pypi.org/simple" } 655 | dependencies = [ 656 | { name = "certifi" }, 657 | { name = "charset-normalizer" }, 658 | { name = "idna" }, 659 | { name = "urllib3" }, 660 | ] 661 | sdist = { url = "https://files.pythonhosted.org/packages/e1/0a/929373653770d8a0d7ea76c37de6e41f11eb07559b103b1c02cafb3f7cf8/requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422", size = 135258, upload-time = "2025-06-09T16:43:07.34Z" } 662 | wheels = [ 663 | { url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847, upload-time = "2025-06-09T16:43:05.728Z" }, 664 | ] 665 | 666 | [[package]] 667 | name = "seedrcc" 668 | version = "2.0.2" 669 | source = { editable = "." } 670 | dependencies = [ 671 | { name = "anyio" }, 672 | { name = "httpx" }, 673 | ] 674 | 675 | [package.dev-dependencies] 676 | docs = [ 677 | { name = "griffe-fieldz" }, 678 | { name = "mkdocs" }, 679 | { name = "mkdocs-material" }, 680 | { name = "mkdocstrings", extra = ["python"] }, 681 | ] 682 | 683 | [package.metadata] 684 | requires-dist = [ 685 | { name = "anyio" }, 686 | { name = "httpx" }, 687 | ] 688 | 689 | [package.metadata.requires-dev] 690 | docs = [ 691 | { name = "griffe-fieldz", specifier = ">=0.3.0" }, 692 | { name = "mkdocs", specifier = ">=1.6.1" }, 693 | { name = "mkdocs-material", specifier = ">=9.6.17" }, 694 | { name = "mkdocstrings", extras = ["python"], specifier = ">=0.30.0" }, 695 | ] 696 | 697 | [[package]] 698 | name = "six" 699 | version = "1.17.0" 700 | source = { registry = "https://pypi.org/simple" } 701 | sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } 702 | wheels = [ 703 | { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, 704 | ] 705 | 706 | [[package]] 707 | name = "sniffio" 708 | version = "1.3.1" 709 | source = { registry = "https://pypi.org/simple" } 710 | sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } 711 | wheels = [ 712 | { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, 713 | ] 714 | 715 | [[package]] 716 | name = "typing-extensions" 717 | version = "4.14.1" 718 | source = { registry = "https://pypi.org/simple" } 719 | sdist = { url = "https://files.pythonhosted.org/packages/98/5a/da40306b885cc8c09109dc2e1abd358d5684b1425678151cdaed4731c822/typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", size = 107673, upload-time = "2025-07-04T13:28:34.16Z" } 720 | wheels = [ 721 | { url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906, upload-time = "2025-07-04T13:28:32.743Z" }, 722 | ] 723 | 724 | [[package]] 725 | name = "urllib3" 726 | version = "2.5.0" 727 | source = { registry = "https://pypi.org/simple" } 728 | sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" } 729 | wheels = [ 730 | { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" }, 731 | ] 732 | 733 | [[package]] 734 | name = "watchdog" 735 | version = "6.0.0" 736 | source = { registry = "https://pypi.org/simple" } 737 | sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220, upload-time = "2024-11-01T14:07:13.037Z" } 738 | wheels = [ 739 | { url = "https://files.pythonhosted.org/packages/0c/56/90994d789c61df619bfc5ce2ecdabd5eeff564e1eb47512bd01b5e019569/watchdog-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26", size = 96390, upload-time = "2024-11-01T14:06:24.793Z" }, 740 | { url = "https://files.pythonhosted.org/packages/55/46/9a67ee697342ddf3c6daa97e3a587a56d6c4052f881ed926a849fcf7371c/watchdog-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112", size = 88389, upload-time = "2024-11-01T14:06:27.112Z" }, 741 | { url = "https://files.pythonhosted.org/packages/44/65/91b0985747c52064d8701e1075eb96f8c40a79df889e59a399453adfb882/watchdog-6.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c897ac1b55c5a1461e16dae288d22bb2e412ba9807df8397a635d88f671d36c3", size = 89020, upload-time = "2024-11-01T14:06:29.876Z" }, 742 | { url = "https://files.pythonhosted.org/packages/e0/24/d9be5cd6642a6aa68352ded4b4b10fb0d7889cb7f45814fb92cecd35f101/watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c", size = 96393, upload-time = "2024-11-01T14:06:31.756Z" }, 743 | { url = "https://files.pythonhosted.org/packages/63/7a/6013b0d8dbc56adca7fdd4f0beed381c59f6752341b12fa0886fa7afc78b/watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2", size = 88392, upload-time = "2024-11-01T14:06:32.99Z" }, 744 | { url = "https://files.pythonhosted.org/packages/d1/40/b75381494851556de56281e053700e46bff5b37bf4c7267e858640af5a7f/watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c", size = 89019, upload-time = "2024-11-01T14:06:34.963Z" }, 745 | { url = "https://files.pythonhosted.org/packages/39/ea/3930d07dafc9e286ed356a679aa02d777c06e9bfd1164fa7c19c288a5483/watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948", size = 96471, upload-time = "2024-11-01T14:06:37.745Z" }, 746 | { url = "https://files.pythonhosted.org/packages/12/87/48361531f70b1f87928b045df868a9fd4e253d9ae087fa4cf3f7113be363/watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860", size = 88449, upload-time = "2024-11-01T14:06:39.748Z" }, 747 | { url = "https://files.pythonhosted.org/packages/5b/7e/8f322f5e600812e6f9a31b75d242631068ca8f4ef0582dd3ae6e72daecc8/watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0", size = 89054, upload-time = "2024-11-01T14:06:41.009Z" }, 748 | { url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480, upload-time = "2024-11-01T14:06:42.952Z" }, 749 | { url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451, upload-time = "2024-11-01T14:06:45.084Z" }, 750 | { url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057, upload-time = "2024-11-01T14:06:47.324Z" }, 751 | { url = "https://files.pythonhosted.org/packages/05/52/7223011bb760fce8ddc53416beb65b83a3ea6d7d13738dde75eeb2c89679/watchdog-6.0.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e6f0e77c9417e7cd62af82529b10563db3423625c5fce018430b249bf977f9e8", size = 96390, upload-time = "2024-11-01T14:06:49.325Z" }, 752 | { url = "https://files.pythonhosted.org/packages/9c/62/d2b21bc4e706d3a9d467561f487c2938cbd881c69f3808c43ac1ec242391/watchdog-6.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:90c8e78f3b94014f7aaae121e6b909674df5b46ec24d6bebc45c44c56729af2a", size = 88386, upload-time = "2024-11-01T14:06:50.536Z" }, 753 | { url = "https://files.pythonhosted.org/packages/ea/22/1c90b20eda9f4132e4603a26296108728a8bfe9584b006bd05dd94548853/watchdog-6.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e7631a77ffb1f7d2eefa4445ebbee491c720a5661ddf6df3498ebecae5ed375c", size = 89017, upload-time = "2024-11-01T14:06:51.717Z" }, 754 | { url = "https://files.pythonhosted.org/packages/30/ad/d17b5d42e28a8b91f8ed01cb949da092827afb9995d4559fd448d0472763/watchdog-6.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c7ac31a19f4545dd92fc25d200694098f42c9a8e391bc00bdd362c5736dbf881", size = 87902, upload-time = "2024-11-01T14:06:53.119Z" }, 755 | { url = "https://files.pythonhosted.org/packages/5c/ca/c3649991d140ff6ab67bfc85ab42b165ead119c9e12211e08089d763ece5/watchdog-6.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9513f27a1a582d9808cf21a07dae516f0fab1cf2d7683a742c498b93eedabb11", size = 88380, upload-time = "2024-11-01T14:06:55.19Z" }, 756 | { url = "https://files.pythonhosted.org/packages/5b/79/69f2b0e8d3f2afd462029031baafb1b75d11bb62703f0e1022b2e54d49ee/watchdog-6.0.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7a0e56874cfbc4b9b05c60c8a1926fedf56324bb08cfbc188969777940aef3aa", size = 87903, upload-time = "2024-11-01T14:06:57.052Z" }, 757 | { url = "https://files.pythonhosted.org/packages/e2/2b/dc048dd71c2e5f0f7ebc04dd7912981ec45793a03c0dc462438e0591ba5d/watchdog-6.0.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:e6439e374fc012255b4ec786ae3c4bc838cd7309a540e5fe0952d03687d8804e", size = 88381, upload-time = "2024-11-01T14:06:58.193Z" }, 758 | { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload-time = "2024-11-01T14:06:59.472Z" }, 759 | { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload-time = "2024-11-01T14:07:01.431Z" }, 760 | { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076, upload-time = "2024-11-01T14:07:02.568Z" }, 761 | { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077, upload-time = "2024-11-01T14:07:03.893Z" }, 762 | { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078, upload-time = "2024-11-01T14:07:05.189Z" }, 763 | { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077, upload-time = "2024-11-01T14:07:06.376Z" }, 764 | { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078, upload-time = "2024-11-01T14:07:07.547Z" }, 765 | { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065, upload-time = "2024-11-01T14:07:09.525Z" }, 766 | { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070, upload-time = "2024-11-01T14:07:10.686Z" }, 767 | { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" }, 768 | ] 769 | 770 | [[package]] 771 | name = "zipp" 772 | version = "3.23.0" 773 | source = { registry = "https://pypi.org/simple" } 774 | sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" } 775 | wheels = [ 776 | { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, 777 | ] 778 | --------------------------------------------------------------------------------