Merge pull request #557 from arabcoders/dev
Some checks failed
native-build / build (amd64, ubuntu-latest) (push) Has been cancelled
native-build / build (amd64, windows-latest) (push) Has been cancelled
native-build / build (arm64, macos-latest) (push) Has been cancelled
native-build / build (arm64, ubuntu-latest) (push) Has been cancelled
native-build / build (arm64, windows-latest) (push) Has been cancelled

fix: use yt-dlp[default] for upgrading logic
This commit is contained in:
Abdulmohsen 2026-02-05 18:08:35 +03:00 committed by GitHub
commit 0473c86fd6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
32 changed files with 1693 additions and 1577 deletions

View file

@ -42,7 +42,7 @@ jobs:
VER=$(uv pip list --outdated | awk '$1 == "yt-dlp" {print $3}') VER=$(uv pip list --outdated | awk '$1 == "yt-dlp" {print $3}')
if [ -n "$VER" ]; then if [ -n "$VER" ]; then
echo "YTLDLP_VER=${VER}" >> "$GITHUB_OUTPUT" echo "YTLDLP_VER=${VER}" >> "$GITHUB_OUTPUT"
uv pip install --upgrade yt-dlp uv pip install --upgrade yt-dlp[default]
uv sync --upgrade uv sync --upgrade
UPDATED=true UPDATED=true
else else

18
FAQ.md
View file

@ -317,22 +317,13 @@ Then simply create a new preset, and in the `Command options for yt-dlp` field s
```bash ```bash
--extractor-args "youtubepot-bgutilhttp:base_url=http://bgutil_provider:4416" --extractor-args "youtubepot-bgutilhttp:base_url=http://bgutil_provider:4416"
--extractor-args "youtube:player-client=default,tv,mweb;formats=incomplete"
``` ```
you and also enable the fallback by using the follow extractor args
```bash
--extractor-args "youtubepot-bgutilhttp:base_url=http://bgutil_provider:4416;disable_innertube=1"
--extractor-args "youtube:player-client=default,tv,mweb;formats=incomplete"
```
Use this alternative extractor args in case the extractor fails to get the pot tokens from the bgutil provider server.
For more information please visit [bgutil-ytdlp-pot-provider](https://github.com/Brainicism/bgutil-ytdlp-pot-provider) project. For more information please visit [bgutil-ytdlp-pot-provider](https://github.com/Brainicism/bgutil-ytdlp-pot-provider) project.
# Troubleshooting and submitting issues # Troubleshooting and submitting issues
Before asking a question or submitting an issue for YTPTube, please remember that YTPTube is only a thin wrapper for Before asking a question or submitting an issue for YTPTube, please remember that YTPTube is only a wrapper for
[yt-dlp](https://github.com/yt-dlp/yt-dlp). Any issues you might be experiencing with authentication to video websites, [yt-dlp](https://github.com/yt-dlp/yt-dlp). Any issues you might be experiencing with authentication to video websites,
postprocessing, permissions, other `yt-dlp options` configurations which seem not to work, or anything else that postprocessing, permissions, other `yt-dlp options` configurations which seem not to work, or anything else that
concerns the workings of the underlying yt-dlp library, need not be opened on the YTPTube project. concerns the workings of the underlying yt-dlp library, need not be opened on the YTPTube project.
@ -343,7 +334,8 @@ and once that is working, importing the options that worked for you into a new `
## Via HTTP ## Via HTTP
If you have enabled the web terminal via `YTP_CONSOLE_ENABLED` environment variable, simply go to `Other > Terminal` use If you have enabled the web terminal via `YTP_CONSOLE_ENABLED` environment variable, simply go to `Other > Terminal` use
the yt-dlp command, the interface is jailed to the `yt-dlp` binary you can't access anything else. the yt-dlp command, the interface is jailed to the `yt-dlp` binary you can't access anything else. Or from download form
by clicking `advanced options` button than the yellow terminal icon `Run directly in console`.
## Via CLI ## Via CLI
@ -503,7 +495,7 @@ as we only test for GPU encoding once on first video stream.
# Allowing internal URLs requests # Allowing internal URLs requests
By default, YTPTube prevents requests to internal resources, for security reasons. However, if you want to allow requests to internal URLs, you can set the `YTP_ALLOW_INTERNAL_URLS` environment variable to `true`. This will allow requests to internal URLs. By default, YTPTube prevents requests to internal resources. However, if you want to allow requests to internal URLs, you can set the `YTP_ALLOW_INTERNAL_URLS` environment variable to `true`. This will allow requests to internal URLs.
We do not recommend enabling this option unless you know what you are doing, as it can expose your internal network to We do not recommend enabling this option unless you know what you are doing, as it can expose your internal network to
potential security risks. This should only be used if it's truly needed. potential security risks. This should only be used if it's truly needed.
@ -557,7 +549,6 @@ services:
After making the changes, restart your container. This should resolve the "No space left on device" After making the changes, restart your container. This should resolve the "No space left on device"
error during download. error during download.
# How to prevent loading screen during YouTube premieres? # How to prevent loading screen during YouTube premieres?
Depending on how you look at it, YTPTube live download implementation is rather great and fast. However, during YouTube Depending on how you look at it, YTPTube live download implementation is rather great and fast. However, during YouTube
@ -566,7 +557,6 @@ playing. By default we wait for 5min + the duration of the video before starting
the loading screen. However, you can override the behavior by setting the following environment variable: the loading screen. However, you can override the behavior by setting the following environment variable:
```env ```env
YTP_PREVENT_LIVE_PREMIERE=true
YTP_LIVE_PREMIERE_BUFFER=10 YTP_LIVE_PREMIERE_BUFFER=10
``` ```

View file

@ -94,7 +94,7 @@ class Segments:
} }
caps: dict[str, dict[str, bool]] = detect_qsv_capabilities() caps: dict[str, dict[str, bool]] = detect_qsv_capabilities()
base_codec: str = s_codec.split("_")[0] base_codec: str = s_codec.split("_", maxsplit=1)[0]
codec_caps: dict[str, bool] = caps.get(base_codec, {"full": False, "lp": False}) codec_caps: dict[str, bool] = caps.get(base_codec, {"full": False, "lp": False})
ctx["qsv"] = codec_caps ctx["qsv"] = codec_caps

View file

@ -213,7 +213,7 @@ async def segments_stream(request: Request, config: Config, app: web.Application
await Segments( await Segments(
download_path=config.download_path, download_path=config.download_path,
index=int(segment), index=int(segment),
duration=float(f"{float(sd if sd else M3u8.duration):.6f}"), duration=float(f"{float(sd or M3u8.duration):.6f}"),
vconvert=vc == 1, vconvert=vc == 1,
aconvert=ac == 1, aconvert=ac == 1,
).stream(realFile, resp) ).stream(realFile, resp)

View file

@ -397,7 +397,7 @@ class TaskHandle:
return TaskFailure( return TaskFailure(
message=extraction.message, message=extraction.message,
error=extraction.error if extraction.error else extraction.message, error=extraction.error or extraction.message,
metadata=combined_failure_metadata, metadata=combined_failure_metadata,
) )

View file

@ -235,7 +235,7 @@ def extract_info_sync(
archive_id: str | None = f".{id_dict['id']}" if id_dict.get("id") else None archive_id: str | None = f".{id_dict['id']}" if id_dict.get("id") else None
log_wrapper.add_target( log_wrapper.add_target(
target=logging.getLogger(f"yt-dlp{archive_id if archive_id else '.extract_info'}"), target=logging.getLogger(f"yt-dlp{archive_id or '.extract_info'}"),
level=logging.DEBUG if debug else logging.WARNING, level=logging.DEBUG if debug else logging.WARNING,
) )

View file

@ -394,7 +394,7 @@ async def get_info(request: Request, cache: Cache, config: Config) -> Response:
data["is_archived"] = False data["is_archived"] = False
archive_file: str | None = ytdlp_opts.get("download_archive") archive_file: str | None = ytdlp_opts.get("download_archive")
data["archive_file"] = archive_file if archive_file else None data["archive_file"] = archive_file or None
if archive_file and (archive_id := get_archive_id(url=url).get("archive_id")): if archive_file and (archive_id := get_archive_id(url=url).get("archive_id")):
data["archive_id"] = archive_id data["archive_id"] = archive_id

View file

@ -130,7 +130,7 @@ class YTDLPCli:
raise ValueError(msg) raise ValueError(msg)
self.item = item self.item = item
preset_name = item.preset if item.preset else self._config.default_preset preset_name = item.preset or self._config.default_preset
self.preset: Preset | None = YTDLPCli._get_presets().get(preset_name) self.preset: Preset | None = YTDLPCli._get_presets().get(preset_name)
self._config: Config = config or Config.get_instance() self._config: Config = config or Config.get_instance()

View file

@ -227,7 +227,7 @@ class Item:
""" """
from app.features.presets.service import Presets from app.features.presets.service import Presets
return Presets.get_instance().get(self.preset if self.preset else self._default_preset()) return Presets.get_instance().get(self.preset or self._default_preset())
def get_archive_id(self) -> str | None: def get_archive_id(self) -> str | None:
""" """
@ -553,7 +553,7 @@ class ItemDTO:
""" """
from app.features.presets.service import Presets from app.features.presets.service import Presets
return Presets.get_instance().get(self.preset if self.preset else "default") return Presets.get_instance().get(self.preset or "default")
def archive_status(self, force: bool = False) -> None: def archive_status(self, force: bool = False) -> None:
""" """

View file

@ -113,15 +113,16 @@ class PackageInstaller:
if "yt_dlp" == pkg: if "yt_dlp" == pkg:
pkg = "yt-dlp" pkg = "yt-dlp"
pkg_name: str = "yt-dlp[default]" if pkg == "yt-dlp" else pkg
if version: if version:
if "nightly" == version and pkg == "yt-dlp": if "nightly" == version and "yt-dlp" == pkg:
cmd.extend(["--pre", "yt-dlp[default]"]) cmd.extend(["--pre", pkg_name])
elif "master" == version and pkg == "yt-dlp": elif "master" == version and "yt-dlp" == pkg:
cmd.append("git+https://github.com/yt-dlp/yt-dlp.git@master") cmd.append("git+https://github.com/yt-dlp/yt-dlp.git@master")
else: else:
cmd.append(version if str(version).startswith("git+") else f"{pkg}=={version}") cmd.append(version if str(version).startswith("git+") else f"{pkg_name}=={version}")
else: else:
cmd.extend(["--disable-pip-version-check", pkg]) cmd.extend(["--disable-pip-version-check", pkg_name])
try: try:
proc: subprocess.CompletedProcess[bytes] = subprocess.run( proc: subprocess.CompletedProcess[bytes] = subprocess.run(

View file

@ -131,8 +131,8 @@ class UpdateChecker(metaclass=Singleton):
if app_cached and ytdlp_cached: if app_cached and ytdlp_cached:
return (app_cached, ytdlp_cached) return (app_cached, ytdlp_cached)
app_result = app_cached if app_cached else await self._check_app_version() app_result = app_cached or await self._check_app_version()
ytdlp_result = ytdlp_cached if ytdlp_cached else await self._check_ytdlp_version() ytdlp_result = ytdlp_cached or await self._check_ytdlp_version()
return (app_result, ytdlp_result) return (app_result, ytdlp_result)

View file

@ -201,7 +201,7 @@ class Config(metaclass=Singleton):
is_native: bool = False is_native: bool = False
"Is the application running in natively." "Is the application running in natively."
prevent_live_premiere: bool = False prevent_live_premiere: bool = True
"""Prevent downloading of the initial premiere live broadcast.""" """Prevent downloading of the initial premiere live broadcast."""
live_premiere_buffer: int = 5 live_premiere_buffer: int = 5

View file

@ -62,10 +62,10 @@ class Download:
self.max_workers = int(config.max_workers) self.max_workers = int(config.max_workers)
self.is_live: bool = bool(info.is_live) or info.live_in is not None self.is_live: bool = bool(info.is_live) or info.live_in is not None
self.info_dict: dict | None = info_dict self.info_dict: dict | None = info_dict
self.logger: logging.Logger = logging.getLogger(f"Download.{info.id if info.id else info._id}") self.logger: logging.Logger = logging.getLogger(f"Download.{info.id or info._id}")
self.started_time = 0 self.started_time = 0
self.queue_time: datetime = datetime.now(tz=UTC) self.queue_time: datetime = datetime.now(tz=UTC)
self.logs: list[str] = logs if logs else [] self.logs: list[str] = logs or []
self._process_manager = ProcessManager( self._process_manager = ProcessManager(
download_id=self.id, download_id=self.id,
@ -131,10 +131,7 @@ class Download:
if self.info.cookies: if self.info.cookies:
try: try:
cookie_file = ( cookie_file = Path(self._temp_manager.temp_path or self.temp_dir) / f"cookie_{self.info._id}.txt"
Path(self._temp_manager.temp_path if self._temp_manager.temp_path else self.temp_dir)
/ f"cookie_{self.info._id}.txt"
)
self.logger.debug( self.logger.debug(
f"Creating cookie file for '{self.info.id}: {self.info.title}' - '{cookie_file}'." f"Creating cookie file for '{self.info.id}: {self.info.title}' - '{cookie_file}'."
) )
@ -372,7 +369,7 @@ class Download:
self.logger.debug(f"Download task '{self.info.title}: {self.info.id}' started for '{elapsed}' seconds.") self.logger.debug(f"Download task '{self.info.title}: {self.info.id}' started for '{elapsed}' seconds.")
return False return False
status: str = self.info.status if self.info.status else "unknown" status: str = self.info.status or "unknown"
is_stale = is_download_stale(self.started_time, status, self.running(), self.info.auto_start, min_elapsed=300) is_stale = is_download_stale(self.started_time, status, self.running(), self.info.auto_start, min_elapsed=300)
if is_stale: if is_stale:

View file

@ -185,8 +185,7 @@ class StatusTracker:
if next_status is None or isinstance(next_status, Terminator): if next_status is None or isinstance(next_status, Terminator):
continue continue
await self.process_status_update(next_status) await self.process_status_update(next_status)
except Exception as e: except Exception: # noqa: S112
self.logger.warning(f"Error processing status update during drain: {e}")
continue continue
def cancel_update_task(self) -> None: def cancel_update_task(self) -> None:

View file

@ -112,12 +112,12 @@ async def add_video(queue: "DownloadQueue", entry: dict, item: "Item", logs: lis
download_dir=download_dir, download_dir=download_dir,
temp_dir=queue.config.temp_path, temp_dir=queue.config.temp_path,
cookies=item.cookies, cookies=item.cookies,
template=item.template if item.template else queue.config.output_template, template=item.template or queue.config.output_template,
template_chapter=queue.config.output_template_chapter, template_chapter=queue.config.output_template_chapter,
datetime=formatdate(time.time()), datetime=formatdate(time.time()),
error=error, error=error,
is_live=is_live, is_live=is_live,
live_in=live_in if live_in else item.extras.get("live_in", None), live_in=live_in or item.extras.get("live_in", None),
options=options, options=options,
cli=item.cli, cli=item.cli,
auto_start=item.auto_start, auto_start=item.auto_start,

View file

@ -133,7 +133,7 @@ class Database:
else: else:
self.conn: AsyncConnection = db_url self.conn: AsyncConnection = db_url
async def __aenter__(self) -> "Database": async def __aenter__(self):
if self._owns_connection and self.conn is None: if self._owns_connection and self.conn is None:
# Create connection from string URL # Create connection from string URL
from sqlalchemy.ext.asyncio import create_async_engine from sqlalchemy.ext.asyncio import create_async_engine
@ -284,7 +284,7 @@ def create_migration(name: str, directory: str | None = None) -> str:
Create a migration with the given name. If no directory is specified, Create a migration with the given name. If no directory is specified,
the current working directory will be used. the current working directory will be used.
""" """
directory = directory if directory else "." directory = directory or "."
if not os.path.exists(directory) or not os.path.isdir(directory): if not os.path.exists(directory) or not os.path.isdir(directory):
msg: str = f"{directory} is not a directory." msg: str = f"{directory} is not a directory."
raise Error(msg) raise Error(msg)

View file

@ -83,7 +83,7 @@ def route(
return await func(*args, **kwargs) return await func(*args, **kwargs)
for m in methods: for m in methods:
route_name = name if name else make_route_name(m, path) route_name = name or make_route_name(m, path)
route_type: str = RouteType.SOCKET if RouteType.SOCKET == m else RouteType.HTTP route_type: str = RouteType.SOCKET if RouteType.SOCKET == m else RouteType.HTTP
if route_type not in ROUTES: if route_type not in ROUTES:
@ -124,7 +124,7 @@ def add_route(
methods = [method] if isinstance(method, (str, RouteType)) else method methods = [method] if isinstance(method, (str, RouteType)) else method
for m in methods: for m in methods:
route_name = name if name else make_route_name(m, path) route_name = name or make_route_name(m, path)
route_type: str = RouteType.SOCKET if RouteType.SOCKET == m else RouteType.HTTP route_type: str = RouteType.SOCKET if RouteType.SOCKET == m else RouteType.HTTP
if route_type not in ROUTES: if route_type not in ROUTES:

View file

@ -75,7 +75,7 @@ class SqliteStore(metaclass=ThreadSafe):
self._flush_interval: float = flush_interval self._flush_interval: float = flush_interval
self._max_pending: int = max_pending self._max_pending: int = max_pending
async def __aenter__(self) -> "SqliteStore": async def __aenter__(self):
await self.get_connection() await self.get_connection()
return self return self

View file

@ -425,14 +425,12 @@ async def items_add(request: Request, queue: DownloadQueue, encoder: Encoder) ->
if not item.extras: if not item.extras:
item.extras = {} item.extras = {}
item.extras["batch_id"] = batch_id if len(items) > 1:
item.extras["batch_index"] = idx item.extras["batch_id"] = batch_id
item.extras["batch_total"] = len(items) item.extras["batch_index"] = idx
item.extras["batch_total"] = len(items)
task = asyncio.create_task( task = asyncio.create_task(queue.add(item=item), name=f"bulk_add_{batch_id}_{idx}")
queue.add(item=item),
name=f"bulk_add_{batch_id}_{idx}",
)
task.add_done_callback(lambda t: handle_task_exception(t, LOG)) task.add_done_callback(lambda t: handle_task_exception(t, LOG))
return web.json_response( return web.json_response(

View file

@ -199,12 +199,12 @@ async def check_updates(config: Config, encoder: Encoder, update_checker: Update
"app": { "app": {
"status": "update_available" if config.new_version else "up_to_date", "status": "update_available" if config.new_version else "up_to_date",
"current_version": config.app_version, "current_version": config.app_version,
"new_version": config.new_version if config.new_version else None, "new_version": config.new_version or None,
}, },
"ytdlp": { "ytdlp": {
"status": "update_available" if config.yt_new_version else "up_to_date", "status": "update_available" if config.yt_new_version else "up_to_date",
"current_version": config._ytdlp_version(), "current_version": config._ytdlp_version(),
"new_version": config.yt_new_version if config.yt_new_version else None, "new_version": config.yt_new_version or None,
}, },
}, },
status=web.HTTPOk.status_code, status=web.HTTPOk.status_code,

View file

@ -170,171 +170,7 @@ class TestUpdateChecker:
mock_ytdlp_response = MagicMock() mock_ytdlp_response = MagicMock()
mock_ytdlp_response.status_code = 200 mock_ytdlp_response.status_code = 200
mock_ytdlp_response.json.return_value = {"tag_name": "9999.12.31"} mock_ytdlp_response.json.return_value = {"tag_name": "2026.12.31"}
mock_get = AsyncMock(side_effect=[mock_app_response, mock_ytdlp_response])
mock_http = MagicMock()
mock_http.get = mock_get
mock_client.return_value = mock_http
with patch("app.library.UpdateChecker.APP_VERSION", "v1.0.0"):
checker = UpdateChecker.get_instance(config=config)
(app_status, app_version), (ytdlp_status, ytdlp_version) = await checker.check_for_updates()
assert "update_available" == app_status, "Should return update_available status for app"
assert "v99.0.0" == app_version, "Should return new app version tag"
assert "v99.0.0" == config.new_version, "Should store new app version tag"
assert "update_available" == ytdlp_status, "Should return update_available status for yt-dlp"
assert "9999.12.31" == ytdlp_version, "Should return new yt-dlp version tag"
assert "9999.12.31" == config.yt_new_version, "Should store new yt-dlp version tag"
assert checker._job_id is None, "Should stop scheduled task after finding app update"
@pytest.mark.asyncio
@patch("app.library.UpdateChecker.get_async_client")
async def test_check_for_updates_no_update_available(self, mock_client):
"""Test that check_for_updates correctly handles when no update is available."""
from app.library.config import Config
from app.library.UpdateChecker import UpdateChecker
config = Config.get_instance()
config.check_for_updates = True
config.new_version = ""
config.yt_new_version = ""
mock_app_response = MagicMock()
mock_app_response.status_code = 200
mock_app_response.json.return_value = {"tag_name": "v1.0.0"}
mock_ytdlp_response = MagicMock()
mock_ytdlp_response.status_code = 200
mock_ytdlp_response.json.return_value = {"tag_name": "2020.01.01"}
mock_get = AsyncMock(side_effect=[mock_app_response, mock_ytdlp_response])
mock_http = MagicMock()
mock_http.get = mock_get
mock_client.return_value = mock_http
checker = UpdateChecker.get_instance(config=config)
checker._job_id = "test-job"
(app_status, app_version), (ytdlp_status, ytdlp_version) = await checker.check_for_updates()
assert "up_to_date" == app_status, "Should return up_to_date status for app"
assert app_version is None, "Should return None for app new_version"
assert "" == config.new_version, "Should clear new_version when no app update available"
assert "up_to_date" == ytdlp_status, "Should return up_to_date status for yt-dlp"
assert ytdlp_version is None, "Should return None for yt-dlp new_version"
assert "" == config.yt_new_version, "Should clear yt_new_version when no yt-dlp update available"
assert "test-job" == checker._job_id, "Should keep scheduled task running"
@pytest.mark.asyncio
@patch("app.library.UpdateChecker.get_async_client")
async def test_check_for_updates_handles_http_error(self, mock_client):
"""Test that check_for_updates handles HTTP errors gracefully."""
from app.library.config import Config
from app.library.UpdateChecker import UpdateChecker
config = Config.get_instance()
config.check_for_updates = True
config.new_version = ""
config.yt_new_version = ""
mock_response = MagicMock()
mock_response.status_code = 404
mock_http = MagicMock()
mock_http.get = AsyncMock(return_value=mock_response)
mock_client.return_value = mock_http
checker = UpdateChecker.get_instance(config=config)
(app_status, app_version), (ytdlp_status, ytdlp_version) = await checker.check_for_updates()
assert "error" == app_status, "Should return error status for app"
assert app_version is None, "Should return None for app new_version on error"
assert "" == config.new_version, "Should not set new_version on HTTP error"
assert "error" == ytdlp_status, "Should return error status for yt-dlp"
assert ytdlp_version is None, "Should return None for yt-dlp new_version on error"
assert "" == config.yt_new_version, "Should not set yt_new_version on HTTP error"
@pytest.mark.asyncio
@patch("app.library.UpdateChecker.get_async_client")
async def test_check_for_updates_handles_exception(self, mock_client):
"""Test that check_for_updates handles exceptions gracefully."""
from app.library.config import Config
from app.library.UpdateChecker import UpdateChecker
config = Config.get_instance()
config.check_for_updates = True
config.new_version = ""
config.yt_new_version = ""
mock_http = MagicMock()
mock_http.get = AsyncMock(side_effect=Exception("Network error"))
mock_client.return_value = mock_http
checker = UpdateChecker.get_instance(config=config)
(app_status, app_version), (ytdlp_status, ytdlp_version) = await checker.check_for_updates()
assert "error" == app_status, "Should return error status on exception for app"
assert app_version is None, "Should return None for app new_version on exception"
assert "" == config.new_version, "Should not set new_version on exception"
assert "error" == ytdlp_status, "Should return error status on exception for yt-dlp"
assert ytdlp_version is None, "Should return None for yt-dlp new_version on exception"
assert "" == config.yt_new_version, "Should not set yt_new_version on exception"
def test_compare_versions_newer_available(self):
"""Test version comparison detects newer version."""
from app.library.UpdateChecker import UpdateChecker
checker = UpdateChecker.get_instance()
assert checker._compare_versions("1.0.0", "2.0.0") is True, "Should detect 2.0.0 > 1.0.0"
assert checker._compare_versions("1.0.0", "1.1.0") is True, "Should detect 1.1.0 > 1.0.0"
assert checker._compare_versions("1.0.0", "1.0.1") is True, "Should detect 1.0.1 > 1.0.0"
def test_compare_versions_same_version(self):
"""Test version comparison with same version."""
from app.library.UpdateChecker import UpdateChecker
checker = UpdateChecker.get_instance()
assert checker._compare_versions("1.0.0", "1.0.0") is False, "Should detect versions are equal"
def test_compare_versions_older_version(self):
"""Test version comparison with older version."""
from app.library.UpdateChecker import UpdateChecker
checker = UpdateChecker.get_instance()
assert checker._compare_versions("2.0.0", "1.0.0") is False, "Should detect 1.0.0 is not newer than 2.0.0"
def test_github_api_url_constant(self):
"""Test that GitHub API URL is correctly defined."""
from app.library.UpdateChecker import UpdateChecker
expected_url = "https://api.github.com/repos/arabcoders/ytptube/releases/latest"
assert UpdateChecker.GITHUB_API_URL == expected_url, "GitHub API URL should be correct"
@pytest.mark.asyncio
async def test_check_for_updates_stores_tag_name(self):
"""Test that check_for_updates stores the tag_name when update found."""
from app.library.config import Config
from app.library.UpdateChecker import UpdateChecker
config = Config.get_instance()
config.check_for_updates = True
config.new_version = ""
config.yt_new_version = ""
mock_app_response = MagicMock()
mock_app_response.status_code = 200
mock_app_response.json.return_value = {"tag_name": "v2.0.0"}
mock_ytdlp_response = MagicMock()
mock_ytdlp_response.status_code = 200
mock_ytdlp_response.json.return_value = {"tag_name": "2026.01.01"}
mock_get = AsyncMock(side_effect=[mock_app_response, mock_ytdlp_response]) mock_get = AsyncMock(side_effect=[mock_app_response, mock_ytdlp_response])
mock_http = MagicMock() mock_http = MagicMock()
@ -342,11 +178,12 @@ class TestUpdateChecker:
with patch("app.library.UpdateChecker.get_async_client", return_value=mock_http): with patch("app.library.UpdateChecker.get_async_client", return_value=mock_http):
with patch("app.library.UpdateChecker.APP_VERSION", "v1.0.0"): with patch("app.library.UpdateChecker.APP_VERSION", "v1.0.0"):
checker = UpdateChecker.get_instance(config=config) with patch("app.library.config.Config._ytdlp_version", return_value="2026.01.01"):
await checker.check_for_updates() checker = UpdateChecker.get_instance(config=config)
await checker.check_for_updates()
assert "v2.0.0" == config.new_version, "Should store full tag_name including 'v' prefix" assert "v99.0.0" == config.new_version, "Should store full tag_name including 'v' prefix"
assert "2026.01.01" == config.yt_new_version, "Should store yt-dlp tag_name" assert "2026.12.31" == config.yt_new_version, "Should store yt-dlp tag_name"
def test_subscribe_to_started_event(self): def test_subscribe_to_started_event(self):
"""Test that attach subscribes to Events.STARTED.""" """Test that attach subscribes to Events.STARTED."""
@ -401,8 +238,9 @@ class TestUpdateChecker:
mock_http.get = AsyncMock(return_value=mock_response) mock_http.get = AsyncMock(return_value=mock_response)
mock_client.return_value = mock_http mock_client.return_value = mock_http
checker = UpdateChecker.get_instance(config=config) with patch("app.library.config.Config._ytdlp_version", return_value="2024.01.01"):
status, new_version = await checker._check_ytdlp_version() checker = UpdateChecker.get_instance(config=config)
status, new_version = await checker._check_ytdlp_version()
assert "update_available" == status, "Should return update_available status for yt-dlp" assert "update_available" == status, "Should return update_available status for yt-dlp"
assert "9999.12.31" == new_version, "Should return new yt-dlp version tag" assert "9999.12.31" == new_version, "Should return new yt-dlp version tag"
@ -427,8 +265,9 @@ class TestUpdateChecker:
mock_http.get = AsyncMock(return_value=mock_response) mock_http.get = AsyncMock(return_value=mock_response)
mock_client.return_value = mock_http mock_client.return_value = mock_http
checker = UpdateChecker.get_instance(config=config) with patch("app.library.config.Config._ytdlp_version", return_value="2025.01.01"):
status, new_version = await checker._check_ytdlp_version() checker = UpdateChecker.get_instance(config=config)
status, new_version = await checker._check_ytdlp_version()
assert "up_to_date" == status, "Should return up_to_date status for yt-dlp" assert "up_to_date" == status, "Should return up_to_date status for yt-dlp"
assert new_version is None, "Should return None for yt-dlp new_version" assert new_version is None, "Should return None for yt-dlp new_version"
@ -477,7 +316,7 @@ class TestUpdateChecker:
mock_ytdlp_response = MagicMock() mock_ytdlp_response = MagicMock()
mock_ytdlp_response.status_code = 200 mock_ytdlp_response.status_code = 200
mock_ytdlp_response.json.return_value = {"tag_name": "2026.01.01"} mock_ytdlp_response.json.return_value = {"tag_name": "2026.12.31"}
mock_get = AsyncMock(side_effect=[mock_app_response, mock_ytdlp_response]) mock_get = AsyncMock(side_effect=[mock_app_response, mock_ytdlp_response])
mock_http = MagicMock() mock_http = MagicMock()
@ -485,8 +324,9 @@ class TestUpdateChecker:
mock_client.return_value = mock_http mock_client.return_value = mock_http
with patch("app.library.UpdateChecker.APP_VERSION", "v1.0.0"): with patch("app.library.UpdateChecker.APP_VERSION", "v1.0.0"):
checker = UpdateChecker.get_instance(config=config) with patch("app.library.config.Config._ytdlp_version", return_value="2026.01.01"):
(app_status1, app_version1), (ytdlp_status1, ytdlp_version1) = await checker.check_for_updates() checker = UpdateChecker.get_instance(config=config)
(app_status1, app_version1), (ytdlp_status1, ytdlp_version1) = await checker.check_for_updates()
assert "update_available" == app_status1, "Should find app update on first call" assert "update_available" == app_status1, "Should find app update on first call"
assert "update_available" == ytdlp_status1, "Should find yt-dlp update on first call" assert "update_available" == ytdlp_status1, "Should find yt-dlp update on first call"

View file

@ -157,7 +157,7 @@ class NFOMakerPP(PostProcessor):
try: try:
first_date: str = next((str(nfo_data[k]) for k in self._DATE_FIELDS if nfo_data.get(k)), "") first_date: str = next((str(nfo_data[k]) for k in self._DATE_FIELDS if nfo_data.get(k)), "")
if first_date: if first_date:
nfo_data["year"] = first_date.split("-")[0] nfo_data["year"] = first_date.split("-", maxsplit=1)[0]
except Exception as e: except Exception as e:
self.report_warning(f"Error extracting year from date: {e}") self.report_warning(f"Error extracting year from date: {e}")

View file

@ -162,7 +162,13 @@ ignore = [
"PT011", "PT011",
"RUF001", "RUF001",
"S311", "S311",
"TC001" "TC001",
"ASYNC240",
# will be fixed in the future
"UP042",
"FURB171",
"FURB110",
"PLW0108",
] ]
fixable = ["ALL"] fixable = ["ALL"]
@ -186,7 +192,7 @@ pythonpath = ["."]
testpaths = ["app/tests", "app/features"] testpaths = ["app/tests", "app/features"]
addopts = "-v --tb=short" addopts = "-v --tb=short"
filterwarnings = [ filterwarnings = [
"ignore:Parsing dates involving a day of month without a year:DeprecationWarning" "ignore:Parsing dates involving a day of month without a year:DeprecationWarning",
] ]
[dependency-groups] [dependency-groups]
@ -195,4 +201,5 @@ dev = [
"pytest>=8.4.2", "pytest>=8.4.2",
"pytest-asyncio>=1.1.0", "pytest-asyncio>=1.1.0",
"ruff>=0.13.0", "ruff>=0.13.0",
"pip>=26.0.0",
] ]

View file

@ -474,7 +474,7 @@
<div class="modal-background" @click="closeVideo"></div> <div class="modal-background" @click="closeVideo"></div>
<div class="modal-content is-unbounded-model"> <div class="modal-content is-unbounded-model">
<VideoPlayer type="default" :isMuted="false" autoplay="true" :isControls="true" :item="video_item" <VideoPlayer type="default" :isMuted="false" autoplay="true" :isControls="true" :item="video_item"
class="is-fullwidth" @closeModel="closeVideo" /> class="is-fullwidth" @closeModel="closeVideo" @error="async (error: string) => await box.alert(error)" />
</div> </div>
<button class="modal-close is-large" aria-label="close" @click="closeVideo"></button> <button class="modal-close is-large" aria-label="close" @click="closeVideo"></button>
</div> </div>
@ -499,10 +499,11 @@ import { useStorage, useIntersectionObserver } from '@vueuse/core'
import type { StoreItem } from '~/types/store' import type { StoreItem } from '~/types/store'
import { useConfirm } from '~/composables/useConfirm' import { useConfirm } from '~/composables/useConfirm'
import { deepIncludes, getPath, getImage } from '~/utils' import { deepIncludes, getPath, getImage } from '~/utils'
import type { item_request } from '~/types/item'
const emitter = defineEmits<{ const emitter = defineEmits<{
(e: 'getInfo', url: string, preset: string, cli: string): void (e: 'getInfo', url: string, preset: string, cli: string): void
(e: 'add_new', item: Partial<StoreItem>): void (e: 'add_new', item: item_request): void
(e: 'getItemInfo', id: string): void (e: 'getItemInfo', id: string): void
(e: 'clear_search'): void (e: 'clear_search'): void
}>() }>()
@ -850,7 +851,7 @@ const removeItem = async (item: StoreItem) => {
} }
const retryItem = async (item: StoreItem, re_add: boolean = false, remove_file: boolean = false) => { const retryItem = async (item: StoreItem, re_add: boolean = false, remove_file: boolean = false) => {
const item_req: Partial<StoreItem> = { const item_req: item_request = {
url: item.url, url: item.url,
preset: item.preset, preset: item.preset,
folder: item.folder, folder: item.folder,

View file

@ -289,7 +289,7 @@
<span>Pause</span> <span>Pause</span>
</button> </button>
</div> </div>
<div class="column is-half-mobile"> <div class="column">
<Dropdown icons="fa-solid fa-cogs" label="Actions"> <Dropdown icons="fa-solid fa-cogs" label="Actions">
<template v-if="isEmbedable(item.url)"> <template v-if="isEmbedable(item.url)">
<NuxtLink class="dropdown-item has-text-danger" @click="embed_url = getEmbedable(item.url) as string"> <NuxtLink class="dropdown-item has-text-danger" @click="embed_url = getEmbedable(item.url) as string">

View file

@ -260,11 +260,12 @@ import type { StoreItem } from '~/types/store'
import type { file_info, video_source_element, video_track_element } from '~/types/video' import type { file_info, video_source_element, video_track_element } from '~/types/video'
const config = useConfigStore() const config = useConfigStore()
const toast = useNotification()
const props = defineProps({ item: { type: Object as () => StoreItem, default: () => ({}) } }) const props = defineProps<{ item: StoreItem }>()
const emitter = defineEmits<{
const emitter = defineEmits(['closeModel']) (e: 'closeModel'): void,
(e: 'error', message: string): void,
}>()
const video = useTemplateRef<HTMLVideoElement>('video') const video = useTemplateRef<HTMLVideoElement>('video')
const tracks = ref<Array<video_track_element>>([]) const tracks = ref<Array<video_track_element>>([])
@ -486,7 +487,7 @@ onMounted(async () => {
const response: file_info = await req.json() const response: file_info = await req.json()
if (!req.ok) { if (!req.ok) {
toast.error(`Failed to fetch video info. ${response?.error}`) emitter('error', response?.error || 'Failed to fetch video info. Unknown error')
emitter('closeModel') emitter('closeModel')
return return
} }
@ -746,7 +747,7 @@ const forceSwitchToHls = () => {
} }
if (!hasVideoStream.value) { if (!hasVideoStream.value) {
toast.error('Cannot switch to HLS: stream has no video track.') useNotification().error('Cannot switch to HLS: stream has no video track.')
return return
} }

View file

@ -93,7 +93,7 @@
<div v-show="'history' === activeTab"> <div v-show="'history' === activeTab">
<History @getInfo="(url: string, preset: string = '', cli: string = '') => view_info(url, false, preset, cli)" <History @getInfo="(url: string, preset: string = '', cli: string = '') => view_info(url, false, preset, cli)"
@add_new="(item: Partial<StoreItem>) => toNewDownload(item)" :query="query" :thumbnails="show_thumbnail" @add_new="(item: item_request) => toNewDownload(item)" :query="query" :thumbnails="show_thumbnail"
@getItemInfo="(id: string) => view_info(`/api/history/${id}`, true)" @clear_search="query = ''" /> @getItemInfo="(id: string) => view_info(`/api/history/${id}`, true)" @clear_search="query = ''" />
</div> </div>
</div> </div>

View file

@ -90,13 +90,13 @@
<div class="column is-2-tablet is-5-mobile"> <div class="column is-2-tablet is-5-mobile">
<Dropdown label="Actions" icons="fa-solid fa-list"> <Dropdown label="Actions" icons="fa-solid fa-list">
<a class="dropdown-item has-text-purple" @click="(selectedElms.length > 0 && !massRun) ? runConfirm() : null" <a class="dropdown-item has-text-purple" @click="(selectedElms.length > 0 && !massRun) ? runSelected() : null"
:style="{ opacity: (selectedElms.length < 1 || massRun) ? 0.5 : 1, cursor: (selectedElms.length < 1 || massRun) ? 'not-allowed' : 'pointer' }"> :style="{ opacity: (selectedElms.length < 1 || massRun) ? 0.5 : 1, cursor: (selectedElms.length < 1 || massRun) ? 'not-allowed' : 'pointer' }">
<span class="icon"><i class="fa-solid fa-up-right-from-square" /></span> <span class="icon"><i class="fa-solid fa-up-right-from-square" /></span>
<span>Run Selected</span> <span>Run Selected</span>
</a> </a>
<a class="dropdown-item has-text-danger" <a class="dropdown-item has-text-danger"
@click="(selectedElms.length > 0 && !massDelete) ? deleteConfirm() : null" @click="(selectedElms.length > 0 && !massDelete) ? deleteSelected() : null"
:style="{ opacity: (selectedElms.length < 1 || massDelete) ? 0.5 : 1, cursor: (selectedElms.length < 1 || massDelete) ? 'not-allowed' : 'pointer' }"> :style="{ opacity: (selectedElms.length < 1 || massDelete) ? 0.5 : 1, cursor: (selectedElms.length < 1 || massDelete) ? 'not-allowed' : 'pointer' }">
<span class="icon"><i class="fa-solid fa-trash-can" /></span> <span class="icon"><i class="fa-solid fa-trash-can" /></span>
<span>Remove Selected</span> <span>Remove Selected</span>
@ -448,10 +448,6 @@
</div> </div>
</div> </div>
<ConfirmDialog v-if="dialog_confirm.visible" :visible="dialog_confirm.visible" :title="dialog_confirm.title"
:message="dialog_confirm.message" :options="dialog_confirm.options" @confirm="dialog_confirm.confirm"
:html_message="dialog_confirm.html_message" @cancel="dialog_confirm = reset_dialog()" />
<Modal v-if="inspectTask" @close="() => inspectTask = null" :contentClass="`modal-content-max`"> <Modal v-if="inspectTask" @close="() => inspectTask = null" :contentClass="`modal-content-max`">
<TaskInspect :url="inspectTask.url" :preset="inspectTask.preset" /> <TaskInspect :url="inspectTask.url" :preset="inspectTask.preset" />
</Modal> </Modal>
@ -470,6 +466,7 @@ import type { Task, ExportedTask } from '~/types/tasks'
import type { WSEP } from '~/types/sockets' import type { WSEP } from '~/types/sockets'
import { sleep } from '~/utils' import { sleep } from '~/utils'
import { useSessionCache } from '~/utils/cache' import { useSessionCache } from '~/utils/cache'
import type { item_request } from '~/types/item'
type TaskWithUI = Task & { in_progress?: boolean } type TaskWithUI = Task & { in_progress?: boolean }
@ -478,16 +475,15 @@ const toast = useNotification()
const config = useConfigStore() const config = useConfigStore()
const socket = useSocketStore() const socket = useSocketStore()
const stateStore = useStateStore() const stateStore = useStateStore()
const { confirmDialog: cDialog } = useDialog() const { confirmDialog } = useDialog()
const sessionCache = useSessionCache() const sessionCache = useSessionCache()
const display_style = useStorage<string>("tasks_display_style", "cards") const display_style = useStorage<string>("tasks_display_style", "cards")
const isMobile = useMediaQuery({ maxWidth: 1024 }) const isMobile = useMediaQuery({ maxWidth: 1024 })
// Use the tasks composable
const tasksComposable = useTasks() const tasksComposable = useTasks()
const { tasks, isLoading, addInProgress } = tasksComposable const { tasks, isLoading, addInProgress } = tasksComposable
const task = ref<TaskWithUI | Record<string, unknown>>({}) const task = ref<Partial<TaskWithUI>>({})
const taskRef = ref<number | null>(null) const taskRef = ref<number | null>(null)
const toggleForm = ref<boolean>(false) const toggleForm = ref<boolean>(false)
const selectedElms = ref<Array<number>>([]) const selectedElms = ref<Array<number>>([])
@ -496,20 +492,10 @@ const massRun = ref<boolean>(false)
const massDelete = ref<boolean>(false) const massDelete = ref<boolean>(false)
const table_container = ref(false) const table_container = ref(false)
const inspectTask = ref<TaskWithUI | null>(null) const inspectTask = ref<TaskWithUI | null>(null)
const reset_dialog = () => ({
visible: false,
title: 'Confirm Action',
confirm: (_opts: any) => { },
message: '',
html_message: '',
options: [],
});
const dialog_confirm = ref(reset_dialog())
const query = ref() const query = ref()
const toggleFilter = ref(false) const toggleFilter = ref(false)
const CACHE_KEY = 'tasks:handler_support'
const taskHandlerSupport = ref<Record<string, boolean>>(sessionCache.get(CACHE_KEY) || {})
watch(toggleFilter, () => { watch(toggleFilter, () => {
if (!toggleFilter.value) { if (!toggleFilter.value) {
@ -522,18 +508,19 @@ watch(query, () => {
selectedElms.value = [] selectedElms.value = []
}) })
watch(masterSelectAll, value => { watch(masterSelectAll, value => {
if (!value) { if (!value) {
selectedElms.value = [] selectedElms.value = []
return return
} }
for (const key in filteredTasks.value) { for (const key in filteredTasks.value) {
const element = filteredTasks.value[key] as Task const element = filteredTasks.value[key] as Task
if (element.id) { if (element.id) {
selectedElms.value.push(element.id) selectedElms.value.push(element.id)
} }
} }
}) })
watch(() => socket.isConnected, async () => { watch(() => socket.isConnected, async () => {
@ -543,10 +530,7 @@ watch(() => socket.isConnected, async () => {
socket.on('item_status', statusHandler) socket.on('item_status', statusHandler)
}) })
watch(taskHandlerSupport, newValue => sessionCache.set(CACHE_KEY, newValue), { deep: true })
const CACHE_KEY = 'tasks:handler_support'
const taskHandlerSupport = ref<Record<string, boolean>>(sessionCache.get(CACHE_KEY) || {})
watch(taskHandlerSupport, (newValue) => sessionCache.set(CACHE_KEY, newValue), { deep: true })
const getCacheKey = (task: Task): string => `${task.id}:${task.url}` const getCacheKey = (task: Task): string => `${task.id}:${task.url}`
@ -581,7 +565,7 @@ const checkHandlerSupport = async (task: Task): Promise<boolean> => {
url: task.url, url: task.url,
static_only: true static_only: true
}) })
const supported = result?.matched === true const supported = true === result?.matched
taskHandlerSupport.value[cacheKey] = supported taskHandlerSupport.value[cacheKey] = supported
return supported return supported
} catch { } catch {
@ -624,7 +608,6 @@ const reloadContent = async (fromMounted: boolean = false) => {
} }
} }
const resetForm = (closeForm: boolean = false) => { const resetForm = (closeForm: boolean = false) => {
task.value = { task.value = {
name: '', name: '',
@ -644,38 +627,39 @@ const resetForm = (closeForm: boolean = false) => {
} }
} }
const deleteConfirm = () => {
if (selectedElms.value.length < 1) {
toast.error('No tasks selected.')
return
}
dialog_confirm.value.visible = true
dialog_confirm.value.title = 'Delete Selected Tasks'
dialog_confirm.value.message = `Delete ${selectedElms.value.length} task/s?`
dialog_confirm.value.confirm = async () => await deleteSelected()
}
const deleteSelected = async () => { const deleteSelected = async () => {
if (selectedElms.value.length < 1) { if (selectedElms.value.length < 1) {
toast.error('No tasks selected.') toast.error('No tasks selected.')
return return
} }
massDelete.value = true const { status } = await confirmDialog({
dialog_confirm.value = reset_dialog() title: 'Delete Selected Tasks',
rawHTML: `Delete <strong class="has-text-danger">${selectedElms.value.length}</strong> task/s?<ul>` + selectedElms.value.map(id => {
const item = tasks.value.find(t => t.id === id)
return item ? `<li>${item.id}: ${item.name}</li>` : ''
}).join('') + `</ul>`,
confirmText: 'Delete',
confirmColor: 'is-danger'
})
if (true !== status) {
return
}
const itemsToDelete = tasks.value.filter(t => t.id && selectedElms.value.includes(t.id)) const itemsToDelete = tasks.value.filter(t => t.id && selectedElms.value.includes(t.id))
if (itemsToDelete.length < 1) { if (itemsToDelete.length < 1) {
toast.error('No tasks found to delete.') toast.error('No tasks found to delete.')
massDelete.value = false
return return
} }
// Delete tasks sequentially massDelete.value = true
for (const item of itemsToDelete) { for (const item of itemsToDelete) {
if (item.id) { if (!item.id) {
await tasksComposable.deleteTask(item.id) continue
} }
await tasksComposable.deleteTask(item.id)
} }
selectedElms.value = [] selectedElms.value = []
@ -687,15 +671,9 @@ const deleteSelected = async () => {
} }
const deleteItem = async (item: Task) => { const deleteItem = async (item: Task) => {
if (true !== (await box.confirm(`Delete '${item.name}' task?`))) { if (!item.id || true !== (await box.confirm(`Delete '${item.name}' task?`))) {
return return
} }
if (!item.id) {
toast.error('Task ID is missing')
return
}
await tasksComposable.deleteTask(item.id) await tasksComposable.deleteTask(item.id)
} }
@ -705,15 +683,9 @@ const toggleEnabled = async (item: Task) => {
return return
} }
const newStatus = !item.enabled const updated = await tasksComposable.patchTask(item.id, { enabled: !item.enabled })
const updated = await tasksComposable.patchTask(item.id, { enabled: newStatus })
if (updated) { if (updated) {
// Update local reference
item.enabled = updated.enabled item.enabled = updated.enabled
// Update handler support cache if needed
if (updated.enabled) { if (updated.enabled) {
await checkHandlerSupport(updated) await checkHandlerSupport(updated)
} }
@ -726,15 +698,9 @@ const toggleHandlerEnabled = async (item: Task) => {
return return
} }
const newStatus = !item.handler_enabled const updated = await tasksComposable.patchTask(item.id, { handler_enabled: !item.handler_enabled })
const updated = await tasksComposable.patchTask(item.id, { handler_enabled: newStatus })
if (updated) { if (updated) {
// Update local reference
item.handler_enabled = updated.handler_enabled item.handler_enabled = updated.handler_enabled
// Update handler support cache if needed
if (updated.handler_enabled) { if (updated.handler_enabled) {
await checkHandlerSupport(updated) await checkHandlerSupport(updated)
} }
@ -745,10 +711,8 @@ const updateItem = async ({ reference, task, archive_all }: { reference?: number
let createdOrUpdated: Task | null = null let createdOrUpdated: Task | null = null
if (reference) { if (reference) {
// Update existing task
createdOrUpdated = await tasksComposable.updateTask(reference, task) createdOrUpdated = await tasksComposable.updateTask(reference, task)
} else { } else {
// Create new task
createdOrUpdated = await tasksComposable.createTask(task) createdOrUpdated = await tasksComposable.createTask(task)
} }
@ -756,7 +720,6 @@ const updateItem = async ({ reference, task, archive_all }: { reference?: number
return return
} }
// Check handler support for the new/updated task
await checkHandlerSupport(createdOrUpdated) await checkHandlerSupport(createdOrUpdated)
if (!reference && true === archive_all && createdOrUpdated.id) { if (!reference && true === archive_all && createdOrUpdated.id) {
@ -799,29 +762,22 @@ const tryParse = (expression: string) => {
} }
} }
const runConfirm = () => {
if (selectedElms.value.length < 1) {
toast.error('No tasks selected.')
return
}
dialog_confirm.value.visible = true
dialog_confirm.value.title = 'Run Selected Tasks'
dialog_confirm.value.html_message = `Run the following tasks?<ul>` + selectedElms.value.map(id => {
const item = tasks.value.find(t => t.id === id)
return item ? `<li>${item.name}</li>` : ''
}).join('') + `</ul>`
dialog_confirm.value.confirm = async () => await runSelected()
}
const runSelected = async () => { const runSelected = async () => {
if (selectedElms.value.length < 1) { if (selectedElms.value.length < 1) {
toast.error('No tasks selected.') toast.error('No tasks selected.')
return return
} }
dialog_confirm.value = reset_dialog() const { status } = await confirmDialog({
rawHTML: `Run the following tasks?<ul>` + selectedElms.value.map(id => {
const item = tasks.value.find(t => t.id === id)
return item ? `<li>${item.name}</li>` : ''
}).join('') + `</ul>`
})
if (true !== status) {
return
}
massRun.value = true massRun.value = true
@ -851,9 +807,14 @@ const runNow = async (item: TaskWithUI, mass: boolean = false) => {
item.in_progress = true item.in_progress = true
} }
const data: Record<string, unknown> = { const data: item_request = {
url: item.url, url: item.url,
preset: item.preset, preset: item.preset,
extras: {
source_name: item.name,
source_id: item.id,
source_handler: "Web",
}
} }
if (item.folder) { if (item.folder) {
@ -868,7 +829,7 @@ const runNow = async (item: TaskWithUI, mass: boolean = false) => {
data.cli = item.cli data.cli = item.cli
} }
if (item?.auto_start !== undefined) { if (undefined !== item?.auto_start) {
data.auto_start = item.auto_start data.auto_start = item.auto_start
} }
@ -939,7 +900,7 @@ const archiveAll = async (item: TaskWithUI, by_pass: boolean = false) => {
try { try {
if (true !== by_pass) { if (true !== by_pass) {
const { status } = await cDialog({ const { status } = await confirmDialog({
message: `Mark all '${item.name}' items as downloaded in download archive?` message: `Mark all '${item.name}' items as downloaded in download archive?`
}) })
@ -965,7 +926,7 @@ const unarchiveAll = async (item: TaskWithUI) => {
} }
try { try {
const { status } = await cDialog({ const { status } = await confirmDialog({
message: `Remove all '${item.name}' items from download archive?` message: `Remove all '${item.name}' items from download archive?`
}) })
@ -990,7 +951,7 @@ const generateMeta = async (item: TaskWithUI) => {
} }
try { try {
const { status } = await cDialog({ const { status } = await confirmDialog({
rawHTML: ` rawHTML: `
<p> <p>
Generate '${item.name}' metadata? you will be notified when it is done. Generate '${item.name}' metadata? you will be notified when it is done.

View file

@ -1,4 +1,5 @@
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import type { item_request } from '~/types/item'
import type { StoreItem } from '~/types/store' import type { StoreItem } from '~/types/store'
import { request } from '~/utils' import { request } from '~/utils'
@ -211,7 +212,7 @@ export const useStateStore = defineStore('state', () => {
* @param data - Download data (url, preset, folder, etc.) * @param data - Download data (url, preset, folder, etc.)
* @returns Promise that resolves when download is added * @returns Promise that resolves when download is added
*/ */
const addDownload = async (data: Record<string, unknown>): Promise<void> => { const addDownload = async (data: item_request): Promise<void> => {
const socket = useSocketStore() const socket = useSocketStore()
const toast = useNotification() const toast = useNotification()

View file

@ -21,12 +21,12 @@
"dependencies": { "dependencies": {
"@microsoft/fetch-event-source": "^2.0.1", "@microsoft/fetch-event-source": "^2.0.1",
"@pinia/nuxt": "^0.11.3", "@pinia/nuxt": "^0.11.3",
"@vueuse/core": "^14.1.0", "@vueuse/core": "^14.2.0",
"@vueuse/nuxt": "^14.1.0", "@vueuse/nuxt": "^14.2.0",
"@xterm/addon-fit": "^0.11.0", "@xterm/addon-fit": "^0.11.0",
"@xterm/xterm": "^6.0.0", "@xterm/xterm": "^6.0.0",
"cron-parser": "^5.4.0", "cron-parser": "^5.5.0",
"cronstrue": "^3.9.0", "cronstrue": "^3.11.0",
"floating-vue": "^5.2.2", "floating-vue": "^5.2.2",
"hls.js": "^1.6.15", "hls.js": "^1.6.15",
"marked": "^17.0.1", "marked": "^17.0.1",
@ -34,10 +34,10 @@
"marked-base-url": "^1.1.8", "marked-base-url": "^1.1.8",
"marked-gfm-heading-id": "^4.1.3", "marked-gfm-heading-id": "^4.1.3",
"moment": "^2.30.1", "moment": "^2.30.1",
"nuxt": "^4.2.2", "nuxt": "^4.3.0",
"pinia": "^3.0.4", "pinia": "^3.0.4",
"vue": "^3.5.26", "vue": "^3.5.27",
"vue-router": "^4.6.4", "vue-router": "^5.0.1",
"vue-toastification": "2.0.0-rc.5" "vue-toastification": "2.0.0-rc.5"
}, },
"pnpm": { "pnpm": {
@ -53,13 +53,13 @@
"ansi-regex": "6.2.0" "ansi-regex": "6.2.0"
}, },
"devDependencies": { "devDependencies": {
"@nuxt/eslint": "^1.12.1", "@nuxt/eslint": "^1.13.0",
"@nuxt/eslint-config": "^1.12.1", "@nuxt/eslint-config": "^1.13.0",
"@typescript-eslint/parser": "^8.53.0", "@typescript-eslint/parser": "^8.54.0",
"eslint": "^9.39.2", "eslint": "^9.39.2",
"typescript": "^5.9.3", "typescript": "^5.9.3",
"vitest": "^4.0.17", "vitest": "^4.0.18",
"vue-eslint-parser": "^10.2.0", "vue-eslint-parser": "^10.2.0",
"vue-tsc": "^3.2.2" "vue-tsc": "^3.2.4"
} }
} }

File diff suppressed because it is too large Load diff

330
uv.lock
View file

@ -146,7 +146,7 @@ wheels = [
[[package]] [[package]]
name = "apprise" name = "apprise"
version = "1.9.6" version = "1.9.7"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "certifi" }, { name = "certifi" },
@ -157,9 +157,18 @@ dependencies = [
{ name = "requests-oauthlib" }, { name = "requests-oauthlib" },
{ name = "tzdata", marker = "sys_platform == 'win32'" }, { name = "tzdata", marker = "sys_platform == 'win32'" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/a9/a7/bb182d81f35c3fe405505f0976da4b74f942cfdd53c7193b0fe50412aa27/apprise-1.9.6.tar.gz", hash = "sha256:4206be9cb5694a3d08dd8e0393bbb9b36212ac3a7769c2633620055e75c6caef", size = 1921714 } sdist = { url = "https://files.pythonhosted.org/packages/bc/f5/97dc06b3401bb67abcef6e8bef7155f192b75795c2a2aa4d59eb5aa7fa66/apprise-1.9.7.tar.gz", hash = "sha256:2f73cc1e0264fb119fdb9b7cde82e8fde40a0f531ac885d8c6f0cf0f6e13aec2", size = 1937173 }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/39/df/343d125241f8cd3c9af58fd09688cf2bf59cc1edfd609adafef3556ce8ec/apprise-1.9.6-py3-none-any.whl", hash = "sha256:2fd18e8a5251b6a12f6f9d169f1d895d458d1de36a5faee4db149cedcce51674", size = 1452059 }, { url = "https://files.pythonhosted.org/packages/fb/6b/cfa80a13437896eb8f4504ddac6dfa4ef7f1d2b2261057aa4a30003b8de6/apprise-1.9.7-py3-none-any.whl", hash = "sha256:c7640a81a1097685de66e0508e3da89f49235d566cb44bbead1dd98419bf5ee3", size = 1459879 },
]
[[package]]
name = "async-generator"
version = "1.10"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ce/b6/6fa6b3b598a03cba5e80f829e0dadbb49d7645f523d209b2fb7ea0bbb02a/async_generator-1.10.tar.gz", hash = "sha256:6ebb3d106c12920aaae42ccb6f787ef5eefdcdd166ea3d628fa8476abe712144", size = 29870 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/71/52/39d20e03abd0ac9159c162ec24b93fbcaa111e8400308f2465432495ca2b/async_generator-1.10-py3-none-any.whl", hash = "sha256:01c7bf666359b4967d2cda0000cc2e4af16a0ae098cbffcb8472fb9e8ad6585b", size = 18857 },
] ]
[[package]] [[package]]
@ -371,11 +380,11 @@ wheels = [
[[package]] [[package]]
name = "cssselect" name = "cssselect"
version = "1.3.0" version = "1.4.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/72/0a/c3ea9573b1dc2e151abfe88c7fe0c26d1892fe6ed02d0cdb30f0d57029d5/cssselect-1.3.0.tar.gz", hash = "sha256:57f8a99424cfab289a1b6a816a43075a4b00948c86b4dcf3ef4ee7e15f7ab0c7", size = 42870 } sdist = { url = "https://files.pythonhosted.org/packages/ec/2e/cdfd8b01c37cbf4f9482eefd455853a3cf9c995029a46acd31dfaa9c1dd6/cssselect-1.4.0.tar.gz", hash = "sha256:fdaf0a1425e17dfe8c5cf66191d211b357cf7872ae8afc4c6762ddd8ac47fc92", size = 40589 }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/ee/58/257350f7db99b4ae12b614a36256d9cc870d71d9e451e79c2dc3b23d7c3c/cssselect-1.3.0-py3-none-any.whl", hash = "sha256:56d1bf3e198080cc1667e137bc51de9cadfca259f03c2d4e09037b3e01e30f0d", size = 18786 }, { url = "https://files.pythonhosted.org/packages/20/0c/7bb51e3acfafd16c48875bf3db03607674df16f5b6ef8d056586af7e2b8b/cssselect-1.4.0-py3-none-any.whl", hash = "sha256:c0ec5c0191c8ee39fcc8afc1540331d8b55b0183478c50e9c8a79d44dbceb1d8", size = 18540 },
] ]
[[package]] [[package]]
@ -401,7 +410,7 @@ wheels = [
[[package]] [[package]]
name = "dateparser" name = "dateparser"
version = "1.2.2" version = "1.3.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "python-dateutil" }, { name = "python-dateutil" },
@ -409,26 +418,26 @@ dependencies = [
{ name = "regex" }, { name = "regex" },
{ name = "tzlocal" }, { name = "tzlocal" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/a9/30/064144f0df1749e7bb5faaa7f52b007d7c2d08ec08fed8411aba87207f68/dateparser-1.2.2.tar.gz", hash = "sha256:986316f17cb8cdc23ea8ce563027c5ef12fc725b6fb1d137c14ca08777c5ecf7", size = 329840 } sdist = { url = "https://files.pythonhosted.org/packages/3d/2c/668dfb8c073a5dde3efb80fa382de1502e3b14002fd386a8c1b0b49e92a9/dateparser-1.3.0.tar.gz", hash = "sha256:5bccf5d1ec6785e5be71cc7ec80f014575a09b4923e762f850e57443bddbf1a5", size = 337152 }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/87/22/f020c047ae1346613db9322638186468238bcfa8849b4668a22b97faad65/dateparser-1.2.2-py3-none-any.whl", hash = "sha256:5a5d7211a09013499867547023a2a0c91d5a27d15dd4dbcea676ea9fe66f2482", size = 315453 }, { url = "https://files.pythonhosted.org/packages/9a/c7/95349670e193b2891176e1b8e5f43e12b31bff6d9994f70e74ab385047f6/dateparser-1.3.0-py3-none-any.whl", hash = "sha256:8dc678b0a526e103379f02ae44337d424bd366aac727d3c6cf52ce1b01efbb5a", size = 318688 },
] ]
[[package]] [[package]]
name = "debugpy" name = "debugpy"
version = "1.8.19" version = "1.8.20"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/73/75/9e12d4d42349b817cd545b89247696c67917aab907012ae5b64bbfea3199/debugpy-1.8.19.tar.gz", hash = "sha256:eea7e5987445ab0b5ed258093722d5ecb8bb72217c5c9b1e21f64efe23ddebdb", size = 1644590 } sdist = { url = "https://files.pythonhosted.org/packages/e0/b7/cd8080344452e4874aae67c40d8940e2b4d47b01601a8fd9f44786c757c7/debugpy-1.8.20.tar.gz", hash = "sha256:55bc8701714969f1ab89a6d5f2f3d40c36f91b2cbe2f65d98bf8196f6a6a2c33", size = 1645207 }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/71/3d/388035a31a59c26f1ecc8d86af607d0c42e20ef80074147cd07b180c4349/debugpy-1.8.19-cp313-cp313-macosx_15_0_universal2.whl", hash = "sha256:91e35db2672a0abaf325f4868fcac9c1674a0d9ad9bb8a8c849c03a5ebba3e6d", size = 2538859 }, { url = "https://files.pythonhosted.org/packages/15/e2/fc500524cc6f104a9d049abc85a0a8b3f0d14c0a39b9c140511c61e5b40b/debugpy-1.8.20-cp313-cp313-macosx_15_0_universal2.whl", hash = "sha256:5dff4bb27027821fdfcc9e8f87309a28988231165147c31730128b1c983e282a", size = 2539560 },
{ url = "https://files.pythonhosted.org/packages/4a/19/c93a0772d0962294f083dbdb113af1a7427bb632d36e5314297068f55db7/debugpy-1.8.19-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:85016a73ab84dea1c1f1dcd88ec692993bcbe4532d1b49ecb5f3c688ae50c606", size = 4292575 }, { url = "https://files.pythonhosted.org/packages/90/83/fb33dcea789ed6018f8da20c5a9bc9d82adc65c0c990faed43f7c955da46/debugpy-1.8.20-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:84562982dd7cf5ebebfdea667ca20a064e096099997b175fe204e86817f64eaf", size = 4293272 },
{ url = "https://files.pythonhosted.org/packages/5c/56/09e48ab796b0a77e3d7dc250f95251832b8bf6838c9632f6100c98bdf426/debugpy-1.8.19-cp313-cp313-win32.whl", hash = "sha256:b605f17e89ba0ecee994391194285fada89cee111cfcd29d6f2ee11cbdc40976", size = 5286209 }, { url = "https://files.pythonhosted.org/packages/a6/25/b1e4a01bfb824d79a6af24b99ef291e24189080c93576dfd9b1a2815cd0f/debugpy-1.8.20-cp313-cp313-win32.whl", hash = "sha256:da11dea6447b2cadbf8ce2bec59ecea87cc18d2c574980f643f2d2dfe4862393", size = 5331208 },
{ url = "https://files.pythonhosted.org/packages/fb/4e/931480b9552c7d0feebe40c73725dd7703dcc578ba9efc14fe0e6d31cfd1/debugpy-1.8.19-cp313-cp313-win_amd64.whl", hash = "sha256:c30639998a9f9cd9699b4b621942c0179a6527f083c72351f95c6ab1728d5b73", size = 5328206 }, { url = "https://files.pythonhosted.org/packages/13/f7/a0b368ce54ffff9e9028c098bd2d28cfc5b54f9f6c186929083d4c60ba58/debugpy-1.8.20-cp313-cp313-win_amd64.whl", hash = "sha256:eb506e45943cab2efb7c6eafdd65b842f3ae779f020c82221f55aca9de135ed7", size = 5372930 },
{ url = "https://files.pythonhosted.org/packages/f6/b9/cbec520c3a00508327476c7fce26fbafef98f412707e511eb9d19a2ef467/debugpy-1.8.19-cp314-cp314-macosx_15_0_universal2.whl", hash = "sha256:1e8c4d1bd230067bf1bbcdbd6032e5a57068638eb28b9153d008ecde288152af", size = 2537372 }, { url = "https://files.pythonhosted.org/packages/33/2e/f6cb9a8a13f5058f0a20fe09711a7b726232cd5a78c6a7c05b2ec726cff9/debugpy-1.8.20-cp314-cp314-macosx_15_0_universal2.whl", hash = "sha256:9c74df62fc064cd5e5eaca1353a3ef5a5d50da5eb8058fcef63106f7bebe6173", size = 2538066 },
{ url = "https://files.pythonhosted.org/packages/88/5e/cf4e4dc712a141e10d58405c58c8268554aec3c35c09cdcda7535ff13f76/debugpy-1.8.19-cp314-cp314-manylinux_2_34_x86_64.whl", hash = "sha256:d40c016c1f538dbf1762936e3aeb43a89b965069d9f60f9e39d35d9d25e6b809", size = 4268729 }, { url = "https://files.pythonhosted.org/packages/c5/56/6ddca50b53624e1ca3ce1d1e49ff22db46c47ea5fb4c0cc5c9b90a616364/debugpy-1.8.20-cp314-cp314-manylinux_2_34_x86_64.whl", hash = "sha256:077a7447589ee9bc1ff0cdf443566d0ecf540ac8aa7333b775ebcb8ce9f4ecad", size = 4269425 },
{ url = "https://files.pythonhosted.org/packages/82/a3/c91a087ab21f1047db328c1d3eb5d1ff0e52de9e74f9f6f6fa14cdd93d58/debugpy-1.8.19-cp314-cp314-win32.whl", hash = "sha256:0601708223fe1cd0e27c6cce67a899d92c7d68e73690211e6788a4b0e1903f5b", size = 5286388 }, { url = "https://files.pythonhosted.org/packages/c5/d9/d64199c14a0d4c476df46c82470a3ce45c8d183a6796cfb5e66533b3663c/debugpy-1.8.20-cp314-cp314-win32.whl", hash = "sha256:352036a99dd35053b37b7803f748efc456076f929c6a895556932eaf2d23b07f", size = 5331407 },
{ url = "https://files.pythonhosted.org/packages/17/b8/bfdc30b6e94f1eff09f2dc9cc1f9cd1c6cde3d996bcbd36ce2d9a4956e99/debugpy-1.8.19-cp314-cp314-win_amd64.whl", hash = "sha256:8e19a725f5d486f20e53a1dde2ab8bb2c9607c40c00a42ab646def962b41125f", size = 5327741 }, { url = "https://files.pythonhosted.org/packages/e0/d9/1f07395b54413432624d61524dfd98c1a7c7827d2abfdb8829ac92638205/debugpy-1.8.20-cp314-cp314-win_amd64.whl", hash = "sha256:a98eec61135465b062846112e5ecf2eebb855305acc1dfbae43b72903b8ab5be", size = 5372521 },
{ url = "https://files.pythonhosted.org/packages/25/3e/e27078370414ef35fafad2c06d182110073daaeb5d3bf734b0b1eeefe452/debugpy-1.8.19-py2.py3-none-any.whl", hash = "sha256:360ffd231a780abbc414ba0f005dad409e71c78637efe8f2bd75837132a41d38", size = 5292321 }, { url = "https://files.pythonhosted.org/packages/e0/c3/7f67dea8ccf8fdcb9c99033bbe3e90b9e7395415843accb81428c441be2d/debugpy-1.8.20-py2.py3-none-any.whl", hash = "sha256:5be9bed9ae3be00665a06acaa48f8329d2b9632f15fd09f6a9a8c8d9907e54d7", size = 5337658 },
] ]
[[package]] [[package]]
@ -515,33 +524,36 @@ wheels = [
[[package]] [[package]]
name = "greenlet" name = "greenlet"
version = "3.3.0" version = "3.3.1"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/c7/e5/40dbda2736893e3e53d25838e0f19a2b417dfc122b9989c91918db30b5d3/greenlet-3.3.0.tar.gz", hash = "sha256:a82bb225a4e9e4d653dd2fb7b8b2d36e4fb25bc0165422a11e48b88e9e6f78fb", size = 190651 } sdist = { url = "https://files.pythonhosted.org/packages/8a/99/1cd3411c56a410994669062bd73dd58270c00cc074cac15f385a1fd91f8a/greenlet-3.3.1.tar.gz", hash = "sha256:41848f3230b58c08bb43dee542e74a2a2e34d3c59dc3076cec9151aeeedcae98", size = 184690 }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/02/2f/28592176381b9ab2cafa12829ba7b472d177f3acc35d8fbcf3673d966fff/greenlet-3.3.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:a1e41a81c7e2825822f4e068c48cb2196002362619e2d70b148f20a831c00739", size = 275140 }, { url = "https://files.pythonhosted.org/packages/ec/ab/d26750f2b7242c2b90ea2ad71de70cfcd73a948a49513188a0fc0d6fc15a/greenlet-3.3.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:7ab327905cabb0622adca5971e488064e35115430cec2c35a50fd36e72a315b3", size = 275205 },
{ url = "https://files.pythonhosted.org/packages/2c/80/fbe937bf81e9fca98c981fe499e59a3f45df2a04da0baa5c2be0dca0d329/greenlet-3.3.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9f515a47d02da4d30caaa85b69474cec77b7929b2e936ff7fb853d42f4bf8808", size = 599219 }, { url = "https://files.pythonhosted.org/packages/10/d3/be7d19e8fad7c5a78eeefb2d896a08cd4643e1e90c605c4be3b46264998f/greenlet-3.3.1-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:65be2f026ca6a176f88fb935ee23c18333ccea97048076aef4db1ef5bc0713ac", size = 599284 },
{ url = "https://files.pythonhosted.org/packages/c2/ff/7c985128f0514271b8268476af89aee6866df5eec04ac17dcfbc676213df/greenlet-3.3.0-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7d2d9fd66bfadf230b385fdc90426fcd6eb64db54b40c495b72ac0feb5766c54", size = 610211 }, { url = "https://files.pythonhosted.org/packages/ae/21/fe703aaa056fdb0f17e5afd4b5c80195bbdab701208918938bd15b00d39b/greenlet-3.3.1-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7a3ae05b3d225b4155bda56b072ceb09d05e974bc74be6c3fc15463cf69f33fd", size = 610274 },
{ url = "https://files.pythonhosted.org/packages/79/07/c47a82d881319ec18a4510bb30463ed6891f2ad2c1901ed5ec23d3de351f/greenlet-3.3.0-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30a6e28487a790417d036088b3bcb3f3ac7d8babaa7d0139edbaddebf3af9492", size = 624311 }, { url = "https://files.pythonhosted.org/packages/06/00/95df0b6a935103c0452dad2203f5be8377e551b8466a29650c4c5a5af6cc/greenlet-3.3.1-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:12184c61e5d64268a160226fb4818af4df02cfead8379d7f8b99a56c3a54ff3e", size = 624375 },
{ url = "https://files.pythonhosted.org/packages/fd/8e/424b8c6e78bd9837d14ff7df01a9829fc883ba2ab4ea787d4f848435f23f/greenlet-3.3.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:087ea5e004437321508a8d6f20efc4cfec5e3c30118e1417ea96ed1d93950527", size = 612833 }, { url = "https://files.pythonhosted.org/packages/cb/86/5c6ab23bb3c28c21ed6bebad006515cfe08b04613eb105ca0041fecca852/greenlet-3.3.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6423481193bbbe871313de5fd06a082f2649e7ce6e08015d2a76c1e9186ca5b3", size = 612904 },
{ url = "https://files.pythonhosted.org/packages/b5/ba/56699ff9b7c76ca12f1cdc27a886d0f81f2189c3455ff9f65246780f713d/greenlet-3.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ab97cf74045343f6c60a39913fa59710e4bd26a536ce7ab2397adf8b27e67c39", size = 1567256 }, { url = "https://files.pythonhosted.org/packages/c2/f3/7949994264e22639e40718c2daf6f6df5169bf48fb038c008a489ec53a50/greenlet-3.3.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:33a956fe78bbbda82bfc95e128d61129b32d66bcf0a20a1f0c08aa4839ffa951", size = 1567316 },
{ url = "https://files.pythonhosted.org/packages/1e/37/f31136132967982d698c71a281a8901daf1a8fbab935dce7c0cf15f942cc/greenlet-3.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5375d2e23184629112ca1ea89a53389dddbffcf417dad40125713d88eb5f96e8", size = 1636483 }, { url = "https://files.pythonhosted.org/packages/8d/6e/d73c94d13b6465e9f7cd6231c68abde838bb22408596c05d9059830b7872/greenlet-3.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b065d3284be43728dd280f6f9a13990b56470b81be20375a207cdc814a983f2", size = 1636549 },
{ url = "https://files.pythonhosted.org/packages/7e/71/ba21c3fb8c5dce83b8c01f458a42e99ffdb1963aeec08fff5a18588d8fd7/greenlet-3.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:9ee1942ea19550094033c35d25d20726e4f1c40d59545815e1128ac58d416d38", size = 301833 }, { url = "https://files.pythonhosted.org/packages/5e/b3/c9c23a6478b3bcc91f979ce4ca50879e4d0b2bd7b9a53d8ecded719b92e2/greenlet-3.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:27289986f4e5b0edec7b5a91063c109f0276abb09a7e9bdab08437525977c946", size = 227042 },
{ url = "https://files.pythonhosted.org/packages/d7/7c/f0a6d0ede2c7bf092d00bc83ad5bafb7e6ec9b4aab2fbdfa6f134dc73327/greenlet-3.3.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:60c2ef0f578afb3c8d92ea07ad327f9a062547137afe91f38408f08aacab667f", size = 275671 }, { url = "https://files.pythonhosted.org/packages/90/e7/824beda656097edee36ab15809fd063447b200cc03a7f6a24c34d520bc88/greenlet-3.3.1-cp313-cp313-win_arm64.whl", hash = "sha256:2f080e028001c5273e0b42690eaf359aeef9cb1389da0f171ea51a5dc3c7608d", size = 226294 },
{ url = "https://files.pythonhosted.org/packages/44/06/dac639ae1a50f5969d82d2e3dd9767d30d6dbdbab0e1a54010c8fe90263c/greenlet-3.3.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a5d554d0712ba1de0a6c94c640f7aeba3f85b3a6e1f2899c11c2c0428da9365", size = 646360 }, { url = "https://files.pythonhosted.org/packages/ae/fb/011c7c717213182caf78084a9bea51c8590b0afda98001f69d9f853a495b/greenlet-3.3.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:bd59acd8529b372775cd0fcbc5f420ae20681c5b045ce25bd453ed8455ab99b5", size = 275737 },
{ url = "https://files.pythonhosted.org/packages/e0/94/0fb76fe6c5369fba9bf98529ada6f4c3a1adf19e406a47332245ef0eb357/greenlet-3.3.0-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3a898b1e9c5f7307ebbde4102908e6cbfcb9ea16284a3abe15cab996bee8b9b3", size = 658160 }, { url = "https://files.pythonhosted.org/packages/41/2e/a3a417d620363fdbb08a48b1dd582956a46a61bf8fd27ee8164f9dfe87c2/greenlet-3.3.1-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b31c05dd84ef6871dd47120386aed35323c944d86c3d91a17c4b8d23df62f15b", size = 646422 },
{ url = "https://files.pythonhosted.org/packages/93/79/d2c70cae6e823fac36c3bbc9077962105052b7ef81db2f01ec3b9bf17e2b/greenlet-3.3.0-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:dcd2bdbd444ff340e8d6bdf54d2f206ccddbb3ccfdcd3c25bf4afaa7b8f0cf45", size = 671388 }, { url = "https://files.pythonhosted.org/packages/b4/09/c6c4a0db47defafd2d6bab8ddfe47ad19963b4e30f5bed84d75328059f8c/greenlet-3.3.1-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:02925a0bfffc41e542c70aa14c7eda3593e4d7e274bfcccca1827e6c0875902e", size = 658219 },
{ url = "https://files.pythonhosted.org/packages/b8/14/bab308fc2c1b5228c3224ec2bf928ce2e4d21d8046c161e44a2012b5203e/greenlet-3.3.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5773edda4dc00e173820722711d043799d3adb4f01731f40619e07ea2750b955", size = 660166 }, { url = "https://files.pythonhosted.org/packages/e2/89/b95f2ddcc5f3c2bc09c8ee8d77be312df7f9e7175703ab780f2014a0e781/greenlet-3.3.1-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3e0f3878ca3a3ff63ab4ea478585942b53df66ddde327b59ecb191b19dbbd62d", size = 671455 },
{ url = "https://files.pythonhosted.org/packages/4b/d2/91465d39164eaa0085177f61983d80ffe746c5a1860f009811d498e7259c/greenlet-3.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ac0549373982b36d5fd5d30beb8a7a33ee541ff98d2b502714a09f1169f31b55", size = 1615193 }, { url = "https://files.pythonhosted.org/packages/80/38/9d42d60dffb04b45f03dbab9430898352dba277758640751dc5cc316c521/greenlet-3.3.1-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34a729e2e4e4ffe9ae2408d5ecaf12f944853f40ad724929b7585bca808a9d6f", size = 660237 },
{ url = "https://files.pythonhosted.org/packages/42/1b/83d110a37044b92423084d52d5d5a3b3a73cafb51b547e6d7366ff62eff1/greenlet-3.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d198d2d977460358c3b3a4dc844f875d1adb33817f0613f663a656f463764ccc", size = 1683653 }, { url = "https://files.pythonhosted.org/packages/96/61/373c30b7197f9e756e4c81ae90a8d55dc3598c17673f91f4d31c3c689c3f/greenlet-3.3.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:aec9ab04e82918e623415947921dea15851b152b822661cce3f8e4393c3df683", size = 1615261 },
{ url = "https://files.pythonhosted.org/packages/7c/9a/9030e6f9aa8fd7808e9c31ba4c38f87c4f8ec324ee67431d181fe396d705/greenlet-3.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:73f51dd0e0bdb596fb0417e475fa3c5e32d4c83638296e560086b8d7da7c4170", size = 305387 }, { url = "https://files.pythonhosted.org/packages/fd/d3/ca534310343f5945316f9451e953dcd89b36fe7a19de652a1dc5a0eeef3f/greenlet-3.3.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:71c767cf281a80d02b6c1bdc41c9468e1f5a494fb11bc8688c360524e273d7b1", size = 1683719 },
{ url = "https://files.pythonhosted.org/packages/a0/66/bd6317bc5932accf351fc19f177ffba53712a202f9df10587da8df257c7e/greenlet-3.3.0-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:d6ed6f85fae6cdfdb9ce04c9bf7a08d666cfcfb914e7d006f44f840b46741931", size = 282638 }, { url = "https://files.pythonhosted.org/packages/52/cb/c21a3fd5d2c9c8b622e7bede6d6d00e00551a5ee474ea6d831b5f567a8b4/greenlet-3.3.1-cp314-cp314-win_amd64.whl", hash = "sha256:96aff77af063b607f2489473484e39a0bbae730f2ea90c9e5606c9b73c44174a", size = 228125 },
{ url = "https://files.pythonhosted.org/packages/30/cf/cc81cb030b40e738d6e69502ccbd0dd1bced0588e958f9e757945de24404/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d9125050fcf24554e69c4cacb086b87b3b55dc395a8b3ebe6487b045b2614388", size = 651145 }, { url = "https://files.pythonhosted.org/packages/6a/8e/8a2db6d11491837af1de64b8aff23707c6e85241be13c60ed399a72e2ef8/greenlet-3.3.1-cp314-cp314-win_arm64.whl", hash = "sha256:b066e8b50e28b503f604fa538adc764a638b38cf8e81e025011d26e8a627fa79", size = 227519 },
{ url = "https://files.pythonhosted.org/packages/9c/ea/1020037b5ecfe95ca7df8d8549959baceb8186031da83d5ecceff8b08cd2/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:87e63ccfa13c0a0f6234ed0add552af24cc67dd886731f2261e46e241608bee3", size = 654236 }, { url = "https://files.pythonhosted.org/packages/28/24/cbbec49bacdcc9ec652a81d3efef7b59f326697e7edf6ed775a5e08e54c2/greenlet-3.3.1-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:3e63252943c921b90abb035ebe9de832c436401d9c45f262d80e2d06cc659242", size = 282706 },
{ url = "https://files.pythonhosted.org/packages/69/cc/1e4bae2e45ca2fa55299f4e85854606a78ecc37fead20d69322f96000504/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2662433acbca297c9153a4023fe2161c8dcfdcc91f10433171cf7e7d94ba2221", size = 662506 }, { url = "https://files.pythonhosted.org/packages/86/2e/4f2b9323c144c4fe8842a4e0d92121465485c3c2c5b9e9b30a52e80f523f/greenlet-3.3.1-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:76e39058e68eb125de10c92524573924e827927df5d3891fbc97bd55764a8774", size = 651209 },
{ url = "https://files.pythonhosted.org/packages/57/b9/f8025d71a6085c441a7eaff0fd928bbb275a6633773667023d19179fe815/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3c6e9b9c1527a78520357de498b0e709fb9e2f49c3a513afd5a249007261911b", size = 653783 }, { url = "https://files.pythonhosted.org/packages/d9/87/50ca60e515f5bb55a2fbc5f0c9b5b156de7d2fc51a0a69abc9d23914a237/greenlet-3.3.1-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c9f9d5e7a9310b7a2f416dd13d2e3fd8b42d803968ea580b7c0f322ccb389b97", size = 654300 },
{ url = "https://files.pythonhosted.org/packages/f6/c7/876a8c7a7485d5d6b5c6821201d542ef28be645aa024cfe1145b35c120c1/greenlet-3.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:286d093f95ec98fdd92fcb955003b8a3d054b4e2cab3e2707a5039e7b50520fd", size = 1614857 }, { url = "https://files.pythonhosted.org/packages/7c/25/c51a63f3f463171e09cb586eb64db0861eb06667ab01a7968371a24c4f3b/greenlet-3.3.1-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4b9721549a95db96689458a1e0ae32412ca18776ed004463df3a9299c1b257ab", size = 662574 },
{ url = "https://files.pythonhosted.org/packages/4f/dc/041be1dff9f23dac5f48a43323cd0789cb798342011c19a248d9c9335536/greenlet-3.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c10513330af5b8ae16f023e8ddbfb486ab355d04467c4679c5cfe4659975dd9", size = 1676034 }, { url = "https://files.pythonhosted.org/packages/1d/94/74310866dfa2b73dd08659a3d18762f83985ad3281901ba0ee9a815194fb/greenlet-3.3.1-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:92497c78adf3ac703b57f1e3813c2d874f27f71a178f9ea5887855da413cd6d2", size = 653842 },
{ url = "https://files.pythonhosted.org/packages/97/43/8bf0ffa3d498eeee4c58c212a3905dd6146c01c8dc0b0a046481ca29b18c/greenlet-3.3.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ed6b402bc74d6557a705e197d47f9063733091ed6357b3de33619d8a8d93ac53", size = 1614917 },
{ url = "https://files.pythonhosted.org/packages/89/90/a3be7a5f378fc6e84abe4dcfb2ba32b07786861172e502388b4c90000d1b/greenlet-3.3.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:59913f1e5ada20fde795ba906916aea25d442abcc0593fba7e26c92b7ad76249", size = 1676092 },
{ url = "https://files.pythonhosted.org/packages/e1/2b/98c7f93e6db9977aaee07eb1e51ca63bd5f779b900d362791d3252e60558/greenlet-3.3.1-cp314-cp314t-win_amd64.whl", hash = "sha256:301860987846c24cb8964bdec0e31a96ad4a2a801b41b4ef40963c1b44f33451", size = 233181 },
] ]
[[package]] [[package]]
@ -662,6 +674,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008 }, { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008 },
] ]
[[package]]
name = "importlib-metadata"
version = "8.7.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "zipp" },
]
sdist = { url = "https://files.pythonhosted.org/packages/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", size = 57107 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865 },
]
[[package]] [[package]]
name = "iniconfig" name = "iniconfig"
version = "2.3.0" version = "2.3.0"
@ -673,11 +697,11 @@ wheels = [
[[package]] [[package]]
name = "jmespath" name = "jmespath"
version = "1.0.1" version = "1.1.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/00/2a/e867e8531cf3e36b41201936b7fa7ba7b5702dbef42922193f05c8976cd6/jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe", size = 25843 } sdist = { url = "https://files.pythonhosted.org/packages/d3/59/322338183ecda247fb5d1763a6cbe46eff7222eaeebafd9fa65d4bf5cb11/jmespath-1.1.0.tar.gz", hash = "sha256:472c87d80f36026ae83c6ddd0f1d05d4e510134ed462851fd5f754c8c3cbb88d", size = 27377 }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/31/b4/b9b800c45527aadd64d5b442f9b932b00648617eb5d63d2c7a6587b7cafc/jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980", size = 20256 }, { url = "https://files.pythonhosted.org/packages/14/2f/967ba146e6d58cf6a652da73885f52fc68001525b4197effc174321d70b4/jmespath-1.1.0-py3-none-any.whl", hash = "sha256:a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64", size = 20419 },
] ]
[[package]] [[package]]
@ -783,11 +807,11 @@ wheels = [
[[package]] [[package]]
name = "markdown" name = "markdown"
version = "3.10" version = "3.10.1"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/7d/ab/7dd27d9d863b3376fcf23a5a13cb5d024aed1db46f963f1b5735ae43b3be/markdown-3.10.tar.gz", hash = "sha256:37062d4f2aa4b2b6b32aefb80faa300f82cc790cb949a35b8caede34f2b68c0e", size = 364931 } sdist = { url = "https://files.pythonhosted.org/packages/b7/b1/af95bcae8549f1f3fd70faacb29075826a0d689a27f232e8cee315efa053/markdown-3.10.1.tar.gz", hash = "sha256:1c19c10bd5c14ac948c53d0d762a04e2fa35a6d58a6b7b1e6bfcbe6fefc0001a", size = 365402 }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/70/81/54e3ce63502cd085a0c556652a4e1b919c45a446bd1e5300e10c44c8c521/markdown-3.10-py3-none-any.whl", hash = "sha256:b5b99d6951e2e4948d939255596523444c0e677c669700b1d17aa4a8a464cb7c", size = 107678 }, { url = "https://files.pythonhosted.org/packages/59/1b/6ef961f543593969d25b2afe57a3564200280528caa9bd1082eecdd7b3bc/markdown-3.10.1-py3-none-any.whl", hash = "sha256:867d788939fe33e4b736426f5b9f651ad0c0ae0ecf89df0ca5d1176c70812fe3", size = 107684 },
] ]
[[package]] [[package]]
@ -840,6 +864,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b0/7a/620f945b96be1f6ee357d211d5bf74ab1b7fe72a9f1525aafbfe3aee6875/mutagen-1.47.0-py3-none-any.whl", hash = "sha256:edd96f50c5907a9539d8e5bba7245f62c9f520aef333d13392a79a4f70aca719", size = 194391 }, { url = "https://files.pythonhosted.org/packages/b0/7a/620f945b96be1f6ee357d211d5bf74ab1b7fe72a9f1525aafbfe3aee6875/mutagen-1.47.0-py3-none-any.whl", hash = "sha256:edd96f50c5907a9539d8e5bba7245f62c9f520aef333d13392a79a4f70aca719", size = 194391 },
] ]
[[package]]
name = "mypy-extensions"
version = "1.1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963 },
]
[[package]] [[package]]
name = "oauthlib" name = "oauthlib"
version = "3.3.1" version = "3.3.1"
@ -863,16 +896,16 @@ wheels = [
[[package]] [[package]]
name = "packaging" name = "packaging"
version = "25.0" version = "26.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727 } sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416 }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469 }, { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366 },
] ]
[[package]] [[package]]
name = "parsel" name = "parsel"
version = "1.10.0" version = "1.11.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "cssselect" }, { name = "cssselect" },
@ -881,9 +914,9 @@ dependencies = [
{ name = "packaging" }, { name = "packaging" },
{ name = "w3lib" }, { name = "w3lib" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/f6/df/acd504c154c0b9028b0d8491a77fdd5f86e9c06ee04f986abf85e36d9a5f/parsel-1.10.0.tar.gz", hash = "sha256:14f17db9559f51b43357b9dfe43cec870a8efb5ea4857abb624ec6ff80d8a080", size = 51421 } sdist = { url = "https://files.pythonhosted.org/packages/91/c8/4ace3a5c61e39ca21734a5715d0e076eea6200dd8daea2a5b99452f5a0d6/parsel-1.11.0.tar.gz", hash = "sha256:5925fe087eb16fc404a7ed91e31e2c1e2a9b230da4b64f34d81358c0d0e27e88", size = 106849 }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/12/18/35d1d947553d24909dca37e2ff11720eecb601360d1bac8d7a9a1bc7eb08/parsel-1.10.0-py2.py3-none-any.whl", hash = "sha256:6a0c28bd81f9df34ba665884c88efa0b18b8d2c44c81f64e27f2f0cb37d46169", size = 17266 }, { url = "https://files.pythonhosted.org/packages/08/23/4e0dae5e5bee14aea26dba003a682e621563451a20f751ed985810f818b6/parsel-1.11.0-py3-none-any.whl", hash = "sha256:bda82575df1774dd64e1c1396163f3cadb3e383e0f8080d43d45fa6705355daa", size = 14176 },
] ]
[[package]] [[package]]
@ -895,6 +928,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/54/16/12b82f791c7f50ddec566873d5bdd245baa1491bac11d15ffb98aecc8f8b/pefile-2024.8.26-py3-none-any.whl", hash = "sha256:76f8b485dcd3b1bb8166f1128d395fa3d87af26360c2358fb75b80019b957c6f", size = 74766 }, { url = "https://files.pythonhosted.org/packages/54/16/12b82f791c7f50ddec566873d5bdd245baa1491bac11d15ffb98aecc8f8b/pefile-2024.8.26-py3-none-any.whl", hash = "sha256:76f8b485dcd3b1bb8166f1128d395fa3d87af26360c2358fb75b80019b957c6f", size = 74766 },
] ]
[[package]]
name = "pip"
version = "26.0.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/48/83/0d7d4e9efe3344b8e2fe25d93be44f64b65364d3c8d7bc6dc90198d5422e/pip-26.0.1.tar.gz", hash = "sha256:c4037d8a277c89b320abe636d59f91e6d0922d08a05b60e85e53b296613346d8", size = 1812747 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/de/f0/c81e05b613866b76d2d1066490adf1a3dbc4ee9d9c839961c3fc8a6997af/pip-26.0.1-py3-none-any.whl", hash = "sha256:bdb1b08f4274833d62c1aa29e20907365a2ceb950410df15fc9521bad440122b", size = 1787723 },
]
[[package]] [[package]]
name = "platformdirs" name = "platformdirs"
version = "4.5.1" version = "4.5.1"
@ -984,11 +1026,11 @@ wheels = [
[[package]] [[package]]
name = "pycparser" name = "pycparser"
version = "2.23" version = "3.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", size = 173734 } sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492 }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140 }, { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172 },
] ]
[[package]] [[package]]
@ -1158,15 +1200,15 @@ wheels = [
[[package]] [[package]]
name = "pyinstaller-hooks-contrib" name = "pyinstaller-hooks-contrib"
version = "2025.11" version = "2026.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "packaging" }, { name = "packaging" },
{ name = "setuptools" }, { name = "setuptools" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/45/2f/2c68b6722d233dae3e5243751aafc932940b836919cfaca22dd0c60d417c/pyinstaller_hooks_contrib-2025.11.tar.gz", hash = "sha256:dfe18632e06655fa88d218e0d768fd753e1886465c12a6d4bce04f1aaeec917d", size = 169183 } sdist = { url = "https://files.pythonhosted.org/packages/31/8f/8052ff65067697ee80fde45b9731842e160751c41ac5690ba232c22030e8/pyinstaller_hooks_contrib-2026.0.tar.gz", hash = "sha256:0120893de491a000845470ca9c0b39284731ac6bace26f6849dea9627aaed48e", size = 170311 }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/a7/c4/3a096c6e701832443b957b9dac18a163103360d0c7f5842ca41695371148/pyinstaller_hooks_contrib-2025.11-py3-none-any.whl", hash = "sha256:777e163e2942474aa41a8e6d31ac1635292d63422c3646c176d584d04d971c34", size = 449478 }, { url = "https://files.pythonhosted.org/packages/d5/b1/9da6ec3e88696018ee7bb9dc4a7310c2cfaebf32923a19598cd342767c10/pyinstaller_hooks_contrib-2026.0-py3-none-any.whl", hash = "sha256:0590db8edeba3e6c30c8474937021f5cd39c0602b4d10f74a064c73911efaca5", size = 452318 },
] ]
[[package]] [[package]]
@ -1566,54 +1608,56 @@ wheels = [
[[package]] [[package]]
name = "ruff" name = "ruff"
version = "0.14.13" version = "0.15.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/50/0a/1914efb7903174b381ee2ffeebb4253e729de57f114e63595114c8ca451f/ruff-0.14.13.tar.gz", hash = "sha256:83cd6c0763190784b99650a20fec7633c59f6ebe41c5cc9d45ee42749563ad47", size = 6059504 } sdist = { url = "https://files.pythonhosted.org/packages/c8/39/5cee96809fbca590abea6b46c6d1c586b49663d1d2830a751cc8fc42c666/ruff-0.15.0.tar.gz", hash = "sha256:6bdea47cdbea30d40f8f8d7d69c0854ba7c15420ec75a26f463290949d7f7e9a", size = 4524893 }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/c3/ae/0deefbc65ca74b0ab1fd3917f94dc3b398233346a74b8bbb0a916a1a6bf6/ruff-0.14.13-py3-none-linux_armv6l.whl", hash = "sha256:76f62c62cd37c276cb03a275b198c7c15bd1d60c989f944db08a8c1c2dbec18b", size = 13062418 }, { url = "https://files.pythonhosted.org/packages/bc/88/3fd1b0aa4b6330d6aaa63a285bc96c9f71970351579152d231ed90914586/ruff-0.15.0-py3-none-linux_armv6l.whl", hash = "sha256:aac4ebaa612a82b23d45964586f24ae9bc23ca101919f5590bdb368d74ad5455", size = 10354332 },
{ url = "https://files.pythonhosted.org/packages/47/df/5916604faa530a97a3c154c62a81cb6b735c0cb05d1e26d5ad0f0c8ac48a/ruff-0.14.13-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:914a8023ece0528d5cc33f5a684f5f38199bbb566a04815c2c211d8f40b5d0ed", size = 13442344 }, { url = "https://files.pythonhosted.org/packages/72/f6/62e173fbb7eb75cc29fe2576a1e20f0a46f671a2587b5f604bfb0eaf5f6f/ruff-0.15.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:dcd4be7cc75cfbbca24a98d04d0b9b36a270d0833241f776b788d59f4142b14d", size = 10767189 },
{ url = "https://files.pythonhosted.org/packages/4c/f3/e0e694dd69163c3a1671e102aa574a50357536f18a33375050334d5cd517/ruff-0.14.13-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d24899478c35ebfa730597a4a775d430ad0d5631b8647a3ab368c29b7e7bd063", size = 12354720 }, { url = "https://files.pythonhosted.org/packages/99/e4/968ae17b676d1d2ff101d56dc69cf333e3a4c985e1ec23803df84fc7bf9e/ruff-0.15.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d747e3319b2bce179c7c1eaad3d884dc0a199b5f4d5187620530adf9105268ce", size = 10075384 },
{ url = "https://files.pythonhosted.org/packages/c3/e8/67f5fcbbaee25e8fc3b56cc33e9892eca7ffe09f773c8e5907757a7e3bdb/ruff-0.14.13-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9aaf3870f14d925bbaf18b8a2347ee0ae7d95a2e490e4d4aea6813ed15ebc80e", size = 12774493 }, { url = "https://files.pythonhosted.org/packages/a2/bf/9843c6044ab9e20af879c751487e61333ca79a2c8c3058b15722386b8cae/ruff-0.15.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:650bd9c56ae03102c51a5e4b554d74d825ff3abe4db22b90fd32d816c2e90621", size = 10481363 },
{ url = "https://files.pythonhosted.org/packages/6b/ce/d2e9cb510870b52a9565d885c0d7668cc050e30fa2c8ac3fb1fda15c083d/ruff-0.14.13-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac5b7f63dd3b27cc811850f5ffd8fff845b00ad70e60b043aabf8d6ecc304e09", size = 12815174 }, { url = "https://files.pythonhosted.org/packages/55/d9/4ada5ccf4cd1f532db1c8d44b6f664f2208d3d93acbeec18f82315e15193/ruff-0.15.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a6664b7eac559e3048223a2da77769c2f92b43a6dfd4720cef42654299a599c9", size = 10187736 },
{ url = "https://files.pythonhosted.org/packages/88/00/c38e5da58beebcf4fa32d0ddd993b63dfacefd02ab7922614231330845bf/ruff-0.14.13-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:78d2b1097750d90ba82ce4ba676e85230a0ed694178ca5e61aa9b459970b3eb9", size = 13680909 }, { url = "https://files.pythonhosted.org/packages/86/e2/f25eaecd446af7bb132af0a1d5b135a62971a41f5366ff41d06d25e77a91/ruff-0.15.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f811f97b0f092b35320d1556f3353bf238763420ade5d9e62ebd2b73f2ff179", size = 10968415 },
{ url = "https://files.pythonhosted.org/packages/61/61/cd37c9dd5bd0a3099ba79b2a5899ad417d8f3b04038810b0501a80814fd7/ruff-0.14.13-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:7d0bf87705acbbcb8d4c24b2d77fbb73d40210a95c3903b443cd9e30824a5032", size = 15144215 }, { url = "https://files.pythonhosted.org/packages/e7/dc/f06a8558d06333bf79b497d29a50c3a673d9251214e0d7ec78f90b30aa79/ruff-0.15.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:761ec0a66680fab6454236635a39abaf14198818c8cdf691e036f4bc0f406b2d", size = 11809643 },
{ url = "https://files.pythonhosted.org/packages/56/8a/85502d7edbf98c2df7b8876f316c0157359165e16cdf98507c65c8d07d3d/ruff-0.14.13-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a3eb5da8e2c9e9f13431032fdcbe7681de9ceda5835efee3269417c13f1fed5c", size = 14706067 }, { url = "https://files.pythonhosted.org/packages/dd/45/0ece8db2c474ad7df13af3a6d50f76e22a09d078af63078f005057ca59eb/ruff-0.15.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:940f11c2604d317e797b289f4f9f3fa5555ffe4fb574b55ed006c3d9b6f0eb78", size = 11234787 },
{ url = "https://files.pythonhosted.org/packages/7e/2f/de0df127feb2ee8c1e54354dc1179b4a23798f0866019528c938ba439aca/ruff-0.14.13-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:642442b42957093811cd8d2140dfadd19c7417030a7a68cf8d51fcdd5f217427", size = 14133916 }, { url = "https://files.pythonhosted.org/packages/8a/d9/0e3a81467a120fd265658d127db648e4d3acfe3e4f6f5d4ea79fac47e587/ruff-0.15.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bcbca3d40558789126da91d7ef9a7c87772ee107033db7191edefa34e2c7f1b4", size = 11112797 },
{ url = "https://files.pythonhosted.org/packages/0d/77/9b99686bb9fe07a757c82f6f95e555c7a47801a9305576a9c67e0a31d280/ruff-0.14.13-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4acdf009f32b46f6e8864af19cbf6841eaaed8638e65c8dac845aea0d703c841", size = 13859207 }, { url = "https://files.pythonhosted.org/packages/b2/cb/8c0b3b0c692683f8ff31351dfb6241047fa873a4481a76df4335a8bff716/ruff-0.15.0-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:9a121a96db1d75fa3eb39c4539e607f628920dd72ff1f7c5ee4f1b768ac62d6e", size = 11033133 },
{ url = "https://files.pythonhosted.org/packages/7d/46/2bdcb34a87a179a4d23022d818c1c236cb40e477faf0d7c9afb6813e5876/ruff-0.14.13-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:591a7f68860ea4e003917d19b5c4f5ac39ff558f162dc753a2c5de897fd5502c", size = 14043686 }, { url = "https://files.pythonhosted.org/packages/f8/5e/23b87370cf0f9081a8c89a753e69a4e8778805b8802ccfe175cc410e50b9/ruff-0.15.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5298d518e493061f2eabd4abd067c7e4fb89e2f63291c94332e35631c07c3662", size = 10442646 },
{ url = "https://files.pythonhosted.org/packages/1a/a9/5c6a4f56a0512c691cf143371bcf60505ed0f0860f24a85da8bd123b2bf1/ruff-0.14.13-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:774c77e841cc6e046fc3e91623ce0903d1cd07e3a36b1a9fe79b81dab3de506b", size = 12663837 }, { url = "https://files.pythonhosted.org/packages/e1/9a/3c94de5ce642830167e6d00b5c75aacd73e6347b4c7fc6828699b150a5ee/ruff-0.15.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:afb6e603d6375ff0d6b0cee563fa21ab570fd15e65c852cb24922cef25050cf1", size = 10195750 },
{ url = "https://files.pythonhosted.org/packages/fe/bb/b920016ece7651fa7fcd335d9d199306665486694d4361547ccb19394c44/ruff-0.14.13-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:61f4e40077a1248436772bb6512db5fc4457fe4c49e7a94ea7c5088655dd21ae", size = 12805867 }, { url = "https://files.pythonhosted.org/packages/30/15/e396325080d600b436acc970848d69df9c13977942fb62bb8722d729bee8/ruff-0.15.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:77e515f6b15f828b94dc17d2b4ace334c9ddb7d9468c54b2f9ed2b9c1593ef16", size = 10676120 },
{ url = "https://files.pythonhosted.org/packages/7d/b3/0bd909851e5696cd21e32a8fc25727e5f58f1934b3596975503e6e85415c/ruff-0.14.13-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6d02f1428357fae9e98ac7aa94b7e966fd24151088510d32cf6f902d6c09235e", size = 13208528 }, { url = "https://files.pythonhosted.org/packages/8d/c9/229a23d52a2983de1ad0fb0ee37d36e0257e6f28bfd6b498ee2c76361874/ruff-0.15.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:6f6e80850a01eb13b3e42ee0ebdf6e4497151b48c35051aab51c101266d187a3", size = 11201636 },
{ url = "https://files.pythonhosted.org/packages/3b/3b/e2d94cb613f6bbd5155a75cbe072813756363eba46a3f2177a1fcd0cd670/ruff-0.14.13-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e399341472ce15237be0c0ae5fbceca4b04cd9bebab1a2b2c979e015455d8f0c", size = 13929242 }, { url = "https://files.pythonhosted.org/packages/6f/b0/69adf22f4e24f3677208adb715c578266842e6e6a3cc77483f48dd999ede/ruff-0.15.0-py3-none-win32.whl", hash = "sha256:238a717ef803e501b6d51e0bdd0d2c6e8513fe9eec14002445134d3907cd46c3", size = 10465945 },
{ url = "https://files.pythonhosted.org/packages/6a/c5/abd840d4132fd51a12f594934af5eba1d5d27298a6f5b5d6c3be45301caf/ruff-0.14.13-py3-none-win32.whl", hash = "sha256:ef720f529aec113968b45dfdb838ac8934e519711da53a0456038a0efecbd680", size = 12919024 }, { url = "https://files.pythonhosted.org/packages/51/ad/f813b6e2c97e9b4598be25e94a9147b9af7e60523b0cb5d94d307c15229d/ruff-0.15.0-py3-none-win_amd64.whl", hash = "sha256:dd5e4d3301dc01de614da3cdffc33d4b1b96fb89e45721f1598e5532ccf78b18", size = 11564657 },
{ url = "https://files.pythonhosted.org/packages/c2/55/6384b0b8ce731b6e2ade2b5449bf07c0e4c31e8a2e68ea65b3bafadcecc5/ruff-0.14.13-py3-none-win_amd64.whl", hash = "sha256:6070bd026e409734b9257e03e3ef18c6e1a216f0435c6751d7a8ec69cb59abef", size = 14097887 }, { url = "https://files.pythonhosted.org/packages/f6/b0/2d823f6e77ebe560f4e397d078487e8d52c1516b331e3521bc75db4272ca/ruff-0.15.0-py3-none-win_arm64.whl", hash = "sha256:c480d632cc0ca3f0727acac8b7d053542d9e114a462a145d0b00e7cd658c515a", size = 10865753 },
{ url = "https://files.pythonhosted.org/packages/4d/e1/7348090988095e4e39560cfc2f7555b1b2a7357deba19167b600fdf5215d/ruff-0.14.13-py3-none-win_arm64.whl", hash = "sha256:7ab819e14f1ad9fe39f246cfcc435880ef7a9390d81a2b6ac7e01039083dd247", size = 13080224 },
] ]
[[package]] [[package]]
name = "selenium" name = "selenium"
version = "4.39.0" version = "4.40.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "certifi" }, { name = "certifi" },
{ name = "trio" }, { name = "trio" },
{ name = "trio-typing" },
{ name = "trio-websocket" }, { name = "trio-websocket" },
{ name = "types-certifi" },
{ name = "types-urllib3" },
{ name = "typing-extensions" }, { name = "typing-extensions" },
{ name = "urllib3", extra = ["socks"] }, { name = "urllib3", extra = ["socks"] },
{ name = "websocket-client" }, { name = "websocket-client" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/af/19/27c1bf9eb1f7025632d35a956b50746efb4b10aa87f961b263fa7081f4c5/selenium-4.39.0.tar.gz", hash = "sha256:12f3325f02d43b6c24030fc9602b34a3c6865abbb1db9406641d13d108aa1889", size = 928575 } sdist = { url = "https://files.pythonhosted.org/packages/66/ef/a5727fa7b33d20d296322adf851b76072d8d3513e1b151969d3228437faf/selenium-4.40.0.tar.gz", hash = "sha256:a88f5905d88ad0b84991c2386ea39e2bbde6d6c334be38df5842318ba98eaa8c", size = 930444 }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/58/d0/55a6b7c6f35aad4c8a54be0eb7a52c1ff29a59542fc3e655f0ecbb14456d/selenium-4.39.0-py3-none-any.whl", hash = "sha256:c85f65d5610642ca0f47dae9d5cc117cd9e831f74038bc09fe1af126288200f9", size = 9655249 }, { url = "https://files.pythonhosted.org/packages/9d/74/eb9d6540aca1911106fa0877b8e9ef24171bc18857937a6b0ffe0586c623/selenium-4.40.0-py3-none-any.whl", hash = "sha256:c8823fc02e2c771d9ad9a0cf899cee7de1a57a6697e3d0b91f67566129f2b729", size = 9608184 },
] ]
[[package]] [[package]]
name = "setuptools" name = "setuptools"
version = "80.9.0" version = "80.10.2"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/18/5d/3bf57dcd21979b887f014ea83c24ae194cfcd12b9e0fda66b957c69d1fca/setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c", size = 1319958 } sdist = { url = "https://files.pythonhosted.org/packages/76/95/faf61eb8363f26aa7e1d762267a8d602a1b26d4f3a1e758e92cb3cb8b054/setuptools-80.10.2.tar.gz", hash = "sha256:8b0e9d10c784bf7d262c4e5ec5d4ec94127ce206e8738f29a437945fbc219b70", size = 1200343 }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486 }, { url = "https://files.pythonhosted.org/packages/94/b8/f1f62a5e3c0ad2ff1d189590bfa4c46b4f3b6e49cef6f26c6ee4e575394d/setuptools-80.10.2-py3-none-any.whl", hash = "sha256:95b30ddfb717250edb492926c92b5221f7ef3fbcc2b07579bcd4a27da21d0173", size = 1064234 },
] ]
[[package]] [[package]]
@ -1654,31 +1698,37 @@ wheels = [
[[package]] [[package]]
name = "sqlalchemy" name = "sqlalchemy"
version = "2.0.45" version = "2.0.46"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" }, { name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" },
{ name = "typing-extensions" }, { name = "typing-extensions" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/be/f9/5e4491e5ccf42f5d9cfc663741d261b3e6e1683ae7812114e7636409fcc6/sqlalchemy-2.0.45.tar.gz", hash = "sha256:1632a4bda8d2d25703fdad6363058d882541bdaaee0e5e3ddfa0cd3229efce88", size = 9869912 } sdist = { url = "https://files.pythonhosted.org/packages/06/aa/9ce0f3e7a9829ead5c8ce549392f33a12c4555a6c0609bb27d882e9c7ddf/sqlalchemy-2.0.46.tar.gz", hash = "sha256:cf36851ee7219c170bb0793dbc3da3e80c582e04a5437bc601bfe8c85c9216d7", size = 9865393 }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/6a/c8/7cc5221b47a54edc72a0140a1efa56e0a2730eefa4058d7ed0b4c4357ff8/sqlalchemy-2.0.45-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fe187fc31a54d7fd90352f34e8c008cf3ad5d064d08fedd3de2e8df83eb4a1cf", size = 3277082 }, { url = "https://files.pythonhosted.org/packages/b3/4b/fa7838fe20bb752810feed60e45625a9a8b0102c0c09971e2d1d95362992/sqlalchemy-2.0.46-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:93a12da97cca70cea10d4b4fc602589c4511f96c1f8f6c11817620c021d21d00", size = 2150268 },
{ url = "https://files.pythonhosted.org/packages/0e/50/80a8d080ac7d3d321e5e5d420c9a522b0aa770ec7013ea91f9a8b7d36e4a/sqlalchemy-2.0.45-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:672c45cae53ba88e0dad74b9027dddd09ef6f441e927786b05bec75d949fbb2e", size = 3293131 }, { url = "https://files.pythonhosted.org/packages/46/c1/b34dccd712e8ea846edf396e00973dda82d598cb93762e55e43e6835eba9/sqlalchemy-2.0.46-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:af865c18752d416798dae13f83f38927c52f085c52e2f32b8ab0fef46fdd02c2", size = 3276511 },
{ url = "https://files.pythonhosted.org/packages/da/4c/13dab31266fc9904f7609a5dc308a2432a066141d65b857760c3bef97e69/sqlalchemy-2.0.45-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:470daea2c1ce73910f08caf10575676a37159a6d16c4da33d0033546bddebc9b", size = 3225389 }, { url = "https://files.pythonhosted.org/packages/96/48/a04d9c94753e5d5d096c628c82a98c4793b9c08ca0e7155c3eb7d7db9f24/sqlalchemy-2.0.46-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8d679b5f318423eacb61f933a9a0f75535bfca7056daeadbf6bd5bcee6183aee", size = 3292881 },
{ url = "https://files.pythonhosted.org/packages/74/04/891b5c2e9f83589de202e7abaf24cd4e4fa59e1837d64d528829ad6cc107/sqlalchemy-2.0.45-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9c6378449e0940476577047150fd09e242529b761dc887c9808a9a937fe990c8", size = 3266054 }, { url = "https://files.pythonhosted.org/packages/be/f4/06eda6e91476f90a7d8058f74311cb65a2fb68d988171aced81707189131/sqlalchemy-2.0.46-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:64901e08c33462acc9ec3bad27fc7a5c2b6491665f2aa57564e57a4f5d7c52ad", size = 3224559 },
{ url = "https://files.pythonhosted.org/packages/f1/24/fc59e7f71b0948cdd4cff7a286210e86b0443ef1d18a23b0d83b87e4b1f7/sqlalchemy-2.0.45-cp313-cp313-win32.whl", hash = "sha256:4b6bec67ca45bc166c8729910bd2a87f1c0407ee955df110d78948f5b5827e8a", size = 2110299 }, { url = "https://files.pythonhosted.org/packages/ab/a2/d2af04095412ca6345ac22b33b89fe8d6f32a481e613ffcb2377d931d8d0/sqlalchemy-2.0.46-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e8ac45e8f4eaac0f9f8043ea0e224158855c6a4329fd4ee37c45c61e3beb518e", size = 3262728 },
{ url = "https://files.pythonhosted.org/packages/c0/c5/d17113020b2d43073412aeca09b60d2009442420372123b8d49cc253f8b8/sqlalchemy-2.0.45-cp313-cp313-win_amd64.whl", hash = "sha256:afbf47dc4de31fa38fd491f3705cac5307d21d4bb828a4f020ee59af412744ee", size = 2136264 }, { url = "https://files.pythonhosted.org/packages/31/48/1980c7caa5978a3b8225b4d230e69a2a6538a3562b8b31cea679b6933c83/sqlalchemy-2.0.46-cp313-cp313-win32.whl", hash = "sha256:8d3b44b3d0ab2f1319d71d9863d76eeb46766f8cf9e921ac293511804d39813f", size = 2111295 },
{ url = "https://files.pythonhosted.org/packages/3d/8d/bb40a5d10e7a5f2195f235c0b2f2c79b0bf6e8f00c0c223130a4fbd2db09/sqlalchemy-2.0.45-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:83d7009f40ce619d483d26ac1b757dfe3167b39921379a8bd1b596cf02dab4a6", size = 3521998 }, { url = "https://files.pythonhosted.org/packages/2d/54/f8d65bbde3d877617c4720f3c9f60e99bb7266df0d5d78b6e25e7c149f35/sqlalchemy-2.0.46-cp313-cp313-win_amd64.whl", hash = "sha256:77f8071d8fbcbb2dd11b7fd40dedd04e8ebe2eb80497916efedba844298065ef", size = 2137076 },
{ url = "https://files.pythonhosted.org/packages/75/a5/346128b0464886f036c039ea287b7332a410aa2d3fb0bb5d404cb8861635/sqlalchemy-2.0.45-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d8a2ca754e5415cde2b656c27900b19d50ba076aa05ce66e2207623d3fe41f5a", size = 3473434 }, { url = "https://files.pythonhosted.org/packages/56/ba/9be4f97c7eb2b9d5544f2624adfc2853e796ed51d2bb8aec90bc94b7137e/sqlalchemy-2.0.46-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a1e8cc6cc01da346dc92d9509a63033b9b1bda4fed7a7a7807ed385c7dccdc10", size = 3556533 },
{ url = "https://files.pythonhosted.org/packages/cc/64/4e1913772646b060b025d3fc52ce91a58967fe58957df32b455de5a12b4f/sqlalchemy-2.0.45-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f46ec744e7f51275582e6a24326e10c49fbdd3fc99103e01376841213028774", size = 3272404 }, { url = "https://files.pythonhosted.org/packages/20/a6/b1fc6634564dbb4415b7ed6419cdfeaadefd2c39cdab1e3aa07a5f2474c2/sqlalchemy-2.0.46-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:96c7cca1a4babaaf3bfff3e4e606e38578856917e52f0384635a95b226c87764", size = 3523208 },
{ url = "https://files.pythonhosted.org/packages/b3/27/caf606ee924282fe4747ee4fd454b335a72a6e018f97eab5ff7f28199e16/sqlalchemy-2.0.45-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:883c600c345123c033c2f6caca18def08f1f7f4c3ebeb591a63b6fceffc95cce", size = 3277057 }, { url = "https://files.pythonhosted.org/packages/a1/d8/41e0bdfc0f930ff236f86fccd12962d8fa03713f17ed57332d38af6a3782/sqlalchemy-2.0.46-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b2a9f9aee38039cf4755891a1e50e1effcc42ea6ba053743f452c372c3152b1b", size = 3464292 },
{ url = "https://files.pythonhosted.org/packages/85/d0/3d64218c9724e91f3d1574d12eb7ff8f19f937643815d8daf792046d88ab/sqlalchemy-2.0.45-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2c0b74aa79e2deade948fe8593654c8ef4228c44ba862bb7c9585c8e0db90f33", size = 3222279 }, { url = "https://files.pythonhosted.org/packages/f0/8b/9dcbec62d95bea85f5ecad9b8d65b78cc30fb0ffceeb3597961f3712549b/sqlalchemy-2.0.46-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:db23b1bf8cfe1f7fda19018e7207b20cdb5168f83c437ff7e95d19e39289c447", size = 3473497 },
{ url = "https://files.pythonhosted.org/packages/24/10/dd7688a81c5bc7690c2a3764d55a238c524cd1a5a19487928844cb247695/sqlalchemy-2.0.45-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8a420169cef179d4c9064365f42d779f1e5895ad26ca0c8b4c0233920973db74", size = 3244508 }, { url = "https://files.pythonhosted.org/packages/e9/f8/5ecdfc73383ec496de038ed1614de9e740a82db9ad67e6e4514ebc0708a3/sqlalchemy-2.0.46-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:56bdd261bfd0895452006d5316cbf35739c53b9bb71a170a331fa0ea560b2ada", size = 2152079 },
{ url = "https://files.pythonhosted.org/packages/aa/41/db75756ca49f777e029968d9c9fee338c7907c563267740c6d310a8e3f60/sqlalchemy-2.0.45-cp314-cp314-win32.whl", hash = "sha256:e50dcb81a5dfe4b7b4a4aa8f338116d127cb209559124f3694c70d6cd072b68f", size = 2113204 }, { url = "https://files.pythonhosted.org/packages/e5/bf/eba3036be7663ce4d9c050bc3d63794dc29fbe01691f2bf5ccb64e048d20/sqlalchemy-2.0.46-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:33e462154edb9493f6c3ad2125931e273bbd0be8ae53f3ecd1c161ea9a1dd366", size = 3272216 },
{ url = "https://files.pythonhosted.org/packages/89/a2/0e1590e9adb292b1d576dbcf67ff7df8cf55e56e78d2c927686d01080f4b/sqlalchemy-2.0.45-cp314-cp314-win_amd64.whl", hash = "sha256:4748601c8ea959e37e03d13dcda4a44837afcd1b21338e637f7c935b8da06177", size = 2138785 }, { url = "https://files.pythonhosted.org/packages/05/45/1256fb597bb83b58a01ddb600c59fe6fdf0e5afe333f0456ed75c0f8d7bd/sqlalchemy-2.0.46-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9bcdce05f056622a632f1d44bb47dbdb677f58cad393612280406ce37530eb6d", size = 3277208 },
{ url = "https://files.pythonhosted.org/packages/42/39/f05f0ed54d451156bbed0e23eb0516bcad7cbb9f18b3bf219c786371b3f0/sqlalchemy-2.0.45-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd337d3526ec5298f67d6a30bbbe4ed7e5e68862f0bf6dd21d289f8d37b7d60b", size = 3522029 }, { url = "https://files.pythonhosted.org/packages/d9/a0/2053b39e4e63b5d7ceb3372cface0859a067c1ddbd575ea7e9985716f771/sqlalchemy-2.0.46-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8e84b09a9b0f19accedcbeff5c2caf36e0dd537341a33aad8d680336152dc34e", size = 3221994 },
{ url = "https://files.pythonhosted.org/packages/54/0f/d15398b98b65c2bce288d5ee3f7d0a81f77ab89d9456994d5c7cc8b2a9db/sqlalchemy-2.0.45-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9a62b446b7d86a3909abbcd1cd3cc550a832f99c2bc37c5b22e1925438b9367b", size = 3475142 }, { url = "https://files.pythonhosted.org/packages/1e/87/97713497d9502553c68f105a1cb62786ba1ee91dea3852ae4067ed956a50/sqlalchemy-2.0.46-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4f52f7291a92381e9b4de9050b0a65ce5d6a763333406861e33906b8aa4906bf", size = 3243990 },
{ url = "https://files.pythonhosted.org/packages/bf/e1/3ccb13c643399d22289c6a9786c1a91e3dcbb68bce4beb44926ac2c557bf/sqlalchemy-2.0.45-py3-none-any.whl", hash = "sha256:5225a288e4c8cc2308dbdd874edad6e7d0fd38eac1e9e5f23503425c8eee20d0", size = 1936672 }, { url = "https://files.pythonhosted.org/packages/a8/87/5d1b23548f420ff823c236f8bea36b1a997250fd2f892e44a3838ca424f4/sqlalchemy-2.0.46-cp314-cp314-win32.whl", hash = "sha256:70ed2830b169a9960193f4d4322d22be5c0925357d82cbf485b3369893350908", size = 2114215 },
{ url = "https://files.pythonhosted.org/packages/3a/20/555f39cbcf0c10cf452988b6a93c2a12495035f68b3dbd1a408531049d31/sqlalchemy-2.0.46-cp314-cp314-win_amd64.whl", hash = "sha256:3c32e993bc57be6d177f7d5d31edb93f30726d798ad86ff9066d75d9bf2e0b6b", size = 2139867 },
{ url = "https://files.pythonhosted.org/packages/3e/f0/f96c8057c982d9d8a7a68f45d69c674bc6f78cad401099692fe16521640a/sqlalchemy-2.0.46-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4dafb537740eef640c4d6a7c254611dca2df87eaf6d14d6a5fca9d1f4c3fc0fa", size = 3561202 },
{ url = "https://files.pythonhosted.org/packages/d7/53/3b37dda0a5b137f21ef608d8dfc77b08477bab0fe2ac9d3e0a66eaeab6fc/sqlalchemy-2.0.46-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:42a1643dc5427b69aca967dae540a90b0fbf57eaf248f13a90ea5930e0966863", size = 3526296 },
{ url = "https://files.pythonhosted.org/packages/33/75/f28622ba6dde79cd545055ea7bd4062dc934e0621f7b3be2891f8563f8de/sqlalchemy-2.0.46-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ff33c6e6ad006bbc0f34f5faf941cfc62c45841c64c0a058ac38c799f15b5ede", size = 3470008 },
{ url = "https://files.pythonhosted.org/packages/a9/42/4afecbbc38d5e99b18acef446453c76eec6fbd03db0a457a12a056836e22/sqlalchemy-2.0.46-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:82ec52100ec1e6ec671563bbd02d7c7c8d0b9e71a0723c72f22ecf52d1755330", size = 3476137 },
{ url = "https://files.pythonhosted.org/packages/fc/a1/9c4efa03300926601c19c18582531b45aededfb961ab3c3585f1e24f120b/sqlalchemy-2.0.46-py3-none-any.whl", hash = "sha256:f9c11766e7e7c0a2767dda5acb006a118640c9fc0a4104214b96269bfb78399e", size = 1937882 },
] ]
[[package]] [[package]]
@ -1698,6 +1748,23 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/41/bf/945d527ff706233636c73880b22c7c953f3faeb9d6c7e2e85bfbfd0134a0/trio-0.32.0-py3-none-any.whl", hash = "sha256:4ab65984ef8370b79a76659ec87aa3a30c5c7c83ff250b4de88c29a8ab6123c5", size = 512030 }, { url = "https://files.pythonhosted.org/packages/41/bf/945d527ff706233636c73880b22c7c953f3faeb9d6c7e2e85bfbfd0134a0/trio-0.32.0-py3-none-any.whl", hash = "sha256:4ab65984ef8370b79a76659ec87aa3a30c5c7c83ff250b4de88c29a8ab6123c5", size = 512030 },
] ]
[[package]]
name = "trio-typing"
version = "0.10.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "async-generator" },
{ name = "importlib-metadata" },
{ name = "mypy-extensions" },
{ name = "packaging" },
{ name = "trio" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b5/74/a87aafa40ec3a37089148b859892cbe2eef08d132c816d58a60459be5337/trio-typing-0.10.0.tar.gz", hash = "sha256:065ee684296d52a8ab0e2374666301aec36ee5747ac0e7a61f230250f8907ac3", size = 38747 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/89/ff/9bd795273eb14fac7f6a59d16cc8c4d0948a619a1193d375437c7f50f3eb/trio_typing-0.10.0-py3-none-any.whl", hash = "sha256:6d0e7ec9d837a2fe03591031a172533fbf4a1a95baf369edebfc51d5a49f0264", size = 42224 },
]
[[package]] [[package]]
name = "trio-websocket" name = "trio-websocket"
version = "0.12.2" version = "0.12.2"
@ -1712,6 +1779,24 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/c7/19/eb640a397bba49ba49ef9dbe2e7e5c04202ba045b6ce2ec36e9cadc51e04/trio_websocket-0.12.2-py3-none-any.whl", hash = "sha256:df605665f1db533f4a386c94525870851096a223adcb97f72a07e8b4beba45b6", size = 21221 }, { url = "https://files.pythonhosted.org/packages/c7/19/eb640a397bba49ba49ef9dbe2e7e5c04202ba045b6ce2ec36e9cadc51e04/trio_websocket-0.12.2-py3-none-any.whl", hash = "sha256:df605665f1db533f4a386c94525870851096a223adcb97f72a07e8b4beba45b6", size = 21221 },
] ]
[[package]]
name = "types-certifi"
version = "2021.10.8.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/52/68/943c3aeaf14624712a0357c4a67814dba5cea36d194f5c764dad7959a00c/types-certifi-2021.10.8.3.tar.gz", hash = "sha256:72cf7798d165bc0b76e1c10dd1ea3097c7063c42c21d664523b928e88b554a4f", size = 2095 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b5/63/2463d89481e811f007b0e1cd0a91e52e141b47f9de724d20db7b861dcfec/types_certifi-2021.10.8.3-py3-none-any.whl", hash = "sha256:b2d1e325e69f71f7c78e5943d410e650b4707bb0ef32e4ddf3da37f54176e88a", size = 2136 },
]
[[package]]
name = "types-urllib3"
version = "1.26.25.14"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/73/de/b9d7a68ad39092368fb21dd6194b362b98a1daeea5dcfef5e1adb5031c7e/types-urllib3-1.26.25.14.tar.gz", hash = "sha256:229b7f577c951b8c1b92c1bc2b2fdb0b49847bd2af6d1cc2a2e3dd340f3bda8f", size = 11239 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/11/7b/3fc711b2efea5e85a7a0bbfe269ea944aa767bbba5ec52f9ee45d362ccf3/types_urllib3-1.26.25.14-py3-none-any.whl", hash = "sha256:9683bbb7fb72e32bfe9d2be6e04875fbe1b3eeec3cbb4ea231435aa7fd6b4f0e", size = 15377 },
]
[[package]] [[package]]
name = "typing-extensions" name = "typing-extensions"
version = "4.15.0" version = "4.15.0"
@ -1770,11 +1855,11 @@ socks = [
[[package]] [[package]]
name = "w3lib" name = "w3lib"
version = "2.3.1" version = "2.4.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/bf/7d/1172cfaa1e29beb9bf938e484c122b3bdc82e8e37b17a4f753ba6d6e009f/w3lib-2.3.1.tar.gz", hash = "sha256:5c8ac02a3027576174c2b61eb9a2170ba1b197cae767080771b6f1febda249a4", size = 49531 } sdist = { url = "https://files.pythonhosted.org/packages/f1/91/186665abf1a6d16c0c5ea1f0e681d9c852b45c3a750aa8657f8f956690a8/w3lib-2.4.0.tar.gz", hash = "sha256:e233ad21649b69d0e047a10f30181ae9677524a29f6f71f6f3c758dc0c8d2648", size = 48302 }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/58/dd/56f0d8af71e475ed194d702f8b4cf9cea812c95e82ad823d239023c6558c/w3lib-2.3.1-py3-none-any.whl", hash = "sha256:9ccd2ae10c8c41c7279cd8ad4fe65f834be894fe7bfdd7304b991fd69325847b", size = 21751 }, { url = "https://files.pythonhosted.org/packages/2e/f5/ce3ab627e0cb51591c9e3dc4b9b173f15d7f2bec1c0010420b15fc442940/w3lib-2.4.0-py3-none-any.whl", hash = "sha256:260b5a22aeb86ae73213857f69ed20829a45150f8d5b12050b1f02ada414db79", size = 21603 },
] ]
[[package]] [[package]]
@ -1914,11 +1999,11 @@ wheels = [
[[package]] [[package]]
name = "yt-dlp" name = "yt-dlp"
version = "2025.12.8" version = "2026.2.4"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/14/77/db924ebbd99d0b2b571c184cb08ed232cf4906c6f9b76eed763cd2c84170/yt_dlp-2025.12.8.tar.gz", hash = "sha256:b773c81bb6b71cb2c111cfb859f453c7a71cf2ef44eff234ff155877184c3e4f", size = 3088947 } sdist = { url = "https://files.pythonhosted.org/packages/16/be/8e099f3f34bac6851490525fb1a8b62d525a95fcb5af082e8c52ba884fb5/yt_dlp-2026.2.4.tar.gz", hash = "sha256:24733ef081116f29d8ee6eae7a48127101e6c56eb7aa228dd604a60654760022", size = 3100305 }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/6e/2f/98c3596ad923f8efd32c90dca62e241e8ad9efcebf20831173c357042ba0/yt_dlp-2025.12.8-py3-none-any.whl", hash = "sha256:36e2584342e409cfbfa0b5e61448a1c5189e345cf4564294456ee509e7d3e065", size = 3291464 }, { url = "https://files.pythonhosted.org/packages/96/38/b17cbeaf6712a4c1b97f7f9ec3a55f3a8ddee678cc88742af47dca0315b7/yt_dlp-2026.2.4-py3-none-any.whl", hash = "sha256:d6ea83257e8127a0097b1d37ee36201f99a292067e4616b2e5d51ab153b3dbb9", size = 3299165 },
] ]
[package.optional-dependencies] [package.optional-dependencies]
@ -1936,11 +2021,11 @@ default = [
[[package]] [[package]]
name = "yt-dlp-ejs" name = "yt-dlp-ejs"
version = "0.3.2" version = "0.4.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/de/72/57d02cf78eb45126bd171298d6a58a5bd48ce1a398b6b7ff00fc904f1f0c/yt_dlp_ejs-0.3.2.tar.gz", hash = "sha256:31a41292799992bdc913e03c9fac2a8c90c82a5cbbc792b2e3373b01da841e3e", size = 34678 } sdist = { url = "https://files.pythonhosted.org/packages/e9/80/4b6c7f91b373e01cdc18080f41fa399592945abce7db74c2e6d0fb8468db/yt_dlp_ejs-0.4.0.tar.gz", hash = "sha256:3c67e0beb6f9f3603fbcb56f425eabaa37c52243d90d20ccbcce1dd941cfbd07", size = 96768 }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/9d/0d/1f0d7a735ca60b87953271b15d00eff5eef05f6118390ddf6f81982526ed/yt_dlp_ejs-0.3.2-py3-none-any.whl", hash = "sha256:f2dc6b3d1b909af1f13e021621b0af048056fca5fb07c4db6aa9bbb37a4f66a9", size = 53252 }, { url = "https://files.pythonhosted.org/packages/a4/90/8911146822364666be47f184c4180cec20fcc537a268ef40d1ab077dd25b/yt_dlp_ejs-0.4.0-py3-none-any.whl", hash = "sha256:19278cff397b243074df46342bb7616c404296aeaff01986b62b4e21823b0b9c", size = 53600 },
] ]
[[package]] [[package]]
@ -1993,6 +2078,7 @@ installer = [
[package.dev-dependencies] [package.dev-dependencies]
dev = [ dev = [
{ name = "debugpy" }, { name = "debugpy" },
{ name = "pip" },
{ name = "pytest" }, { name = "pytest" },
{ name = "pytest-asyncio" }, { name = "pytest-asyncio" },
{ name = "ruff" }, { name = "ruff" },
@ -2042,11 +2128,21 @@ provides-extras = ["installer"]
[package.metadata.requires-dev] [package.metadata.requires-dev]
dev = [ dev = [
{ name = "debugpy", specifier = ">=1.8.16" }, { name = "debugpy", specifier = ">=1.8.16" },
{ name = "pip", specifier = ">=26.0.0" },
{ name = "pytest", specifier = ">=8.4.2" }, { name = "pytest", specifier = ">=8.4.2" },
{ name = "pytest-asyncio", specifier = ">=1.1.0" }, { name = "pytest-asyncio", specifier = ">=1.1.0" },
{ name = "ruff", specifier = ">=0.13.0" }, { name = "ruff", specifier = ">=0.13.0" },
] ]
[[package]]
name = "zipp"
version = "3.23.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276 },
]
[[package]] [[package]]
name = "zipstream-ng" name = "zipstream-ng"
version = "1.9.0" version = "1.9.0"