Completed the events system upgrade and expanded the notifications events with support for apprise.

This commit is contained in:
arabcoders 2025-07-19 19:45:07 +03:00
parent 0f7f22bdbf
commit a7e8d242c9
19 changed files with 1639 additions and 1110 deletions

View file

@ -14,7 +14,7 @@ live streams, and includes features like scheduling downloads, sending notificat
* Random beautiful background. `can be disabled or source changed`. * Random beautiful background. `can be disabled or source changed`.
* Can handle live streams. * Can handle live streams.
* Scheduler to queue channels or playlists to be downloaded automatically at a specified time. * Scheduler to queue channels or playlists to be downloaded automatically at a specified time.
* Send notification to targets based on selected events. * Send notification to targets based on selected events. includes [Apprise](https://github.com/caronc/apprise?tab=readme-ov-file#readme) support for non http/https URLs.
* Support per link `cli options` & `cookies`. * Support per link `cli options` & `cookies`.
* Queue multiple URLs separated by comma. * Queue multiple URLs separated by comma.
* Presets system to re-use frequently used yt-dlp options. * Presets system to re-use frequently used yt-dlp options.
@ -31,13 +31,7 @@ live streams, and includes features like scheduling downloads, sending notificat
* Automatic upcoming live stream re-queue. * Automatic upcoming live stream re-queue.
* Apply `yt-dlp` options per custom defined conditions. * Apply `yt-dlp` options per custom defined conditions.
* Custom browser extensions, bookmarklets and iOS shortcuts to send links to YTPTube instance. * Custom browser extensions, bookmarklets and iOS shortcuts to send links to YTPTube instance.
* A executable for Windows, macOS and Linux, which can be found in the release page.
### Side project
We have a side project related to YTPTube, which takes YTPTube source code and build an executable for Windows, macOS and Linux.
The project is in early stages, and might not even work yet. However keep an eye at [This build action](https://github.com/arabcoders/ytptube/actions/workflows/native-build.yml) for builds, simply select last successful build and download the executable for your platform. Help us test out the platforms
and report any issues you might find. We only have windows/linux machines to test on, so we need your help to test it out on macOS.
# Installation # Installation

View file

@ -4,9 +4,15 @@ import logging
import threading import threading
from queue import Empty, Queue from queue import Empty, Queue
from aiohttp import web
from .Singleton import Singleton from .Singleton import Singleton
LOG: logging.Logger = logging.getLogger(__name__) LOG: logging.Logger = logging.getLogger("BackgroundWorker")
class CloseThread:
pass
class BackgroundWorker(metaclass=Singleton): class BackgroundWorker(metaclass=Singleton):
@ -19,12 +25,30 @@ class BackgroundWorker(metaclass=Singleton):
_instance = None _instance = None
"""The instance of the Notification class.""" """The instance of the Notification class."""
thread: threading.Thread
"""The thread that runs the background worker."""
def __init__(self): def __init__(self):
self.queue = Queue() self.queue = Queue()
self.running = True self.running = True
def attach(self, app: web.Application):
app.on_shutdown.append(self.on_shutdown)
LOG.debug("Starting background worker...")
self.thread = threading.Thread(target=self._run, daemon=True) self.thread = threading.Thread(target=self._run, daemon=True)
self.thread.start() self.thread.start()
async def on_shutdown(self, _: web.Application):
self.running = False
try:
LOG.debug("Shutting down background worker...")
self.queue.put((CloseThread, (), {}))
self.thread.join(timeout=5)
LOG.debug("Background worker has been shut down.")
except Exception as e:
LOG.error(f"Failed to shut down background worker: {e}")
@staticmethod @staticmethod
def get_instance() -> "BackgroundWorker": def get_instance() -> "BackgroundWorker":
if BackgroundWorker._instance is None: if BackgroundWorker._instance is None:
@ -40,15 +64,20 @@ class BackgroundWorker(metaclass=Singleton):
def _loop_runner(): def _loop_runner():
try: try:
loop.run_forever() loop.run_forever()
except Exception as e: except Exception:
LOG.exception("Loop error: %s", e) pass
threading.Thread(target=_loop_runner, daemon=True, name="Background Runner").start() loop_thread = threading.Thread(target=_loop_runner, daemon=True, name="Background Runner")
loop_thread.start()
while self.running: while self.running:
try: try:
fn, args, kwargs = self.queue.get(timeout=1) fn, args, kwargs = self.queue.get(timeout=1)
try: try:
if isinstance(fn, CloseThread):
LOG.info("Received shutdown signal for background worker.")
break
result = fn(*args, **kwargs) result = fn(*args, **kwargs)
if inspect.iscoroutine(result): if inspect.iscoroutine(result):
loop.call_soon_threadsafe(loop.create_task, result) loop.call_soon_threadsafe(loop.create_task, result)
@ -58,9 +87,13 @@ class BackgroundWorker(metaclass=Singleton):
except Empty: except Empty:
continue continue
try:
loop.call_soon_threadsafe(loop.stop)
loop_thread.join(timeout=5)
loop.close()
LOG.debug("Event loop has been stopped and closed.")
except Exception as e:
LOG.error(f"Failed to stop the event loop: {e!s}")
def submit(self, fn, *args, **kwargs): def submit(self, fn, *args, **kwargs):
self.queue.put((fn, args, kwargs)) self.queue.put((fn, args, kwargs))
def shutdown(self):
self.running = False
self.thread.join()

View file

@ -18,7 +18,6 @@ LOG = logging.getLogger("datastore")
class StoreType(str, Enum): class StoreType(str, Enum):
DONE = "done" DONE = "done"
QUEUE = "queue" QUEUE = "queue"
PENDING = "pending"
@classmethod @classmethod
def all(cls) -> list[str]: def all(cls) -> list[str]:

View file

@ -331,7 +331,7 @@ class Download:
self.proc.start() self.proc.start()
self.info.status = "preparing" self.info.status = "preparing"
await self._notify.emit(Events.UPDATED, data=self.info) await self._notify.emit(Events.ITEM_UPDATED, data=self.info)
asyncio.create_task(self.progress_update(), name=f"update-{self.id}") asyncio.create_task(self.progress_update(), name=f"update-{self.id}")
return await asyncio.get_running_loop().run_in_executor(None, self.proc.join) return await asyncio.get_running_loop().run_in_executor(None, self.proc.join)
@ -465,7 +465,7 @@ class Download:
self.logger.debug(f"Status Update: {self.info._id=} {status=}") self.logger.debug(f"Status Update: {self.info._id=} {status=}")
if isinstance(status, str): if isinstance(status, str):
await self._notify.emit(Events.UPDATED, data=self.info) await self._notify.emit(Events.ITEM_UPDATED, data=self.info)
continue continue
self.tmpfilename = status.get("tmpfilename") self.tmpfilename = status.get("tmpfilename")
@ -529,7 +529,7 @@ class Download:
self.logger.exception(e) self.logger.exception(e)
self.logger.error(f"Failed to run ffprobe. {status.get}. {e}") self.logger.error(f"Failed to run ffprobe. {status.get}. {e}")
await self._notify.emit(Events.UPDATED, data=self.info) await self._notify.emit(Events.ITEM_UPDATED, data=self.info)
def is_stale(self) -> bool: def is_stale(self) -> bool:
""" """

View file

@ -65,9 +65,6 @@ class DownloadQueue(metaclass=Singleton):
done: DataStore done: DataStore
"""DataStore for the completed downloads.""" """DataStore for the completed downloads."""
pending: DataStore
"""DataStore for the pending downloads."""
workers: asyncio.Semaphore workers: asyncio.Semaphore
"""Semaphore to limit the number of concurrent downloads.""" """Semaphore to limit the number of concurrent downloads."""
@ -179,7 +176,15 @@ class DownloadQueue(metaclass=Singleton):
item.info.auto_start = True item.info.auto_start = True
updated = self.queue.put(item) updated = self.queue.put(item)
tasks.append(self._notify.emit(Events.UPDATED, data=updated.info)) tasks.append(self._notify.emit(Events.ITEM_UPDATED, data=updated.info))
tasks.append(
self._notify.emit(
Events.ITEM_RESUMED,
data=item.info,
title="Download Resumed",
message=f"Download '{item.info.title}' has been resumed.",
)
)
status[item_id] = "started" status[item_id] = "started"
started = True started = True
LOG.debug(f"Item {item.info.name()} marked as started.") LOG.debug(f"Item {item.info.name()} marked as started.")
@ -227,7 +232,15 @@ class DownloadQueue(metaclass=Singleton):
item.info.auto_start = False item.info.auto_start = False
updated = self.queue.put(item) updated = self.queue.put(item)
tasks.append(self._notify.emit(Events.UPDATED, data=updated.info)) tasks.append(self._notify.emit(Events.ITEM_UPDATED, data=updated.info))
tasks.append(
self._notify.emit(
Events.ITEM_PAUSED,
data=item.info,
title="Download Paused",
message=f"Download '{item.info.title}' has been paused.",
)
)
status[item_id] = "paused" status[item_id] = "paused"
LOG.debug(f"Item {item.info.name()} marked as paused.") LOG.debug(f"Item {item.info.name()} marked as paused.")
@ -450,60 +463,56 @@ class DownloadQueue(metaclass=Singleton):
try: try:
dlInfo: Download = Download(info=dl, info_dict=entry if item.auto_start else None, logs=logs) dlInfo: Download = Download(info=dl, info_dict=entry if item.auto_start else None, logs=logs)
notifyTitle: str | None = None nEvent: str | None = None
notifyMessage: str | None = None nTitle: str | None = None
nMessage: str | None = None
nStore: str = "queue"
text_logs: str = "" text_logs: str = ""
if filtered_logs := extract_ytdlp_logs(logs): if filtered_logs := extract_ytdlp_logs(logs):
text_logs = f" Logs: {', '.join(filtered_logs)}" text_logs = ", ".join(filtered_logs)
if "is_upcoming" == entry.get("live_status"): if "is_upcoming" == entry.get("live_status"):
notifyEvent = Events.COMPLETED nEvent = Events.ITEM_MOVED
notifyTitle = "Upcoming Premiere" if is_premiere else "Upcoming Live Stream" nStore = "history"
notifyMessage = f"{'Premiere video' if is_premiere else 'Stream' } '{dlInfo.info.title}' is not available yet. {text_logs}" nTitle = "Upcoming Premiere" if is_premiere else "Upcoming Live Stream"
nMessage = f"{'Premiere video' if is_premiere else 'Stream' } '{dlInfo.info.title}' is not available yet. {text_logs}"
dlInfo.info.status = "not_live" dlInfo.info.status = "not_live"
dlInfo.info.msg = notifyMessage.replace(f" '{dlInfo.info.title}'", "") dlInfo.info.msg = nMessage.replace(f" '{dlInfo.info.title}'", "")
await self._notify.emit(Events.LOG_INFO, data={"lowPriority": True}, title=nTitle, message=nMessage)
await self._notify.emit(
Events.LOG_INFO,
data={"lowPriority": True},
title=notifyTitle,
message=notifyMessage,
)
itemDownload: Download = self.done.put(dlInfo) itemDownload: Download = self.done.put(dlInfo)
elif len(entry.get("formats", [])) < 1: elif len(entry.get("formats", [])) < 1:
ava: str = entry.get("availability", "public") ava: str = entry.get("availability", "public")
notifyTitle = "Download Error" nTitle = "Download Error"
notifyMessage: str = f"No formats for '{dl.title}'." nMessage: str = f"No formats for '{dl.title}'."
if ava and ava not in ("public",): nEvent = Events.ITEM_MOVED
notifyMessage += f" Availability is set for '{ava}'." nStore = "history"
dlInfo.info.error = notifyMessage.replace(f" for '{dl.title}'.", ".") + text_logs if ava and ava not in ("public",):
nMessage += f" Availability is set for '{ava}'."
dlInfo.info.error = nMessage.replace(f" for '{dl.title}'.", ".") + text_logs
dlInfo.info.status = "error" dlInfo.info.status = "error"
itemDownload = self.done.put(dlInfo) itemDownload = self.done.put(dlInfo)
notifyEvent = Events.COMPLETED
await self._notify.emit( await self._notify.emit(Events.LOG_WARNING, data={"logs": text_logs}, title=nTitle, message=nMessage)
Events.LOG_WARNING,
data={"logs": text_logs},
title=notifyTitle,
message=notifyMessage,
)
elif is_premiere and self.config.prevent_live_premiere: elif is_premiere and self.config.prevent_live_premiere:
notifyTitle = "Premiere Video" nStore = "history"
nTitle = "Premiere Video"
dlInfo.info.error = "Premiering right now." dlInfo.info.error = "Premiering right now."
_requeue = True _requeue = True
if release_in: if release_in:
try: try:
starts_in = str_to_dt(release_in) starts_in: datetime = str_to_dt(release_in)
starts_in = ( starts_in: datetime = (
starts_in.replace(tzinfo=UTC) if starts_in.tzinfo is None else starts_in.astimezone(UTC) starts_in.replace(tzinfo=UTC) if starts_in.tzinfo is None else starts_in.astimezone(UTC)
) )
starts_in = starts_in + timedelta(minutes=5, seconds=dl.extras.get("duration", 0)) starts_in = starts_in + timedelta(minutes=5, seconds=dl.extras.get("duration", 0))
dlInfo.info.error += f" Download will start at {starts_in.astimezone().isoformat()}." dlInfo.info.error += f" Download will start at {starts_in.astimezone().isoformat()}."
notifyMessage = dlInfo.info.error.strip() nMessage = dlInfo.info.error.strip()
_requeue = False _requeue = False
except Exception as e: except Exception as e:
LOG.error(f"Failed to parse live_in date '{release_in}'. {e!s}") LOG.error(f"Failed to parse live_in date '{release_in}'. {e!s}")
@ -511,24 +520,25 @@ class DownloadQueue(metaclass=Singleton):
else: else:
dlInfo.info.error += f" Delaying download by '{300+dl.extras.get('duration',0)}' seconds." dlInfo.info.error += f" Delaying download by '{300+dl.extras.get('duration',0)}' seconds."
notifyMessage = dlInfo.info.error.strip() nMessage = dlInfo.info.error.strip()
if _requeue: if _requeue:
notifyEvent = Events.ADDED nEvent = Events.ITEM_ADDED
itemDownload = self.queue.put(dlInfo) itemDownload = self.queue.put(dlInfo)
if item.auto_start: if item.auto_start:
self.event.set() self.event.set()
else: else:
dlInfo.info.status = "not_live" dlInfo.info.status = "not_live"
itemDownload = self.done.put(dlInfo) itemDownload = self.done.put(dlInfo)
notifyEvent = Events.COMPLETED nStore = "history"
notifyTitle = "Item Not Live" nEvent = Events.ITEM_MOVED
notifyMessage = f"Item '{dlInfo.info.title}' is not live." nTitle = "Item Not Live"
await self._notify.emit(Events.LOG_INFO, title=notifyTitle, message=notifyMessage) nMessage = f"Item '{dlInfo.info.title}' is not live."
await self._notify.emit(Events.LOG_INFO, title=nTitle, message=nMessage)
else: else:
notifyEvent = Events.ADDED nEvent = Events.ITEM_ADDED
notifyTitle = "Item Added" nTitle = "Item Added"
notifyMessage = f"Item '{dlInfo.info.title}' has been added to the download queue." nMessage = f"Item '{dlInfo.info.title}' has been added to the download queue."
itemDownload = self.queue.put(dlInfo) itemDownload = self.queue.put(dlInfo)
if item.auto_start: if item.auto_start:
self.event.set() self.event.set()
@ -536,10 +546,10 @@ class DownloadQueue(metaclass=Singleton):
LOG.debug(f"Item {itemDownload.info.name()} is not set to auto-start.") LOG.debug(f"Item {itemDownload.info.name()} is not set to auto-start.")
await self._notify.emit( await self._notify.emit(
notifyEvent, nEvent,
data=itemDownload.info.serialize(), data={"to": nStore, "item": itemDownload.info} if Events.ITEM_MOVED == nEvent else itemDownload.info,
title=notifyTitle, title=nTitle,
message=notifyMessage, message=nMessage,
) )
return {"status": "ok"} return {"status": "ok"}
@ -746,7 +756,7 @@ class DownloadQueue(metaclass=Singleton):
LOG.debug(f"Deleting from queue {item_ref}") LOG.debug(f"Deleting from queue {item_ref}")
self.queue.delete(id) self.queue.delete(id)
await self._notify.emit( await self._notify.emit(
Events.CANCELLED, Events.ITEM_CANCELLED,
data=item.info, data=item.info,
title="Download Cancelled", title="Download Cancelled",
message=f"Download '{item.info.title}' has been cancelled.", message=f"Download '{item.info.title}' has been cancelled.",
@ -754,8 +764,8 @@ class DownloadQueue(metaclass=Singleton):
item.info.status = "cancelled" item.info.status = "cancelled"
self.done.put(item) self.done.put(item)
await self._notify.emit( await self._notify.emit(
Events.COMPLETED, Events.ITEM_MOVED,
data=item.info, data={"to": "history", "item": item.info},
title="Download Cancelled", title="Download Cancelled",
message=f"Download '{item.info.title}' has been cancelled.", message=f"Download '{item.info.title}' has been cancelled.",
) )
@ -829,7 +839,7 @@ class DownloadQueue(metaclass=Singleton):
_status: str = "Removed" if removed_files > 0 else "Cleared" _status: str = "Removed" if removed_files > 0 else "Cleared"
await self._notify.emit( await self._notify.emit(
Events.CLEARED, Events.ITEM_DELETED,
data=item.info, data=item.info,
title=f"Download {_status}", title=f"Download {_status}",
message=f"{_status} '{item.info.title}' from history.", message=f"{_status} '{item.info.title}' from history.",
@ -946,7 +956,7 @@ class DownloadQueue(metaclass=Singleton):
None None
""" """
filePath = calc_download_path(base_path=self.config.download_path, folder=entry.info.folder) filePath: str = calc_download_path(base_path=self.config.download_path, folder=entry.info.folder)
LOG.info(f"Downloading 'id: {id}', 'Title: {entry.info.title}', 'URL: {entry.info.url}' To '{filePath}'.") LOG.info(f"Downloading 'id: {id}', 'Title: {entry.info.title}', 'URL: {entry.info.url}' To '{filePath}'.")
try: try:
@ -962,28 +972,35 @@ class DownloadQueue(metaclass=Singleton):
await entry.close() await entry.close()
if self.queue.exists(key=id): if self.queue.exists(key=id):
_tasks = []
LOG.debug(f"Download Task '{id}' is completed. Removing from queue.") LOG.debug(f"Download Task '{id}' is completed. Removing from queue.")
self.queue.delete(key=id) self.queue.delete(key=id)
notifyTitle: str | None = None nTitle: str | None = None
notifyMessage: str | None = None nMessage: str | None = None
if entry.is_cancelled() is True: if entry.is_cancelled() is True:
await self._notify.emit( nTitle = "Download Cancelled"
Events.CANCELLED, nMessage = f"Download '{entry.info.title}' has been cancelled."
data=entry.info, await self._notify.emit(Events.ITEM_CANCELLED, data=entry.info, title=nTitle, message=nMessage)
title="Download Cancelled",
message=f"Download '{entry.info.title}' has been cancelled.",
)
entry.info.status = "cancelled" entry.info.status = "cancelled"
notifyTitle = "Download Cancelled"
notifyMessage = f"Download '{entry.info.title}' has been cancelled." if entry.info.status == "finished" and entry.info.filename:
elif entry.info.status == "finished": nTitle = "Download Completed"
notifyTitle = "Download Completed" nMessage = f"Download '{entry.info.title}' has been finished."
notifyMessage = f"Download '{entry.info.title}' has been completed." _tasks.append(self._notify.emit(Events.ITEM_COMPLETED, data=entry.info, title=nTitle, message=nMessage))
self.done.put(value=entry) self.done.put(value=entry)
await self._notify.emit(Events.COMPLETED, data=entry.info, title=notifyTitle, message=notifyMessage) _tasks.append(
self._notify.emit(
Events.ITEM_MOVED,
data={"to": "history", "item": entry.info},
title=nTitle,
message=nMessage,
)
)
await asyncio.gather(*_tasks)
else: else:
LOG.warning(f"Download '{id}' not found in queue.") LOG.warning(f"Download '{id}' not found in queue.")

View file

@ -17,53 +17,54 @@ class Events:
The events that can be emitted. The events that can be emitted.
""" """
STARTUP = "startup" STARTUP: str = "startup"
LOADED = "loaded" LOADED: str = "loaded"
STARTED = "started" STARTED: str = "started"
SHUTDOWN = "shutdown" SHUTDOWN: str = "shutdown"
ADDED = "added" CONNECTED: str = "connected"
UPDATE = "update"
UPDATED = "updated"
COMPLETED = "completed"
CANCELLED = "cancelled"
CLEARED = "cleared"
CONNECTED = "connected"
STATUS = "status"
LOG_INFO = "log_info" LOG_INFO: str = "log_info"
LOG_WARNING = "log_warning" LOG_WARNING: str = "log_warning"
LOG_ERROR = "log_error" LOG_ERROR: str = "log_error"
LOG_SUCCESS = "log_success" LOG_SUCCESS: str = "log_success"
ITEM_DELETE = "item_delete" ITEM_ADDED: str = "item_added"
ITEM_CANCEL = "item_cancel" ITEM_UPDATED: str = "item_updated"
ITEM_ERROR = "item_error" ITEM_COMPLETED: str = "item_completed"
ITEM_CANCELLED: str = "item_cancelled"
ITEM_DELETED: str = "item_deleted"
ITEM_PAUSED: str = "item_paused"
ITEM_RESUMED: str = "item_resumed"
ITEM_MOVED: str = "item_moved"
ITEM_STATUS: str = "item_status"
ITEM_ERROR: str = "item_error"
TEST = "test" TEST: str = "test"
ADD_URL = "add_url" ADD_URL: str = "add_url"
PAUSED = "paused" PAUSED: str = "paused"
RESUMED: str = "resumed"
CLI_POST = "cli_post" CLI_POST: str = "cli_post"
CLI_CLOSE = "cli_close" CLI_CLOSE: str = "cli_close"
CLI_OUTPUT = "cli_output" CLI_OUTPUT: str = "cli_output"
TASKS_ADD = "task_add" TASKS_ADD: str = "task_add"
TASK_DISPATCHED = "task_dispatched" TASK_DISPATCHED: str = "task_dispatched"
TASK_FINISHED = "task_finished" TASK_FINISHED: str = "task_finished"
TASK_ERROR = "task_error" TASK_ERROR: str = "task_error"
PRESETS_ADD = "presets_add" PRESETS_ADD: str = "presets_add"
PRESETS_UPDATE = "presets_update" PRESETS_UPDATE: str = "presets_update"
SCHEDULE_ADD = "schedule_add" SCHEDULE_ADD: str = "schedule_add"
CONDITIONS_ADD = "conditions_add" CONDITIONS_ADD: str = "conditions_add"
CONDITIONS_UPDATE = "conditions_update" CONDITIONS_UPDATE: str = "conditions_update"
SUBSCRIBED = "subscribed" SUBSCRIBED: str = "subscribed"
UNSUBSCRIBED = "unsubscribed" UNSUBSCRIBED: str = "unsubscribed"
def get_all() -> list: def get_all() -> list:
""" """
@ -87,21 +88,22 @@ class Events:
""" """
return [ return [
Events.CONNECTED, Events.CONNECTED,
Events.ADDED,
Events.LOG_INFO, Events.LOG_INFO,
Events.LOG_WARNING, Events.LOG_WARNING,
Events.LOG_ERROR, Events.LOG_ERROR,
Events.LOG_SUCCESS, Events.LOG_SUCCESS,
Events.COMPLETED, Events.ITEM_ADDED,
Events.CANCELLED, Events.ITEM_UPDATED,
Events.CLEARED, Events.ITEM_COMPLETED,
Events.UPDATED, Events.ITEM_CANCELLED,
Events.UPDATE, Events.ITEM_DELETED,
Events.ITEM_MOVED,
Events.ITEM_STATUS,
Events.PAUSED, Events.PAUSED,
Events.PRESETS_UPDATE, Events.RESUMED,
Events.STATUS,
Events.CLI_CLOSE, Events.CLI_CLOSE,
Events.CLI_OUTPUT, Events.CLI_OUTPUT,
Events.PRESETS_UPDATE,
] ]
def only_debug() -> list: def only_debug() -> list:
@ -112,7 +114,7 @@ class Events:
list: The list of debug events. list: The list of debug events.
""" """
return [Events.UPDATED, Events.CLI_OUTPUT] return [Events.ITEM_UPDATED, Events.CLI_OUTPUT]
@dataclass(kw_only=True) @dataclass(kw_only=True)

View file

@ -90,15 +90,25 @@ class Target:
class NotificationEvents: class NotificationEvents:
ADDED = Events.ADDED TEST: str = Events.TEST
COMPLETED = Events.COMPLETED
CANCELLED = Events.CANCELLED ITEM_ADDED: str = Events.ITEM_ADDED
CLEARED = Events.CLEARED ITEM_COMPLETED: str = Events.ITEM_COMPLETED
LOG_INFO = Events.LOG_INFO ITEM_CANCELLED: str = Events.ITEM_CANCELLED
LOG_SUCCESS = Events.LOG_SUCCESS ITEM_DELETED: str = Events.ITEM_DELETED
LOG_WARNING = Events.LOG_WARNING ITEM_PAUSED: str = Events.ITEM_PAUSED
LOG_ERROR = Events.LOG_ERROR ITEM_RESUMED: str = Events.ITEM_RESUMED
TEST = Events.TEST ITEM_MOVED: str = Events.ITEM_MOVED
PAUSED: str = Events.PAUSED
RESUMED: str = Events.RESUMED
LOG_INFO: str = Events.LOG_INFO
LOG_SUCCESS: str = Events.LOG_SUCCESS
LOG_WARNING: str = Events.LOG_WARNING
LOG_ERROR: str = Events.LOG_ERROR
TASK_DISPATCHED: str = Events.TASK_DISPATCHED
@staticmethod @staticmethod
def get_events() -> dict[str, str]: def get_events() -> dict[str, str]:

View file

@ -11,12 +11,13 @@ from typing import Any
from aiohttp import web from aiohttp import web
from app.library.Services import Services
from .config import Config from .config import Config
from .DownloadQueue import DownloadQueue
from .encoder import Encoder from .encoder import Encoder
from .Events import EventBus, Events from .Events import EventBus, Events
from .ItemDTO import Item
from .Scheduler import Scheduler from .Scheduler import Scheduler
from .Services import Services
from .Singleton import Singleton from .Singleton import Singleton
from .Utils import init_class, validate_url from .Utils import init_class, validate_url
@ -76,6 +77,7 @@ class Tasks(metaclass=Singleton):
self._scheduler: Scheduler = scheduler or Scheduler.get_instance() self._scheduler: Scheduler = scheduler or Scheduler.get_instance()
self._notify: EventBus = EventBus.get_instance() self._notify: EventBus = EventBus.get_instance()
self._task_handler = HandleTask(self._scheduler, self, config) self._task_handler = HandleTask(self._scheduler, self, config)
self._downloadQueue = DownloadQueue.get_instance()
if self._file.exists() and "600" != self._file.stat().st_mode: if self._file.exists() and "600" != self._file.stat().st_mode:
try: try:
@ -316,31 +318,38 @@ class Tasks(metaclass=Singleton):
template: str = task.template if task.template else "" template: str = task.template if task.template else ""
cli: str = task.cli if task.cli else "" cli: str = task.cli if task.cli else ""
await self._notify.emit( status = await self._downloadQueue.add(
Events.ADD_URL, item=Item.format(
data={ {
"url": task.url, "url": task.url,
"preset": preset, "preset": preset,
"folder": folder, "folder": folder,
"template": template, "template": template,
"cli": cli, "cli": cli,
}, }
title="Tasks", )
message=f"Task '{task.name}' started at '{timeNow}'",
id=task.id,
) )
timeNow = datetime.now(UTC).isoformat() timeNow = datetime.now(UTC).isoformat()
ended: float = time.time() ended: float = time.time()
LOG.info(f"Task '{task.name}' completed at '{timeNow}' took '{ended - started:.2f}' seconds.") LOG.info(f"Task '{task.name}' completed at '{timeNow}' took '{ended - started:.2f}' seconds.")
await self._notify.emit( _tasks = [
Events.LOG_SUCCESS, self._notify.emit(
data={"lowPriority": True}, Events.TASK_DISPATCHED,
title="Task completed", data=status,
message=f"Task '{task.name}' completed in '{ended - started:.2f}'.", title=f"Task '{task.name}' dispatched",
) message=f"Task '{task.name}' dispatched at '{timeNow}'.",
),
self._notify.emit(
Events.LOG_SUCCESS,
data={"lowPriority": True},
title="Task completed",
message=f"Task '{task.name}' completed in '{ended - started:.2f}'.",
),
]
await asyncio.gather(*_tasks)
except Exception as e: except Exception as e:
LOG.error(f"Failed to execute '{task.name}' at '{timeNow}'. '{e!s}'.") LOG.error(f"Failed to execute '{task.name}' at '{timeNow}'. '{e!s}'.")
await self._notify.emit( await self._notify.emit(

View file

@ -1188,6 +1188,8 @@ def extract_ytdlp_logs(logs: list[str], filters: list[str | re.Pattern] = None)
"This video is available to this channel", "This video is available to this channel",
"Private video. Sign in if you've been granted access to this video", "Private video. Sign in if you've been granted access to this video",
"[youtube] Premieres in", "[youtube] Premieres in",
"Falling back on generic information extractor",
"URL could be a direct video link, returning it as such",
] + (filters or []) ] + (filters or [])
compiled: list[re.Pattern] = [ compiled: list[re.Pattern] = [

View file

@ -7,7 +7,7 @@ from typing import TYPE_CHECKING
from app.library.config import Config from app.library.config import Config
from app.library.DownloadQueue import DownloadQueue from app.library.DownloadQueue import DownloadQueue
from app.library.Events import EventBus, Events from app.library.Events import EventBus, Events
from app.library.ItemDTO import ItemDTO from app.library.ItemDTO import Item, ItemDTO
from app.library.Tasks import Task from app.library.Tasks import Task
from app.library.Utils import is_downloaded from app.library.Utils import is_downloaded
from app.library.YTDLPOpts import YTDLPOpts from app.library.YTDLPOpts import YTDLPOpts
@ -147,18 +147,18 @@ class YoutubeHandler:
template: str = task.template if task.template else "" template: str = task.template if task.template else ""
cli: str = task.cli if task.cli else "" cli: str = task.cli if task.cli else ""
queued = asyncio.gather(
*[
notify.emit(
Events.ADD_URL,
data={"url": item["url"], "preset": preset, "folder": folder, "template": template, "cli": cli},
)
for item in filtered
]
)
try: try:
await queued await asyncio.gather(
*[
notify.emit(
Events.ADD_URL,
data=Item.format(
{"url": item["url"], "preset": preset, "folder": folder, "template": template, "cli": cli}
).serialize(),
)
for item in filtered
]
)
except Exception as e: except Exception as e:
LOG.error(f"Error while adding items from '{task.id}: {task.name}'. {e!s}") LOG.error(f"Error while adding items from '{task.id}: {task.name}'. {e!s}")
return return

View file

@ -127,6 +127,7 @@ class Main:
Presets.get_instance().attach(self._app) Presets.get_instance().attach(self._app)
Notification.get_instance().attach(self._app) Notification.get_instance().attach(self._app)
Conditions.get_instance().attach(self._app) Conditions.get_instance().attach(self._app)
self._background_worker.attach(self._app)
EventBus.get_instance().sync_emit( EventBus.get_instance().sync_emit(
Events.LOADED, Events.LOADED,

View file

@ -137,7 +137,7 @@ async def item_update(request: Request, queue: DownloadQueue, encoder: Encoder,
if updated: if updated:
queue.done.put(item) queue.done.put(item)
await notify.emit(Events.UPDATE, data=item.info) await notify.emit(Events.ITEM_UPDATED, data=item.info)
return web.json_response( return web.json_response(
data=item.info, data=item.info,

View file

@ -23,7 +23,7 @@ async def pause(notify: EventBus, queue: DownloadQueue):
Events.PAUSED, Events.PAUSED,
data={"paused": True, "at": time.time()}, data={"paused": True, "at": time.time()},
title="Downloads Paused", title="Downloads Paused",
message="Download pool has been paused.", message="Non-active downloads have been paused.",
) )
@ -31,10 +31,10 @@ async def pause(notify: EventBus, queue: DownloadQueue):
async def resume(notify: EventBus, queue: DownloadQueue): async def resume(notify: EventBus, queue: DownloadQueue):
queue.resume() queue.resume()
await notify.emit( await notify.emit(
Events.PAUSED, Events.RESUMED,
data={"paused": False, "at": time.time()}, data={"paused": False, "at": time.time()},
title="Downloads Resumed", title="Downloads Resumed",
message="Download pool has been resumed.", message="Resumed all downloads.",
) )
@ -54,7 +54,7 @@ async def add_url(queue: DownloadQueue, notify: EventBus, sid: str, data: dict):
try: try:
status = await queue.add(item=Item.format(data)) status = await queue.add(item=Item.format(data))
await notify.emit( await notify.emit(
event=Events.STATUS, event=Events.ITEM_STATUS,
title="Adding URL", title="Adding URL",
message=f"Adding URL '{url}' to the download queue.", message=f"Adding URL '{url}' to the download queue.",
data=status, data=status,
@ -76,12 +76,26 @@ async def item_cancel(queue: DownloadQueue, notify: EventBus, sid: str, data: st
) )
return return
try:
item = queue.get_item(id=data)
except KeyError:
await notify.emit(
Events.LOG_ERROR,
title="Item Not Found",
message=f"Item with ID '{data}' not found.",
to=sid,
)
return
status: dict[str, str] = {} status: dict[str, str] = {}
status = await queue.cancel([data]) status = await queue.cancel([data])
status.update({"identifier": data}) status.update({"identifier": data})
await notify.emit( await notify.emit(
Events.ITEM_CANCEL, data=status, title="Item Cancelled", message=f"Item '{data}': has been cancelled." Events.ITEM_CANCELLED,
data=item.info,
title="Item Cancelled",
message=f"Cancelled '{item.info.title}'.",
) )
@ -106,23 +120,7 @@ async def item_delete(queue: DownloadQueue, notify: EventBus, sid: str, data: di
) )
return return
item = queue.get_item(id=id) await queue.clear([id], remove_file=bool(data.get("remove_file", False)))
if not item:
await notify.emit(
Events.LOG_ERROR,
title="Item Not Found",
message=f"Item with ID '{id}' not found.",
to=sid,
)
return
status: dict[str, str] = {}
status = await queue.clear([id], remove_file=bool(data.get("remove_file", False)))
status.update({"identifier": id})
await notify.emit(
Events.ITEM_DELETE, data=status, title="Item Deleted", message=f"Item '{item.info.title}': has been deleted."
)
@route(RouteType.SOCKET, "archive_item", "archive_item") @route(RouteType.SOCKET, "archive_item", "archive_item")

View file

@ -4,25 +4,25 @@
"type": "module", "type": "module",
"scripts": { "scripts": {
"build": "nuxt build", "build": "nuxt build",
"dev": "nuxt dev", "dev": "nuxt dev --host",
"generate": "nuxt generate", "generate": "nuxt generate",
"preview": "nuxt preview", "preview": "nuxt preview",
"postinstall": "nuxt prepare" "postinstall": "nuxt prepare"
}, },
"web-types": "./web-types.json", "web-types": "./web-types.json",
"dependencies": { "dependencies": {
"@pinia/nuxt": "^0.11.1", "@pinia/nuxt": "^0.11.2",
"@sentry/nuxt": "^9.35.0", "@sentry/nuxt": "^9.40.0",
"@vueuse/core": "^13.5.0", "@vueuse/core": "^13.5.0",
"@vueuse/nuxt": "^13.5.0", "@vueuse/nuxt": "^13.5.0",
"@xterm/addon-fit": "^0.10.0", "@xterm/addon-fit": "^0.10.0",
"@xterm/xterm": "^5.5.0", "@xterm/xterm": "^5.5.0",
"cron-parser": "^5.3.0", "cron-parser": "^5.3.0",
"cronstrue": "^3.0.0", "cronstrue": "^3.1.0",
"floating-vue": "^5.2.2", "floating-vue": "^5.2.2",
"hls.js": "^1.6.7", "hls.js": "^1.6.7",
"moment": "^2.30.1", "moment": "^2.30.1",
"nuxt": "^3.17.6", "nuxt": "^4.0.0",
"pinia": "^3.0.3", "pinia": "^3.0.3",
"socket.io-client": "^4.8.1", "socket.io-client": "^4.8.1",
"vue": "^3.5.17", "vue": "^3.5.17",

View file

@ -89,7 +89,7 @@ watch(() => config.app.basic_mode, async () => {
return return
} }
await navigateTo('/') await navigateTo('/')
}, { immediate: true }) })
watch(() => config.app.console_enabled, async () => { watch(() => config.app.console_enabled, async () => {
if (config.app.console_enabled) { if (config.app.console_enabled) {
@ -97,7 +97,7 @@ watch(() => config.app.console_enabled, async () => {
} }
toast.error('Console is disabled in the configuration. Please enable it to use this feature.') toast.error('Console is disabled in the configuration. Please enable it to use this feature.')
await navigateTo('/') await navigateTo('/')
}, { immediate: true }) })
const handle_event = () => { const handle_event = () => {
if (!terminal.value) { if (!terminal.value) {
@ -111,6 +111,12 @@ const runCommand = async () => {
return return
} }
if (config.app.basic_mode || !config.app.console_enabled) {
await navigateTo('/')
toast.error('Console is disabled in the configuration. Please enable it to use this feature.')
return
}
if (command.value.startsWith('yt-dlp')) { if (command.value.startsWith('yt-dlp')) {
command.value = command.value.replace(/^yt-dlp/, '').trim() command.value = command.value.replace(/^yt-dlp/, '').trim()
await nextTick() await nextTick()

View file

@ -397,7 +397,7 @@ watch(() => config.app.basic_mode, async () => {
watch(() => socket.isConnected, async () => { watch(() => socket.isConnected, async () => {
if (socket.isConnected && initialLoad.value) { if (socket.isConnected && initialLoad.value) {
socket.on('status', statusHandler) socket.on('item_status', statusHandler)
await reloadContent(true) await reloadContent(true)
initialLoad.value = false initialLoad.value = false
} }
@ -586,7 +586,7 @@ onMounted(async () => {
if (!socket.isConnected) { if (!socket.isConnected) {
return; return;
} }
socket.on('status', statusHandler) socket.on('item_status', statusHandler)
await reloadContent(true) await reloadContent(true)
}); });
@ -683,7 +683,7 @@ const runNow = async (item: task_item, mass: boolean = false) => {
}, 500) }, 500)
} }
onBeforeUnmount(() => socket.off('status', statusHandler)) onBeforeUnmount(() => socket.off('item_status', statusHandler))
const statusHandler = async (stream: string) => { const statusHandler = async (stream: string) => {
const json = JSON.parse(stream) const json = JSON.parse(stream)

File diff suppressed because it is too large Load diff

View file

@ -12,6 +12,21 @@ export const useSocketStore = defineStore('socket', () => {
const socket = ref<IOSocket | null>(null) const socket = ref<IOSocket | null>(null)
const isConnected = ref<boolean>(false) const isConnected = ref<boolean>(false)
const emit = (event: string, data?: any): any => socket.value?.emit(event, data)
const on = (event: string | string[], callback: (...args: any[]) => void, withEvent: boolean = false) => {
if (!Array.isArray(event)) {
event = [event]
}
event.forEach(e => socket.value?.on(e, (...args) => true === withEvent ? callback(e, ...args) : callback(...args)))
}
const off = (event: string | string[], callback: (...args: any[]) => void): any => {
if (!Array.isArray(event)) {
event = [event]
}
event.forEach(e => socket.value?.off(e, callback));
}
const connect = () => { const connect = () => {
let opts = { let opts = {
transports: ['websocket', 'polling'], transports: ['websocket', 'polling'],
@ -23,6 +38,8 @@ export const useSocketStore = defineStore('socket', () => {
if ('development' !== runtimeConfig.public?.APP_ENV) { if ('development' !== runtimeConfig.public?.APP_ENV) {
url = window.origin; url = window.origin;
opts.path = `${runtimeConfig.app.baseURL.replace(/\/$/, '')}/socket.io`; opts.path = `${runtimeConfig.app.baseURL.replace(/\/$/, '')}/socket.io`;
} else {
window.ws = socket.value;
} }
socket.value = io(url, opts) socket.value = io(url, opts)
@ -45,13 +62,13 @@ export const useSocketStore = defineStore('socket', () => {
stateStore.addAll('history', json.data.done || {}) stateStore.addAll('history', json.data.done || {})
}) })
socket.value.on('added', stream => { on('item_added', stream => {
const json = JSON.parse(stream); const json = JSON.parse(stream);
stateStore.add('queue', json.data._id, json.data); stateStore.add('queue', json.data._id, json.data);
toast.success(`Item queued: ${ag(stateStore.get('queue', json.data._id, {}), 'title')}`); toast.success(`Item queued: ${ag(stateStore.get('queue', json.data._id, {}), 'title')}`);
}); });
['log_info', 'log_success', 'log_warning', 'log_error'].forEach(event => socket.value?.on(event, stream => { on(['log_info', 'log_success', 'log_warning', 'log_error'], (event: string, stream: string) => {
const json = JSON.parse(stream); const json = JSON.parse(stream);
const message = json?.message || json?.data?.message; const message = json?.message || json?.data?.message;
const data = json.data?.data || json.data || {}; const data = json.data?.data || json.data || {};
@ -69,19 +86,24 @@ export const useSocketStore = defineStore('socket', () => {
toast.error(message, data); toast.error(message, data);
break; break;
} }
})); }, true);
socket.value.on('completed', stream => { on('item_completed', (stream: string) => {
const json = JSON.parse(stream); const json = JSON.parse(stream);
if (true === stateStore.has('queue', json.data._id)) { if (true === stateStore.has('queue', json.data._id)) {
stateStore.remove('queue', json.data._id); stateStore.remove('queue', json.data._id);
} }
if (true === stateStore.has('history', json.data._id)) {
stateStore.update('history', json.data._id, json.data);
return;
}
stateStore.add('history', json.data._id, json.data); stateStore.add('history', json.data._id, json.data);
}); });
socket.value.on('cancelled', stream => { on('item_cancelled', (stream: string) => {
const item = JSON.parse(stream); const item = JSON.parse(stream);
const id = item.data._id const id = item.data._id
@ -96,7 +118,7 @@ export const useSocketStore = defineStore('socket', () => {
} }
}); });
socket.value.on('cleared', stream => { on('item_deleted', (stream: string) => {
const item = JSON.parse(stream); const item = JSON.parse(stream);
const id = item.data._id const id = item.data._id
@ -107,7 +129,7 @@ export const useSocketStore = defineStore('socket', () => {
stateStore.remove('history', id); stateStore.remove('history', id);
}); });
socket.value.on("updated", stream => { on('item_updated', (stream: string) => {
const json = JSON.parse(stream); const json = JSON.parse(stream);
const id = json.data._id; const id = json.data._id;
@ -116,42 +138,51 @@ export const useSocketStore = defineStore('socket', () => {
return; return;
} }
stateStore.update('queue', id, json.data); if (true === stateStore.has('queue', id)) {
}); stateStore.update('queue', id, json.data);
socket.value.on("update", stream => {
const json = JSON.parse(stream);
if (true === stateStore.has('history', json.data._id)) {
stateStore.update('history', json.data._id, json.data);
return;
} }
}); });
socket.value.on('paused', data => { on('item_moved', (stream: string) => {
const json = JSON.parse(stream);
const to = json.data.to;
const id = json.data.item._id;
if ('queue' === to) {
if (true === stateStore.has('history', id)) {
stateStore.remove('history', id);
}
stateStore.add('queue', id, json.data.item);
}
if ('history' === to) {
if (true === stateStore.has('queue', id)) {
stateStore.remove('queue', id);
}
stateStore.add('history', id, json.data.item);
}
});
on(['paused', 'resumed'], (event: string, data: string) => {
const json = JSON.parse(data); const json = JSON.parse(data);
const pausedState = Boolean(json.data.paused); const pausedState = Boolean(json.data.paused);
config.update('paused', pausedState); config.update('paused', pausedState);
if (false === pausedState) { if ('resumed' === event) {
toast.success('Download queue resumed.'); toast.success('Download queue resumed.');
return; return;
} }
toast.warning('Download queue paused.', { timeout: 10000 }); toast.warning('Download queue paused.', { timeout: 10000 });
}); }, true);
socket.value.on('presets_update', (data: string) => config.update('presets', JSON.parse(data).data || [])); on('presets_update', (data: string) => config.update('presets', JSON.parse(data).data || []));
} }
const emit = (event: string, data?: any): any => socket.value?.emit(event, data)
const on = (event: string, callback: (...args: any[]) => void): any => socket.value?.on(event, callback)
const off = (event: string, callback: (...args: any[]) => void): any => socket.value?.off(event, callback)
if (false === isConnected.value) { if (false === isConnected.value) {
connect(); connect();
} }
window.ws = socket.value;
return { connect, on, off, emit, socket, isConnected }; return { connect, on, off, emit, socket, isConnected };
}); });

148
uv.lock
View file

@ -27,7 +27,7 @@ wheels = [
[[package]] [[package]]
name = "aiohttp" name = "aiohttp"
version = "3.12.13" version = "3.12.14"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "aiohappyeyeballs" }, { name = "aiohappyeyeballs" },
@ -38,59 +38,59 @@ dependencies = [
{ name = "propcache" }, { name = "propcache" },
{ name = "yarl" }, { name = "yarl" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/42/6e/ab88e7cb2a4058bed2f7870276454f85a7c56cd6da79349eb314fc7bbcaa/aiohttp-3.12.13.tar.gz", hash = "sha256:47e2da578528264a12e4e3dd8dd72a7289e5f812758fe086473fab037a10fcce", size = 7819160 } sdist = { url = "https://files.pythonhosted.org/packages/e6/0b/e39ad954107ebf213a2325038a3e7a506be3d98e1435e1f82086eec4cde2/aiohttp-3.12.14.tar.gz", hash = "sha256:6e06e120e34d93100de448fd941522e11dafa78ef1a893c179901b7d66aa29f2", size = 7822921 }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/6a/65/5566b49553bf20ffed6041c665a5504fb047cefdef1b701407b8ce1a47c4/aiohttp-3.12.13-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7c229b1437aa2576b99384e4be668af1db84b31a45305d02f61f5497cfa6f60c", size = 709401 }, { url = "https://files.pythonhosted.org/packages/53/e1/8029b29316971c5fa89cec170274582619a01b3d82dd1036872acc9bc7e8/aiohttp-3.12.14-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f4552ff7b18bcec18b60a90c6982049cdb9dac1dba48cf00b97934a06ce2e597", size = 709960 },
{ url = "https://files.pythonhosted.org/packages/14/b5/48e4cc61b54850bdfafa8fe0b641ab35ad53d8e5a65ab22b310e0902fa42/aiohttp-3.12.13-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:04076d8c63471e51e3689c93940775dc3d12d855c0c80d18ac5a1c68f0904358", size = 481669 }, { url = "https://files.pythonhosted.org/packages/96/bd/4f204cf1e282041f7b7e8155f846583b19149e0872752711d0da5e9cc023/aiohttp-3.12.14-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8283f42181ff6ccbcf25acaae4e8ab2ff7e92b3ca4a4ced73b2c12d8cd971393", size = 482235 },
{ url = "https://files.pythonhosted.org/packages/04/4f/e3f95c8b2a20a0437d51d41d5ccc4a02970d8ad59352efb43ea2841bd08e/aiohttp-3.12.13-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:55683615813ce3601640cfaa1041174dc956d28ba0511c8cbd75273eb0587014", size = 469933 }, { url = "https://files.pythonhosted.org/packages/d6/0f/2a580fcdd113fe2197a3b9df30230c7e85bb10bf56f7915457c60e9addd9/aiohttp-3.12.14-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:040afa180ea514495aaff7ad34ec3d27826eaa5d19812730fe9e529b04bb2179", size = 470501 },
{ url = "https://files.pythonhosted.org/packages/41/c9/c5269f3b6453b1cfbd2cfbb6a777d718c5f086a3727f576c51a468b03ae2/aiohttp-3.12.13-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:921bc91e602d7506d37643e77819cb0b840d4ebb5f8d6408423af3d3bf79a7b7", size = 1740128 }, { url = "https://files.pythonhosted.org/packages/38/78/2c1089f6adca90c3dd74915bafed6d6d8a87df5e3da74200f6b3a8b8906f/aiohttp-3.12.14-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b413c12f14c1149f0ffd890f4141a7471ba4b41234fe4fd4a0ff82b1dc299dbb", size = 1740696 },
{ url = "https://files.pythonhosted.org/packages/6f/49/a3f76caa62773d33d0cfaa842bdf5789a78749dbfe697df38ab1badff369/aiohttp-3.12.13-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e72d17fe0974ddeae8ed86db297e23dba39c7ac36d84acdbb53df2e18505a013", size = 1688796 }, { url = "https://files.pythonhosted.org/packages/4a/c8/ce6c7a34d9c589f007cfe064da2d943b3dee5aabc64eaecd21faf927ab11/aiohttp-3.12.14-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:1d6f607ce2e1a93315414e3d448b831238f1874b9968e1195b06efaa5c87e245", size = 1689365 },
{ url = "https://files.pythonhosted.org/packages/ad/e4/556fccc4576dc22bf18554b64cc873b1a3e5429a5bdb7bbef7f5d0bc7664/aiohttp-3.12.13-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0653d15587909a52e024a261943cf1c5bdc69acb71f411b0dd5966d065a51a47", size = 1787589 }, { url = "https://files.pythonhosted.org/packages/18/10/431cd3d089de700756a56aa896faf3ea82bee39d22f89db7ddc957580308/aiohttp-3.12.14-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:565e70d03e924333004ed101599902bba09ebb14843c8ea39d657f037115201b", size = 1788157 },
{ url = "https://files.pythonhosted.org/packages/b9/3d/d81b13ed48e1a46734f848e26d55a7391708421a80336e341d2aef3b6db2/aiohttp-3.12.13-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a77b48997c66722c65e157c06c74332cdf9c7ad00494b85ec43f324e5c5a9b9a", size = 1826635 }, { url = "https://files.pythonhosted.org/packages/fa/b2/26f4524184e0f7ba46671c512d4b03022633bcf7d32fa0c6f1ef49d55800/aiohttp-3.12.14-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4699979560728b168d5ab63c668a093c9570af2c7a78ea24ca5212c6cdc2b641", size = 1827203 },
{ url = "https://files.pythonhosted.org/packages/75/a5/472e25f347da88459188cdaadd1f108f6292f8a25e62d226e63f860486d1/aiohttp-3.12.13-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d6946bae55fd36cfb8e4092c921075cde029c71c7cb571d72f1079d1e4e013bc", size = 1729095 }, { url = "https://files.pythonhosted.org/packages/e0/30/aadcdf71b510a718e3d98a7bfeaea2396ac847f218b7e8edb241b09bd99a/aiohttp-3.12.14-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad5fdf6af93ec6c99bf800eba3af9a43d8bfd66dce920ac905c817ef4a712afe", size = 1729664 },
{ url = "https://files.pythonhosted.org/packages/b9/fe/322a78b9ac1725bfc59dfc301a5342e73d817592828e4445bd8f4ff83489/aiohttp-3.12.13-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f95db8c8b219bcf294a53742c7bda49b80ceb9d577c8e7aa075612b7f39ffb7", size = 1666170 }, { url = "https://files.pythonhosted.org/packages/67/7f/7ccf11756ae498fdedc3d689a0c36ace8fc82f9d52d3517da24adf6e9a74/aiohttp-3.12.14-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4ac76627c0b7ee0e80e871bde0d376a057916cb008a8f3ffc889570a838f5cc7", size = 1666741 },
{ url = "https://files.pythonhosted.org/packages/7a/77/ec80912270e231d5e3839dbd6c065472b9920a159ec8a1895cf868c2708e/aiohttp-3.12.13-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:03d5eb3cfb4949ab4c74822fb3326cd9655c2b9fe22e4257e2100d44215b2e2b", size = 1714444 }, { url = "https://files.pythonhosted.org/packages/6b/4d/35ebc170b1856dd020c92376dbfe4297217625ef4004d56587024dc2289c/aiohttp-3.12.14-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:798204af1180885651b77bf03adc903743a86a39c7392c472891649610844635", size = 1715013 },
{ url = "https://files.pythonhosted.org/packages/21/b2/fb5aedbcb2b58d4180e58500e7c23ff8593258c27c089abfbcc7db65bd40/aiohttp-3.12.13-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:6383dd0ffa15515283c26cbf41ac8e6705aab54b4cbb77bdb8935a713a89bee9", size = 1709604 }, { url = "https://files.pythonhosted.org/packages/7b/24/46dc0380146f33e2e4aa088b92374b598f5bdcde1718c77e8d1a0094f1a4/aiohttp-3.12.14-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:4f1205f97de92c37dd71cf2d5bcfb65fdaed3c255d246172cce729a8d849b4da", size = 1710172 },
{ url = "https://files.pythonhosted.org/packages/e3/15/a94c05f7c4dc8904f80b6001ad6e07e035c58a8ebfcc15e6b5d58500c858/aiohttp-3.12.13-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6548a411bc8219b45ba2577716493aa63b12803d1e5dc70508c539d0db8dbf5a", size = 1689786 }, { url = "https://files.pythonhosted.org/packages/2f/0a/46599d7d19b64f4d0fe1b57bdf96a9a40b5c125f0ae0d8899bc22e91fdce/aiohttp-3.12.14-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:76ae6f1dd041f85065d9df77c6bc9c9703da9b5c018479d20262acc3df97d419", size = 1690355 },
{ url = "https://files.pythonhosted.org/packages/1d/fd/0d2e618388f7a7a4441eed578b626bda9ec6b5361cd2954cfc5ab39aa170/aiohttp-3.12.13-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:81b0fcbfe59a4ca41dc8f635c2a4a71e63f75168cc91026c61be665945739e2d", size = 1783389 }, { url = "https://files.pythonhosted.org/packages/08/86/b21b682e33d5ca317ef96bd21294984f72379454e689d7da584df1512a19/aiohttp-3.12.14-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a194ace7bc43ce765338ca2dfb5661489317db216ea7ea700b0332878b392cab", size = 1783958 },
{ url = "https://files.pythonhosted.org/packages/a6/6b/6986d0c75996ef7e64ff7619b9b7449b1d1cbbe05c6755e65d92f1784fe9/aiohttp-3.12.13-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:6a83797a0174e7995e5edce9dcecc517c642eb43bc3cba296d4512edf346eee2", size = 1803853 }, { url = "https://files.pythonhosted.org/packages/4f/45/f639482530b1396c365f23c5e3b1ae51c9bc02ba2b2248ca0c855a730059/aiohttp-3.12.14-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:16260e8e03744a6fe3fcb05259eeab8e08342c4c33decf96a9dad9f1187275d0", size = 1804423 },
{ url = "https://files.pythonhosted.org/packages/21/65/cd37b38f6655d95dd07d496b6d2f3924f579c43fd64b0e32b547b9c24df5/aiohttp-3.12.13-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a5734d8469a5633a4e9ffdf9983ff7cdb512524645c7a3d4bc8a3de45b935ac3", size = 1716909 }, { url = "https://files.pythonhosted.org/packages/7e/e5/39635a9e06eed1d73671bd4079a3caf9cf09a49df08490686f45a710b80e/aiohttp-3.12.14-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8c779e5ebbf0e2e15334ea404fcce54009dc069210164a244d2eac8352a44b28", size = 1717479 },
{ url = "https://files.pythonhosted.org/packages/fd/20/2de7012427dc116714c38ca564467f6143aec3d5eca3768848d62aa43e62/aiohttp-3.12.13-cp311-cp311-win32.whl", hash = "sha256:fef8d50dfa482925bb6b4c208b40d8e9fa54cecba923dc65b825a72eed9a5dbd", size = 427036 }, { url = "https://files.pythonhosted.org/packages/51/e1/7f1c77515d369b7419c5b501196526dad3e72800946c0099594c1f0c20b4/aiohttp-3.12.14-cp311-cp311-win32.whl", hash = "sha256:a289f50bf1bd5be227376c067927f78079a7bdeccf8daa6a9e65c38bae14324b", size = 427907 },
{ url = "https://files.pythonhosted.org/packages/f8/b6/98518bcc615ef998a64bef371178b9afc98ee25895b4f476c428fade2220/aiohttp-3.12.13-cp311-cp311-win_amd64.whl", hash = "sha256:9a27da9c3b5ed9d04c36ad2df65b38a96a37e9cfba6f1381b842d05d98e6afe9", size = 451427 }, { url = "https://files.pythonhosted.org/packages/06/24/a6bf915c85b7a5b07beba3d42b3282936b51e4578b64a51e8e875643c276/aiohttp-3.12.14-cp311-cp311-win_amd64.whl", hash = "sha256:0b8a69acaf06b17e9c54151a6c956339cf46db4ff72b3ac28516d0f7068f4ced", size = 452334 },
{ url = "https://files.pythonhosted.org/packages/b4/6a/ce40e329788013cd190b1d62bbabb2b6a9673ecb6d836298635b939562ef/aiohttp-3.12.13-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0aa580cf80558557285b49452151b9c69f2fa3ad94c5c9e76e684719a8791b73", size = 700491 }, { url = "https://files.pythonhosted.org/packages/c3/0d/29026524e9336e33d9767a1e593ae2b24c2b8b09af7c2bd8193762f76b3e/aiohttp-3.12.14-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a0ecbb32fc3e69bc25efcda7d28d38e987d007096cbbeed04f14a6662d0eee22", size = 701055 },
{ url = "https://files.pythonhosted.org/packages/28/d9/7150d5cf9163e05081f1c5c64a0cdf3c32d2f56e2ac95db2a28fe90eca69/aiohttp-3.12.13-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b103a7e414b57e6939cc4dece8e282cfb22043efd0c7298044f6594cf83ab347", size = 475104 }, { url = "https://files.pythonhosted.org/packages/0a/b8/a5e8e583e6c8c1056f4b012b50a03c77a669c2e9bf012b7cf33d6bc4b141/aiohttp-3.12.14-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0400f0ca9bb3e0b02f6466421f253797f6384e9845820c8b05e976398ac1d81a", size = 475670 },
{ url = "https://files.pythonhosted.org/packages/f8/91/d42ba4aed039ce6e449b3e2db694328756c152a79804e64e3da5bc19dffc/aiohttp-3.12.13-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:78f64e748e9e741d2eccff9597d09fb3cd962210e5b5716047cbb646dc8fe06f", size = 467948 }, { url = "https://files.pythonhosted.org/packages/29/e8/5202890c9e81a4ec2c2808dd90ffe024952e72c061729e1d49917677952f/aiohttp-3.12.14-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a56809fed4c8a830b5cae18454b7464e1529dbf66f71c4772e3cfa9cbec0a1ff", size = 468513 },
{ url = "https://files.pythonhosted.org/packages/99/3b/06f0a632775946981d7c4e5a865cddb6e8dfdbaed2f56f9ade7bb4a1039b/aiohttp-3.12.13-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c955989bf4c696d2ededc6b0ccb85a73623ae6e112439398935362bacfaaf6", size = 1714742 }, { url = "https://files.pythonhosted.org/packages/23/e5/d11db8c23d8923d3484a27468a40737d50f05b05eebbb6288bafcb467356/aiohttp-3.12.14-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27f2e373276e4755691a963e5d11756d093e346119f0627c2d6518208483fb6d", size = 1715309 },
{ url = "https://files.pythonhosted.org/packages/92/a6/2552eebad9ec5e3581a89256276009e6a974dc0793632796af144df8b740/aiohttp-3.12.13-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d640191016763fab76072c87d8854a19e8e65d7a6fcfcbf017926bdbbb30a7e5", size = 1697393 }, { url = "https://files.pythonhosted.org/packages/53/44/af6879ca0eff7a16b1b650b7ea4a827301737a350a464239e58aa7c387ef/aiohttp-3.12.14-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:ca39e433630e9a16281125ef57ece6817afd1d54c9f1bf32e901f38f16035869", size = 1697961 },
{ url = "https://files.pythonhosted.org/packages/d8/9f/bd08fdde114b3fec7a021381b537b21920cdd2aa29ad48c5dffd8ee314f1/aiohttp-3.12.13-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4dc507481266b410dede95dd9f26c8d6f5a14315372cc48a6e43eac652237d9b", size = 1752486 }, { url = "https://files.pythonhosted.org/packages/bb/94/18457f043399e1ec0e59ad8674c0372f925363059c276a45a1459e17f423/aiohttp-3.12.14-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9c748b3f8b14c77720132b2510a7d9907a03c20ba80f469e58d5dfd90c079a1c", size = 1753055 },
{ url = "https://files.pythonhosted.org/packages/f7/e1/affdea8723aec5bd0959171b5490dccd9a91fcc505c8c26c9f1dca73474d/aiohttp-3.12.13-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8a94daa873465d518db073bd95d75f14302e0208a08e8c942b2f3f1c07288a75", size = 1798643 }, { url = "https://files.pythonhosted.org/packages/26/d9/1d3744dc588fafb50ff8a6226d58f484a2242b5dd93d8038882f55474d41/aiohttp-3.12.14-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f0a568abe1b15ce69d4cc37e23020720423f0728e3cb1f9bcd3f53420ec3bfe7", size = 1799211 },
{ url = "https://files.pythonhosted.org/packages/f3/9d/666d856cc3af3a62ae86393baa3074cc1d591a47d89dc3bf16f6eb2c8d32/aiohttp-3.12.13-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:177f52420cde4ce0bb9425a375d95577fe082cb5721ecb61da3049b55189e4e6", size = 1718082 }, { url = "https://files.pythonhosted.org/packages/73/12/2530fb2b08773f717ab2d249ca7a982ac66e32187c62d49e2c86c9bba9b4/aiohttp-3.12.14-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9888e60c2c54eaf56704b17feb558c7ed6b7439bca1e07d4818ab878f2083660", size = 1718649 },
{ url = "https://files.pythonhosted.org/packages/f3/ce/3c185293843d17be063dada45efd2712bb6bf6370b37104b4eda908ffdbd/aiohttp-3.12.13-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f7df1f620ec40f1a7fbcb99ea17d7326ea6996715e78f71a1c9a021e31b96b8", size = 1633884 }, { url = "https://files.pythonhosted.org/packages/b9/34/8d6015a729f6571341a311061b578e8b8072ea3656b3d72329fa0faa2c7c/aiohttp-3.12.14-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3006a1dc579b9156de01e7916d38c63dc1ea0679b14627a37edf6151bc530088", size = 1634452 },
{ url = "https://files.pythonhosted.org/packages/3a/5b/f3413f4b238113be35dfd6794e65029250d4b93caa0974ca572217745bdb/aiohttp-3.12.13-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3062d4ad53b36e17796dce1c0d6da0ad27a015c321e663657ba1cc7659cfc710", size = 1694943 }, { url = "https://files.pythonhosted.org/packages/ff/4b/08b83ea02595a582447aeb0c1986792d0de35fe7a22fb2125d65091cbaf3/aiohttp-3.12.14-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:aa8ec5c15ab80e5501a26719eb48a55f3c567da45c6ea5bb78c52c036b2655c7", size = 1695511 },
{ url = "https://files.pythonhosted.org/packages/82/c8/0e56e8bf12081faca85d14a6929ad5c1263c146149cd66caa7bc12255b6d/aiohttp-3.12.13-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:8605e22d2a86b8e51ffb5253d9045ea73683d92d47c0b1438e11a359bdb94462", size = 1716398 }, { url = "https://files.pythonhosted.org/packages/b5/66/9c7c31037a063eec13ecf1976185c65d1394ded4a5120dd5965e3473cb21/aiohttp-3.12.14-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:39b94e50959aa07844c7fe2206b9f75d63cc3ad1c648aaa755aa257f6f2498a9", size = 1716967 },
{ url = "https://files.pythonhosted.org/packages/ea/f3/33192b4761f7f9b2f7f4281365d925d663629cfaea093a64b658b94fc8e1/aiohttp-3.12.13-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:54fbbe6beafc2820de71ece2198458a711e224e116efefa01b7969f3e2b3ddae", size = 1657051 }, { url = "https://files.pythonhosted.org/packages/ba/02/84406e0ad1acb0fb61fd617651ab6de760b2d6a31700904bc0b33bd0894d/aiohttp-3.12.14-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:04c11907492f416dad9885d503fbfc5dcb6768d90cad8639a771922d584609d3", size = 1657620 },
{ url = "https://files.pythonhosted.org/packages/5e/0b/26ddd91ca8f84c48452431cb4c5dd9523b13bc0c9766bda468e072ac9e29/aiohttp-3.12.13-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:050bd277dfc3768b606fd4eae79dd58ceda67d8b0b3c565656a89ae34525d15e", size = 1736611 }, { url = "https://files.pythonhosted.org/packages/07/53/da018f4013a7a179017b9a274b46b9a12cbeb387570f116964f498a6f211/aiohttp-3.12.14-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:88167bd9ab69bb46cee91bd9761db6dfd45b6e76a0438c7e884c3f8160ff21eb", size = 1737179 },
{ url = "https://files.pythonhosted.org/packages/c3/8d/e04569aae853302648e2c138a680a6a2f02e374c5b6711732b29f1e129cc/aiohttp-3.12.13-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:2637a60910b58f50f22379b6797466c3aa6ae28a6ab6404e09175ce4955b4e6a", size = 1764586 }, { url = "https://files.pythonhosted.org/packages/49/e8/ca01c5ccfeaafb026d85fa4f43ceb23eb80ea9c1385688db0ef322c751e9/aiohttp-3.12.14-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:791504763f25e8f9f251e4688195e8b455f8820274320204f7eafc467e609425", size = 1765156 },
{ url = "https://files.pythonhosted.org/packages/ac/98/c193c1d1198571d988454e4ed75adc21c55af247a9fda08236602921c8c8/aiohttp-3.12.13-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e986067357550d1aaa21cfe9897fa19e680110551518a5a7cf44e6c5638cb8b5", size = 1724197 }, { url = "https://files.pythonhosted.org/packages/22/32/5501ab525a47ba23c20613e568174d6c63aa09e2caa22cded5c6ea8e3ada/aiohttp-3.12.14-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2785b112346e435dd3a1a67f67713a3fe692d288542f1347ad255683f066d8e0", size = 1724766 },
{ url = "https://files.pythonhosted.org/packages/e7/9e/07bb8aa11eec762c6b1ff61575eeeb2657df11ab3d3abfa528d95f3e9337/aiohttp-3.12.13-cp312-cp312-win32.whl", hash = "sha256:ac941a80aeea2aaae2875c9500861a3ba356f9ff17b9cb2dbfb5cbf91baaf5bf", size = 421771 }, { url = "https://files.pythonhosted.org/packages/06/af/28e24574801fcf1657945347ee10df3892311c2829b41232be6089e461e7/aiohttp-3.12.14-cp312-cp312-win32.whl", hash = "sha256:15f5f4792c9c999a31d8decf444e79fcfd98497bf98e94284bf390a7bb8c1729", size = 422641 },
{ url = "https://files.pythonhosted.org/packages/52/66/3ce877e56ec0813069cdc9607cd979575859c597b6fb9b4182c6d5f31886/aiohttp-3.12.13-cp312-cp312-win_amd64.whl", hash = "sha256:671f41e6146a749b6c81cb7fd07f5a8356d46febdaaaf07b0e774ff04830461e", size = 447869 }, { url = "https://files.pythonhosted.org/packages/98/d5/7ac2464aebd2eecac38dbe96148c9eb487679c512449ba5215d233755582/aiohttp-3.12.14-cp312-cp312-win_amd64.whl", hash = "sha256:3b66e1a182879f579b105a80d5c4bd448b91a57e8933564bf41665064796a338", size = 449316 },
{ url = "https://files.pythonhosted.org/packages/11/0f/db19abdf2d86aa1deec3c1e0e5ea46a587b97c07a16516b6438428b3a3f8/aiohttp-3.12.13-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d4a18e61f271127465bdb0e8ff36e8f02ac4a32a80d8927aa52371e93cd87938", size = 694910 }, { url = "https://files.pythonhosted.org/packages/06/48/e0d2fa8ac778008071e7b79b93ab31ef14ab88804d7ba71b5c964a7c844e/aiohttp-3.12.14-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:3143a7893d94dc82bc409f7308bc10d60285a3cd831a68faf1aa0836c5c3c767", size = 695471 },
{ url = "https://files.pythonhosted.org/packages/d5/81/0ab551e1b5d7f1339e2d6eb482456ccbe9025605b28eed2b1c0203aaaade/aiohttp-3.12.13-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:532542cb48691179455fab429cdb0d558b5e5290b033b87478f2aa6af5d20ace", size = 472566 }, { url = "https://files.pythonhosted.org/packages/8d/e7/f73206afa33100804f790b71092888f47df65fd9a4cd0e6800d7c6826441/aiohttp-3.12.14-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3d62ac3d506cef54b355bd34c2a7c230eb693880001dfcda0bf88b38f5d7af7e", size = 473128 },
{ url = "https://files.pythonhosted.org/packages/34/3f/6b7d336663337672d29b1f82d1f252ec1a040fe2d548f709d3f90fa2218a/aiohttp-3.12.13-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d7eea18b52f23c050ae9db5d01f3d264ab08f09e7356d6f68e3f3ac2de9dfabb", size = 464856 }, { url = "https://files.pythonhosted.org/packages/df/e2/4dd00180be551a6e7ee979c20fc7c32727f4889ee3fd5b0586e0d47f30e1/aiohttp-3.12.14-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:48e43e075c6a438937c4de48ec30fa8ad8e6dfef122a038847456bfe7b947b63", size = 465426 },
{ url = "https://files.pythonhosted.org/packages/26/7f/32ca0f170496aa2ab9b812630fac0c2372c531b797e1deb3deb4cea904bd/aiohttp-3.12.13-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad7c8e5c25f2a26842a7c239de3f7b6bfb92304593ef997c04ac49fb703ff4d7", size = 1703683 }, { url = "https://files.pythonhosted.org/packages/de/dd/525ed198a0bb674a323e93e4d928443a680860802c44fa7922d39436b48b/aiohttp-3.12.14-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:077b4488411a9724cecc436cbc8c133e0d61e694995b8de51aaf351c7578949d", size = 1704252 },
{ url = "https://files.pythonhosted.org/packages/ec/53/d5513624b33a811c0abea8461e30a732294112318276ce3dbf047dbd9d8b/aiohttp-3.12.13-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6af355b483e3fe9d7336d84539fef460120c2f6e50e06c658fe2907c69262d6b", size = 1684946 }, { url = "https://files.pythonhosted.org/packages/d8/b1/01e542aed560a968f692ab4fc4323286e8bc4daae83348cd63588e4f33e3/aiohttp-3.12.14-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d8c35632575653f297dcbc9546305b2c1133391089ab925a6a3706dfa775ccab", size = 1685514 },
{ url = "https://files.pythonhosted.org/packages/37/72/4c237dd127827b0247dc138d3ebd49c2ded6114c6991bbe969058575f25f/aiohttp-3.12.13-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a95cf9f097498f35c88e3609f55bb47b28a5ef67f6888f4390b3d73e2bac6177", size = 1737017 }, { url = "https://files.pythonhosted.org/packages/b3/06/93669694dc5fdabdc01338791e70452d60ce21ea0946a878715688d5a191/aiohttp-3.12.14-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6b8ce87963f0035c6834b28f061df90cf525ff7c9b6283a8ac23acee6502afd4", size = 1737586 },
{ url = "https://files.pythonhosted.org/packages/0d/67/8a7eb3afa01e9d0acc26e1ef847c1a9111f8b42b82955fcd9faeb84edeb4/aiohttp-3.12.13-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b8ed8c38a1c584fe99a475a8f60eefc0b682ea413a84c6ce769bb19a7ff1c5ef", size = 1786390 }, { url = "https://files.pythonhosted.org/packages/a5/3a/18991048ffc1407ca51efb49ba8bcc1645961f97f563a6c480cdf0286310/aiohttp-3.12.14-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f0a2cf66e32a2563bb0766eb24eae7e9a269ac0dc48db0aae90b575dc9583026", size = 1786958 },
{ url = "https://files.pythonhosted.org/packages/48/19/0377df97dd0176ad23cd8cad4fd4232cfeadcec6c1b7f036315305c98e3f/aiohttp-3.12.13-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a0b9170d5d800126b5bc89d3053a2363406d6e327afb6afaeda2d19ee8bb103", size = 1708719 }, { url = "https://files.pythonhosted.org/packages/30/a8/81e237f89a32029f9b4a805af6dffc378f8459c7b9942712c809ff9e76e5/aiohttp-3.12.14-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdea089caf6d5cde975084a884c72d901e36ef9c2fd972c9f51efbbc64e96fbd", size = 1709287 },
{ url = "https://files.pythonhosted.org/packages/61/97/ade1982a5c642b45f3622255173e40c3eed289c169f89d00eeac29a89906/aiohttp-3.12.13-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:372feeace612ef8eb41f05ae014a92121a512bd5067db8f25101dd88a8db11da", size = 1622424 }, { url = "https://files.pythonhosted.org/packages/8c/e3/bd67a11b0fe7fc12c6030473afd9e44223d456f500f7cf526dbaa259ae46/aiohttp-3.12.14-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8a7865f27db67d49e81d463da64a59365ebd6b826e0e4847aa111056dcb9dc88", size = 1622990 },
{ url = "https://files.pythonhosted.org/packages/99/ab/00ad3eea004e1d07ccc406e44cfe2b8da5acb72f8c66aeeb11a096798868/aiohttp-3.12.13-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a946d3702f7965d81f7af7ea8fb03bb33fe53d311df48a46eeca17e9e0beed2d", size = 1675447 }, { url = "https://files.pythonhosted.org/packages/83/ba/e0cc8e0f0d9ce0904e3cf2d6fa41904e379e718a013c721b781d53dcbcca/aiohttp-3.12.14-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0ab5b38a6a39781d77713ad930cb5e7feea6f253de656a5f9f281a8f5931b086", size = 1676015 },
{ url = "https://files.pythonhosted.org/packages/3f/fe/74e5ce8b2ccaba445fe0087abc201bfd7259431d92ae608f684fcac5d143/aiohttp-3.12.13-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:a0c4725fae86555bbb1d4082129e21de7264f4ab14baf735278c974785cd2041", size = 1707110 }, { url = "https://files.pythonhosted.org/packages/d8/b3/1e6c960520bda094c48b56de29a3d978254637ace7168dd97ddc273d0d6c/aiohttp-3.12.14-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:9b3b15acee5c17e8848d90a4ebc27853f37077ba6aec4d8cb4dbbea56d156933", size = 1707678 },
{ url = "https://files.pythonhosted.org/packages/ef/c4/39af17807f694f7a267bd8ab1fbacf16ad66740862192a6c8abac2bff813/aiohttp-3.12.13-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:9b28ea2f708234f0a5c44eb6c7d9eb63a148ce3252ba0140d050b091b6e842d1", size = 1649706 }, { url = "https://files.pythonhosted.org/packages/0a/19/929a3eb8c35b7f9f076a462eaa9830b32c7f27d3395397665caa5e975614/aiohttp-3.12.14-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e4c972b0bdaac167c1e53e16a16101b17c6d0ed7eac178e653a07b9f7fad7151", size = 1650274 },
{ url = "https://files.pythonhosted.org/packages/38/e8/f5a0a5f44f19f171d8477059aa5f28a158d7d57fe1a46c553e231f698435/aiohttp-3.12.13-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d4f5becd2a5791829f79608c6f3dc745388162376f310eb9c142c985f9441cc1", size = 1725839 }, { url = "https://files.pythonhosted.org/packages/22/e5/81682a6f20dd1b18ce3d747de8eba11cbef9b270f567426ff7880b096b48/aiohttp-3.12.14-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7442488b0039257a3bdbc55f7209587911f143fca11df9869578db6c26feeeb8", size = 1726408 },
{ url = "https://files.pythonhosted.org/packages/fd/ac/81acc594c7f529ef4419d3866913f628cd4fa9cab17f7bf410a5c3c04c53/aiohttp-3.12.13-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:60f2ce6b944e97649051d5f5cc0f439360690b73909230e107fd45a359d3e911", size = 1759311 }, { url = "https://files.pythonhosted.org/packages/8c/17/884938dffaa4048302985483f77dfce5ac18339aad9b04ad4aaa5e32b028/aiohttp-3.12.14-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:f68d3067eecb64c5e9bab4a26aa11bd676f4c70eea9ef6536b0a4e490639add3", size = 1759879 },
{ url = "https://files.pythonhosted.org/packages/38/0d/aabe636bd25c6ab7b18825e5a97d40024da75152bec39aa6ac8b7a677630/aiohttp-3.12.13-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:69fc1909857401b67bf599c793f2183fbc4804717388b0b888f27f9929aa41f3", size = 1708202 }, { url = "https://files.pythonhosted.org/packages/95/78/53b081980f50b5cf874359bde707a6eacd6c4be3f5f5c93937e48c9d0025/aiohttp-3.12.14-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f88d3704c8b3d598a08ad17d06006cb1ca52a1182291f04979e305c8be6c9758", size = 1708770 },
{ url = "https://files.pythonhosted.org/packages/1f/ab/561ef2d8a223261683fb95a6283ad0d36cb66c87503f3a7dde7afe208bb2/aiohttp-3.12.13-cp313-cp313-win32.whl", hash = "sha256:7d7e68787a2046b0e44ba5587aa723ce05d711e3a3665b6b7545328ac8e3c0dd", size = 420794 }, { url = "https://files.pythonhosted.org/packages/ed/91/228eeddb008ecbe3ffa6c77b440597fdf640307162f0c6488e72c5a2d112/aiohttp-3.12.14-cp313-cp313-win32.whl", hash = "sha256:a3c99ab19c7bf375c4ae3debd91ca5d394b98b6089a03231d4c580ef3c2ae4c5", size = 421688 },
{ url = "https://files.pythonhosted.org/packages/9d/47/b11d0089875a23bff0abd3edb5516bcd454db3fefab8604f5e4b07bd6210/aiohttp-3.12.13-cp313-cp313-win_amd64.whl", hash = "sha256:5a178390ca90419bfd41419a809688c368e63c86bd725e1186dd97f6b89c2706", size = 446735 }, { url = "https://files.pythonhosted.org/packages/66/5f/8427618903343402fdafe2850738f735fd1d9409d2a8f9bcaae5e630d3ba/aiohttp-3.12.14-cp313-cp313-win_amd64.whl", hash = "sha256:3f8aad695e12edc9d571f878c62bedc91adf30c760c8632f09663e5f564f4baa", size = 448098 },
] ]
[[package]] [[package]]
@ -264,11 +264,11 @@ wheels = [
[[package]] [[package]]
name = "certifi" name = "certifi"
version = "2025.6.15" version = "2025.7.14"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/73/f7/f14b46d4bcd21092d7d3ccef689615220d8a08fb25e564b65d20738e672e/certifi-2025.6.15.tar.gz", hash = "sha256:d747aa5a8b9bbbb1bb8c22bb13e22bd1f18e9796defa16bab421f7f7a317323b", size = 158753 } sdist = { url = "https://files.pythonhosted.org/packages/b3/76/52c535bcebe74590f296d6c77c86dabf761c41980e1347a2422e4aa2ae41/certifi-2025.7.14.tar.gz", hash = "sha256:8ea99dbdfaaf2ba2f9bac77b9249ef62ec5218e7c2b2e903378ed5fccf765995", size = 163981 }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/84/ae/320161bd181fc06471eed047ecce67b693fd7515b16d495d8932db763426/certifi-2025.6.15-py3-none-any.whl", hash = "sha256:2e0c7ce7cb5d8f8634ca55d2ba7e6ec2689a2fd6537d8dec1296a477a4910057", size = 157650 }, { url = "https://files.pythonhosted.org/packages/4f/52/34c6cf5bb9285074dc3531c437b3919e825d976fde097a7a73f79e726d03/certifi-2025.7.14-py3-none-any.whl", hash = "sha256:6b31f564a415d79ee77df69d757bb49a5bb53bd9f756cbbe24394ffd6fc1f4b2", size = 162722 },
] ]
[[package]] [[package]]
@ -456,23 +456,23 @@ wheels = [
[[package]] [[package]]
name = "debugpy" name = "debugpy"
version = "1.8.14" version = "1.8.15"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/bd/75/087fe07d40f490a78782ff3b0a30e3968936854105487decdb33446d4b0e/debugpy-1.8.14.tar.gz", hash = "sha256:7cd287184318416850aa8b60ac90105837bb1e59531898c07569d197d2ed5322", size = 1641444 } sdist = { url = "https://files.pythonhosted.org/packages/8c/8b/3a9a28ddb750a76eaec445c7f4d3147ea2c579a97dbd9e25d39001b92b21/debugpy-1.8.15.tar.gz", hash = "sha256:58d7a20b7773ab5ee6bdfb2e6cf622fdf1e40c9d5aef2857d85391526719ac00", size = 1643279 }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/67/e8/57fe0c86915671fd6a3d2d8746e40485fd55e8d9e682388fbb3a3d42b86f/debugpy-1.8.14-cp311-cp311-macosx_14_0_universal2.whl", hash = "sha256:1b2ac8c13b2645e0b1eaf30e816404990fbdb168e193322be8f545e8c01644a9", size = 2175064 }, { url = "https://files.pythonhosted.org/packages/d2/b3/1c44a2ed311199ab11c2299c9474a6c7cd80d19278defd333aeb7c287995/debugpy-1.8.15-cp311-cp311-macosx_14_0_universal2.whl", hash = "sha256:babc4fb1962dd6a37e94d611280e3d0d11a1f5e6c72ac9b3d87a08212c4b6dd3", size = 2183442 },
{ url = "https://files.pythonhosted.org/packages/3b/97/2b2fd1b1c9569c6764ccdb650a6f752e4ac31be465049563c9eb127a8487/debugpy-1.8.14-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cf431c343a99384ac7eab2f763980724834f933a271e90496944195318c619e2", size = 3132359 }, { url = "https://files.pythonhosted.org/packages/f6/69/e2dcb721491e1c294d348681227c9b44fb95218f379aa88e12a19d85528d/debugpy-1.8.15-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f778e68f2986a58479d0ac4f643e0b8c82fdd97c2e200d4d61e7c2d13838eb53", size = 3134215 },
{ url = "https://files.pythonhosted.org/packages/c0/ee/b825c87ed06256ee2a7ed8bab8fb3bb5851293bf9465409fdffc6261c426/debugpy-1.8.14-cp311-cp311-win32.whl", hash = "sha256:c99295c76161ad8d507b413cd33422d7c542889fbb73035889420ac1fad354f2", size = 5133269 }, { url = "https://files.pythonhosted.org/packages/17/76/4ce63b95d8294dcf2fd1820860b300a420d077df4e93afcaa25a984c2ca7/debugpy-1.8.15-cp311-cp311-win32.whl", hash = "sha256:f9d1b5abd75cd965e2deabb1a06b0e93a1546f31f9f621d2705e78104377c702", size = 5154037 },
{ url = "https://files.pythonhosted.org/packages/d5/a6/6c70cd15afa43d37839d60f324213843174c1d1e6bb616bd89f7c1341bac/debugpy-1.8.14-cp311-cp311-win_amd64.whl", hash = "sha256:7816acea4a46d7e4e50ad8d09d963a680ecc814ae31cdef3622eb05ccacf7b01", size = 5158156 }, { url = "https://files.pythonhosted.org/packages/c2/a7/e5a7c784465eb9c976d84408873d597dc7ce74a0fc69ed009548a1a94813/debugpy-1.8.15-cp311-cp311-win_amd64.whl", hash = "sha256:62954fb904bec463e2b5a415777f6d1926c97febb08ef1694da0e5d1463c5c3b", size = 5178133 },
{ url = "https://files.pythonhosted.org/packages/d9/2a/ac2df0eda4898f29c46eb6713a5148e6f8b2b389c8ec9e425a4a1d67bf07/debugpy-1.8.14-cp312-cp312-macosx_14_0_universal2.whl", hash = "sha256:8899c17920d089cfa23e6005ad9f22582fd86f144b23acb9feeda59e84405b84", size = 2501268 }, { url = "https://files.pythonhosted.org/packages/ab/4a/4508d256e52897f5cdfee6a6d7580974811e911c6d01321df3264508a5ac/debugpy-1.8.15-cp312-cp312-macosx_14_0_universal2.whl", hash = "sha256:3dcc7225cb317469721ab5136cda9ff9c8b6e6fb43e87c9e15d5b108b99d01ba", size = 2511197 },
{ url = "https://files.pythonhosted.org/packages/10/53/0a0cb5d79dd9f7039169f8bf94a144ad3efa52cc519940b3b7dde23bcb89/debugpy-1.8.14-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6bb5c0dcf80ad5dbc7b7d6eac484e2af34bdacdf81df09b6a3e62792b722826", size = 4221077 }, { url = "https://files.pythonhosted.org/packages/99/8d/7f6ef1097e7fecf26b4ef72338d08e41644a41b7ee958a19f494ffcffc29/debugpy-1.8.15-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:047a493ca93c85ccede1dbbaf4e66816794bdc214213dde41a9a61e42d27f8fc", size = 4229517 },
{ url = "https://files.pythonhosted.org/packages/f8/d5/84e01821f362327bf4828728aa31e907a2eca7c78cd7c6ec062780d249f8/debugpy-1.8.14-cp312-cp312-win32.whl", hash = "sha256:281d44d248a0e1791ad0eafdbbd2912ff0de9eec48022a5bfbc332957487ed3f", size = 5255127 }, { url = "https://files.pythonhosted.org/packages/3f/e8/e8c6a9aa33a9c9c6dacbf31747384f6ed2adde4de2e9693c766bdf323aa3/debugpy-1.8.15-cp312-cp312-win32.whl", hash = "sha256:b08e9b0bc260cf324c890626961dad4ffd973f7568fbf57feb3c3a65ab6b6327", size = 5276132 },
{ url = "https://files.pythonhosted.org/packages/33/16/1ed929d812c758295cac7f9cf3dab5c73439c83d9091f2d91871e648093e/debugpy-1.8.14-cp312-cp312-win_amd64.whl", hash = "sha256:5aa56ef8538893e4502a7d79047fe39b1dae08d9ae257074c6464a7b290b806f", size = 5297249 }, { url = "https://files.pythonhosted.org/packages/e9/ad/231050c6177b3476b85fcea01e565dac83607b5233d003ff067e2ee44d8f/debugpy-1.8.15-cp312-cp312-win_amd64.whl", hash = "sha256:e2a4fe357c92334272eb2845fcfcdbec3ef9f22c16cf613c388ac0887aed15fa", size = 5317645 },
{ url = "https://files.pythonhosted.org/packages/4d/e4/395c792b243f2367d84202dc33689aa3d910fb9826a7491ba20fc9e261f5/debugpy-1.8.14-cp313-cp313-macosx_14_0_universal2.whl", hash = "sha256:329a15d0660ee09fec6786acdb6e0443d595f64f5d096fc3e3ccf09a4259033f", size = 2485676 }, { url = "https://files.pythonhosted.org/packages/28/70/2928aad2310726d5920b18ed9f54b9f06df5aa4c10cf9b45fa18ff0ab7e8/debugpy-1.8.15-cp313-cp313-macosx_14_0_universal2.whl", hash = "sha256:f5e01291ad7d6649aed5773256c5bba7a1a556196300232de1474c3c372592bf", size = 2495538 },
{ url = "https://files.pythonhosted.org/packages/ba/f1/6f2ee3f991327ad9e4c2f8b82611a467052a0fb0e247390192580e89f7ff/debugpy-1.8.14-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f920c7f9af409d90f5fd26e313e119d908b0dd2952c2393cd3247a462331f15", size = 4217514 }, { url = "https://files.pythonhosted.org/packages/9e/c6/9b8ffb4ca91fac8b2877eef63c9cc0e87dd2570b1120054c272815ec4cd0/debugpy-1.8.15-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94dc0f0d00e528d915e0ce1c78e771475b2335b376c49afcc7382ee0b146bab6", size = 4221874 },
{ url = "https://files.pythonhosted.org/packages/79/28/b9d146f8f2dc535c236ee09ad3e5ac899adb39d7a19b49f03ac95d216beb/debugpy-1.8.14-cp313-cp313-win32.whl", hash = "sha256:3784ec6e8600c66cbdd4ca2726c72d8ca781e94bce2f396cc606d458146f8f4e", size = 5254756 }, { url = "https://files.pythonhosted.org/packages/55/8a/9b8d59674b4bf489318c7c46a1aab58e606e583651438084b7e029bf3c43/debugpy-1.8.15-cp313-cp313-win32.whl", hash = "sha256:fcf0748d4f6e25f89dc5e013d1129ca6f26ad4da405e0723a4f704583896a709", size = 5275949 },
{ url = "https://files.pythonhosted.org/packages/e0/62/a7b4a57013eac4ccaef6977966e6bec5c63906dd25a86e35f155952e29a1/debugpy-1.8.14-cp313-cp313-win_amd64.whl", hash = "sha256:684eaf43c95a3ec39a96f1f5195a7ff3d4144e4a18d69bb66beeb1a6de605d6e", size = 5297119 }, { url = "https://files.pythonhosted.org/packages/72/83/9e58e6fdfa8710a5e6ec06c2401241b9ad48b71c0a7eb99570a1f1edb1d3/debugpy-1.8.15-cp313-cp313-win_amd64.whl", hash = "sha256:73c943776cb83e36baf95e8f7f8da765896fd94b05991e7bc162456d25500683", size = 5317720 },
{ url = "https://files.pythonhosted.org/packages/97/1a/481f33c37ee3ac8040d3d51fc4c4e4e7e61cb08b8bc8971d6032acc2279f/debugpy-1.8.14-py2.py3-none-any.whl", hash = "sha256:5cd9a579d553b6cb9759a7908a41988ee6280b961f24f63336835d9418216a20", size = 5256230 }, { url = "https://files.pythonhosted.org/packages/07/d5/98748d9860e767a1248b5e31ffa7ce8cb7006e97bf8abbf3d891d0a8ba4e/debugpy-1.8.15-py2.py3-none-any.whl", hash = "sha256:bce2e6c5ff4f2e00b98d45e7e01a49c7b489ff6df5f12d881c67d2f1ac635f3d", size = 5282697 },
] ]
[[package]] [[package]]
@ -906,15 +906,15 @@ wheels = [
[[package]] [[package]]
name = "pyinstaller-hooks-contrib" name = "pyinstaller-hooks-contrib"
version = "2025.5" version = "2025.6"
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/5f/ff/e3376595935d5f8135964d2177cd3e3e0c1b5a6237497d9775237c247a5d/pyinstaller_hooks_contrib-2025.5.tar.gz", hash = "sha256:707386770b8fe066c04aad18a71bc483c7b25e18b4750a756999f7da2ab31982", size = 163124 } sdist = { url = "https://files.pythonhosted.org/packages/ea/f7/d375cf8112d839afaf05b90c70dd128624473fd915fce211f5646b0afbc7/pyinstaller_hooks_contrib-2025.6.tar.gz", hash = "sha256:223ae773733fb7a0ee9cb5e817480998a90a6c7a9c3d2b7b580d2dfa2b325751", size = 163799 }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/0c/2c/b4d317534e17dd1df95c394d4b37febb15ead006a1c07c2bb006481fb5e7/pyinstaller_hooks_contrib-2025.5-py3-none-any.whl", hash = "sha256:ebfae1ba341cb0002fb2770fad0edf2b3e913c2728d92df7ad562260988ca373", size = 437246 }, { url = "https://files.pythonhosted.org/packages/da/e6/ab065bd226099a4e39aa08a54810f846beb7a9c534fa221ee750a3befa25/pyinstaller_hooks_contrib-2025.6-py3-none-any.whl", hash = "sha256:06779d024f7d60dd75b05520923bba16b17df5f64073434b23e570ffb71094dc", size = 440590 },
] ]
[[package]] [[package]]