├── 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 |
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 |
20 |
21 |
22 |
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 |
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 |
20 |
21 |
22 |
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 |
--------------------------------------------------------------------------------