Merge pull request #460 from arabcoders/dev
Some checks failed
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
native-build / build (amd64, ubuntu-latest) (push) Has been cancelled
native-build / build (amd64, windows-latest) (push) Has been cancelled

v1.0.2
This commit is contained in:
Abdulmohsen 2025-10-22 19:02:44 +03:00 committed by GitHub
commit b95b6eb6be
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 2039 additions and 750 deletions

653
API.md
View file

@ -72,6 +72,51 @@ This document describes the available endpoints and their usage. All endpoints r
- [GET /api/dev/loop](#get-apidevloop) - [GET /api/dev/loop](#get-apidevloop)
- [GET /api/dev/pip](#get-apidevpip) - [GET /api/dev/pip](#get-apidevpip)
- [GET /api/docs/{file}](#get-apidocsfile) - [GET /api/docs/{file}](#get-apidocsfile)
- [WebSocket API](#websocket-api)
- [Connection](#connection)
- [Authentication](#authentication-1)
- [Message Format](#message-format)
- [Core Events](#core-events)
- [Connection Events](#connection-events)
- [`connect` (Built-in)](#connect-built-in)
- [`disconnect` (Built-in)](#disconnect-built-in)
- [`connected` (Server → Client)](#connected-server--client)
- [Subscription Events](#subscription-events)
- [`subscribe` (Client → Server)](#subscribe-client--server)
- [`unsubscribe` (Client → Server)](#unsubscribe-client--server)
- [`subscribed` (Server → Client)](#subscribed-server--client)
- [`unsubscribed` (Server → Client)](#unsubscribed-server--client)
- [Logging Events](#logging-events)
- [`log_info` (Server → Client)](#log_info-server--client)
- [`log_success` (Server → Client)](#log_success-server--client)
- [`log_warning` (Server → Client)](#log_warning-server--client)
- [`log_error` (Server → Client)](#log_error-server--client)
- [`log_lines` (Server → Client)](#log_lines-server--client)
- [Download Queue Events](#download-queue-events)
- [`add_url` (Client → Server)](#add_url-client--server)
- [`item_added` (Server → Client)](#item_added-server--client)
- [`item_updated` (Server → Client)](#item_updated-server--client)
- [`item_completed` (Server → Client)](#item_completed-server--client)
- [`item_cancelled` (Server → Client)](#item_cancelled-server--client)
- [`item_deleted` (Server → Client)](#item_deleted-server--client)
- [`item_cancel` (Client → Server)](#item_cancel-client--server)
- [`item_delete` (Client → Server)](#item_delete-client--server)
- [`item_start` (Client → Server)](#item_start-client--server)
- [`item_pause` (Client → Server)](#item_pause-client--server)
- [`item_moved` (Server → Client)](#item_moved-server--client)
- [Queue Control Events](#queue-control-events)
- [`paused` (Server → Client)](#paused-server--client)
- [`resumed` (Server → Client)](#resumed-server--client)
- [Terminal/CLI Events](#terminalcli-events)
- [`cli_post` (Client → Server)](#cli_post-client--server)
- [`cli_output` (Server → Client)](#cli_output-server--client)
- [`cli_close` (Server → Client)](#cli_close-server--client)
- [Configuration Events](#configuration-events)
- [`presets_update` (Server → Client)](#presets_update-server--client)
- [`dlfields_update` (Server → Client)](#dlfields_update-server--client)
- [Client Implementation Examples](#client-implementation-examples)
- [Vue 3 Composable Pattern](#vue-3-composable-pattern)
- [Python Client Example](#python-client-example)
- [Error Responses](#error-responses) - [Error Responses](#error-responses)
--- ---
@ -1517,6 +1562,614 @@ or an error:
--- ---
## WebSocket API
The WebSocket API provides real-time bidirectional communication between the client and server using Socket.IO protocol. It enables live updates for downloads, queue status, notifications, and terminal access.
### Connection
**URL**: `ws://localhost:8081/socket.io/` (development) or `https://yourdomain.com/socket.io/` (production)
The client automatically connects to the WebSocket server and receives a `connected` event with initial state. The connection uses automatic reconnection with exponential backoff (default: up to 30 attempts, 5s delay).
**Connection Options**:
```javascript
{
transports: ['websocket', 'polling'], // Fallback to long-polling if WebSocket unavailable
withCredentials: true, // Include cookies for authentication
reconnection: true, // Enable automatic reconnection
reconnectionAttempts: 30, // Max reconnection attempts
reconnectionDelay: 5000 // Initial reconnection delay in ms
}
```
### Authentication
If Basic Authentication is configured, include credentials when establishing the WebSocket connection:
1. **Via HTTP Headers** (automatic in browsers):
```
Authorization: Basic base64("<username>:<password>")
```
2. **Via Query Parameter**:
```
ws://localhost:8081/socket.io/?apikey=<base64_urlsafe("<username>:<password>")>
```
### Message Format
All WebSocket messages are JSON-encoded and follow a consistent structure:
**Server-to-Client (Event)** - Emitted by server, received by client:
```json
{
"id": "unique-event-id",
"created_at": "2024-01-15T10:30:00.000000+00:00",
"event": "item_added",
"title": "Item Queued",
"message": "Video added to download queue",
"data": {...}
}
```
### Core Events
#### Connection Events
##### `connect` (Built-in)
Fired when WebSocket connection is established. No data payload.
```typescript
socket.on('connect', () => console.log('WebSocket connected'));
```
##### `disconnect` (Built-in)
Fired when WebSocket connection is closed. No data payload.
```typescript
socket.on('disconnect', (reason: string) => console.log('WebSocket disconnected:', reason));
```
##### `connected` (Server → Client)
Initial connection event with full application state.
**Data Fields**:
- `config`: Global configuration object
- `queue`: Current download queue (array of items)
- `done`: Download history (array of completed items)
- `tasks`: Scheduled tasks
- `presets`: Available download presets
- `dl_fields`: Available download fields
- `folders`: Directory structure for downloads
- `paused`: Queue pause status (boolean)
**Example**:
```typescript
socket.on('connected', (data: string) => {
const json = JSON.parse(data);
const queueItems = json.data.queue || {};
const historyItems = json.data.done || {};
console.log('Connected with', Object.keys(queueItems).length, 'queued downloads');
});
```
#### Subscription Events
##### `subscribe` (Client → Server)
Subscribe to a specific event stream. Only supported events is `log_lines`.
**Data**: Event name as string
```javascript
socket.emit('subscribe', 'log_lines');
```
**Response**: `subscribed` event with confirmation.
##### `unsubscribe` (Client → Server)
Unsubscribe from an event stream.
**Data**: Event name as string
```javascript
socket.emit('unsubscribe', 'log_lines');
```
**Response**: `unsubscribed` event with confirmation.
##### `subscribed` (Server → Client)
Confirmation of successful subscription.
```typescript
socket.on('subscribed', (data: string) => {
const json = JSON.parse(data);
console.log('Subscribed to event:', json.data.event);
});
```
##### `unsubscribed` (Server → Client)
Confirmation of successful unsubscription.
```typescript
socket.on('unsubscribed', (data: string) => {
const json = JSON.parse(data);
console.log('Unsubscribed from event:', json.data.event);
});
```
#### Logging Events
All logging events follow the same structure with JSON-encoded message:
##### `log_info` (Server → Client)
General informational message.
```typescript
socket.on('log_info', (stream: string) => {
const json = JSON.parse(stream);
console.info(json.message, json.data);
});
```
##### `log_success` (Server → Client)
Success notification message.
```typescript
socket.on('log_success', (stream: string) => {
const json = JSON.parse(stream);
console.log('✓', json.message);
});
```
##### `log_warning` (Server → Client)
Warning notification message.
```typescript
socket.on('log_warning', (stream: string) => {
const json = JSON.parse(stream);
console.warn('⚠', json.message);
});
```
##### `log_error` (Server → Client)
Error notification message.
```typescript
socket.on('log_error', (stream: string) => {
const json = JSON.parse(stream);
console.error('✗', json.message);
});
```
##### `log_lines` (Server → Client)
Continuous application log lines (requires subscription).
**Data Fields**:
- `line`: Log line content
- `timestamp`: Log timestamp
```typescript
socket.on('log_lines', (stream: string) => {
const json = JSON.parse(stream);
console.log('[LOG]', json.data.line);
});
```
### Download Queue Events
#### `add_url` (Client → Server)
Add a new URL to the download queue. Payload is exactly the same as
the POST `/api/queue/` endpoint.
**Data**:
```json
{
"url": "https://youtube.com/watch?v=...",
"preset": "default",
...
}
```
**Responses**: `item_added` or `log_error` event
```typescript
socket.emit('add_url', {
url: 'https://youtube.com/watch?v=dQw4w9WgXcQ',
preset: 'default'
});
```
#### `item_added` (Server → Client)
Emitted when a new item is successfully added to the queue. The response payload is the complete item object.
```typescript
socket.on('item_added', (stream: string) => {
const json = JSON.parse(stream);
const item = json.data;
console.log(`Added: ${item.title} [${item.url}]`);
});
```
#### `item_updated` (Server → Client)
Emitted when an item's status or progress changes **(high-frequency event)**.
**Data Fields**: Same as `item_added`
```typescript
socket.on('item_updated', (stream: string) => {
const json = JSON.parse(stream);
const item = json.data;
console.log(`Progress: ${item.title} - ${item.progress}%`);
});
```
#### `item_completed` (Server → Client)
Emitted when a download completes. Item moves from queue to history.
**Data Fields**: Complete item object with final status
```typescript
socket.on('item_completed', (stream: string) => {
const json = JSON.parse(stream);
const item = json.data;
console.log(`✓ Completed: ${item.title}`);
console.log(`Saved to: ${item.output_path}`);
});
```
#### `item_cancelled` (Server → Client)
Emitted when a download is cancelled by user.
**Data Fields**: Item object with status `cancelled`
```typescript
socket.on('item_cancelled', (stream: string) => {
const json = JSON.parse(stream);
const item = json.data;
console.log(`✗ Cancelled: ${item.title}`);
});
```
#### `item_deleted` (Server → Client)
Emitted when an item is deleted from history.
**Data Fields**: Item object
```typescript
socket.on('item_deleted', (stream: string) => {
const json = JSON.parse(stream);
console.log(`Deleted from history: ${json.data._id}`);
});
```
#### `item_cancel` (Client → Server)
Cancel an active download.
**Data**: Item ID (string)
```typescript
socket.emit('item_cancel', 'download-id-here');
```
**Response**: `item_cancelled` event
#### `item_delete` (Client → Server)
Delete an item from history.
**Data**:
```json
{
"id": "item-id",
"remove_file": false
}
```
**Parameters**:
- `id`: Item ID (required)
- `remove_file`: Also delete downloaded file (optional)
```typescript
socket.emit('item_delete', {
id: 'item-id',
remove_file: true // Also delete the file if deleting is enabled server-side.
});
```
**Response**: `item_deleted` event
#### `item_start` (Client → Server)
Start or resume a paused download.
**Data**: Item ID (string) or array of IDs
```typescript
socket.emit('item_start', 'download-id');
// OR
socket.emit('item_start', ['id1', 'id2']);
```
**Response**: Queue status updated via `item_updated`
#### `item_pause` (Client → Server)
Pause an active download.
**Data**: Item ID (string) or array of IDs
```typescript
socket.emit('item_pause', 'download-id');
// OR
socket.emit('item_pause', ['id1', 'id2']);
```
**Response**: Queue status updated via `item_updated`
#### `item_moved` (Server → Client)
Emitted when an item moves between queue and history.
**Data Fields**:
- `to`: Destination location (`queue` or `history`)
- `item`: Complete item object
```typescript
socket.on('item_moved', (stream: string) => {
const json = JSON.parse(stream);
console.log(`Item moved to: ${json.data.to}`);
});
```
### Queue Control Events
#### `paused` (Server → Client)
Emitted when the entire download queue is paused.
**Data Fields**:
- `paused`: Boolean true
```typescript
socket.on('paused', (stream: string) => {
const json = JSON.parse(stream);
console.log('Queue paused');
});
```
#### `resumed` (Server → Client)
Emitted when the download queue is resumed.
**Data Fields**:
- `paused`: Boolean false
```typescript
socket.on('resumed', (stream: string) => {
const json = JSON.parse(stream);
console.log('Queue resumed');
});
```
### Terminal/CLI Events
The terminal feature requires `console_enabled: true` in configuration.
#### `cli_post` (Client → Server)
Execute a command via yt-dlp CLI.
**Data**: Command arguments as string (without `yt-dlp` prefix)
```typescript
socket.emit('cli_post', '--help');
socket.emit('cli_post', 'https://youtube.com/watch?v=... --info-json');
```
**Responses**: `cli_output` events followed by `cli_close`
#### `cli_output` (Server → Client)
Output line from the CLI command execution.
**Data Fields**:
- `type`: Output type (`stdout` or `stderr`)
- `line`: Output line content
```typescript
socket.on('cli_output', (stream: string) => {
const json = JSON.parse(stream);
if (json.data.type === 'stderr') {
console.error(json.data.line);
} else {
console.log(json.data.line);
}
});
```
#### `cli_close` (Server → Client)
Emitted when CLI command execution completes.
**Data Fields**:
- `exitcode`: Command exit code (0 = success, non-zero = error)
```typescript
socket.on('cli_close', (stream: string) => {
const json = JSON.parse(stream);
if (json.data.exitcode === 0) {
console.log('Command completed successfully');
} else {
console.error(`Command failed with exit code ${json.data.exitcode}`);
}
});
```
### Configuration Events
#### `presets_update` (Server → Client)
Emitted when download presets are updated or created.
**Data**: Array of preset objects
```typescript
socket.on('presets_update', (stream: string) => {
const json = JSON.parse(stream);
const presets = json.data || [];
console.log('Presets updated:', presets);
});
```
#### `dlfields_update` (Server → Client)
Emitted when download fields configuration is updated.
**Data**: Array of field objects
```typescript
socket.on('dlfields_update', (stream: string) => {
const json = JSON.parse(stream);
const fields = json.data || [];
console.log('Download fields updated:', fields);
});
```
### Client Implementation Examples
#### Vue 3 Composable Pattern
```typescript
// composables/useSocket.ts
import { ref, computed } from 'vue'
import { io, type Socket } from 'socket.io-client'
export function useSocket() {
const socket = ref<Socket | null>(null)
const isConnected = ref(false)
const queueItems = ref<Record<string, any>>({})
const historyItems = ref<Record<string, any>>({})
const connect = () => {
socket.value = io('/', {
transports: ['websocket', 'polling'],
withCredentials: true,
reconnection: true,
reconnectionAttempts: 30,
reconnectionDelay: 5000
})
socket.value.on('connect', () => {
isConnected.value = true
console.log('Connected to server')
})
socket.value.on('connected', (stream: string) => {
const json = JSON.parse(stream)
queueItems.value = json.data.queue || {}
historyItems.value = json.data.done || {}
})
socket.value.on('item_added', (stream: string) => {
const json = JSON.parse(stream)
queueItems.value[json.data._id] = json.data
})
socket.value.on('item_updated', (stream: string) => {
const json = JSON.parse(stream)
const item = json.data
if (queueItems.value[item._id]) {
queueItems.value[item._id] = item
}
})
socket.value.on('item_completed', (stream: string) => {
const json = JSON.parse(stream)
const item = json.data
delete queueItems.value[item._id]
historyItems.value[item._id] = item
})
socket.value.on('disconnect', () => {
isConnected.value = false
console.log('Disconnected from server')
})
}
const addDownload = (url: string, preset: string = 'default') => {
socket.value?.emit('add_url', { url, preset })
}
const cancelDownload = (id: string) => {
socket.value?.emit('item_cancel', id)
}
return {
socket,
isConnected,
queueItems,
historyItems,
connect,
addDownload,
cancelDownload
}
}
```
#### Python Client Example
```python
# WebSocket client for YTPTube
import json
import asyncio
import socketio
class YTPTubeClient:
def __init__(self, url: str = 'http://localhost:8081'):
self.sio = socketio.AsyncClient(
transports=['websocket', 'polling'],
reconnection_attempts=30,
reconnection_delay=5
)
self.url = url
self.queue = {}
self.history = {}
async def connect(self):
await self.sio.connect(self.url)
await self.sio.wait()
async def setup_listeners(self):
@self.sio.on('connected')
async def on_connected(data: str):
json_data = json.loads(data)
self.queue = json_data['data'].get('queue', {})
self.history = json_data['data'].get('done', {})
print('Connected to YTPTube')
@self.sio.on('item_added')
async def on_item_added(data: str):
json_data = json.loads(data)
item = json_data['data']
self.queue[item['_id']] = item
print(f"Added: {item['title']}")
@self.sio.on('item_completed')
async def on_item_completed(data: str):
json_data = json.loads(data)
item = json_data['data']
self.queue.pop(item['_id'], None)
self.history[item['_id']] = item
print(f"Completed: {item['title']}")
async def add_download(self, url: str, preset: str = 'default'):
await self.sio.emit('add_url', {'url': url, 'preset': preset})
async def cancel_download(self, item_id: str):
await self.sio.emit('item_cancel', item_id)
# Usage
async def main():
client = YTPTubeClient('http://localhost:8081')
await client.setup_listeners()
await client.connect()
await client.add_download('https://youtube.com/watch?v=...')
if __name__ == '__main__':
asyncio.run(main())
```
---
## Error Responses ## Error Responses
Most endpoints return standard error codes (`400`, `403`, `404`, `500`, etc.) and a JSON body on failure. For example: Most endpoints return standard error codes (`400`, `403`, `404`, `500`, etc.) and a JSON body on failure. For example:

View file

@ -33,6 +33,7 @@ from .Utils import (
extract_info, extract_info,
extract_ytdlp_logs, extract_ytdlp_logs,
load_cookies, load_cookies,
merge_dict,
str_to_dt, str_to_dt,
ytdlp_reject, ytdlp_reject,
) )
@ -358,10 +359,15 @@ class DownloadQueue(metaclass=Singleton):
if "thumbnail" not in etr and "youtube:" in entry.get("extractor", ""): if "thumbnail" not in etr and "youtube:" in entry.get("extractor", ""):
extras["thumbnail"] = f"https://img.youtube.com/vi/{etr['id']}/maxresdefault.jpg" extras["thumbnail"] = f"https://img.youtube.com/vi/{etr['id']}/maxresdefault.jpg"
return await self.add( newItem = item.new_with(url=etr.get("url") or etr.get("webpage_url"), extras=extras)
item=item.new_with(url=etr.get("url") or etr.get("webpage_url"), extras=extras),
already=already, if "formats" in etr and isinstance(etr["formats"], list) and len(etr["formats"]) > 0:
) LOG.warning(f"Unexpected formats entries in --flat-playlist for {item_name}, treating as video.")
return await self._add_video(
entry=merge_dict(merge_dict({"_type": "video"}, etr), entry), item=newItem, logs=[]
)
return await self.add(item=newItem, already=already)
finally: finally:
self.processors.release() self.processors.release()

View file

@ -449,7 +449,7 @@ class Notification(metaclass=Singleton):
notify.add(t.request.url) notify.add(t.request.url)
status = await notify.async_notify( status = await notify.async_notify(
body=ev.message or json.dumps(ev.serialize(), sort_keys=False, ensure_ascii=False), body=ev.message or self._encoder.encode(ev.serialize()),
title=ev.title or f"YTPTube Event: {ev.event}", title=ev.title or f"YTPTube Event: {ev.event}",
) )

View file

@ -251,7 +251,7 @@ def extract_info(
no_archive: bool = False, no_archive: bool = False,
follow_redirect: bool = False, follow_redirect: bool = False,
sanitize_info: bool = False, sanitize_info: bool = False,
**kwargs, # noqa: ARG001 **kwargs,
) -> dict: ) -> dict:
""" """
Extracts video information from the given URL. Extracts video information from the given URL.
@ -315,6 +315,11 @@ def extract_info(
params["logger"] = log_wrapper params["logger"] = log_wrapper
if kwargs.get("no_log", False):
params["logger"] = LogWrapper()
params["quiet"] = True
params["no_warnings"] = True
if no_archive and "download_archive" in params: if no_archive and "download_archive" in params:
del params["download_archive"] del params["download_archive"]
@ -368,7 +373,7 @@ def merge_dict(
source: dict, destination: dict, max_depth: int = 50, max_list_size: int = 10000, _depth: int = 0, _seen: set = None source: dict, destination: dict, max_depth: int = 50, max_list_size: int = 10000, _depth: int = 0, _seen: set = None
) -> dict: ) -> dict:
""" """
Merge data from source into destination safely with protection against DoS attacks. Merge data from source into destination.
Args: Args:
source (dict): Source data source (dict): Source data

View file

@ -4,6 +4,7 @@ from __future__ import annotations
import asyncio import asyncio
import fnmatch import fnmatch
import hashlib
import json import json
import logging import logging
import re import re
@ -19,9 +20,10 @@ from parsel import Selector
from parsel.selector import SelectorList from parsel.selector import SelectorList
from yt_dlp.utils.networking import random_user_agent from yt_dlp.utils.networking import random_user_agent
from app.library.cache import Cache
from app.library.config import Config from app.library.config import Config
from app.library.Tasks import Task, TaskFailure, TaskItem, TaskResult from app.library.Tasks import Task, TaskFailure, TaskItem, TaskResult
from app.library.Utils import get_archive_id from app.library.Utils import extract_info, get_archive_id
from ._base_handler import BaseHandler from ._base_handler import BaseHandler
@ -29,7 +31,7 @@ if TYPE_CHECKING:
from parsel.selector import SelectorList from parsel.selector import SelectorList
LOG: logging.Logger = logging.getLogger(__name__) LOG: logging.Logger = logging.getLogger(__name__)
CACHE: Cache = Cache()
@dataclass(slots=True) @dataclass(slots=True)
class MatchRule: class MatchRule:
@ -700,11 +702,35 @@ class GenericTaskHandler(BaseHandler):
idDict: str | None = get_archive_id(url=url) idDict: str | None = get_archive_id(url=url)
archive_id: str | None = idDict.get("archive_id") archive_id: str | None = idDict.get("archive_id")
if not archive_id: if not archive_id:
LOG.warning( cache_key: str = hashlib.sha256(f"{task.name}-{url}".encode()).hexdigest()
f"[{definition.name}]: '{task.name}': Could not compute archive ID for video '{url}' in feed. generating one." if CACHE.has(cache_key):
) archive_id = CACHE.get(cache_key)
if not archive_id:
continue
else:
LOG.warning(
f"[{definition.name}]: '{task.name}': Unable to generate static archive id for '{url}' in feed. Doing real request to fetch yt-dlp archive id."
)
archive_id = f"generic {_generic_id(url)}" info = extract_info(
config=task.get_ytdlp_opts().get_all(),
url=url,
no_archive=True,
no_log=True,
)
if not info:
LOG.error(f"[{definition.name}]: '{task.name}': Failed to extract info for URL '{url}' to generate archive ID. Skipping.")
CACHE.set(cache_key, None)
continue
if not info.get("id") or not info.get("extractor_key"):
LOG.error(f"[{definition.name}]: '{task.name}': Incomplete info extracted for URL '{url}' to generate archive ID. Skipping.")
CACHE.set(cache_key, None)
continue
archive_id = f"{str(info.get('extractor_key', '')).lower()} {info.get('id')}"
CACHE.set(cache_key, archive_id)
metadata: dict[str, str] = { metadata: dict[str, str] = {
k: v for k, v in entry.items() if k not in {"link", "url", "title", "published", "archive_id"} k: v for k, v in entry.items() if k not in {"link", "url", "title", "published", "archive_id"}

View file

@ -0,0 +1,260 @@
import hashlib
import logging
import re
from typing import TYPE_CHECKING, Any
from xml.etree.ElementTree import Element
from app.library.cache import Cache
from app.library.Tasks import Task, TaskFailure, TaskItem, TaskResult
from app.library.Utils import extract_info, get_archive_id
from ._base_handler import BaseHandler
if TYPE_CHECKING:
from xml.etree.ElementTree import Element
LOG: logging.Logger = logging.getLogger(__name__)
CACHE: Cache = Cache()
class RssGenericHandler(BaseHandler):
FEED_PATTERN: re.Pattern[str] = re.compile(
r"\.(rss|atom)(\?.*)?$|handler=rss",
re.IGNORECASE,
)
@staticmethod
def can_handle(task: Task) -> bool:
LOG.debug(f"'{task.name}': Checking if task URL is parsable RSS feed: {task.url}")
return RssGenericHandler.parse(task.url) is not None
@staticmethod
async def _get(
task: Task,
params: dict,
parsed: dict[str, str],
) -> tuple[str, list[dict[str, str]], int]:
"""
Fetch the feed and return raw entries.
Args:
task (Task): The task containing the feed URL.
params (dict): The ytdlp options.
parsed (dict): The parsed URL components (contains 'url' key).
Returns:
tuple[str, list[dict[str, str]], int]: The feed URL, list of entry dictionaries, and entry count.
"""
from defusedxml.ElementTree import fromstring
feed_url: str = parsed["url"]
LOG.debug(f"'{task.name}': Fetching RSS/Atom feed from {feed_url}")
response = await RssGenericHandler.request(url=feed_url, ytdlp_opts=params)
response.raise_for_status()
root: Element = fromstring(response.text)
# Define namespaces for different feed formats
ns: dict[str, str] = {
"atom": "http://www.w3.org/2005/Atom",
"rss": "http://www.rssboard.org/specification",
"content": "http://purl.org/rss/1.0/modules/content/",
"media": "http://search.yahoo.com/mrss/",
}
items: list[dict[str, str]] = []
real_count = 0
# Try to parse as Atom feed first
entries = root.findall("atom:entry", ns)
if entries:
LOG.debug(f"'{task.name}': Detected Atom feed format with {len(entries)} entries")
for entry in entries:
link_elem: Element | None = entry.find("atom:link[@rel='alternate']", ns)
if link_elem is None:
link_elem = entry.find("atom:link", ns)
url: str = ""
if link_elem is not None and link_elem.get("href"):
url = link_elem.get("href", "")
if not url:
LOG.warning(f"'{task.name}': Atom entry missing URL. Skipping.")
continue
title_elem: Element | None = entry.find("atom:title", ns)
title: str = title_elem.text if title_elem is not None and title_elem.text else ""
pub_elem: Element | None = entry.find("atom:published", ns)
published: str = pub_elem.text if pub_elem is not None and pub_elem.text else ""
real_count += 1
items.append({"url": url, "title": title, "published": published})
else:
# Try to parse as RSS feed
rss_items = root.findall(".//item")
LOG.debug(f"'{task.name}': Detected RSS feed format with {len(rss_items)} items")
for item in rss_items:
# Try different link element names (link, url, media:content)
url: str = ""
link_elem = item.find("link")
if link_elem is not None and link_elem.text:
url = link_elem.text
else:
# Try media:content
media_elem = item.find("media:content", ns)
if media_elem is not None and media_elem.get("url"):
url = media_elem.get("url", "")
else:
# Try enclosure
enclosure_elem = item.find("enclosure")
if enclosure_elem is not None and enclosure_elem.get("url"):
url = enclosure_elem.get("url", "")
if not url:
LOG.warning(f"'{task.name}': RSS item missing URL. Skipping.")
continue
title_elem = item.find("title")
title: str = title_elem.text if title_elem is not None and title_elem.text else ""
pub_elem = item.find("pubDate")
published: str = pub_elem.text if pub_elem is not None and pub_elem.text else ""
real_count += 1
items.append({"url": url, "title": title, "published": published})
return feed_url, items, real_count
@staticmethod
async def extract(task: Task) -> TaskResult | TaskFailure:
"""
Extract items from an RSS/Atom feed.
Args:
task (Task): The task containing the feed URL.
Returns:
TaskResult | TaskFailure: Extraction result with parsed items or failure information.
"""
parsed: dict[str, str] | None = RssGenericHandler.parse(task.url)
if not parsed:
return TaskFailure(message="Unrecognized RSS/Atom feed URL.")
params: dict = task.get_ytdlp_opts().get_all()
try:
feed_url, items, real_count = await RssGenericHandler._get(task, params, parsed)
except Exception as exc:
LOG.exception(exc)
return TaskFailure(message="Failed to fetch RSS/Atom feed.", error=str(exc))
task_items: list[TaskItem] = []
for entry in items:
if not (url := entry.get("url")):
continue
# Try to get static archive ID first
id_dict: dict[str, str | None] = get_archive_id(url=url)
archive_id: str | None = id_dict.get("archive_id")
# If static archive_id fails, try to fetch it via yt-dlp (like generic.py)
if not archive_id:
cache_key: str = hashlib.sha256(f"{task.name}-{url}".encode()).hexdigest()
if CACHE.has(cache_key):
archive_id = CACHE.get(cache_key)
if not archive_id:
LOG.debug(f"'{task.name}': Cached failure for URL '{url}'. Skipping.")
continue
else:
LOG.warning(
f"'{task.name}': Unable to generate static archive ID for '{url}' in feed. "
"Doing real request to fetch yt-dlp archive ID."
)
info = extract_info(
config=params,
url=url,
no_archive=True,
no_log=True,
)
if not info:
LOG.error(
f"'{task.name}': Failed to extract info for URL '{url}' to generate archive ID. Skipping."
)
CACHE.set(cache_key, None)
continue
if not info.get("id") or not info.get("extractor_key"):
LOG.error(
f"'{task.name}': Incomplete info extracted for URL '{url}' to generate archive ID. Skipping."
)
CACHE.set(cache_key, None)
continue
archive_id = f"{str(info.get('extractor_key', '')).lower()} {info.get('id')}"
CACHE.set(cache_key, archive_id)
metadata: dict[str, Any] = {k: v for k, v in entry.items() if k not in {"url", "title", "published"}}
task_items.append(
TaskItem(
url=url,
title=entry.get("title"),
archive_id=archive_id,
metadata={"published": entry.get("published"), **metadata},
)
)
return TaskResult(
items=task_items,
metadata={"feed_url": feed_url, "entry_count": real_count},
)
@staticmethod
def parse(url: str) -> dict[str, str] | None:
"""
Parse URL for valid RSS/Atom feed.
Args:
url (str): The URL to parse.
Returns:
dict[str, str] | None: A dictionary with 'url' key if valid RSS/Atom feed, None otherwise.
"""
if not isinstance(url, str) or not url:
return None
return {"url": url} if RssGenericHandler.FEED_PATTERN.search(url) else None
@staticmethod
def tests() -> list[tuple[str, bool]]:
"""
Test cases for the URL parser.
Returns:
list[tuple[str, bool]]: A list of tuples containing the URL and expected result.
"""
return [
("https://www.example.com/test.rss", True),
("https://www.example.com/test.atom", True),
("https://www.example.com/test.atom#handler=rss", True),
("https://www.example.com/test.atom?handler=rss", True),
("https://www.example.com/feed.rss?version=2.0", True),
("https://www.example.com/test.xml", False),
("https://www.example.com/channel/UC_x5XG1OV2P6uZZ5FSM9Ttw", False),
("https://www.example.com/playlist?list=PLBCF2DAC6FFB574DE", False),
("https://www.example.com/user/SomeUser", False),
("https://example.com/feed.ATOM", True),
("https://example.com/feed.RSS", True),
]

View file

@ -25,6 +25,7 @@ FRONTEND_ROUTES: list[str] = [
"/console/", "/console/",
"/presets/", "/presets/",
"/tasks/", "/tasks/",
"/task_definitions/",
"/notifications/", "/notifications/",
"/changelog/", "/changelog/",
"/logs/", "/logs/",

View file

@ -244,7 +244,7 @@ async def get_options() -> Response:
@route("POST", "api/yt-dlp/archive_id/", "get_archive_ids") @route("POST", "api/yt-dlp/archive_id/", "get_archive_ids")
async def get_archive_ids(request: Request, config: Config) -> Response: async def get_archive_ids(request: Request, config: Config) -> Response:
""" """
Get the yt-dlp CLI options. Get the archive IDs for the given URLs.
Returns: Returns:
Response: The response object with the yt-dlp CLI options. Response: The response object with the yt-dlp CLI options.

View file

@ -1,6 +1,7 @@
import json import json
from pathlib import Path from pathlib import Path
from types import SimpleNamespace from types import SimpleNamespace
from unittest.mock import patch
import pytest import pytest
@ -308,14 +309,19 @@ async def test_generic_task_handler_inspect(monkeypatch):
monkeypatch.setattr(GenericTaskHandler, "_fetch_content", staticmethod(fake_fetch_content)) monkeypatch.setattr(GenericTaskHandler, "_fetch_content", staticmethod(fake_fetch_content))
task = Task(id="inspect", name="Inspect", url="https://example.com/api") # Mock extract_info to return valid info with required fields for archive ID generation
result: TaskResult | TaskFailure = await GenericTaskHandler.extract(task) def fake_extract_info(config, url, **kwargs): # noqa: ARG001
return {"id": "test_video_1", "extractor_key": "Example"}
assert isinstance(result, TaskResult) with patch("app.library.task_handlers.generic.extract_info", side_effect=fake_extract_info):
assert len(result.items) == 1 task = Task(id="inspect", name="Inspect", url="https://example.com/api")
item = result.items[0] result: TaskResult | TaskFailure = await GenericTaskHandler.extract(task)
assert item.url == "https://example.com/video/1"
assert item.title == "First" assert isinstance(result, TaskResult)
assert len(result.items) == 1
item = result.items[0]
assert item.url == "https://example.com/video/1"
assert item.title == "First"
def test_parse_items_handles_json_top_level_list(): def test_parse_items_handles_json_top_level_list():

View file

@ -0,0 +1,261 @@
import pytest
from app.library.task_handlers.rss import RssGenericHandler
from app.library.Tasks import Task, TaskResult
class DummyResponse:
def __init__(self, text: str):
self.text = text
def raise_for_status(self) -> None:
return None
class DummyOpts:
def __init__(self, data):
self._data = data
def get_all(self):
return self._data
class TestRssHandlerParsing:
"""Test URL parsing for RSS/Atom feeds using the tests() method."""
@pytest.mark.parametrize(("url", "expected"), RssGenericHandler.tests())
def test_url_parsing(self, url: str, expected: bool):
"""Test URL parsing against all defined test cases."""
result = RssGenericHandler.parse(url)
is_matched = result is not None
assert is_matched == expected, f"URL '{url}' expected {expected}, got {is_matched}"
@pytest.mark.parametrize(("url", "expected"), RssGenericHandler.tests())
def test_returns_url_dict_on_match(self, url: str, expected: bool):
"""Test that parse returns dict with 'url' key for valid feeds."""
result = RssGenericHandler.parse(url)
if expected:
assert result is not None
assert "url" in result
assert result["url"] == url
else:
assert result is None
class TestRssHandlerExtraction:
"""Test RSS feed extraction and parsing."""
@pytest.mark.asyncio
async def test_rss_atom_feed_extraction(self, monkeypatch):
"""Test extraction from Atom feed."""
atom_feed = """<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<title>Example Feed</title>
<entry>
<title>Video 1</title>
<link href="https://www.youtube.com/watch?v=abc123" rel="alternate" />
<published>2024-01-01T00:00:00Z</published>
</entry>
<entry>
<title>Video 2</title>
<link href="https://www.youtube.com/watch?v=def456" rel="alternate" />
<published>2024-01-02T00:00:00Z</published>
</entry>
</feed>
""".strip()
async def fake_request(**kwargs): # noqa: ARG001
return DummyResponse(atom_feed)
monkeypatch.setattr(RssGenericHandler, "request", staticmethod(fake_request))
monkeypatch.setattr(Task, "get_ytdlp_opts", lambda self: DummyOpts({"download_archive": "/tmp/archive"})) # noqa: ARG005
task = Task(
id="test_rss",
name="Test Atom Feed",
url="https://example.com/feed.atom",
preset="default",
)
result = await RssGenericHandler.extract(task)
assert isinstance(result, TaskResult)
assert len(result.items) == 2
assert result.items[0].title == "Video 1"
assert result.items[0].url == "https://www.youtube.com/watch?v=abc123"
assert result.items[1].title == "Video 2"
assert result.items[1].url == "https://www.youtube.com/watch?v=def456"
assert result.metadata["entry_count"] == 2
@pytest.mark.asyncio
async def test_rss_feed_extraction(self, monkeypatch):
"""Test extraction from RSS feed."""
rss_feed = """<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
<channel>
<title>Example Channel</title>
<item>
<title>Video 1</title>
<link>https://www.youtube.com/watch?v=abc123</link>
<pubDate>Mon, 01 Jan 2024 00:00:00 +0000</pubDate>
</item>
<item>
<title>Video 2</title>
<link>https://www.youtube.com/watch?v=def456</link>
<pubDate>Tue, 02 Jan 2024 00:00:00 +0000</pubDate>
</item>
</channel>
</rss>
""".strip()
async def fake_request(**kwargs): # noqa: ARG001
return DummyResponse(rss_feed)
monkeypatch.setattr(RssGenericHandler, "request", staticmethod(fake_request))
monkeypatch.setattr(Task, "get_ytdlp_opts", lambda self: DummyOpts({"download_archive": "/tmp/archive"})) # noqa: ARG005
task = Task(
id="test_rss",
name="Test RSS Feed",
url="https://example.com/feed.rss",
preset="default",
)
result = await RssGenericHandler.extract(task)
assert isinstance(result, TaskResult)
assert len(result.items) == 2
assert result.items[0].title == "Video 1"
assert result.items[0].url == "https://www.youtube.com/watch?v=abc123"
assert result.items[1].title == "Video 2"
assert result.items[1].url == "https://www.youtube.com/watch?v=def456"
assert result.metadata["entry_count"] == 2
@pytest.mark.asyncio
async def test_can_handle(self, monkeypatch):
"""Test can_handle method."""
atom_feed = """<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<entry>
<link href="https://www.youtube.com/watch?v=abc123" />
</entry>
</feed>
""".strip()
async def fake_request(**kwargs): # noqa: ARG001
return DummyResponse(atom_feed)
monkeypatch.setattr(RssGenericHandler, "request", staticmethod(fake_request))
monkeypatch.setattr(Task, "get_ytdlp_opts", lambda self: DummyOpts({"download_archive": "/tmp/archive"})) # noqa: ARG005
task = Task(
id="test_rss",
name="Test Feed",
url="https://example.com/feed.atom",
preset="default",
)
assert RssGenericHandler.can_handle(task) is True
non_feed_task = Task(
id="test_youtube",
name="YouTube Video",
url="https://www.youtube.com/watch?v=abc123",
preset="default",
)
assert RssGenericHandler.can_handle(non_feed_task) is False
class TestRssHandlerEdgeCases:
"""Test edge cases in RSS handling."""
@pytest.mark.asyncio
async def test_empty_feed(self, monkeypatch):
"""Test handling of empty feed."""
empty_feed = """<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
<channel>
<title>Empty Channel</title>
</channel>
</rss>
""".strip()
async def fake_request(**kwargs): # noqa: ARG001
return DummyResponse(empty_feed)
monkeypatch.setattr(RssGenericHandler, "request", staticmethod(fake_request))
monkeypatch.setattr(Task, "get_ytdlp_opts", lambda self: DummyOpts({"download_archive": "/tmp/archive"})) # noqa: ARG005
task = Task(
id="test_empty",
name="Empty Feed",
url="https://example.com/feed.rss",
preset="default",
)
result = await RssGenericHandler.extract(task)
assert isinstance(result, TaskResult)
assert len(result.items) == 0
assert result.metadata["entry_count"] == 0
@pytest.mark.asyncio
async def test_invalid_feed_url(self, monkeypatch):
"""Test handling of invalid feed URL."""
from app.library.Tasks import TaskFailure
async def fake_request(**kwargs): # noqa: ARG001
msg = "Network error"
raise Exception(msg)
monkeypatch.setattr(RssGenericHandler, "request", staticmethod(fake_request))
monkeypatch.setattr(Task, "get_ytdlp_opts", lambda self: DummyOpts({"download_archive": "/tmp/archive"})) # noqa: ARG005
task = Task(
id="test_invalid",
name="Invalid Feed",
url="https://example.com/feed.rss",
preset="default",
)
result = await RssGenericHandler.extract(task)
assert isinstance(result, TaskFailure)
@pytest.mark.asyncio
async def test_missing_urls_in_feed(self, monkeypatch):
"""Test handling of entries missing URLs."""
feed = """<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
<channel>
<item>
<title>No URL Item</title>
</item>
<item>
<title>Valid Item</title>
<link>https://www.youtube.com/watch?v=abc123</link>
</item>
</channel>
</rss>
""".strip()
async def fake_request(**kwargs): # noqa: ARG001
return DummyResponse(feed)
monkeypatch.setattr(RssGenericHandler, "request", staticmethod(fake_request))
monkeypatch.setattr(Task, "get_ytdlp_opts", lambda self: DummyOpts({"download_archive": "/tmp/archive"})) # noqa: ARG005
task = Task(
id="test_missing",
name="Feed with Missing URLs",
url="https://example.com/feed.rss",
preset="default",
)
result = await RssGenericHandler.extract(task)
# Should only include the item with URL
assert isinstance(result, TaskResult)
assert len(result.items) == 1
assert result.items[0].title == "Valid Item"

View file

@ -81,7 +81,7 @@
<div class="columns is-multiline" v-if="'list' === display_style"> <div class="columns is-multiline" v-if="'list' === display_style">
<div class="column is-12" v-if="hasItems"> <div class="column is-12" v-if="hasItems">
<div :class="{ 'table-container': table_container }"> <div class="table-container">
<table class="table is-striped is-hoverable is-fullwidth is-bordered" <table class="table is-striped is-hoverable is-fullwidth is-bordered"
style="min-width: 1300px; table-layout: fixed;"> style="min-width: 1300px; table-layout: fixed;">
<thead> <thead>
@ -479,7 +479,6 @@ const thumbnail_ratio = useStorage<'is-16by9' | 'is-3by1'>('thumbnail_ratio', 'i
const selectedElms = ref<string[]>([]) const selectedElms = ref<string[]>([])
const masterSelectAll = ref(false) const masterSelectAll = ref(false)
const table_container = ref(false)
const embed_url = ref('') const embed_url = ref('')
const video_item = ref<StoreItem | null>(null) const video_item = ref<StoreItem | null>(null)
const dialog_confirm = ref<{ const dialog_confirm = ref<{

View file

@ -46,7 +46,7 @@
<div class="columns is-multiline" v-if="'list' === display_style"> <div class="columns is-multiline" v-if="'list' === display_style">
<div class="column is-12" v-if="filteredItems.length > 0"> <div class="column is-12" v-if="filteredItems.length > 0">
<div :class="{ 'table-container': table_container }"> <div class="table-container">
<table class="table is-striped is-hoverable is-fullwidth is-bordered" <table class="table is-striped is-hoverable is-fullwidth is-bordered"
style="min-width: 1300px; table-layout: fixed;"> style="min-width: 1300px; table-layout: fixed;">
<thead> <thead>
@ -341,7 +341,6 @@ const thumbnail_ratio = useStorage<'is-16by9' | 'is-3by1'>('thumbnail_ratio', 'i
const selectedElms = ref<string[]>([]) const selectedElms = ref<string[]>([])
const masterSelectAll = ref(false) const masterSelectAll = ref(false)
const embed_url = ref('') const embed_url = ref('')
const table_container = ref(false)
const showThumbnails = computed(() => !!props.thumbnails && !hideThumbnail.value) const showThumbnails = computed(() => !!props.thumbnails && !hideThumbnail.value)

View file

@ -7,6 +7,12 @@
URL</b></NuxtLink> link.</span> URL</b></NuxtLink> link.</span>
</Message> </Message>
</div> </div>
<div class="column is-12" v-if="form?.url && is_generic_rss(form.url)">
<Message title="Information" class="is-info is-background-info-80 has-text-dark" icon="fas fa-info-circle">
<span>You are using a generic RSS/Atom feed URL. The task handler will automatically download new items found
in this feed.</span>
</Message>
</div>
<div class="column is-12"> <div class="column is-12">
<form autocomplete="off" id="taskForm" @submit.prevent="checkInfo()"> <form autocomplete="off" id="taskForm" @submit.prevent="checkInfo()">
<div class="card"> <div class="card">
@ -238,8 +244,8 @@
Mark all existing items as downloaded Mark all existing items as downloaded
</label> </label>
<div class="control is-unselectable"> <div class="control is-unselectable">
<input id="archive_all_after_add" type="checkbox" v-model="archiveAllAfterAdd" :disabled="addInProgress" <input id="archive_all_after_add" type="checkbox" v-model="archiveAllAfterAdd"
class="switch is-danger" /> :disabled="addInProgress" class="switch is-danger" />
<label for="archive_all_after_add" class="is-unselectable"> <label for="archive_all_after_add" class="is-unselectable">
{{ archiveAllAfterAdd ? 'Yes' : 'No' }} {{ archiveAllAfterAdd ? 'Yes' : 'No' }}
</label> </label>
@ -299,21 +305,23 @@
<Message title="Tips" class="is-info is-background-info-80 has-text-dark" icon="fas fa-info-circle"> <Message title="Tips" class="is-info is-background-info-80 has-text-dark" icon="fas fa-info-circle">
<span> <span>
<ul> <ul>
<li>To enable YouTube RSS feed monitoring, The task URL must include a <code>channel_id</code> or <li><strong>YouTube RSS:</strong> Use <code>channel_id</code> or <code>playlist_id</code> URLs. Other link
<code>playlist_id</code>. Other link types wont work. types (custom names, handles, user profiles) are not supported.
</li> </li>
<li>RSS monitoring runs every hour alongside the actual task execution. Regardless if the task has timer set <li><strong>Generic RSS/Atom:</strong> URL must end with <code>.rss</code> or <code>.atom</code>. If not
or not. To opt out of RSS monitoring for a specific task, simply disable the <code>Enable Handler</code> possible, append <code>&handler=rss</code> to existing query parameters, or add <code>#handler=rss</code>
option. To have the task only monitor RSS feed, <b>do not set timer</b>.</li> as a fragment.
<li>RSS Feed monitoring will only work if you have <code>--download-archive</code> set in <code>Command options </li>
for yt-dlp</code> via the task itself, or preset. command options <li><strong>RSS Monitoring Basics:</strong> Runs hourly independently. Timer controls scheduled downloads to
yt-dlp. Disable <code>Enable Handler</code> to disable RSS monitoring.
</li>
<li><strong>Archive Requirement:</strong> RSS monitoring requires <code>--download-archive</code> in
<code>Command options for yt-dlp</code> (set in task or preset).
</li> </li>
</ul> </ul>
</span> </span>
</Message> </Message>
</div> </div> <datalist id="folders" v-if="config?.folders">
<datalist id="folders" v-if="config?.folders">
<option v-for="dir in config.folders" :key="dir" :value="dir" /> <option v-for="dir in config.folders" :key="dir" :value="dir" />
</datalist> </datalist>
<Modal v-if="showOptions" @close="showOptions = false" :contentClass="'modal-content-max'"> <Modal v-if="showOptions" @close="showOptions = false" :contentClass="'modal-content-max'">
@ -329,7 +337,7 @@ import { CronExpressionParser } from 'cron-parser'
import TextareaAutocomplete from '~/components/TextareaAutocomplete.vue' import TextareaAutocomplete from '~/components/TextareaAutocomplete.vue'
import type { AutoCompleteOptions } from '~/types/autocomplete' import type { AutoCompleteOptions } from '~/types/autocomplete'
import type { exported_task, task_item } from '~/types/tasks' import type { exported_task, task_item } from '~/types/tasks'
import {useConfirm} from '~/composables/useConfirm' import { useConfirm } from '~/composables/useConfirm'
const props = defineProps<{ const props = defineProps<{
reference?: string | null | undefined reference?: string | null | undefined
@ -354,7 +362,7 @@ const ytDlpOpt = ref<AutoCompleteOptions>([])
const archiveAllAfterAdd = ref<boolean>(false) const archiveAllAfterAdd = ref<boolean>(false)
const CHANNEL_REGEX = /^https?:\/\/(?:www\.)?youtube\.com\/(?:(?:channel\/(?<channelId>UC[0-9A-Za-z_-]{22}))|(?:c\/(?<customName>[A-Za-z0-9_-]+))|(?:user\/(?<userName>[A-Za-z0-9_-]+))|(?:@(?<handle>[A-Za-z0-9_-]+)))(?<suffix>\/.*)?\/?$/ const CHANNEL_REGEX = /^https?:\/\/(?:www\.)?youtube\.com\/(?:(?:channel\/(?<channelId>UC[0-9A-Za-z_-]{22}))|(?:c\/(?<customName>[A-Za-z0-9_-]+))|(?:user\/(?<userName>[A-Za-z0-9_-]+))|(?:@(?<handle>[A-Za-z0-9_-]+)))(?<suffix>\/.*)?\/?$/
const GENERIC_RSS_REGEX = /\.(rss|atom)(\?.*)?$|handler=rss/i
const form = reactive<task_item>({ ...props.task }) const form = reactive<task_item>({ ...props.task })
watch(() => config.ytdlp_options, newOptions => ytDlpOpt.value = newOptions watch(() => config.ytdlp_options, newOptions => ytDlpOpt.value = newOptions
@ -497,6 +505,13 @@ const is_yt_handle = (url: string): boolean => {
return false return false
} }
const is_generic_rss = (url: string): boolean => {
if (!url || '' === url) {
return false
}
return GENERIC_RSS_REGEX.test(url)
}
const convert_url = async (url: string): Promise<string> => { const convert_url = async (url: string): Promise<string> => {
if (!url || '' === url) { if (!url || '' === url) {
return url return url

View file

@ -22,6 +22,23 @@ hr {
CHANGELOG CHANGELOG
</span> </span>
<div class="is-pulled-right">
<div class="field is-grouped">
<p class="control has-icons-left" v-if="toggleFilter && logs && logs.length > 0">
<input type="search" v-model.lazy="query" class="input" id="filter"
placeholder="Filter changelog entries">
<span class="icon is-left"><i class="fas fa-filter" /></span>
</p>
<p class="control" v-if="logs && logs.length > 0">
<button class="button is-danger is-light" @click="toggleFilter = !toggleFilter">
<span class="icon"><i class="fas fa-filter" /></span>
<span v-if="!isMobile">Filter</span>
</button>
</p>
</div>
</div>
<div class="is-hidden-mobile"> <div class="is-hidden-mobile">
<span class="subtitle">This page display the latest changes and updates from the project.</span> <span class="subtitle">This page display the latest changes and updates from the project.</span>
</div> </div>
@ -36,9 +53,9 @@ hr {
</div> </div>
<template v-if="!isLoading"> <template v-if="!isLoading">
<div class="logs-container"> <div class="logs-container" v-if="filteredLogs && filteredLogs.length > 0">
<div class="columns is-multiline"> <div class="columns is-multiline">
<div class="column p-0 m-0 is-12" v-for="(log, index) in logs" :key="log.tag"> <div class="column p-0 m-0 is-12" v-for="(log, index) in filteredLogs" :key="log.tag">
<div class="content p-0 m-0"> <div class="content p-0 m-0">
<h1 class="is-4"> <h1 class="is-4">
<span class="icon"><i class="fas fa-code-branch" /></span> <span class="icon"><i class="fas fa-code-branch" /></span>
@ -72,6 +89,16 @@ hr {
</div> </div>
</div> </div>
</div> </div>
<div class="columns is-multiline" v-if="!filteredLogs || filteredLogs.length < 1">
<div class="column is-12">
<Message title="No Results" class="is-background-warning-80 has-text-dark" icon="fas fa-search"
v-if="query" :useClose="true" @close="query = ''">
<p>No changelog entries found for the query: <strong>{{ query }}</strong>.</p>
<p>Please try a different search term.</p>
</Message>
</div>
</div>
</template> </template>
</main> </main>
</template> </template>
@ -82,6 +109,7 @@ import type { changelogs, changeset } from '~/types/changelogs'
const toast = useNotification() const toast = useNotification()
const config = useConfigStore() const config = useConfigStore()
const isMobile = useMediaQuery({ maxWidth: 1024 })
useHead({ title: 'CHANGELOG' }) useHead({ title: 'CHANGELOG' })
@ -94,6 +122,43 @@ const app_version = ref('')
const app_branch = ref('') const app_branch = ref('')
const app_sha = ref('') const app_sha = ref('')
const isLoading = ref(true) const isLoading = ref(true)
const query = ref<string>('')
const toggleFilter = ref<boolean>(false)
watch(toggleFilter, () => {
if (!toggleFilter.value) {
query.value = ''
}
})
const filteredLogs = computed<changelogs>(() => {
const q = query.value?.toLowerCase()
if (!q) return logs.value
return logs.value
.map(log => {
// Check if tag matches
const tagMatches = log.tag.toLowerCase().includes(q)
// Filter commits that match the query
const filteredCommits = log.commits?.filter(commit =>
commit.message.toLowerCase().includes(q) ||
commit.author.toLowerCase().includes(q) ||
commit.full_sha.toLowerCase().includes(q)
) ?? []
// Return log only if tag matches OR if there are matching commits
if (tagMatches || filteredCommits.length > 0) {
return {
...log,
commits: tagMatches ? log.commits : filteredCommits
}
}
return null
})
.filter((log): log is changeset => log !== null)
})
const loadContent = async () => { const loadContent = async () => {
if ('' === app_version.value || logs.value.length > 0) { if ('' === app_version.value || logs.value.length > 0) {

View file

@ -389,14 +389,9 @@
<Message title="Tips" class="is-info is-background-info-80" icon="fas fa-info-circle"> <Message title="Tips" class="is-info is-background-info-80" icon="fas fa-info-circle">
<span> <span>
<ul> <ul>
<li> <li><strong>Selective Downloads:</strong> To avoid downloading all existing content from a channel/playlist, use <code><span class="icon"><i class="fa-solid fa-cogs" /></span> Actions > <span class="icon"><i class="fa-solid fa-box-archive" /></span> Archive All</code> to mark existing items as already downloaded.
If you don't wish to download <strong>ALL</strong> content from a channel or playlist, click on
<code> <span class="icon"><i class="fa-solid fa-cogs" /></span> Actions > <span class="icon"><i
class="fa-solid fa-box-archive" /></span> Archive All</code> to archive all existing content.
</li> </li>
<li> <li><strong>Custom Handlers:</strong> Leave timer empty for custom handler definitions. The handler runs hourly and doesn't require a scheduled timer.
For custom handler definitions, you shouldn't have a timer set, as yt-dlp wouldn't know what to do with
it.
</li> </li>
</ul> </ul>
</span> </span>

View file

@ -27,7 +27,7 @@
"cronstrue": "^3.3.0", "cronstrue": "^3.3.0",
"floating-vue": "^5.2.2", "floating-vue": "^5.2.2",
"hls.js": "^1.6.13", "hls.js": "^1.6.13",
"marked": "^16.4.0", "marked": "^16.4.1",
"marked-alert": "^2.1.2", "marked-alert": "^2.1.2",
"marked-base-url": "^1.1.7", "marked-base-url": "^1.1.7",
"marked-gfm-heading-id": "^4.1.2", "marked-gfm-heading-id": "^4.1.2",
@ -36,7 +36,7 @@
"pinia": "^3.0.3", "pinia": "^3.0.3",
"socket.io-client": "^4.8.1", "socket.io-client": "^4.8.1",
"vue": "^3.5.22", "vue": "^3.5.22",
"vue-router": "^4.5.1", "vue-router": "^4.6.3",
"vue-toastification": "2.0.0-rc.5" "vue-toastification": "2.0.0-rc.5"
}, },
"pnpm": { "pnpm": {
@ -54,8 +54,8 @@
"devDependencies": { "devDependencies": {
"@nuxt/eslint": "^1.9.0", "@nuxt/eslint": "^1.9.0",
"@nuxt/eslint-config": "^1.9.0", "@nuxt/eslint-config": "^1.9.0",
"@typescript-eslint/parser": "^8.46.0", "@typescript-eslint/parser": "^8.46.1",
"eslint": "^9.37.0", "eslint": "^9.38.0",
"typescript": "^5.9.3", "typescript": "^5.9.3",
"vitest": "^3.2.4", "vitest": "^3.2.4",
"vue-eslint-parser": "^10.2.0", "vue-eslint-parser": "^10.2.0",

File diff suppressed because it is too large Load diff

285
uv.lock
View file

@ -27,7 +27,7 @@ wheels = [
[[package]] [[package]]
name = "aiohttp" name = "aiohttp"
version = "3.13.0" version = "3.13.1"
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/62/f1/8515650ac3121a9e55c7b217c60e7fae3e0134b5acfe65691781b5356929/aiohttp-3.13.0.tar.gz", hash = "sha256:378dbc57dd8cf341ce243f13fa1fa5394d68e2e02c15cd5f28eae35a70ec7f67", size = 7832348 } sdist = { url = "https://files.pythonhosted.org/packages/ba/fa/3ae643cd525cf6844d3dc810481e5748107368eb49563c15a5fb9f680750/aiohttp-3.13.1.tar.gz", hash = "sha256:4b7ee9c355015813a6aa085170b96ec22315dabc3d866fd77d147927000e9464", size = 7835344 }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/86/2c/ac53efdc9c10e41399acc2395af98f835b86d0141d5c3820857eb9f6a14a/aiohttp-3.13.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:00243e51f16f6ec0fb021659d4af92f675f3cf9f9b39efd142aa3ad641d8d1e6", size = 730090 }, { url = "https://files.pythonhosted.org/packages/16/6d/d267b132342e1080f4c1bb7e1b4e96b168b3cbce931ec45780bff693ff95/aiohttp-3.13.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:55785a7f8f13df0c9ca30b5243d9909bd59f48b274262a8fe78cee0828306e5d", size = 730727 },
{ url = "https://files.pythonhosted.org/packages/13/18/1ac95683e1c1d48ef4503965c96f5401618a04c139edae12e200392daae8/aiohttp-3.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:059978d2fddc462e9211362cbc8446747ecd930537fa559d3d25c256f032ff54", size = 488041 }, { url = "https://files.pythonhosted.org/packages/92/c8/1cf495bac85cf71b80fad5f6d7693e84894f11b9fe876b64b0a1e7cbf32f/aiohttp-3.13.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4bef5b83296cebb8167707b4f8d06c1805db0af632f7a72d7c5288a84667e7c3", size = 488678 },
{ url = "https://files.pythonhosted.org/packages/fd/79/ef0d477c771a642d1a881b92d226314c43d3c74bc674c93e12e679397a97/aiohttp-3.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:564b36512a7da3b386143c611867e3f7cfb249300a1bf60889bd9985da67ab77", size = 486989 }, { url = "https://files.pythonhosted.org/packages/a8/19/23c6b81cca587ec96943d977a58d11d05a82837022e65cd5502d665a7d11/aiohttp-3.13.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:27af0619c33f9ca52f06069ec05de1a357033449ab101836f431768ecfa63ff5", size = 487637 },
{ url = "https://files.pythonhosted.org/packages/37/b4/0e440481a0e77a551d6c5dcab5d11f1ff6b2b2ddb8dedc24f54f5caad732/aiohttp-3.13.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4aa995b9156ae499393d949a456a7ab0b994a8241a96db73a3b73c7a090eff6a", size = 1718331 }, { url = "https://files.pythonhosted.org/packages/48/58/8f9464afb88b3eed145ad7c665293739b3a6f91589694a2bb7e5778cbc72/aiohttp-3.13.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a47fe43229a8efd3764ef7728a5c1158f31cdf2a12151fe99fde81c9ac87019c", size = 1718975 },
{ url = "https://files.pythonhosted.org/packages/e6/59/76c421cc4a75bb1aceadb92f20ee6f05a990aa6960c64b59e8e0d340e3f5/aiohttp-3.13.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:55ca0e95a3905f62f00900255ed807c580775174252999286f283e646d675a49", size = 1686263 }, { url = "https://files.pythonhosted.org/packages/e1/8b/c3da064ca392b2702f53949fd7c403afa38d9ee10bf52c6ad59a42537103/aiohttp-3.13.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6e68e126de5b46e8b2bee73cab086b5d791e7dc192056916077aa1e2e2b04437", size = 1686905 },
{ url = "https://files.pythonhosted.org/packages/ec/ac/5095f12a79c7775f402cfc3e83651b6e0a92ade10ddf7f2c78c4fed79f71/aiohttp-3.13.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:49ce7525853a981fc35d380aa2353536a01a9ec1b30979ea4e35966316cace7e", size = 1754265 }, { url = "https://files.pythonhosted.org/packages/0a/a4/9c8a3843ecf526daee6010af1a66eb62579be1531d2d5af48ea6f405ad3c/aiohttp-3.13.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e65ef49dd22514329c55970d39079618a8abf856bae7147913bb774a3ab3c02f", size = 1754907 },
{ url = "https://files.pythonhosted.org/packages/05/d7/a48e4989bd76cc70600c505bbdd0d90ca1ad7f9053eceeb9dbcf9345a9ec/aiohttp-3.13.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2117be9883501eaf95503bd313eb4c7a23d567edd44014ba15835a1e9ec6d852", size = 1856486 }, { url = "https://files.pythonhosted.org/packages/a4/80/1f470ed93e06436e3fc2659a9fc329c192fa893fb7ed4e884d399dbfb2a8/aiohttp-3.13.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0e425a7e0511648b3376839dcc9190098671a47f21a36e815b97762eb7d556b0", size = 1857129 },
{ url = "https://files.pythonhosted.org/packages/1e/02/45b388b49e37933f316e1fb39c0de6fb1d77384b0c8f4cf6af5f2cbe3ea6/aiohttp-3.13.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d169c47e40c911f728439da853b6fd06da83761012e6e76f11cb62cddae7282b", size = 1737545 }, { url = "https://files.pythonhosted.org/packages/cc/e6/33d305e6cce0a8daeb79c7d8d6547d6e5f27f4e35fa4883fc9c9eb638596/aiohttp-3.13.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:010dc9b7110f055006acd3648d5d5955bb6473b37c3663ec42a1b4cba7413e6b", size = 1738189 },
{ url = "https://files.pythonhosted.org/packages/6c/a7/4fde058f1605c34a219348a83a99f14724cc64e68a42480fc03cf40f9ea3/aiohttp-3.13.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:703ad3f742fc81e543638a7bebddd35acadaa0004a5e00535e795f4b6f2c25ca", size = 1552958 }, { url = "https://files.pythonhosted.org/packages/ac/42/8df03367e5a64327fe0c39291080697795430c438fc1139c7cc1831aa1df/aiohttp-3.13.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1b5c722d0ca5f57d61066b5dfa96cdb87111e2519156b35c1f8dd17c703bee7a", size = 1553608 },
{ url = "https://files.pythonhosted.org/packages/d1/12/0bac4d29231981e3aa234e88d1931f6ba38135ff4c2cf3afbb7895527630/aiohttp-3.13.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5bf635c3476f4119b940cc8d94ad454cbe0c377e61b4527f0192aabeac1e9370", size = 1681166 }, { url = "https://files.pythonhosted.org/packages/96/17/6d5c73cd862f1cf29fddcbb54aac147037ff70a043a2829d03a379e95742/aiohttp-3.13.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:93029f0e9b77b714904a281b5aa578cdc8aa8ba018d78c04e51e1c3d8471b8ec", size = 1681809 },
{ url = "https://files.pythonhosted.org/packages/71/95/b829eb5f8ac1ca1d8085bb8df614c8acf3ff32e23ad5ad1173c7c9761daa/aiohttp-3.13.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:cfe6285ef99e7ee51cef20609be2bc1dd0e8446462b71c9db8bb296ba632810a", size = 1710516 }, { url = "https://files.pythonhosted.org/packages/be/31/8926c8ab18533f6076ce28d2c329a203b58c6861681906e2d73b9c397588/aiohttp-3.13.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:d1824c7d08d8ddfc8cb10c847f696942e5aadbd16fd974dfde8bd2c3c08a9fa1", size = 1711161 },
{ url = "https://files.pythonhosted.org/packages/47/6d/15ccf4ef3c254d899f62580e0c7fc717014f4d14a3ac31771e505d2c736c/aiohttp-3.13.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:34d8af6391c5f2e69749d7f037b614b8c5c42093c251f336bdbfa4b03c57d6c4", size = 1731354 }, { url = "https://files.pythonhosted.org/packages/f2/36/2f83e1ca730b1e0a8cf1c8ab9559834c5eec9f5da86e77ac71f0d16b521d/aiohttp-3.13.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:8f47d0ff5b3eb9c1278a2f56ea48fda667da8ebf28bd2cb378b7c453936ce003", size = 1731999 },
{ url = "https://files.pythonhosted.org/packages/46/6a/8acf6c57e03b6fdcc8b4c06392e66abaff3213ea275e41db3edb20738d91/aiohttp-3.13.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:12f5d820fadc5848d4559ea838aef733cf37ed2a1103bba148ac2f5547c14c29", size = 1548040 }, { url = "https://files.pythonhosted.org/packages/b9/ec/1f818cc368dfd4d5ab4e9efc8f2f6f283bfc31e1c06d3e848bcc862d4591/aiohttp-3.13.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:8a396b1da9b51ded79806ac3b57a598f84e0769eaa1ba300655d8b5e17b70c7b", size = 1548684 },
{ url = "https://files.pythonhosted.org/packages/75/7d/fbfd59ab2a83fe2578ce79ac3db49727b81e9f4c3376217ad09c03c6d279/aiohttp-3.13.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:0f1338b61ea66f4757a0544ed8a02ccbf60e38d9cfb3225888888dd4475ebb96", size = 1756031 }, { url = "https://files.pythonhosted.org/packages/d3/ad/33d36efd16e4fefee91b09a22a3a0e1b830f65471c3567ac5a8041fac812/aiohttp-3.13.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:d9c52a65f54796e066b5d674e33b53178014752d28bca555c479c2c25ffcec5b", size = 1756676 },
{ url = "https://files.pythonhosted.org/packages/99/e7/cc9f0fdf06cab3ca61e6b62bff9a4b978b8ca736e9d76ddf54365673ab19/aiohttp-3.13.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:582770f82513419512da096e8df21ca44f86a2e56e25dc93c5ab4df0fe065bf0", size = 1714933 }, { url = "https://files.pythonhosted.org/packages/3c/c4/4a526d84e77d464437713ca909364988ed2e0cd0cdad2c06cb065ece9e08/aiohttp-3.13.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a89da72d18d6c95a653470b78d8ee5aa3c4b37212004c103403d0776cbea6ff0", size = 1715577 },
{ url = "https://files.pythonhosted.org/packages/db/43/7abbe1de94748a58a71881163ee280fd3217db36e8344d109f63638fe16a/aiohttp-3.13.0-cp313-cp313-win32.whl", hash = "sha256:3194b8cab8dbc882f37c13ef1262e0a3d62064fa97533d3aa124771f7bf1ecee", size = 423799 }, { url = "https://files.pythonhosted.org/packages/a2/21/e39638b7d9c7f1362c4113a91870f89287e60a7ea2d037e258b81e8b37d5/aiohttp-3.13.1-cp313-cp313-win32.whl", hash = "sha256:02e0258b7585ddf5d01c79c716ddd674386bfbf3041fbbfe7bdf9c7c32eb4a9b", size = 424468 },
{ url = "https://files.pythonhosted.org/packages/c9/58/afab7f2b9e7df88c995995172eb78cae8a3d5a62d5681abaade86b3f0089/aiohttp-3.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:7897298b3eedc790257fef8a6ec582ca04e9dbe568ba4a9a890913b925b8ea21", size = 450138 }, { url = "https://files.pythonhosted.org/packages/cc/00/f3a92c592a845ebb2f47d102a67f35f0925cb854c5e7386f1a3a1fdff2ab/aiohttp-3.13.1-cp313-cp313-win_amd64.whl", hash = "sha256:ef56ffe60e8d97baac123272bde1ab889ee07d3419606fae823c80c2b86c403e", size = 450806 },
{ url = "https://files.pythonhosted.org/packages/fe/c1/93bb1e35cd0c4665bb422b1ca3d87b588f4bca2656bbe9292b963d5b76a9/aiohttp-3.13.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:c417f8c2e1137775569297c584a8a7144e5d1237789eae56af4faf1894a0b861", size = 733187 }, { url = "https://files.pythonhosted.org/packages/97/be/0f6c41d2fd0aab0af133c509cabaf5b1d78eab882cb0ceb872e87ceeabf7/aiohttp-3.13.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:77f83b3dc5870a2ea79a0fcfdcc3fc398187ec1675ff61ec2ceccad27ecbd303", size = 733828 },
{ url = "https://files.pythonhosted.org/packages/5e/36/2d50eba91992d3fe7a6452506ccdab45d03685ee8d8acaa5b289384a7d4c/aiohttp-3.13.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:f84b53326abf8e56ebc28a35cebf4a0f396a13a76300f500ab11fe0573bf0b52", size = 488684 }, { url = "https://files.pythonhosted.org/packages/75/14/24e2ac5efa76ae30e05813e0f50737005fd52da8ddffee474d4a5e7f38a6/aiohttp-3.13.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:9cafd2609ebb755e47323306c7666283fbba6cf82b5f19982ea627db907df23a", size = 489320 },
{ url = "https://files.pythonhosted.org/packages/82/93/fa4b1d5ecdc7805bdf0815ef00257db4632ccf0a8bffd44f9fc4657b1677/aiohttp-3.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:990a53b9d6a30b2878789e490758e568b12b4a7fb2527d0c89deb9650b0e5813", size = 489255 }, { url = "https://files.pythonhosted.org/packages/da/5a/4cbe599358d05ea7db4869aff44707b57d13f01724d48123dc68b3288d5a/aiohttp-3.13.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9c489309a2ca548d5f11131cfb4092f61d67954f930bba7e413bcdbbb82d7fae", size = 489899 },
{ url = "https://files.pythonhosted.org/packages/05/0f/85241f0d158da5e24e8ac9d50c0849ed24f882cafc53dc95749ef85eef09/aiohttp-3.13.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c811612711e01b901e18964b3e5dec0d35525150f5f3f85d0aee2935f059910a", size = 1715914 }, { url = "https://files.pythonhosted.org/packages/67/96/3aec9d9cfc723273d4386328a1e2562cf23629d2f57d137047c49adb2afb/aiohttp-3.13.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79ac15fe5fdbf3c186aa74b656cd436d9a1e492ba036db8901c75717055a5b1c", size = 1716556 },
{ url = "https://files.pythonhosted.org/packages/ab/fc/c755590d6f6d2b5d1565c72d6ee658d3c30ec61acb18964d1e9bf991d9b5/aiohttp-3.13.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:ee433e594d7948e760b5c2a78cc06ac219df33b0848793cf9513d486a9f90a52", size = 1665171 }, { url = "https://files.pythonhosted.org/packages/b9/99/39a3d250595b5c8172843831221fa5662884f63f8005b00b4034f2a7a836/aiohttp-3.13.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:095414be94fce3bc080684b4cd50fb70d439bc4662b2a1984f45f3bf9ede08aa", size = 1665814 },
{ url = "https://files.pythonhosted.org/packages/3a/de/caa61e213ff546b8815aef5e931d7eae1dbe8c840a3f11ec5aa41c5ae462/aiohttp-3.13.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:19bb08e56f57c215e9572cd65cb6f8097804412c54081d933997ddde3e5ac579", size = 1755124 }, { url = "https://files.pythonhosted.org/packages/3b/96/8319e7060a85db14a9c178bc7b3cf17fad458db32ba6d2910de3ca71452d/aiohttp-3.13.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c68172e1a2dca65fa1272c85ca72e802d78b67812b22827df01017a15c5089fa", size = 1755767 },
{ url = "https://files.pythonhosted.org/packages/fb/b7/40c3219dd2691aa35cf889b4fbb0c00e48a19092928707044bfe92068e01/aiohttp-3.13.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f27b7488144eb5dd9151cf839b195edd1569629d90ace4c5b6b18e4e75d1e63a", size = 1835949 }, { url = "https://files.pythonhosted.org/packages/1c/c6/0a2b3d886b40aa740fa2294cd34ed46d2e8108696748492be722e23082a7/aiohttp-3.13.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3751f9212bcd119944d4ea9de6a3f0fee288c177b8ca55442a2cdff0c8201eb3", size = 1836591 },
{ url = "https://files.pythonhosted.org/packages/57/e8/66e3c32841fc0e26a09539c377aa0f3bbf6deac1957ac5182cf276c5719c/aiohttp-3.13.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d812838c109757a11354a161c95708ae4199c4fd4d82b90959b20914c1d097f6", size = 1714276 }, { url = "https://files.pythonhosted.org/packages/fb/34/8ab5904b3331c91a58507234a1e2f662f837e193741609ee5832eb436251/aiohttp-3.13.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8619dca57d98a8353abdc7a1eeb415548952b39d6676def70d9ce76d41a046a9", size = 1714915 },
{ url = "https://files.pythonhosted.org/packages/6b/a5/c68e5b46ff0410fe3abfa508651b09372428f27036138beacf4ff6b7cb8c/aiohttp-3.13.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7c20db99da682f9180fa5195c90b80b159632fb611e8dbccdd99ba0be0970620", size = 1545929 }, { url = "https://files.pythonhosted.org/packages/b5/d3/d36077ca5f447649112189074ac6c192a666bf68165b693e48c23b0d008c/aiohttp-3.13.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:97795a0cb0a5f8a843759620e9cbd8889f8079551f5dcf1ccd99ed2f056d9632", size = 1546579 },
{ url = "https://files.pythonhosted.org/packages/7a/a6/4c97dc27f9935c0c0aa6e3e10e5b4548823ab5d056636bde374fcd297256/aiohttp-3.13.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:cf8b0870047900eb1f17f453b4b3953b8ffbf203ef56c2f346780ff930a4d430", size = 1679988 }, { url = "https://files.pythonhosted.org/packages/a8/14/dbc426a1bb1305c4fc78ce69323498c9e7c699983366ef676aa5d3f949fa/aiohttp-3.13.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1060e058da8f9f28a7026cdfca9fc886e45e551a658f6a5c631188f72a3736d2", size = 1680633 },
{ url = "https://files.pythonhosted.org/packages/8e/1b/11f9c52fd72b786a47e796e6794883417280cdca8eb1032d8d0939928dfa/aiohttp-3.13.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:5b8a5557d5af3f4e3add52a58c4cf2b8e6e59fc56b261768866f5337872d596d", size = 1678031 }, { url = "https://files.pythonhosted.org/packages/29/83/1e68e519aff9f3ef6d4acb6cdda7b5f592ef5c67c8f095dc0d8e06ce1c3e/aiohttp-3.13.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:f48a2c26333659101ef214907d29a76fe22ad7e912aa1e40aeffdff5e8180977", size = 1678675 },
{ url = "https://files.pythonhosted.org/packages/ea/eb/948903d40505f3a25e53e051488d2714ded3afac1f961df135f2936680f9/aiohttp-3.13.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:052bcdd80c1c54b8a18a9ea0cd5e36f473dc8e38d51b804cea34841f677a9971", size = 1726184 }, { url = "https://files.pythonhosted.org/packages/38/b9/7f3e32a81c08b6d29ea15060c377e1f038ad96cd9923a85f30e817afff22/aiohttp-3.13.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f1dfad638b9c91ff225162b2824db0e99ae2d1abe0dc7272b5919701f0a1e685", size = 1726829 },
{ url = "https://files.pythonhosted.org/packages/44/14/c8ced38c7dfe80804dec17a671963ccf3cb282f12700ec70b1f689d8de7d/aiohttp-3.13.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:76484ba17b2832776581b7ab466d094e48eba74cb65a60aea20154dae485e8bd", size = 1542344 }, { url = "https://files.pythonhosted.org/packages/23/ce/610b1f77525a0a46639aea91377b12348e9f9412cc5ddcb17502aa4681c7/aiohttp-3.13.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:8fa09ab6dd567cb105db4e8ac4d60f377a7a94f67cf669cac79982f626360f32", size = 1542985 },
{ url = "https://files.pythonhosted.org/packages/a4/6e/f2e6bff550a51fd7c45fdab116a1dab7cc502e5d942956f10fc5c626bb15/aiohttp-3.13.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:62d8a0adcdaf62ee56bfb37737153251ac8e4b27845b3ca065862fb01d99e247", size = 1740913 }, { url = "https://files.pythonhosted.org/packages/53/39/3ac8dfdad5de38c401846fa071fcd24cb3b88ccfb024854df6cbd9b4a07e/aiohttp-3.13.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4159fae827f9b5f655538a4f99b7cbc3a2187e5ca2eee82f876ef1da802ccfa9", size = 1741556 },
{ url = "https://files.pythonhosted.org/packages/da/00/8f057300d9b598a706348abb375b3de9a253195fb615f17c0b2be2a72836/aiohttp-3.13.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5004d727499ecb95f7c9147dd0bfc5b5670f71d355f0bd26d7af2d3af8e07d2f", size = 1695535 }, { url = "https://files.pythonhosted.org/packages/2a/48/b1948b74fea7930b0f29595d1956842324336de200593d49a51a40607fdc/aiohttp-3.13.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ad671118c19e9cfafe81a7a05c294449fe0ebb0d0c6d5bb445cd2190023f5cef", size = 1696175 },
{ url = "https://files.pythonhosted.org/packages/8a/ab/6919d584d8f053a14b15f0bfa3f315b3f548435c2142145459da2efa8673/aiohttp-3.13.0-cp314-cp314-win32.whl", hash = "sha256:a1c20c26af48aea984f63f96e5d7af7567c32cb527e33b60a0ef0a6313cf8b03", size = 429548 }, { url = "https://files.pythonhosted.org/packages/96/26/063bba38e4b27b640f56cc89fe83cc3546a7ae162c2e30ca345f0ccdc3d1/aiohttp-3.13.1-cp314-cp314-win32.whl", hash = "sha256:c5c970c148c48cf6acb65224ca3c87a47f74436362dde75c27bc44155ccf7dfc", size = 430254 },
{ url = "https://files.pythonhosted.org/packages/c5/59/5d9e78de6132079066f5077d9687bf524f764a2f8207e04d8d68790060c6/aiohttp-3.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:56f7d230ec66e799fbfd8350e9544f8a45a4353f1cf40c1fea74c1780f555b8f", size = 455548 }, { url = "https://files.pythonhosted.org/packages/88/aa/25fd764384dc4eab714023112d3548a8dd69a058840d61d816ea736097a2/aiohttp-3.13.1-cp314-cp314-win_amd64.whl", hash = "sha256:748a00167b7a88385756fa615417d24081cba7e58c8727d2e28817068b97c18c", size = 456256 },
{ url = "https://files.pythonhosted.org/packages/7c/ea/7d98da03d1e9798bb99c3ca4963229150d45c9b7a3a16210c5b4a5f89e07/aiohttp-3.13.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:2fd35177dc483ae702f07b86c782f4f4b100a8ce4e7c5778cea016979023d9fd", size = 765319 }, { url = "https://files.pythonhosted.org/packages/d4/9f/9ba6059de4bad25c71cd88e3da53f93e9618ea369cf875c9f924b1c167e2/aiohttp-3.13.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:390b73e99d7a1f0f658b3f626ba345b76382f3edc65f49d6385e326e777ed00e", size = 765956 },
{ url = "https://files.pythonhosted.org/packages/5c/02/37f29beced8213bb467c52ad509a5e3b41e6e967de2f6eaf7f8db63bea54/aiohttp-3.13.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:4df1984c8804ed336089e88ac81a9417b1fd0db7c6f867c50a9264488797e778", size = 502567 }, { url = "https://files.pythonhosted.org/packages/1f/30/b86da68b494447d3060f45c7ebb461347535dab4af9162a9267d9d86ca31/aiohttp-3.13.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:27e83abb330e687e019173d8fc1fd6a1cf471769624cf89b1bb49131198a810a", size = 503206 },
{ url = "https://files.pythonhosted.org/packages/e7/22/b0afcafcfe3637bc8d7992abf08ee9452018366c0801e4e7d4efda2ed839/aiohttp-3.13.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e68c0076052dd911a81d3acc4ef2911cc4ef65bf7cadbfbc8ae762da24da858f", size = 507078 }, { url = "https://files.pythonhosted.org/packages/c1/21/d27a506552843ff9eeb9fcc2d45f943b09eefdfdf205aab044f4f1f39f6a/aiohttp-3.13.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2b20eed07131adbf3e873e009c2869b16a579b236e9d4b2f211bf174d8bef44a", size = 507719 },
{ url = "https://files.pythonhosted.org/packages/49/4c/046c847b7a1993b49f3855cc3b97872d5df193d9240de835d0dc6a97b164/aiohttp-3.13.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bc95c49853cd29613e4fe4ff96d73068ff89b89d61e53988442e127e8da8e7ba", size = 1862115 }, { url = "https://files.pythonhosted.org/packages/58/23/4042230ec7e4edc7ba43d0342b5a3d2fe0222ca046933c4251a35aaf17f5/aiohttp-3.13.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:58fee9ef8477fd69e823b92cfd1f590ee388521b5ff8f97f3497e62ee0656212", size = 1862758 },
{ url = "https://files.pythonhosted.org/packages/1a/25/1449a59e3c6405da5e47b0138ee0855414dc12a8c306685d7fc3dd300e1f/aiohttp-3.13.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3b3bdc89413117b40cc39baae08fd09cbdeb839d421c4e7dce6a34f6b54b3ac1", size = 1717147 }, { url = "https://files.pythonhosted.org/packages/df/88/525c45bea7cbb9f65df42cadb4ff69f6a0dbf95931b0ff7d1fdc40a1cb5f/aiohttp-3.13.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:1f62608fcb7b3d034d5e9496bea52d94064b7b62b06edba82cd38191336bbeda", size = 1717790 },
{ url = "https://files.pythonhosted.org/packages/23/8f/50cc34ad267b38608f21c6a74327015dd08a66f1dd8e7ceac954d0953191/aiohttp-3.13.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3e77a729df23be2116acc4e9de2767d8e92445fbca68886dd991dc912f473755", size = 1841443 }, { url = "https://files.pythonhosted.org/packages/1d/80/21e9b5eb77df352a5788713f37359b570a793f0473f3a72db2e46df379b9/aiohttp-3.13.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fdc4d81c3dfc999437f23e36d197e8b557a3f779625cd13efe563a9cfc2ce712", size = 1842088 },
{ url = "https://files.pythonhosted.org/packages/df/b9/b3ab1278faa0d1b8f434c85f9cf34eeb0a25016ffe1ee6bc361d09fef0ec/aiohttp-3.13.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e88ab34826d6eeb6c67e6e92400b9ec653faf5092a35f07465f44c9f1c429f82", size = 1933652 }, { url = "https://files.pythonhosted.org/packages/d2/bf/d1738f6d63fe8b2a0ad49533911b3347f4953cd001bf3223cb7b61f18dff/aiohttp-3.13.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:601d7ec812f746fd80ff8af38eeb3f196e1bab4a4d39816ccbc94c222d23f1d0", size = 1934292 },
{ url = "https://files.pythonhosted.org/packages/88/e2/86050aaa3bd7021b115cdfc88477b754e8cf93ef0079867840eee22d3c34/aiohttp-3.13.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:019dbef24fe28ce2301419dd63a2b97250d9760ca63ee2976c2da2e3f182f82e", size = 1790682 }, { url = "https://files.pythonhosted.org/packages/04/e6/26cab509b42610ca49573f2fc2867810f72bd6a2070182256c31b14f2e98/aiohttp-3.13.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47c3f21c469b840d9609089435c0d9918ae89f41289bf7cc4afe5ff7af5458db", size = 1791328 },
{ url = "https://files.pythonhosted.org/packages/78/8d/9af903324c2ba24a0c4778e9bcc738b773c98dded3a4fcf8041d5211769f/aiohttp-3.13.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2c4aeaedd20771b7b4bcdf0ae791904445df6d856c02fc51d809d12d17cffdc7", size = 1622011 }, { url = "https://files.pythonhosted.org/packages/8a/6d/baf7b462852475c9d045bee8418d9cdf280efb687752b553e82d0c58bcc2/aiohttp-3.13.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d6c6cdc0750db88520332d4aaa352221732b0cafe89fd0e42feec7cb1b5dc236", size = 1622663 },
{ url = "https://files.pythonhosted.org/packages/84/97/5174971ba4986d913554ceb248b0401eb5358cb60672ea0166f9f596cd08/aiohttp-3.13.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:b3a8e6a2058a0240cfde542b641d0e78b594311bc1a710cbcb2e1841417d5cb3", size = 1787148 }, { url = "https://files.pythonhosted.org/packages/c8/48/396a97318af9b5f4ca8b3dc14a67976f71c6400a9609c622f96da341453f/aiohttp-3.13.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:58a12299eeb1fca2414ee2bc345ac69b0f765c20b82c3ab2a75d91310d95a9f6", size = 1787791 },
{ url = "https://files.pythonhosted.org/packages/dd/ae/8b397e980ac613ef3ddd8e996aa7a40a1828df958257800d4bb325657db3/aiohttp-3.13.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:f8e38d55ca36c15f36d814ea414ecb2401d860de177c49f84a327a25b3ee752b", size = 1774816 }, { url = "https://files.pythonhosted.org/packages/a8/e2/6925f6784134ce3ff3ce1a8502ab366432a3b5605387618c1a939ce778d9/aiohttp-3.13.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:0989cbfc195a4de1bb48f08454ef1cb47424b937e53ed069d08404b9d3c7aea1", size = 1775459 },
{ url = "https://files.pythonhosted.org/packages/c7/54/0e8e2111dd92051c787e934b6bbf30c213daaa5e7ee5f51bca8913607492/aiohttp-3.13.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:a921edbe971aade1bf45bcbb3494e30ba6863a5c78f28be992c42de980fd9108", size = 1788610 }, { url = "https://files.pythonhosted.org/packages/c3/e3/b372047ba739fc39f199b99290c4cc5578ce5fd125f69168c967dac44021/aiohttp-3.13.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:feb5ee664300e2435e0d1bc3443a98925013dfaf2cae9699c1f3606b88544898", size = 1789250 },
{ url = "https://files.pythonhosted.org/packages/fa/dd/c9283dbfd9325ed6fa6c91f009db6344d8d370a7bcf09f36e7b2fcbfae02/aiohttp-3.13.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:474cade59a447cb4019c0dce9f0434bf835fb558ea932f62c686fe07fe6db6a1", size = 1615498 }, { url = "https://files.pythonhosted.org/packages/02/8c/9f48b93d7d57fc9ef2ad4adace62e4663ea1ce1753806c4872fb36b54c39/aiohttp-3.13.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:58a6f8702da0c3606fb5cf2e669cce0ca681d072fe830968673bb4c69eb89e88", size = 1616139 },
{ url = "https://files.pythonhosted.org/packages/8c/f6/da76230679bd9ef175d876093f89e7fd6d6476c18505e115e3026fe5ef95/aiohttp-3.13.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:99a303ad960747c33b65b1cb65d01a62ac73fa39b72f08a2e1efa832529b01ed", size = 1815187 }, { url = "https://files.pythonhosted.org/packages/5c/c6/c64e39d61aaa33d7de1be5206c0af3ead4b369bf975dac9fdf907a4291c1/aiohttp-3.13.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:a417ceb433b9d280e2368ffea22d4bc6e3e0d894c4bc7768915124d57d0964b6", size = 1815829 },
{ url = "https://files.pythonhosted.org/packages/d5/78/394003ac738703822616f4f922705b54e5b3d8e7185831ecc1c97904174d/aiohttp-3.13.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:bb34001fc1f05f6b323e02c278090c07a47645caae3aa77ed7ed8a3ce6abcce9", size = 1760281 }, { url = "https://files.pythonhosted.org/packages/22/75/e19e93965ea675f1151753b409af97a14f1d888588a555e53af1e62b83eb/aiohttp-3.13.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8ac8854f7b0466c5d6a9ea49249b3f6176013859ac8f4bb2522ad8ed6b94ded2", size = 1760923 },
{ url = "https://files.pythonhosted.org/packages/bd/b0/4bad0a9dd5910bd01c3119f8bd3d71887cd412d4105e4acddcdacf3cfa76/aiohttp-3.13.0-cp314-cp314t-win32.whl", hash = "sha256:dea698b64235d053def7d2f08af9302a69fcd760d1c7bd9988fd5d3b6157e657", size = 462608 }, { url = "https://files.pythonhosted.org/packages/6c/a4/06ed38f1dabd98ea136fd116cba1d02c9b51af5a37d513b6850a9a567d86/aiohttp-3.13.1-cp314-cp314t-win32.whl", hash = "sha256:be697a5aeff42179ed13b332a411e674994bcd406c81642d014ace90bf4bb968", size = 463318 },
{ url = "https://files.pythonhosted.org/packages/bd/af/ad12d592f623aae2bd1d3463201dc39c201ea362f9ddee0d03efd9e83720/aiohttp-3.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:1f164699a060c0b3616459d13c1464a981fddf36f892f0a5027cbd45121fb14b", size = 496010 }, { url = "https://files.pythonhosted.org/packages/04/0f/27e4fdde899e1e90e35eeff56b54ed63826435ad6cdb06b09ed312d1b3fa/aiohttp-3.13.1-cp314-cp314t-win_amd64.whl", hash = "sha256:f1d6aa90546a4e8f20c3500cb68ab14679cd91f927fa52970035fd3207dfb3da", size = 496721 },
] ]
[[package]] [[package]]
@ -357,11 +357,10 @@ wheels = [
[[package]] [[package]]
name = "cronsim" name = "cronsim"
version = "2.6" version = "2.7"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/5b/d8/cfb8d51a51f6076ffa09902c02978c7db9764cca78f4ee832e691d20f44b/cronsim-2.6.tar.gz", hash = "sha256:5aab98716ef90ab5ac6be294b2c3965dbf76dc869f048846a0af74ebb506c10d", size = 20315 }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/8c/dd/9c40c4e0f4d3cb6cf52eb335e9cc1fa140c1f3a87146fb6987f465b069da/cronsim-2.6-py3-none-any.whl", hash = "sha256:5e153ff8ed64da7ee8d5caac470dbeda8024ab052c3010b1be149772b4801835", size = 13500 }, { url = "https://files.pythonhosted.org/packages/fb/1a/02f105147f7f2e06ed4f734ff5a6439590bb275a53dd91fc73df6312298a/cronsim-2.7-py3-none-any.whl", hash = "sha256:1e1431fa08c51dc7f72e67e571c7c7a09af26420169b607badd4ca9677ffad1e", size = 14213 },
] ]
[[package]] [[package]]
@ -582,11 +581,11 @@ wheels = [
[[package]] [[package]]
name = "iniconfig" name = "iniconfig"
version = "2.1.0" version = "2.3.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793 } sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503 }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050 }, { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484 },
] ]
[[package]] [[package]]
@ -1165,15 +1164,15 @@ wheels = [
[[package]] [[package]]
name = "python-socketio" name = "python-socketio"
version = "5.14.1" version = "5.14.2"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "bidict" }, { name = "bidict" },
{ name = "python-engineio" }, { name = "python-engineio" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/05/c2/a9ae3d0eb4488748a2d9c15defddb7277a852234e29e50c73136834dff1b/python_socketio-5.14.1.tar.gz", hash = "sha256:bf49657073b90ee09e4cbd6651044b46bb526694276621e807a1b8fcc0c1b25b", size = 123068 } sdist = { url = "https://files.pythonhosted.org/packages/ab/80/f31077368adbf65be17245ee69cbccde2891582623e6a8ad647c7a254db3/python_socketio-5.14.2.tar.gz", hash = "sha256:5da35caa04059048a4521b1378340a33a15439c6a748765046112fa0d145fb1b", size = 124111 }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/d5/5e/302c3499a134a52b68e4e6fb345cea52ab1c41460949bcdb09f8bd0e3594/python_socketio-5.14.1-py3-none-any.whl", hash = "sha256:3419f5917f0e3942317836a77146cb4caa23ad804c8fd1a7e3f44a6657a8406e", size = 78496 }, { url = "https://files.pythonhosted.org/packages/d7/5b/821733876bad200638237d1cab671b46cfdafb73c0b4094f59c5947155ae/python_socketio-5.14.2-py3-none-any.whl", hash = "sha256:d7133f040ac7ba540f9a907cb4c36d6c4d1d54eb35e482aad9d45c22fca10654", size = 78941 },
] ]
[[package]] [[package]]
@ -1245,66 +1244,66 @@ wheels = [
[[package]] [[package]]
name = "regex" name = "regex"
version = "2025.9.18" version = "2025.10.23"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/49/d3/eaa0d28aba6ad1827ad1e716d9a93e1ba963ada61887498297d3da715133/regex-2025.9.18.tar.gz", hash = "sha256:c5ba23274c61c6fef447ba6a39333297d0c247f53059dba0bca415cac511edc4", size = 400917 } sdist = { url = "https://files.pythonhosted.org/packages/f8/c8/1d2160d36b11fbe0a61acb7c3c81ab032d9ec8ad888ac9e0a61b85ab99dd/regex-2025.10.23.tar.gz", hash = "sha256:8cbaf8ceb88f96ae2356d01b9adf5e6306fa42fa6f7eab6b97794e37c959ac26", size = 401266 }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/d2/c7/5c48206a60ce33711cf7dcaeaed10dd737733a3569dc7e1dce324dd48f30/regex-2025.9.18-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2a40f929cd907c7e8ac7566ac76225a77701a6221bca937bdb70d56cb61f57b2", size = 485955 }, { url = "https://files.pythonhosted.org/packages/28/c6/195a6217a43719d5a6a12cc192a22d12c40290cecfa577f00f4fb822f07d/regex-2025.10.23-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:b7690f95404a1293923a296981fd943cca12c31a41af9c21ba3edd06398fc193", size = 488956 },
{ url = "https://files.pythonhosted.org/packages/e9/be/74fc6bb19a3c491ec1ace943e622b5a8539068771e8705e469b2da2306a7/regex-2025.9.18-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c90471671c2cdf914e58b6af62420ea9ecd06d1554d7474d50133ff26ae88feb", size = 289583 }, { url = "https://files.pythonhosted.org/packages/4c/93/181070cd1aa2fa541ff2d3afcf763ceecd4937b34c615fa92765020a6c90/regex-2025.10.23-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1a32d77aeaea58a13230100dd8797ac1a84c457f3af2fdf0d81ea689d5a9105b", size = 290997 },
{ url = "https://files.pythonhosted.org/packages/25/c4/9ceaa433cb5dc515765560f22a19578b95b92ff12526e5a259321c4fc1a0/regex-2025.9.18-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1a351aff9e07a2dabb5022ead6380cff17a4f10e4feb15f9100ee56c4d6d06af", size = 287000 }, { url = "https://files.pythonhosted.org/packages/b6/c5/9d37fbe3a40ed8dda78c23e1263002497540c0d1522ed75482ef6c2000f0/regex-2025.10.23-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b24b29402f264f70a3c81f45974323b41764ff7159655360543b7cabb73e7d2f", size = 288686 },
{ url = "https://files.pythonhosted.org/packages/7d/e6/68bc9393cb4dc68018456568c048ac035854b042bc7c33cb9b99b0680afa/regex-2025.9.18-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bc4b8e9d16e20ddfe16430c23468a8707ccad3365b06d4536142e71823f3ca29", size = 797535 }, { url = "https://files.pythonhosted.org/packages/5f/e7/db610ff9f10c2921f9b6ac0c8d8be4681b28ddd40fc0549429366967e61f/regex-2025.10.23-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:563824a08c7c03d96856d84b46fdb3bbb7cfbdf79da7ef68725cda2ce169c72a", size = 798466 },
{ url = "https://files.pythonhosted.org/packages/6a/1c/ebae9032d34b78ecfe9bd4b5e6575b55351dc8513485bb92326613732b8c/regex-2025.9.18-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4b8cdbddf2db1c5e80338ba2daa3cfa3dec73a46fff2a7dda087c8efbf12d62f", size = 862603 }, { url = "https://files.pythonhosted.org/packages/90/10/aab883e1fa7fe2feb15ac663026e70ca0ae1411efa0c7a4a0342d9545015/regex-2025.10.23-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a0ec8bdd88d2e2659c3518087ee34b37e20bd169419ffead4240a7004e8ed03b", size = 863996 },
{ url = "https://files.pythonhosted.org/packages/3b/74/12332c54b3882557a4bcd2b99f8be581f5c6a43cf1660a85b460dd8ff468/regex-2025.9.18-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a276937d9d75085b2c91fb48244349c6954f05ee97bba0963ce24a9d915b8b68", size = 910829 }, { url = "https://files.pythonhosted.org/packages/a2/b0/8f686dd97a51f3b37d0238cd00a6d0f9ccabe701f05b56de1918571d0d61/regex-2025.10.23-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b577601bfe1d33913fcd9276d7607bbac827c4798d9e14d04bf37d417a6c41cb", size = 912145 },
{ url = "https://files.pythonhosted.org/packages/86/70/ba42d5ed606ee275f2465bfc0e2208755b06cdabd0f4c7c4b614d51b57ab/regex-2025.9.18-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:92a8e375ccdc1256401c90e9dc02b8642894443d549ff5e25e36d7cf8a80c783", size = 802059 }, { url = "https://files.pythonhosted.org/packages/a3/ca/639f8cd5b08797bca38fc5e7e07f76641a428cf8c7fca05894caf045aa32/regex-2025.10.23-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c9f2c68ac6cb3de94eea08a437a75eaa2bd33f9e97c84836ca0b610a5804368", size = 803370 },
{ url = "https://files.pythonhosted.org/packages/da/c5/fcb017e56396a7f2f8357412638d7e2963440b131a3ca549be25774b3641/regex-2025.9.18-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0dc6893b1f502d73037cf807a321cdc9be29ef3d6219f7970f842475873712ac", size = 786781 }, { url = "https://files.pythonhosted.org/packages/0d/1e/a40725bb76959eddf8abc42a967bed6f4851b39f5ac4f20e9794d7832aa5/regex-2025.10.23-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:89f8b9ea3830c79468e26b0e21c3585f69f105157c2154a36f6b7839f8afb351", size = 787767 },
{ url = "https://files.pythonhosted.org/packages/c6/ee/21c4278b973f630adfb3bcb23d09d83625f3ab1ca6e40ebdffe69901c7a1/regex-2025.9.18-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:a61e85bfc63d232ac14b015af1261f826260c8deb19401c0597dbb87a864361e", size = 856578 }, { url = "https://files.pythonhosted.org/packages/3d/d8/8ee9858062936b0f99656dce390aa667c6e7fb0c357b1b9bf76fb5e2e708/regex-2025.10.23-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:98fd84c4e4ea185b3bb5bf065261ab45867d8875032f358a435647285c722673", size = 858335 },
{ url = "https://files.pythonhosted.org/packages/87/0b/de51550dc7274324435c8f1539373ac63019b0525ad720132866fff4a16a/regex-2025.9.18-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:1ef86a9ebc53f379d921fb9a7e42b92059ad3ee800fcd9e0fe6181090e9f6c23", size = 849119 }, { url = "https://files.pythonhosted.org/packages/d8/0a/ed5faaa63fa8e3064ab670e08061fbf09e3a10235b19630cf0cbb9e48c0a/regex-2025.10.23-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:1e11d3e5887b8b096f96b4154dfb902f29c723a9556639586cd140e77e28b313", size = 850402 },
{ url = "https://files.pythonhosted.org/packages/60/52/383d3044fc5154d9ffe4321696ee5b2ee4833a28c29b137c22c33f41885b/regex-2025.9.18-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d3bc882119764ba3a119fbf2bd4f1b47bc56c1da5d42df4ed54ae1e8e66fdf8f", size = 788219 }, { url = "https://files.pythonhosted.org/packages/79/14/d05f617342f4b2b4a23561da500ca2beab062bfcc408d60680e77ecaf04d/regex-2025.10.23-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f13450328a6634348d47a88367e06b64c9d84980ef6a748f717b13f8ce64e87", size = 789739 },
{ url = "https://files.pythonhosted.org/packages/20/bd/2614fc302671b7359972ea212f0e3a92df4414aaeacab054a8ce80a86073/regex-2025.9.18-cp313-cp313-win32.whl", hash = "sha256:3810a65675845c3bdfa58c3c7d88624356dd6ee2fc186628295e0969005f928d", size = 264517 }, { url = "https://files.pythonhosted.org/packages/f9/7b/e8ce8eef42a15f2c3461f8b3e6e924bbc86e9605cb534a393aadc8d3aff8/regex-2025.10.23-cp313-cp313-win32.whl", hash = "sha256:37be9296598a30c6a20236248cb8b2c07ffd54d095b75d3a2a2ee5babdc51df1", size = 266054 },
{ url = "https://files.pythonhosted.org/packages/07/0f/ab5c1581e6563a7bffdc1974fb2d25f05689b88e2d416525271f232b1946/regex-2025.9.18-cp313-cp313-win_amd64.whl", hash = "sha256:16eaf74b3c4180ede88f620f299e474913ab6924d5c4b89b3833bc2345d83b3d", size = 275481 }, { url = "https://files.pythonhosted.org/packages/71/2d/55184ed6be6473187868d2f2e6a0708195fc58270e62a22cbf26028f2570/regex-2025.10.23-cp313-cp313-win_amd64.whl", hash = "sha256:ea7a3c283ce0f06fe789365841e9174ba05f8db16e2fd6ae00a02df9572c04c0", size = 276917 },
{ url = "https://files.pythonhosted.org/packages/49/22/ee47672bc7958f8c5667a587c2600a4fba8b6bab6e86bd6d3e2b5f7cac42/regex-2025.9.18-cp313-cp313-win_arm64.whl", hash = "sha256:4dc98ba7dd66bd1261927a9f49bd5ee2bcb3660f7962f1ec02617280fc00f5eb", size = 268598 }, { url = "https://files.pythonhosted.org/packages/9c/d4/927eced0e2bd45c45839e556f987f8c8f8683268dd3c00ad327deb3b0172/regex-2025.10.23-cp313-cp313-win_arm64.whl", hash = "sha256:d9a4953575f300a7bab71afa4cd4ac061c7697c89590a2902b536783eeb49a4f", size = 270105 },
{ url = "https://files.pythonhosted.org/packages/e8/83/6887e16a187c6226cb85d8301e47d3b73ecc4505a3a13d8da2096b44fd76/regex-2025.9.18-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:fe5d50572bc885a0a799410a717c42b1a6b50e2f45872e2b40f4f288f9bce8a2", size = 489765 }, { url = "https://files.pythonhosted.org/packages/3e/b3/95b310605285573341fc062d1d30b19a54f857530e86c805f942c4ff7941/regex-2025.10.23-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:7d6606524fa77b3912c9ef52a42ef63c6cfbfc1077e9dc6296cd5da0da286044", size = 491850 },
{ url = "https://files.pythonhosted.org/packages/51/c5/e2f7325301ea2916ff301c8d963ba66b1b2c1b06694191df80a9c4fea5d0/regex-2025.9.18-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:1b9d9a2d6cda6621551ca8cf7a06f103adf72831153f3c0d982386110870c4d3", size = 291228 }, { url = "https://files.pythonhosted.org/packages/a4/8f/207c2cec01e34e56db1eff606eef46644a60cf1739ecd474627db90ad90b/regex-2025.10.23-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:c037aadf4d64bdc38af7db3dbd34877a057ce6524eefcb2914d6d41c56f968cc", size = 292537 },
{ url = "https://files.pythonhosted.org/packages/91/60/7d229d2bc6961289e864a3a3cfebf7d0d250e2e65323a8952cbb7e22d824/regex-2025.9.18-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:13202e4c4ac0ef9a317fff817674b293c8f7e8c68d3190377d8d8b749f566e12", size = 289270 }, { url = "https://files.pythonhosted.org/packages/98/3b/025240af4ada1dc0b5f10d73f3e5122d04ce7f8908ab8881e5d82b9d61b6/regex-2025.10.23-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:99018c331fb2529084a0c9b4c713dfa49fafb47c7712422e49467c13a636c656", size = 290904 },
{ url = "https://files.pythonhosted.org/packages/3c/d7/b4f06868ee2958ff6430df89857fbf3d43014bbf35538b6ec96c2704e15d/regex-2025.9.18-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:874ff523b0fecffb090f80ae53dc93538f8db954c8bb5505f05b7787ab3402a0", size = 806326 }, { url = "https://files.pythonhosted.org/packages/81/8e/104ac14e2d3450c43db18ec03e1b96b445a94ae510b60138f00ce2cb7ca1/regex-2025.10.23-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fd8aba965604d70306eb90a35528f776e59112a7114a5162824d43b76fa27f58", size = 807311 },
{ url = "https://files.pythonhosted.org/packages/d6/e4/bca99034a8f1b9b62ccf337402a8e5b959dd5ba0e5e5b2ead70273df3277/regex-2025.9.18-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d13ab0490128f2bb45d596f754148cd750411afc97e813e4b3a61cf278a23bb6", size = 871556 }, { url = "https://files.pythonhosted.org/packages/19/63/78aef90141b7ce0be8a18e1782f764f6997ad09de0e05251f0d2503a914a/regex-2025.10.23-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:238e67264b4013e74136c49f883734f68656adf8257bfa13b515626b31b20f8e", size = 873241 },
{ url = "https://files.pythonhosted.org/packages/6d/df/e06ffaf078a162f6dd6b101a5ea9b44696dca860a48136b3ae4a9caf25e2/regex-2025.9.18-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:05440bc172bc4b4b37fb9667e796597419404dbba62e171e1f826d7d2a9ebcef", size = 913817 }, { url = "https://files.pythonhosted.org/packages/b3/a8/80eb1201bb49ae4dba68a1b284b4211ed9daa8e74dc600018a10a90399fb/regex-2025.10.23-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b2eb48bd9848d66fd04826382f5e8491ae633de3233a3d64d58ceb4ecfa2113a", size = 914794 },
{ url = "https://files.pythonhosted.org/packages/9e/05/25b05480b63292fd8e84800b1648e160ca778127b8d2367a0a258fa2e225/regex-2025.9.18-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5514b8e4031fdfaa3d27e92c75719cbe7f379e28cacd939807289bce76d0e35a", size = 811055 }, { url = "https://files.pythonhosted.org/packages/f0/d5/1984b6ee93281f360a119a5ca1af6a8ca7d8417861671388bf750becc29b/regex-2025.10.23-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d36591ce06d047d0c0fe2fc5f14bfbd5b4525d08a7b6a279379085e13f0e3d0e", size = 812581 },
{ url = "https://files.pythonhosted.org/packages/70/97/7bc7574655eb651ba3a916ed4b1be6798ae97af30104f655d8efd0cab24b/regex-2025.9.18-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:65d3c38c39efce73e0d9dc019697b39903ba25b1ad45ebbd730d2cf32741f40d", size = 794534 }, { url = "https://files.pythonhosted.org/packages/c4/39/11ebdc6d9927172a64ae237d16763145db6bd45ebb4055c17b88edab72a7/regex-2025.10.23-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b5d4ece8628d6e364302006366cea3ee887db397faebacc5dacf8ef19e064cf8", size = 795346 },
{ url = "https://files.pythonhosted.org/packages/b4/c2/d5da49166a52dda879855ecdba0117f073583db2b39bb47ce9a3378a8e9e/regex-2025.9.18-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:ae77e447ebc144d5a26d50055c6ddba1d6ad4a865a560ec7200b8b06bc529368", size = 866684 }, { url = "https://files.pythonhosted.org/packages/3b/b4/89a591bcc08b5e436af43315284bd233ba77daf0cf20e098d7af12f006c1/regex-2025.10.23-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:39a7e8083959cb1c4ff74e483eecb5a65d3b3e1d821b256e54baf61782c906c6", size = 868214 },
{ url = "https://files.pythonhosted.org/packages/bd/2d/0a5c4e6ec417de56b89ff4418ecc72f7e3feca806824c75ad0bbdae0516b/regex-2025.9.18-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e3ef8cf53dc8df49d7e28a356cf824e3623764e9833348b655cfed4524ab8a90", size = 853282 }, { url = "https://files.pythonhosted.org/packages/3d/ff/58ba98409c1dbc8316cdb20dafbc63ed267380a07780cafecaf5012dabc9/regex-2025.10.23-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:842d449a8fefe546f311656cf8c0d6729b08c09a185f1cad94c756210286d6a8", size = 854540 },
{ url = "https://files.pythonhosted.org/packages/f4/8e/d656af63e31a86572ec829665d6fa06eae7e144771e0330650a8bb865635/regex-2025.9.18-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:9feb29817df349c976da9a0debf775c5c33fc1c8ad7b9f025825da99374770b7", size = 797830 }, { url = "https://files.pythonhosted.org/packages/9a/f2/4a9e9338d67626e2071b643f828a482712ad15889d7268e11e9a63d6f7e9/regex-2025.10.23-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d614986dc68506be8f00474f4f6960e03e4ca9883f7df47744800e7d7c08a494", size = 799346 },
{ url = "https://files.pythonhosted.org/packages/db/ce/06edc89df8f7b83ffd321b6071be4c54dc7332c0f77860edc40ce57d757b/regex-2025.9.18-cp313-cp313t-win32.whl", hash = "sha256:168be0d2f9b9d13076940b1ed774f98595b4e3c7fc54584bba81b3cc4181742e", size = 267281 }, { url = "https://files.pythonhosted.org/packages/63/be/543d35c46bebf6f7bf2be538cca74d6585f25714700c36f37f01b92df551/regex-2025.10.23-cp313-cp313t-win32.whl", hash = "sha256:a5b7a26b51a9df473ec16a1934d117443a775ceb7b39b78670b2e21893c330c9", size = 268657 },
{ url = "https://files.pythonhosted.org/packages/83/9a/2b5d9c8b307a451fd17068719d971d3634ca29864b89ed5c18e499446d4a/regex-2025.9.18-cp313-cp313t-win_amd64.whl", hash = "sha256:d59ecf3bb549e491c8104fea7313f3563c7b048e01287db0a90485734a70a730", size = 278724 }, { url = "https://files.pythonhosted.org/packages/14/9f/4dd6b7b612037158bb2c9bcaa710e6fb3c40ad54af441b9c53b3a137a9f1/regex-2025.10.23-cp313-cp313t-win_amd64.whl", hash = "sha256:ce81c5544a5453f61cb6f548ed358cfb111e3b23f3cd42d250a4077a6be2a7b6", size = 280075 },
{ url = "https://files.pythonhosted.org/packages/3d/70/177d31e8089a278a764f8ec9a3faac8d14a312d622a47385d4b43905806f/regex-2025.9.18-cp313-cp313t-win_arm64.whl", hash = "sha256:dbef80defe9fb21310948a2595420b36c6d641d9bea4c991175829b2cc4bc06a", size = 269771 }, { url = "https://files.pythonhosted.org/packages/81/7a/5bd0672aa65d38c8da6747c17c8b441bdb53d816c569e3261013af8e83cf/regex-2025.10.23-cp313-cp313t-win_arm64.whl", hash = "sha256:e9bf7f6699f490e4e43c44757aa179dab24d1960999c84ab5c3d5377714ed473", size = 271219 },
{ url = "https://files.pythonhosted.org/packages/44/b7/3b4663aa3b4af16819f2ab6a78c4111c7e9b066725d8107753c2257448a5/regex-2025.9.18-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:c6db75b51acf277997f3adcd0ad89045d856190d13359f15ab5dda21581d9129", size = 486130 }, { url = "https://files.pythonhosted.org/packages/73/f6/0caf29fec943f201fbc8822879c99d31e59c1d51a983d9843ee5cf398539/regex-2025.10.23-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:5b5cb5b6344c4c4c24b2dc87b0bfee78202b07ef7633385df70da7fcf6f7cec6", size = 488960 },
{ url = "https://files.pythonhosted.org/packages/80/5b/4533f5d7ac9c6a02a4725fe8883de2aebc713e67e842c04cf02626afb747/regex-2025.9.18-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8f9698b6f6895d6db810e0bda5364f9ceb9e5b11328700a90cae573574f61eea", size = 289539 }, { url = "https://files.pythonhosted.org/packages/8e/7d/ebb7085b8fa31c24ce0355107cea2b92229d9050552a01c5d291c42aecea/regex-2025.10.23-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:a6ce7973384c37bdf0f371a843f95a6e6f4e1489e10e0cf57330198df72959c5", size = 290932 },
{ url = "https://files.pythonhosted.org/packages/b8/8d/5ab6797c2750985f79e9995fad3254caa4520846580f266ae3b56d1cae58/regex-2025.9.18-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:29cd86aa7cb13a37d0f0d7c21d8d949fe402ffa0ea697e635afedd97ab4b69f1", size = 287233 }, { url = "https://files.pythonhosted.org/packages/27/41/43906867287cbb5ca4cee671c3cc8081e15deef86a8189c3aad9ac9f6b4d/regex-2025.10.23-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2ee3663f2c334959016b56e3bd0dd187cbc73f948e3a3af14c3caaa0c3035d10", size = 288766 },
{ url = "https://files.pythonhosted.org/packages/cb/1e/95afcb02ba8d3a64e6ffeb801718ce73471ad6440c55d993f65a4a5e7a92/regex-2025.9.18-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7c9f285a071ee55cd9583ba24dde006e53e17780bb309baa8e4289cd472bcc47", size = 797876 }, { url = "https://files.pythonhosted.org/packages/ab/9e/ea66132776700fc77a39b1056e7a5f1308032fead94507e208dc6716b7cd/regex-2025.10.23-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2003cc82a579107e70d013482acce8ba773293f2db534fb532738395c557ff34", size = 798884 },
{ url = "https://files.pythonhosted.org/packages/c8/fb/720b1f49cec1f3b5a9fea5b34cd22b88b5ebccc8c1b5de9cc6f65eed165a/regex-2025.9.18-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5adf266f730431e3be9021d3e5b8d5ee65e563fec2883ea8093944d21863b379", size = 863385 }, { url = "https://files.pythonhosted.org/packages/d5/99/aed1453687ab63819a443930770db972c5c8064421f0d9f5da9ad029f26b/regex-2025.10.23-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:182c452279365a93a9f45874f7f191ec1c51e1f1eb41bf2b16563f1a40c1da3a", size = 864768 },
{ url = "https://files.pythonhosted.org/packages/a9/ca/e0d07ecf701e1616f015a720dc13b84c582024cbfbb3fc5394ae204adbd7/regex-2025.9.18-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1137cabc0f38807de79e28d3f6e3e3f2cc8cfb26bead754d02e6d1de5f679203", size = 910220 }, { url = "https://files.pythonhosted.org/packages/99/5d/732fe747a1304805eb3853ce6337eea16b169f7105a0d0dd9c6a5ffa9948/regex-2025.10.23-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b1249e9ff581c5b658c8f0437f883b01f1edcf424a16388591e7c05e5e9e8b0c", size = 911394 },
{ url = "https://files.pythonhosted.org/packages/b6/45/bba86413b910b708eca705a5af62163d5d396d5f647ed9485580c7025209/regex-2025.9.18-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7cc9e5525cada99699ca9223cce2d52e88c52a3d2a0e842bd53de5497c604164", size = 801827 }, { url = "https://files.pythonhosted.org/packages/5e/48/58a1f6623466522352a6efa153b9a3714fc559d9f930e9bc947b4a88a2c3/regex-2025.10.23-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2b841698f93db3ccc36caa1900d2a3be281d9539b822dc012f08fc80b46a3224", size = 803145 },
{ url = "https://files.pythonhosted.org/packages/b8/a6/740fbd9fcac31a1305a8eed30b44bf0f7f1e042342be0a4722c0365ecfca/regex-2025.9.18-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:bbb9246568f72dce29bcd433517c2be22c7791784b223a810225af3b50d1aafb", size = 786843 }, { url = "https://files.pythonhosted.org/packages/ea/f6/7dea79be2681a5574ab3fc237aa53b2c1dfd6bd2b44d4640b6c76f33f4c1/regex-2025.10.23-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:956d89e0c92d471e8f7eee73f73fdff5ed345886378c45a43175a77538a1ffe4", size = 787831 },
{ url = "https://files.pythonhosted.org/packages/80/a7/0579e8560682645906da640c9055506465d809cb0f5415d9976f417209a6/regex-2025.9.18-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:6a52219a93dd3d92c675383efff6ae18c982e2d7651c792b1e6d121055808743", size = 857430 }, { url = "https://files.pythonhosted.org/packages/3a/ad/07b76950fbbe65f88120ca2d8d845047c401450f607c99ed38862904671d/regex-2025.10.23-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5c259cb363299a0d90d63b5c0d7568ee98419861618a95ee9d91a41cb9954462", size = 859162 },
{ url = "https://files.pythonhosted.org/packages/8d/9b/4dc96b6c17b38900cc9fee254fc9271d0dde044e82c78c0811b58754fde5/regex-2025.9.18-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:ae9b3840c5bd456780e3ddf2f737ab55a79b790f6409182012718a35c6d43282", size = 848612 }, { url = "https://files.pythonhosted.org/packages/41/87/374f3b2021b22aa6a4fc0b750d63f9721e53d1631a238f7a1c343c1cd288/regex-2025.10.23-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:185d2b18c062820b3a40d8fefa223a83f10b20a674bf6e8c4a432e8dfd844627", size = 849899 },
{ url = "https://files.pythonhosted.org/packages/b3/6a/6f659f99bebb1775e5ac81a3fb837b85897c1a4ef5acffd0ff8ffe7e67fb/regex-2025.9.18-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d488c236ac497c46a5ac2005a952c1a0e22a07be9f10c3e735bc7d1209a34773", size = 787967 }, { url = "https://files.pythonhosted.org/packages/12/4a/7f7bb17c5a5a9747249807210e348450dab9212a46ae6d23ebce86ba6a2b/regex-2025.10.23-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:281d87fa790049c2b7c1b4253121edd80b392b19b5a3d28dc2a77579cb2a58ec", size = 789372 },
{ url = "https://files.pythonhosted.org/packages/61/35/9e35665f097c07cf384a6b90a1ac11b0b1693084a0b7a675b06f760496c6/regex-2025.9.18-cp314-cp314-win32.whl", hash = "sha256:0c3506682ea19beefe627a38872d8da65cc01ffa25ed3f2e422dffa1474f0788", size = 269847 }, { url = "https://files.pythonhosted.org/packages/c9/dd/9c7728ff544fea09bbc8635e4c9e7c423b11c24f1a7a14e6ac4831466709/regex-2025.10.23-cp314-cp314-win32.whl", hash = "sha256:63b81eef3656072e4ca87c58084c7a9c2b81d41a300b157be635a8a675aacfb8", size = 271451 },
{ url = "https://files.pythonhosted.org/packages/af/64/27594dbe0f1590b82de2821ebfe9a359b44dcb9b65524876cd12fabc447b/regex-2025.9.18-cp314-cp314-win_amd64.whl", hash = "sha256:57929d0f92bebb2d1a83af372cd0ffba2263f13f376e19b1e4fa32aec4efddc3", size = 278755 }, { url = "https://files.pythonhosted.org/packages/48/f8/ef7837ff858eb74079c4804c10b0403c0b740762e6eedba41062225f7117/regex-2025.10.23-cp314-cp314-win_amd64.whl", hash = "sha256:0967c5b86f274800a34a4ed862dfab56928144d03cb18821c5153f8777947796", size = 280173 },
{ url = "https://files.pythonhosted.org/packages/30/a3/0cd8d0d342886bd7d7f252d701b20ae1a3c72dc7f34ef4b2d17790280a09/regex-2025.9.18-cp314-cp314-win_arm64.whl", hash = "sha256:6a4b44df31d34fa51aa5c995d3aa3c999cec4d69b9bd414a8be51984d859f06d", size = 271873 }, { url = "https://files.pythonhosted.org/packages/8e/d0/d576e1dbd9885bfcd83d0e90762beea48d9373a6f7ed39170f44ed22e336/regex-2025.10.23-cp314-cp314-win_arm64.whl", hash = "sha256:c70dfe58b0a00b36aa04cdb0f798bf3e0adc31747641f69e191109fd8572c9a9", size = 273206 },
{ url = "https://files.pythonhosted.org/packages/99/cb/8a1ab05ecf404e18b54348e293d9b7a60ec2bd7aa59e637020c5eea852e8/regex-2025.9.18-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:b176326bcd544b5e9b17d6943f807697c0cb7351f6cfb45bf5637c95ff7e6306", size = 489773 }, { url = "https://files.pythonhosted.org/packages/a6/d0/2025268315e8b2b7b660039824cb7765a41623e97d4cd421510925400487/regex-2025.10.23-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:1f5799ea1787aa6de6c150377d11afad39a38afd033f0c5247aecb997978c422", size = 491854 },
{ url = "https://files.pythonhosted.org/packages/93/3b/6543c9b7f7e734d2404fa2863d0d710c907bef99d4598760ed4563d634c3/regex-2025.9.18-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:0ffd9e230b826b15b369391bec167baed57c7ce39efc35835448618860995946", size = 291221 }, { url = "https://files.pythonhosted.org/packages/44/35/5681c2fec5e8b33454390af209c4353dfc44606bf06d714b0b8bd0454ffe/regex-2025.10.23-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:a9639ab7540cfea45ef57d16dcbea2e22de351998d614c3ad2f9778fa3bdd788", size = 292542 },
{ url = "https://files.pythonhosted.org/packages/cd/91/e9fdee6ad6bf708d98c5d17fded423dcb0661795a49cba1b4ffb8358377a/regex-2025.9.18-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ec46332c41add73f2b57e2f5b642f991f6b15e50e9f86285e08ffe3a512ac39f", size = 289268 }, { url = "https://files.pythonhosted.org/packages/5d/17/184eed05543b724132e4a18149e900f5189001fcfe2d64edaae4fbaf36b4/regex-2025.10.23-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:08f52122c352eb44c3421dab78b9b73a8a77a282cc8314ae576fcaa92b780d10", size = 290903 },
{ url = "https://files.pythonhosted.org/packages/94/a6/bc3e8a918abe4741dadeaeb6c508e3a4ea847ff36030d820d89858f96a6c/regex-2025.9.18-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b80fa342ed1ea095168a3f116637bd1030d39c9ff38dc04e54ef7c521e01fc95", size = 806659 }, { url = "https://files.pythonhosted.org/packages/25/d0/5e3347aa0db0de382dddfa133a7b0ae72f24b4344f3989398980b44a3924/regex-2025.10.23-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ebf1baebef1c4088ad5a5623decec6b52950f0e4d7a0ae4d48f0a99f8c9cb7d7", size = 807546 },
{ url = "https://files.pythonhosted.org/packages/2b/71/ea62dbeb55d9e6905c7b5a49f75615ea1373afcad95830047e4e310db979/regex-2025.9.18-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f4d97071c0ba40f0cf2a93ed76e660654c399a0a04ab7d85472239460f3da84b", size = 871701 }, { url = "https://files.pythonhosted.org/packages/d2/bb/40c589bbdce1be0c55e9f8159789d58d47a22014f2f820cf2b517a5cd193/regex-2025.10.23-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:16b0f1c2e2d566c562d5c384c2b492646be0a19798532fdc1fdedacc66e3223f", size = 873322 },
{ url = "https://files.pythonhosted.org/packages/6a/90/fbe9dedb7dad24a3a4399c0bae64bfa932ec8922a0a9acf7bc88db30b161/regex-2025.9.18-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0ac936537ad87cef9e0e66c5144484206c1354224ee811ab1519a32373e411f3", size = 913742 }, { url = "https://files.pythonhosted.org/packages/fe/56/a7e40c01575ac93360e606278d359f91829781a9f7fb6e5aa435039edbda/regex-2025.10.23-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f7ada5d9dceafaab92646aa00c10a9efd9b09942dd9b0d7c5a4b73db92cc7e61", size = 914855 },
{ url = "https://files.pythonhosted.org/packages/f0/1c/47e4a8c0e73d41eb9eb9fdeba3b1b810110a5139a2526e82fd29c2d9f867/regex-2025.9.18-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dec57f96d4def58c422d212d414efe28218d58537b5445cf0c33afb1b4768571", size = 811117 }, { url = "https://files.pythonhosted.org/packages/5c/4b/d55587b192763db3163c3f508b3b67b31bb6f5e7a0e08b83013d0a59500a/regex-2025.10.23-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3a36b4005770044bf08edecc798f0e41a75795b9e7c9c12fe29da8d792ef870c", size = 812724 },
{ url = "https://files.pythonhosted.org/packages/2a/da/435f29fddfd015111523671e36d30af3342e8136a889159b05c1d9110480/regex-2025.9.18-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:48317233294648bf7cd068857f248e3a57222259a5304d32c7552e2284a1b2ad", size = 794647 }, { url = "https://files.pythonhosted.org/packages/33/20/18bac334955fbe99d17229f4f8e98d05e4a501ac03a442be8facbb37c304/regex-2025.10.23-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:af7b2661dcc032da1fae82069b5ebf2ac1dfcd5359ef8b35e1367bfc92181432", size = 795439 },
{ url = "https://files.pythonhosted.org/packages/23/66/df5e6dcca25c8bc57ce404eebc7342310a0d218db739d7882c9a2b5974a3/regex-2025.9.18-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:274687e62ea3cf54846a9b25fc48a04459de50af30a7bd0b61a9e38015983494", size = 866747 }, { url = "https://files.pythonhosted.org/packages/67/46/c57266be9df8549c7d85deb4cb82280cb0019e46fff677534c5fa1badfa4/regex-2025.10.23-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:1cb976810ac1416a67562c2e5ba0accf6f928932320fef302e08100ed681b38e", size = 868336 },
{ url = "https://files.pythonhosted.org/packages/82/42/94392b39b531f2e469b2daa40acf454863733b674481fda17462a5ffadac/regex-2025.9.18-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:a78722c86a3e7e6aadf9579e3b0ad78d955f2d1f1a8ca4f67d7ca258e8719d4b", size = 853434 }, { url = "https://files.pythonhosted.org/packages/b8/f3/bd5879e41ef8187fec5e678e94b526a93f99e7bbe0437b0f2b47f9101694/regex-2025.10.23-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:1a56a54be3897d62f54290190fbcd754bff6932934529fbf5b29933da28fcd43", size = 854567 },
{ url = "https://files.pythonhosted.org/packages/a8/f8/dcc64c7f7bbe58842a8f89622b50c58c3598fbbf4aad0a488d6df2c699f1/regex-2025.9.18-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:06104cd203cdef3ade989a1c45b6215bf42f8b9dd705ecc220c173233f7cba41", size = 798024 }, { url = "https://files.pythonhosted.org/packages/e6/57/2b6bbdbd2f24dfed5b028033aa17ad8f7d86bb28f1a892cac8b3bc89d059/regex-2025.10.23-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8f3e6d202fb52c2153f532043bbcf618fd177df47b0b306741eb9b60ba96edc3", size = 799565 },
{ url = "https://files.pythonhosted.org/packages/20/8d/edf1c5d5aa98f99a692313db813ec487732946784f8f93145e0153d910e5/regex-2025.9.18-cp314-cp314t-win32.whl", hash = "sha256:2e1eddc06eeaffd249c0adb6fafc19e2118e6308c60df9db27919e96b5656096", size = 273029 }, { url = "https://files.pythonhosted.org/packages/c7/ba/a6168f542ba73b151ed81237adf6b869c7b2f7f8d51618111296674e20ee/regex-2025.10.23-cp314-cp314t-win32.whl", hash = "sha256:1fa1186966b2621b1769fd467c7b22e317e6ba2d2cdcecc42ea3089ef04a8521", size = 274428 },
{ url = "https://files.pythonhosted.org/packages/a7/24/02d4e4f88466f17b145f7ea2b2c11af3a942db6222429c2c146accf16054/regex-2025.9.18-cp314-cp314t-win_amd64.whl", hash = "sha256:8620d247fb8c0683ade51217b459cb4a1081c0405a3072235ba43a40d355c09a", size = 282680 }, { url = "https://files.pythonhosted.org/packages/ef/a0/c84475e14a2829e9b0864ebf77c3f7da909df9d8acfe2bb540ff0072047c/regex-2025.10.23-cp314-cp314t-win_amd64.whl", hash = "sha256:08a15d40ce28362eac3e78e83d75475147869c1ff86bc93285f43b4f4431a741", size = 284140 },
{ url = "https://files.pythonhosted.org/packages/1f/a3/c64894858aaaa454caa7cc47e2f225b04d3ed08ad649eacf58d45817fad2/regex-2025.9.18-cp314-cp314t-win_arm64.whl", hash = "sha256:b7531a8ef61de2c647cdf68b3229b071e46ec326b3138b2180acb4275f470b01", size = 273034 }, { url = "https://files.pythonhosted.org/packages/51/33/6a08ade0eee5b8ba79386869fa6f77afeb835b60510f3525db987e2fffc4/regex-2025.10.23-cp314-cp314t-win_arm64.whl", hash = "sha256:a93e97338e1c8ea2649e130dcfbe8cd69bba5e1e163834752ab64dcb4de6d5ed", size = 274497 },
] ]
[[package]] [[package]]
@ -1403,33 +1402,33 @@ wheels = [
[[package]] [[package]]
name = "ruff" name = "ruff"
version = "0.14.0" version = "0.14.1"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/41/b9/9bd84453ed6dd04688de9b3f3a4146a1698e8faae2ceeccce4e14c67ae17/ruff-0.14.0.tar.gz", hash = "sha256:62ec8969b7510f77945df916de15da55311fade8d6050995ff7f680afe582c57", size = 5452071 } sdist = { url = "https://files.pythonhosted.org/packages/9e/58/6ca66896635352812de66f71cdf9ff86b3a4f79071ca5730088c0cd0fc8d/ruff-0.14.1.tar.gz", hash = "sha256:1dd86253060c4772867c61791588627320abcb6ed1577a90ef432ee319729b69", size = 5513429 }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/3a/4e/79d463a5f80654e93fa653ebfb98e0becc3f0e7cf6219c9ddedf1e197072/ruff-0.14.0-py3-none-linux_armv6l.whl", hash = "sha256:58e15bffa7054299becf4bab8a1187062c6f8cafbe9f6e39e0d5aface455d6b3", size = 12494532 }, { url = "https://files.pythonhosted.org/packages/8d/39/9cc5ab181478d7a18adc1c1e051a84ee02bec94eb9bdfd35643d7c74ca31/ruff-0.14.1-py3-none-linux_armv6l.whl", hash = "sha256:083bfc1f30f4a391ae09c6f4f99d83074416b471775b59288956f5bc18e82f8b", size = 12445415 },
{ url = "https://files.pythonhosted.org/packages/ee/40/e2392f445ed8e02aa6105d49db4bfff01957379064c30f4811c3bf38aece/ruff-0.14.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:838d1b065f4df676b7c9957992f2304e41ead7a50a568185efd404297d5701e8", size = 13160768 }, { url = "https://files.pythonhosted.org/packages/ef/2e/1226961855ccd697255988f5a2474890ac7c5863b080b15bd038df820818/ruff-0.14.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:f6fa757cd717f791009f7669fefb09121cc5f7d9bd0ef211371fad68c2b8b224", size = 12784267 },
{ url = "https://files.pythonhosted.org/packages/75/da/2a656ea7c6b9bd14c7209918268dd40e1e6cea65f4bb9880eaaa43b055cd/ruff-0.14.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:703799d059ba50f745605b04638fa7e9682cc3da084b2092feee63500ff3d9b8", size = 12363376 }, { url = "https://files.pythonhosted.org/packages/c1/ea/fd9e95863124ed159cd0667ec98449ae461de94acda7101f1acb6066da00/ruff-0.14.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d6191903d39ac156921398e9c86b7354d15e3c93772e7dbf26c9fcae59ceccd5", size = 11781872 },
{ url = "https://files.pythonhosted.org/packages/42/e2/1ffef5a1875add82416ff388fcb7ea8b22a53be67a638487937aea81af27/ruff-0.14.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ba9a8925e90f861502f7d974cc60e18ca29c72bb0ee8bfeabb6ade35a3abde7", size = 12608055 }, { url = "https://files.pythonhosted.org/packages/1e/5a/e890f7338ff537dba4589a5e02c51baa63020acfb7c8cbbaea4831562c96/ruff-0.14.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed04f0e04f7a4587244e5c9d7df50e6b5bf2705d75059f409a6421c593a35896", size = 12226558 },
{ url = "https://files.pythonhosted.org/packages/4a/32/986725199d7cee510d9f1dfdf95bf1efc5fa9dd714d0d85c1fb1f6be3bc3/ruff-0.14.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e41f785498bd200ffc276eb9e1570c019c1d907b07cfb081092c8ad51975bbe7", size = 12318544 }, { url = "https://files.pythonhosted.org/packages/a6/7a/8ab5c3377f5bf31e167b73651841217542bcc7aa1c19e83030835cc25204/ruff-0.14.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5c9e6cf6cd4acae0febbce29497accd3632fe2025c0c583c8b87e8dbdeae5f61", size = 12187898 },
{ url = "https://files.pythonhosted.org/packages/9a/ed/4969cefd53315164c94eaf4da7cfba1f267dc275b0abdd593d11c90829a3/ruff-0.14.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30a58c087aef4584c193aebf2700f0fbcfc1e77b89c7385e3139956fa90434e2", size = 14001280 }, { url = "https://files.pythonhosted.org/packages/48/8d/ba7c33aa55406955fc124e62c8259791c3d42e3075a71710fdff9375134f/ruff-0.14.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a6fa2458527794ecdfbe45f654e42c61f2503a230545a91af839653a0a93dbc6", size = 12939168 },
{ url = "https://files.pythonhosted.org/packages/ab/ad/96c1fc9f8854c37681c9613d825925c7f24ca1acfc62a4eb3896b50bacd2/ruff-0.14.0-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:f8d07350bc7af0a5ce8812b7d5c1a7293cf02476752f23fdfc500d24b79b783c", size = 15027286 }, { url = "https://files.pythonhosted.org/packages/b4/c2/70783f612b50f66d083380e68cbd1696739d88e9b4f6164230375532c637/ruff-0.14.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:39f1c392244e338b21d42ab29b8a6392a722c5090032eb49bb4d6defcdb34345", size = 14386942 },
{ url = "https://files.pythonhosted.org/packages/b3/00/1426978f97df4fe331074baf69615f579dc4e7c37bb4c6f57c2aad80c87f/ruff-0.14.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eec3bbbf3a7d5482b5c1f42d5fc972774d71d107d447919fca620b0be3e3b75e", size = 14451506 }, { url = "https://files.pythonhosted.org/packages/48/44/cd7abb9c776b66d332119d67f96acf15830d120f5b884598a36d9d3f4d83/ruff-0.14.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7382fa12a26cce1f95070ce450946bec357727aaa428983036362579eadcc5cf", size = 13990622 },
{ url = "https://files.pythonhosted.org/packages/58/d5/9c1cea6e493c0cf0647674cca26b579ea9d2a213b74b5c195fbeb9678e15/ruff-0.14.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:16b68e183a0e28e5c176d51004aaa40559e8f90065a10a559176713fcf435206", size = 13437384 }, { url = "https://files.pythonhosted.org/packages/eb/56/4259b696db12ac152fe472764b4f78bbdd9b477afd9bc3a6d53c01300b37/ruff-0.14.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd0bf2be3ae8521e1093a487c4aa3b455882f139787770698530d28ed3fbb37c", size = 13431143 },
{ url = "https://files.pythonhosted.org/packages/29/b4/4cd6a4331e999fc05d9d77729c95503f99eae3ba1160469f2b64866964e3/ruff-0.14.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb732d17db2e945cfcbbc52af0143eda1da36ca8ae25083dd4f66f1542fdf82e", size = 13447976 }, { url = "https://files.pythonhosted.org/packages/e0/35/266a80d0eb97bd224b3265b9437bd89dde0dcf4faf299db1212e81824e7e/ruff-0.14.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cabcaa9ccf8089fb4fdb78d17cc0e28241520f50f4c2e88cb6261ed083d85151", size = 13132844 },
{ url = "https://files.pythonhosted.org/packages/3b/c0/ac42f546d07e4f49f62332576cb845d45c67cf5610d1851254e341d563b6/ruff-0.14.0-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:c958f66ab884b7873e72df38dcabee03d556a8f2ee1b8538ee1c2bbd619883dd", size = 13682850 }, { url = "https://files.pythonhosted.org/packages/65/6e/d31ce218acc11a8d91ef208e002a31acf315061a85132f94f3df7a252b18/ruff-0.14.1-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:747d583400f6125ec11a4c14d1c8474bf75d8b419ad22a111a537ec1a952d192", size = 13401241 },
{ url = "https://files.pythonhosted.org/packages/5f/c4/4b0c9bcadd45b4c29fe1af9c5d1dc0ca87b4021665dfbe1c4688d407aa20/ruff-0.14.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:7eb0499a2e01f6e0c285afc5bac43ab380cbfc17cd43a2e1dd10ec97d6f2c42d", size = 12449825 }, { url = "https://files.pythonhosted.org/packages/9f/b5/dbc4221bf0b03774b3b2f0d47f39e848d30664157c15b965a14d890637d2/ruff-0.14.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5a6e74c0efd78515a1d13acbfe6c90f0f5bd822aa56b4a6d43a9ffb2ae6e56cd", size = 12132476 },
{ url = "https://files.pythonhosted.org/packages/4b/a8/e2e76288e6c16540fa820d148d83e55f15e994d852485f221b9524514730/ruff-0.14.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:4c63b2d99fafa05efca0ab198fd48fa6030d57e4423df3f18e03aa62518c565f", size = 12272599 }, { url = "https://files.pythonhosted.org/packages/98/4b/ac99194e790ccd092d6a8b5f341f34b6e597d698e3077c032c502d75ea84/ruff-0.14.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:0ea6a864d2fb41a4b6d5b456ed164302a0d96f4daac630aeba829abfb059d020", size = 12139749 },
{ url = "https://files.pythonhosted.org/packages/18/14/e2815d8eff847391af632b22422b8207704222ff575dec8d044f9ab779b2/ruff-0.14.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:668fce701b7a222f3f5327f86909db2bbe99c30877c8001ff934c5413812ac02", size = 13193828 }, { url = "https://files.pythonhosted.org/packages/47/26/7df917462c3bb5004e6fdfcc505a49e90bcd8a34c54a051953118c00b53a/ruff-0.14.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:0826b8764f94229604fa255918d1cc45e583e38c21c203248b0bfc9a0e930be5", size = 12544758 },
{ url = "https://files.pythonhosted.org/packages/44/c6/61ccc2987cf0aecc588ff8f3212dea64840770e60d78f5606cd7dc34de32/ruff-0.14.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a86bf575e05cb68dcb34e4c7dfe1064d44d3f0c04bbc0491949092192b515296", size = 13628617 }, { url = "https://files.pythonhosted.org/packages/64/d0/81e7f0648e9764ad9b51dd4be5e5dac3fcfff9602428ccbae288a39c2c22/ruff-0.14.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:cbc52160465913a1a3f424c81c62ac8096b6a491468e7d872cb9444a860bc33d", size = 13221811 },
{ url = "https://files.pythonhosted.org/packages/73/e6/03b882225a1b0627e75339b420883dc3c90707a8917d2284abef7a58d317/ruff-0.14.0-py3-none-win32.whl", hash = "sha256:7450a243d7125d1c032cb4b93d9625dea46c8c42b4f06c6b709baac168e10543", size = 12367872 }, { url = "https://files.pythonhosted.org/packages/c3/07/3c45562c67933cc35f6d5df4ca77dabbcd88fddaca0d6b8371693d29fd56/ruff-0.14.1-py3-none-win32.whl", hash = "sha256:e037ea374aaaff4103240ae79168c0945ae3d5ae8db190603de3b4012bd1def6", size = 12319467 },
{ url = "https://files.pythonhosted.org/packages/41/77/56cf9cf01ea0bfcc662de72540812e5ba8e9563f33ef3d37ab2174892c47/ruff-0.14.0-py3-none-win_amd64.whl", hash = "sha256:ea95da28cd874c4d9c922b39381cbd69cb7e7b49c21b8152b014bd4f52acddc2", size = 13464628 }, { url = "https://files.pythonhosted.org/packages/02/88/0ee4ca507d4aa05f67e292d2e5eb0b3e358fbcfe527554a2eda9ac422d6b/ruff-0.14.1-py3-none-win_amd64.whl", hash = "sha256:59d599cdff9c7f925a017f6f2c256c908b094e55967f93f2821b1439928746a1", size = 13401123 },
{ url = "https://files.pythonhosted.org/packages/c6/2a/65880dfd0e13f7f13a775998f34703674a4554906167dce02daf7865b954/ruff-0.14.0-py3-none-win_arm64.whl", hash = "sha256:f42c9495f5c13ff841b1da4cb3c2a42075409592825dada7c5885c2c844ac730", size = 12565142 }, { url = "https://files.pythonhosted.org/packages/b8/81/4b6387be7014858d924b843530e1b2a8e531846807516e9bea2ee0936bf7/ruff-0.14.1-py3-none-win_arm64.whl", hash = "sha256:e3b443c4c9f16ae850906b8d0a707b2a4c16f8d2f0a7fe65c475c5886665ce44", size = 12436636 },
] ]
[[package]] [[package]]
name = "selenium" name = "selenium"
version = "4.36.0" version = "4.37.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "certifi" }, { name = "certifi" },
@ -1439,9 +1438,9 @@ dependencies = [
{ name = "urllib3", extra = ["socks"] }, { name = "urllib3", extra = ["socks"] },
{ name = "websocket-client" }, { name = "websocket-client" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/10/35/33d3d84e3399c9d00b489aeccfdc78115e149e45816fb8fe84274329e8a2/selenium-4.36.0.tar.gz", hash = "sha256:0eced83038736c3a013b824116df0b6dbb83e93721545f51b680451013416723", size = 913613 } sdist = { url = "https://files.pythonhosted.org/packages/fd/0d/2c5b09b56a749f1b43a8dcb9875b3edf81dda69b3a3348c8d90e3ce01555/selenium-4.37.0.tar.gz", hash = "sha256:a5f312fe659fc373a194484c6667b920278493ac98bca1b38e239c1b8bb3a05c", size = 918689 }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/74/9e/642a355e43a4ebf68bc4f00dd4ab264f635079c5dc7ed6d9991a0c2be3d7/selenium-4.36.0-py3-none-any.whl", hash = "sha256:525fdfe96b99c27d9a2c773c75aa7413f4c24bdb7b9749c1950aa3b5f79ed915", size = 9587029 }, { url = "https://files.pythonhosted.org/packages/f2/40/2df5e5bc30358629103875f7ab47aca4b934ad902b65b0f5615d74914f12/selenium-4.37.0-py3-none-any.whl", hash = "sha256:5cfee4f7c430f7150fcc0490cf2936d101a72b76bad74644e2159cec0013d4de", size = 9696815 },
] ]
[[package]] [[package]]