refactor: harden CLI/client/config and centralize serialization
This commit is contained in:
52
README.md
52
README.md
@@ -1,6 +1,6 @@
|
|||||||
# Twitter CLI
|
# Twitter CLI
|
||||||
|
|
||||||
Twitter/X 命令行工具 — 读取 Timeline、管理推文。
|
Twitter/X 命令行工具 — 读取 Timeline、书签和用户信息。
|
||||||
|
|
||||||
**零 API Key** — 使用浏览器 Cookie 认证,免费访问 Twitter。
|
**零 API Key** — 使用浏览器 Cookie 认证,免费访问 Twitter。
|
||||||
|
|
||||||
@@ -22,14 +22,17 @@ twitter feed
|
|||||||
### 读取
|
### 读取
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 抓取首页 timeline
|
# 抓取首页 timeline(For You 算法推荐)
|
||||||
twitter feed
|
twitter feed
|
||||||
|
|
||||||
|
# 抓取关注的人的 timeline(Following 时间线)
|
||||||
|
twitter feed -t following
|
||||||
|
|
||||||
# 自定义抓取条数
|
# 自定义抓取条数
|
||||||
twitter feed --max 50
|
twitter feed --max 50
|
||||||
|
|
||||||
# 跳过筛选
|
# 开启筛选(按 score 排序过滤)
|
||||||
twitter feed --no-filter
|
twitter feed --filter
|
||||||
|
|
||||||
# JSON 输出
|
# JSON 输出
|
||||||
twitter feed --json > tweets.json
|
twitter feed --json > tweets.json
|
||||||
@@ -51,31 +54,6 @@ twitter user elonmusk
|
|||||||
|
|
||||||
# 列出用户推文
|
# 列出用户推文
|
||||||
twitter user-posts elonmusk --max 20
|
twitter user-posts elonmusk --max 20
|
||||||
|
|
||||||
# 查看粉丝
|
|
||||||
twitter followers elonmusk --max 30
|
|
||||||
|
|
||||||
# 查看关注
|
|
||||||
twitter following elonmusk --max 30
|
|
||||||
```
|
|
||||||
|
|
||||||
### 发推
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 发新推文
|
|
||||||
twitter post "Hello World"
|
|
||||||
|
|
||||||
# 回复推文
|
|
||||||
twitter reply <tweet_id> "这是回复内容"
|
|
||||||
|
|
||||||
# 引用转推(传 URL 或 ID 均可)
|
|
||||||
twitter quote <tweet_url_or_id> "这是引用内容"
|
|
||||||
|
|
||||||
# 删除推文(会有确认提示)
|
|
||||||
twitter delete <tweet_id>
|
|
||||||
|
|
||||||
# 跳过删除确认
|
|
||||||
twitter delete <tweet_id> --yes
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Pipeline
|
## Pipeline
|
||||||
@@ -132,14 +110,28 @@ export TWITTER_CT0=your_ct0
|
|||||||
twitter_cli/
|
twitter_cli/
|
||||||
├── __init__.py # 版本信息
|
├── __init__.py # 版本信息
|
||||||
├── cli.py # CLI 入口 (click)
|
├── cli.py # CLI 入口 (click)
|
||||||
├── client.py # Twitter GraphQL API Client (GET + POST)
|
├── client.py # Twitter GraphQL API Client (GET)
|
||||||
├── auth.py # Cookie 提取 (env / browser-cookie3)
|
├── auth.py # Cookie 提取 (env / browser-cookie3)
|
||||||
├── filter.py # Engagement scoring + 筛选
|
├── filter.py # Engagement scoring + 筛选
|
||||||
├── formatter.py # Rich 终端输出 + JSON
|
├── formatter.py # Rich 终端输出 + JSON
|
||||||
├── config.py # YAML 配置加载
|
├── config.py # YAML 配置加载
|
||||||
|
├── serialization.py # Tweet JSON <-> dataclass
|
||||||
└── models.py # 数据模型 (dataclass)
|
└── models.py # 数据模型 (dataclass)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install development tools
|
||||||
|
uv sync --extra dev
|
||||||
|
|
||||||
|
# Run tests
|
||||||
|
uv run pytest
|
||||||
|
|
||||||
|
# Lint
|
||||||
|
uv run ruff check .
|
||||||
|
```
|
||||||
|
|
||||||
## 注意事项
|
## 注意事项
|
||||||
|
|
||||||
- 使用 Cookie 登录存在被平台检测的风险,建议使用**专用小号**
|
- 使用 Cookie 登录存在被平台检测的风险,建议使用**专用小号**
|
||||||
|
|||||||
@@ -29,6 +29,13 @@ dependencies = [
|
|||||||
"browser-cookie3>=0.19",
|
"browser-cookie3>=0.19",
|
||||||
"click>=8.0",
|
"click>=8.0",
|
||||||
"rich>=13.0",
|
"rich>=13.0",
|
||||||
|
"PyYAML>=6.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.optional-dependencies]
|
||||||
|
dev = [
|
||||||
|
"pytest>=8.0",
|
||||||
|
"ruff>=0.8",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.urls]
|
[project.urls]
|
||||||
@@ -38,3 +45,10 @@ Issues = "https://github.com/jackwener/twitter-cli/issues"
|
|||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
twitter = "twitter_cli.cli:cli"
|
twitter = "twitter_cli.cli:cli"
|
||||||
|
|
||||||
|
[tool.pytest.ini_options]
|
||||||
|
testpaths = ["tests"]
|
||||||
|
python_files = ["test_*.py"]
|
||||||
|
|
||||||
|
[tool.ruff]
|
||||||
|
line-length = 100
|
||||||
|
|||||||
@@ -13,8 +13,9 @@ import os
|
|||||||
import ssl
|
import ssl
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
import urllib.error
|
||||||
import urllib.request
|
import urllib.request
|
||||||
from typing import Any, Dict, Optional
|
from typing import Dict, Optional
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -142,7 +143,8 @@ sys.exit(1)
|
|||||||
if not output:
|
if not output:
|
||||||
stderr = result.stderr.strip()
|
stderr = result.stderr.strip()
|
||||||
if stderr:
|
if stderr:
|
||||||
# Maybe browser-cookie3 not installed, try with uv
|
logger.debug("Cookie extraction stderr from current env: %s", stderr[:300])
|
||||||
|
# Maybe browser-cookie3 not installed, try with uv.
|
||||||
result2 = subprocess.run(
|
result2 = subprocess.run(
|
||||||
["uv", "run", "--with", "browser-cookie3", "python3", "-c", extract_script],
|
["uv", "run", "--with", "browser-cookie3", "python3", "-c", extract_script],
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
@@ -151,6 +153,7 @@ sys.exit(1)
|
|||||||
)
|
)
|
||||||
output = result2.stdout.strip()
|
output = result2.stdout.strip()
|
||||||
if not output:
|
if not output:
|
||||||
|
logger.debug("Cookie extraction stderr from uv fallback: %s", result2.stderr.strip()[:300])
|
||||||
return None
|
return None
|
||||||
|
|
||||||
data = json.loads(output)
|
data = json.loads(output)
|
||||||
@@ -185,4 +188,6 @@ def get_cookies() -> Dict[str, str]:
|
|||||||
"Option 2: Make sure you are logged into x.com in your browser (Chrome/Edge/Firefox/Brave)"
|
"Option 2: Make sure you are logged into x.com in your browser (Chrome/Edge/Firefox/Brave)"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Verify only for explicit auth failures; transient endpoint issues are tolerated.
|
||||||
|
verify_cookies(cookies["auth_token"], cookies["ct0"])
|
||||||
return cookies
|
return cookies
|
||||||
|
|||||||
@@ -1,28 +1,24 @@
|
|||||||
"""CLI entry point for twitter-cli.
|
"""CLI entry point for twitter-cli.
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
twitter feed # fetch home timeline → filter
|
twitter feed # fetch home timeline (For You)
|
||||||
|
twitter feed -t following # fetch following feed
|
||||||
twitter feed --max 50 # custom fetch count
|
twitter feed --max 50 # custom fetch count
|
||||||
twitter feed --no-filter # skip filtering
|
twitter feed --filter # enable score-based filtering
|
||||||
twitter feed --json # JSON output
|
twitter feed --json # JSON output
|
||||||
twitter favorite # fetch bookmarks
|
twitter favorite # fetch bookmarks
|
||||||
twitter favorite --max 30
|
|
||||||
twitter feed --input tweets.json # load existing data
|
twitter feed --input tweets.json # load existing data
|
||||||
twitter feed --output out.json # save filtered tweets
|
twitter feed --output out.json # save filtered tweets
|
||||||
twitter post "Hello" # post a tweet
|
twitter user elonmusk # view user profile
|
||||||
twitter reply ID "text" # reply to a tweet
|
twitter user-posts elonmusk # list user tweets
|
||||||
twitter quote ID "text" # quote a tweet
|
|
||||||
twitter delete ID # delete a tweet
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
|
||||||
import logging
|
import logging
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import List
|
|
||||||
|
|
||||||
import click
|
import click
|
||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
@@ -32,17 +28,12 @@ from .auth import get_cookies
|
|||||||
from .client import TwitterClient
|
from .client import TwitterClient
|
||||||
from .config import load_config
|
from .config import load_config
|
||||||
from .filter import filter_tweets
|
from .filter import filter_tweets
|
||||||
from .formatter import (
|
from .formatter import print_filter_stats, print_tweet_table, print_user_profile
|
||||||
print_filter_stats,
|
from .serialization import tweets_from_json, tweets_to_json
|
||||||
print_tweet_table,
|
|
||||||
print_user_profile,
|
|
||||||
print_user_table,
|
|
||||||
tweets_to_json,
|
|
||||||
)
|
|
||||||
from .models import Author, Metrics, Tweet, TweetMedia
|
|
||||||
|
|
||||||
|
|
||||||
console = Console()
|
console = Console()
|
||||||
|
FEED_TYPES = ["for-you", "following"]
|
||||||
|
|
||||||
|
|
||||||
def _setup_logging(verbose):
|
def _setup_logging(verbose):
|
||||||
@@ -58,70 +49,49 @@ def _setup_logging(verbose):
|
|||||||
def _load_tweets_from_json(path):
|
def _load_tweets_from_json(path):
|
||||||
# type: (str) -> List[Tweet]
|
# type: (str) -> List[Tweet]
|
||||||
"""Load tweets from a JSON file (previously exported)."""
|
"""Load tweets from a JSON file (previously exported)."""
|
||||||
raw = Path(path).read_text(encoding="utf-8")
|
file_path = Path(path)
|
||||||
items = json.loads(raw)
|
if not file_path.exists():
|
||||||
tweets = []
|
raise RuntimeError("Input file not found: %s" % path)
|
||||||
for d in items:
|
|
||||||
author_data = d.get("author", {})
|
|
||||||
metrics_data = d.get("metrics", {})
|
|
||||||
media_data = d.get("media", [])
|
|
||||||
|
|
||||||
author = Author(
|
try:
|
||||||
id=author_data.get("id", ""),
|
raw = file_path.read_text(encoding="utf-8")
|
||||||
name=author_data.get("name", ""),
|
return tweets_from_json(raw)
|
||||||
screen_name=author_data.get("screenName", ""),
|
except (ValueError, OSError) as exc:
|
||||||
profile_image_url=author_data.get("profileImageUrl", ""),
|
raise RuntimeError("Invalid tweet JSON file %s: %s" % (path, exc))
|
||||||
verified=author_data.get("verified", False),
|
|
||||||
)
|
|
||||||
metrics = Metrics(
|
|
||||||
likes=metrics_data.get("likes", 0),
|
|
||||||
retweets=metrics_data.get("retweets", 0),
|
|
||||||
replies=metrics_data.get("replies", 0),
|
|
||||||
quotes=metrics_data.get("quotes", 0),
|
|
||||||
views=metrics_data.get("views", 0),
|
|
||||||
bookmarks=metrics_data.get("bookmarks", 0),
|
|
||||||
)
|
|
||||||
media = [
|
|
||||||
TweetMedia(
|
|
||||||
type=m.get("type", ""),
|
|
||||||
url=m.get("url", ""),
|
|
||||||
width=m.get("width"),
|
|
||||||
height=m.get("height"),
|
|
||||||
)
|
|
||||||
for m in media_data
|
|
||||||
]
|
|
||||||
|
|
||||||
qt_data = d.get("quotedTweet")
|
|
||||||
quoted_tweet = None
|
|
||||||
if qt_data:
|
|
||||||
qt_author = qt_data.get("author", {})
|
|
||||||
quoted_tweet = Tweet(
|
|
||||||
id=qt_data.get("id", ""),
|
|
||||||
text=qt_data.get("text", ""),
|
|
||||||
author=Author(
|
|
||||||
id="",
|
|
||||||
name=qt_author.get("name", ""),
|
|
||||||
screen_name=qt_author.get("screenName", ""),
|
|
||||||
),
|
|
||||||
metrics=Metrics(),
|
|
||||||
created_at="",
|
|
||||||
)
|
|
||||||
|
|
||||||
tweets.append(Tweet(
|
def _get_client():
|
||||||
id=d.get("id", ""),
|
# type: () -> TwitterClient
|
||||||
text=d.get("text", ""),
|
"""Create an authenticated API client."""
|
||||||
author=author,
|
console.print("\n🔐 Getting Twitter cookies...")
|
||||||
metrics=metrics,
|
try:
|
||||||
created_at=d.get("createdAt", ""),
|
cookies = get_cookies()
|
||||||
media=media,
|
except RuntimeError as exc:
|
||||||
urls=d.get("urls", []),
|
raise RuntimeError(str(exc))
|
||||||
is_retweet=d.get("isRetweet", False),
|
return TwitterClient(cookies["auth_token"], cookies["ct0"])
|
||||||
lang=d.get("lang", ""),
|
|
||||||
retweeted_by=d.get("retweetedBy"),
|
|
||||||
quoted_tweet=quoted_tweet,
|
def _resolve_fetch_count(max_count, configured):
|
||||||
score=d.get("score", 0.0),
|
# type: (Optional[int], int) -> int
|
||||||
))
|
"""Resolve fetch count with bounds checks."""
|
||||||
return tweets
|
if max_count is not None:
|
||||||
|
if max_count <= 0:
|
||||||
|
raise RuntimeError("--max must be greater than 0")
|
||||||
|
return max_count
|
||||||
|
return max(configured, 1)
|
||||||
|
|
||||||
|
|
||||||
|
def _apply_filter(tweets, do_filter, config):
|
||||||
|
# type: (List[Tweet], bool, dict) -> List[Tweet]
|
||||||
|
"""Optionally apply tweet filtering."""
|
||||||
|
if not do_filter:
|
||||||
|
return tweets
|
||||||
|
filter_config = config.get("filter", {})
|
||||||
|
original_count = len(tweets)
|
||||||
|
filtered = filter_tweets(tweets, filter_config)
|
||||||
|
print_filter_stats(original_count, filtered, console)
|
||||||
|
console.print()
|
||||||
|
return filtered
|
||||||
|
|
||||||
|
|
||||||
@click.group()
|
@click.group()
|
||||||
@@ -133,107 +103,88 @@ def cli(verbose):
|
|||||||
_setup_logging(verbose)
|
_setup_logging(verbose)
|
||||||
|
|
||||||
|
|
||||||
# ===== Feed =====
|
|
||||||
|
|
||||||
@cli.command()
|
@cli.command()
|
||||||
|
@click.option(
|
||||||
|
"--type",
|
||||||
|
"-t",
|
||||||
|
"feed_type",
|
||||||
|
type=click.Choice(FEED_TYPES),
|
||||||
|
default="for-you",
|
||||||
|
help="Feed type: for-you (algorithmic) or following (chronological).",
|
||||||
|
)
|
||||||
@click.option("--max", "-n", "max_count", type=int, default=None, help="Max number of tweets to fetch.")
|
@click.option("--max", "-n", "max_count", type=int, default=None, help="Max number of tweets to fetch.")
|
||||||
@click.option("--json", "as_json", is_flag=True, help="Output as JSON.")
|
@click.option("--json", "as_json", is_flag=True, help="Output as JSON.")
|
||||||
@click.option("--input", "-i", "input_file", type=str, default=None, help="Load tweets from JSON file.")
|
@click.option("--input", "-i", "input_file", type=str, default=None, help="Load tweets from JSON file.")
|
||||||
@click.option("--output", "-o", "output_file", type=str, default=None, help="Save filtered tweets to JSON file.")
|
@click.option("--output", "-o", "output_file", type=str, default=None, help="Save filtered tweets to JSON file.")
|
||||||
@click.option("--no-filter", is_flag=True, help="Skip filtering.")
|
@click.option("--filter", "do_filter", is_flag=True, help="Enable score-based filtering.")
|
||||||
def feed(max_count, as_json, input_file, output_file, no_filter):
|
def feed(feed_type, max_count, as_json, input_file, output_file, do_filter):
|
||||||
# type: (int, bool, str, str, bool) -> None
|
# type: (str, Optional[int], bool, Optional[str], Optional[str], bool) -> None
|
||||||
"""Fetch home timeline with filtering."""
|
"""Fetch home timeline with optional filtering."""
|
||||||
config = load_config()
|
config = load_config()
|
||||||
|
try:
|
||||||
|
if input_file:
|
||||||
|
console.print("📂 Loading tweets from %s..." % input_file)
|
||||||
|
tweets = _load_tweets_from_json(input_file)
|
||||||
|
console.print(" Loaded %d tweets" % len(tweets))
|
||||||
|
else:
|
||||||
|
fetch_count = _resolve_fetch_count(max_count, config.get("fetch", {}).get("count", 50))
|
||||||
|
client = _get_client()
|
||||||
|
label = "following feed" if feed_type == "following" else "home timeline"
|
||||||
|
console.print("📡 Fetching %s (%d tweets)...\n" % (label, fetch_count))
|
||||||
|
start = time.time()
|
||||||
|
if feed_type == "following":
|
||||||
|
tweets = client.fetch_following_feed(fetch_count)
|
||||||
|
else:
|
||||||
|
tweets = client.fetch_home_timeline(fetch_count)
|
||||||
|
elapsed = time.time() - start
|
||||||
|
console.print("✅ Fetched %d tweets in %.1fs\n" % (len(tweets), elapsed))
|
||||||
|
except RuntimeError as exc:
|
||||||
|
console.print("[red]❌ %s[/red]" % exc)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
# Step 1: Get tweets
|
filtered = _apply_filter(tweets, do_filter, config)
|
||||||
if input_file:
|
|
||||||
console.print("📂 Loading tweets from %s..." % input_file)
|
|
||||||
tweets = _load_tweets_from_json(input_file)
|
|
||||||
console.print(" Loaded %d tweets" % len(tweets))
|
|
||||||
else:
|
|
||||||
fetch_count = max_count or config.get("fetch", {}).get("count", 50)
|
|
||||||
console.print("\n🔐 Getting Twitter cookies...")
|
|
||||||
try:
|
|
||||||
cookies = get_cookies()
|
|
||||||
except RuntimeError as e:
|
|
||||||
console.print("[red]❌ %s[/red]" % e)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
client = TwitterClient(cookies["auth_token"], cookies["ct0"])
|
|
||||||
console.print("📡 Fetching home timeline (%d tweets)...\n" % fetch_count)
|
|
||||||
start = time.time()
|
|
||||||
tweets = client.fetch_home_timeline(fetch_count)
|
|
||||||
elapsed = time.time() - start
|
|
||||||
console.print("✅ Fetched %d tweets in %.1fs\n" % (len(tweets), elapsed))
|
|
||||||
|
|
||||||
# Step 2: Filter
|
|
||||||
if no_filter:
|
|
||||||
filtered = tweets
|
|
||||||
else:
|
|
||||||
filter_config = config.get("filter", {})
|
|
||||||
original_count = len(tweets)
|
|
||||||
filtered = filter_tweets(tweets, filter_config)
|
|
||||||
print_filter_stats(original_count, filtered, console)
|
|
||||||
console.print()
|
|
||||||
|
|
||||||
# Save filtered tweets
|
|
||||||
if output_file:
|
if output_file:
|
||||||
Path(output_file).write_text(tweets_to_json(filtered), encoding="utf-8")
|
Path(output_file).write_text(tweets_to_json(filtered), encoding="utf-8")
|
||||||
console.print("💾 Saved filtered tweets to %s\n" % output_file)
|
console.print("💾 Saved filtered tweets to %s\n" % output_file)
|
||||||
|
|
||||||
# Output
|
|
||||||
if as_json:
|
if as_json:
|
||||||
click.echo(tweets_to_json(filtered))
|
click.echo(tweets_to_json(filtered))
|
||||||
return
|
return
|
||||||
|
|
||||||
print_tweet_table(filtered, console)
|
title = "👥 Following" if feed_type == "following" else "📱 Twitter"
|
||||||
|
title += " — %d tweets" % len(filtered)
|
||||||
|
print_tweet_table(filtered, console, title=title)
|
||||||
console.print()
|
console.print()
|
||||||
|
|
||||||
|
|
||||||
# ===== Favorite =====
|
|
||||||
|
|
||||||
@cli.command()
|
@cli.command()
|
||||||
@click.option("--max", "-n", "max_count", type=int, default=None, help="Max number of tweets to fetch.")
|
@click.option("--max", "-n", "max_count", type=int, default=None, help="Max number of tweets to fetch.")
|
||||||
@click.option("--json", "as_json", is_flag=True, help="Output as JSON.")
|
@click.option("--json", "as_json", is_flag=True, help="Output as JSON.")
|
||||||
@click.option("--output", "-o", "output_file", type=str, default=None, help="Save tweets to JSON file.")
|
@click.option("--output", "-o", "output_file", type=str, default=None, help="Save tweets to JSON file.")
|
||||||
@click.option("--no-filter", is_flag=True, help="Skip filtering.")
|
@click.option("--filter", "do_filter", is_flag=True, help="Enable score-based filtering.")
|
||||||
def favorite(max_count, as_json, output_file, no_filter):
|
def favorite(max_count, as_json, output_file, do_filter):
|
||||||
# type: (int, bool, str, bool) -> None
|
# type: (Optional[int], bool, Optional[str], bool) -> None
|
||||||
"""Fetch bookmarked (favorite) tweets."""
|
"""Fetch bookmarked (favorite) tweets."""
|
||||||
config = load_config()
|
config = load_config()
|
||||||
fetch_count = max_count or 50
|
|
||||||
|
|
||||||
console.print("\n🔐 Getting Twitter cookies...")
|
|
||||||
try:
|
try:
|
||||||
cookies = get_cookies()
|
fetch_count = _resolve_fetch_count(max_count, config.get("fetch", {}).get("count", 50))
|
||||||
except RuntimeError as e:
|
client = _get_client()
|
||||||
console.print("[red]❌ %s[/red]" % e)
|
console.print("🔖 Fetching favorites (%d tweets)...\n" % fetch_count)
|
||||||
|
start = time.time()
|
||||||
|
tweets = client.fetch_bookmarks(fetch_count)
|
||||||
|
elapsed = time.time() - start
|
||||||
|
console.print("✅ Fetched %d favorites in %.1fs\n" % (len(tweets), elapsed))
|
||||||
|
except RuntimeError as exc:
|
||||||
|
console.print("[red]❌ %s[/red]" % exc)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
client = TwitterClient(cookies["auth_token"], cookies["ct0"])
|
filtered = _apply_filter(tweets, do_filter, config)
|
||||||
console.print("🔖 Fetching favorites (%d tweets)...\n" % fetch_count)
|
|
||||||
start = time.time()
|
|
||||||
tweets = client.fetch_bookmarks(fetch_count)
|
|
||||||
elapsed = time.time() - start
|
|
||||||
console.print("✅ Fetched %d favorites in %.1fs\n" % (len(tweets), elapsed))
|
|
||||||
|
|
||||||
# Filter
|
|
||||||
if no_filter:
|
|
||||||
filtered = tweets
|
|
||||||
else:
|
|
||||||
filter_config = config.get("filter", {})
|
|
||||||
original_count = len(tweets)
|
|
||||||
filtered = filter_tweets(tweets, filter_config)
|
|
||||||
print_filter_stats(original_count, filtered, console)
|
|
||||||
console.print()
|
|
||||||
|
|
||||||
# Save
|
|
||||||
if output_file:
|
if output_file:
|
||||||
Path(output_file).write_text(tweets_to_json(filtered), encoding="utf-8")
|
Path(output_file).write_text(tweets_to_json(filtered), encoding="utf-8")
|
||||||
console.print("💾 Saved to %s\n" % output_file)
|
console.print("💾 Saved to %s\n" % output_file)
|
||||||
|
|
||||||
# Output
|
|
||||||
if as_json:
|
if as_json:
|
||||||
click.echo(tweets_to_json(filtered))
|
click.echo(tweets_to_json(filtered))
|
||||||
return
|
return
|
||||||
@@ -242,24 +193,23 @@ def favorite(max_count, as_json, output_file, no_filter):
|
|||||||
console.print()
|
console.print()
|
||||||
|
|
||||||
|
|
||||||
# ===== User =====
|
|
||||||
|
|
||||||
@cli.command()
|
@cli.command()
|
||||||
@click.argument("screen_name")
|
@click.argument("screen_name")
|
||||||
def user(screen_name):
|
def user(screen_name):
|
||||||
# type: (str,) -> None
|
# type: (str,) -> None
|
||||||
"""View a user's profile. SCREEN_NAME is the @handle (without @)."""
|
"""View a user's profile. SCREEN_NAME is the @handle (without @)."""
|
||||||
screen_name = screen_name.lstrip("@")
|
screen_name = screen_name.lstrip("@")
|
||||||
client = _get_client()
|
|
||||||
console.print("👤 Fetching user @%s..." % screen_name)
|
|
||||||
try:
|
try:
|
||||||
|
client = _get_client()
|
||||||
|
console.print("👤 Fetching user @%s..." % screen_name)
|
||||||
profile = client.fetch_user(screen_name)
|
profile = client.fetch_user(screen_name)
|
||||||
console.print()
|
except RuntimeError as exc:
|
||||||
print_user_profile(profile, console)
|
console.print("[red]❌ %s[/red]" % exc)
|
||||||
except RuntimeError as e:
|
|
||||||
console.print("[red]❌ %s[/red]" % e)
|
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
console.print()
|
||||||
|
print_user_profile(profile, console)
|
||||||
|
|
||||||
|
|
||||||
@cli.command("user-posts")
|
@cli.command("user-posts")
|
||||||
@click.argument("screen_name")
|
@click.argument("screen_name")
|
||||||
@@ -269,24 +219,20 @@ def user_posts(screen_name, max_count, as_json):
|
|||||||
# type: (str, int, bool) -> None
|
# type: (str, int, bool) -> None
|
||||||
"""List a user's tweets. SCREEN_NAME is the @handle (without @)."""
|
"""List a user's tweets. SCREEN_NAME is the @handle (without @)."""
|
||||||
screen_name = screen_name.lstrip("@")
|
screen_name = screen_name.lstrip("@")
|
||||||
client = _get_client()
|
|
||||||
console.print("👤 Fetching @%s's profile..." % screen_name)
|
|
||||||
try:
|
try:
|
||||||
|
fetch_count = _resolve_fetch_count(max_count, 20)
|
||||||
|
client = _get_client()
|
||||||
|
console.print("👤 Fetching @%s's profile..." % screen_name)
|
||||||
profile = client.fetch_user(screen_name)
|
profile = client.fetch_user(screen_name)
|
||||||
except RuntimeError as e:
|
console.print("📝 Fetching tweets (%d)...\n" % fetch_count)
|
||||||
console.print("[red]❌ %s[/red]" % e)
|
start = time.time()
|
||||||
|
tweets = client.fetch_user_tweets(profile.id, fetch_count)
|
||||||
|
elapsed = time.time() - start
|
||||||
|
console.print("✅ Fetched %d tweets in %.1fs\n" % (len(tweets), elapsed))
|
||||||
|
except RuntimeError as exc:
|
||||||
|
console.print("[red]❌ %s[/red]" % exc)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
console.print("📝 Fetching tweets (%d)...\n" % max_count)
|
|
||||||
start = time.time()
|
|
||||||
try:
|
|
||||||
tweets = client.fetch_user_tweets(profile.id, max_count)
|
|
||||||
except RuntimeError as e:
|
|
||||||
console.print("[red]❌ %s[/red]" % e)
|
|
||||||
sys.exit(1)
|
|
||||||
elapsed = time.time() - start
|
|
||||||
console.print("✅ Fetched %d tweets in %.1fs\n" % (len(tweets), elapsed))
|
|
||||||
|
|
||||||
if as_json:
|
if as_json:
|
||||||
click.echo(tweets_to_json(tweets))
|
click.echo(tweets_to_json(tweets))
|
||||||
return
|
return
|
||||||
@@ -295,148 +241,5 @@ def user_posts(screen_name, max_count, as_json):
|
|||||||
console.print()
|
console.print()
|
||||||
|
|
||||||
|
|
||||||
@cli.command()
|
|
||||||
@click.argument("screen_name")
|
|
||||||
@click.option("--max", "-n", "max_count", type=int, default=20, help="Max number of users to show.")
|
|
||||||
def followers(screen_name, max_count):
|
|
||||||
# type: (str, int) -> None
|
|
||||||
"""List a user's followers. SCREEN_NAME is the @handle (without @)."""
|
|
||||||
screen_name = screen_name.lstrip("@")
|
|
||||||
client = _get_client()
|
|
||||||
console.print("👤 Fetching @%s's profile..." % screen_name)
|
|
||||||
try:
|
|
||||||
profile = client.fetch_user(screen_name)
|
|
||||||
except RuntimeError as e:
|
|
||||||
console.print("[red]❌ %s[/red]" % e)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
console.print("👥 Fetching followers...\n")
|
|
||||||
try:
|
|
||||||
users = client.fetch_followers(profile.id, max_count)
|
|
||||||
except RuntimeError as e:
|
|
||||||
console.print("[red]❌ %s[/red]" % e)
|
|
||||||
sys.exit(1)
|
|
||||||
print_user_table(users, console, title="👥 @%s's followers — %d" % (screen_name, len(users)))
|
|
||||||
console.print()
|
|
||||||
|
|
||||||
|
|
||||||
@cli.command()
|
|
||||||
@click.argument("screen_name")
|
|
||||||
@click.option("--max", "-n", "max_count", type=int, default=20, help="Max number of users to show.")
|
|
||||||
def following(screen_name, max_count):
|
|
||||||
# type: (str, int) -> None
|
|
||||||
"""List users that someone follows. SCREEN_NAME is the @handle (without @)."""
|
|
||||||
screen_name = screen_name.lstrip("@")
|
|
||||||
client = _get_client()
|
|
||||||
console.print("👤 Fetching @%s's profile..." % screen_name)
|
|
||||||
try:
|
|
||||||
profile = client.fetch_user(screen_name)
|
|
||||||
except RuntimeError as e:
|
|
||||||
console.print("[red]❌ %s[/red]" % e)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
console.print("👥 Fetching following...\n")
|
|
||||||
try:
|
|
||||||
users = client.fetch_following(profile.id, max_count)
|
|
||||||
except RuntimeError as e:
|
|
||||||
console.print("[red]❌ %s[/red]" % e)
|
|
||||||
sys.exit(1)
|
|
||||||
print_user_table(users, console, title="👥 @%s follows — %d" % (screen_name, len(users)))
|
|
||||||
console.print()
|
|
||||||
|
|
||||||
|
|
||||||
# ===== Post / Reply / Quote / Delete =====
|
|
||||||
|
|
||||||
def _get_client():
|
|
||||||
# type: () -> TwitterClient
|
|
||||||
"""Helper to authenticate and create a TwitterClient."""
|
|
||||||
console.print("\n🔐 Getting Twitter cookies...")
|
|
||||||
try:
|
|
||||||
cookies = get_cookies()
|
|
||||||
except RuntimeError as e:
|
|
||||||
console.print("[red]❌ %s[/red]" % e)
|
|
||||||
sys.exit(1)
|
|
||||||
return TwitterClient(cookies["auth_token"], cookies["ct0"])
|
|
||||||
|
|
||||||
|
|
||||||
@cli.command()
|
|
||||||
@click.argument("text")
|
|
||||||
def post(text):
|
|
||||||
# type: (str,) -> None
|
|
||||||
"""Post a new tweet."""
|
|
||||||
client = _get_client()
|
|
||||||
console.print("✏️ Posting tweet...")
|
|
||||||
try:
|
|
||||||
result = client.create_tweet(text)
|
|
||||||
tweet_id = result["tweet_id"]
|
|
||||||
console.print("\n[green]✅ Tweet posted![/green]")
|
|
||||||
console.print(" ID: %s" % tweet_id)
|
|
||||||
console.print(" URL: https://x.com/i/status/%s" % tweet_id)
|
|
||||||
console.print(' Text: "%s"' % result["text"][:100])
|
|
||||||
except RuntimeError as e:
|
|
||||||
console.print("[red]❌ %s[/red]" % e)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
|
|
||||||
@cli.command()
|
|
||||||
@click.argument("tweet_id")
|
|
||||||
@click.argument("text")
|
|
||||||
def reply(tweet_id, text):
|
|
||||||
# type: (str, str) -> None
|
|
||||||
"""Reply to a tweet."""
|
|
||||||
client = _get_client()
|
|
||||||
console.print("💬 Replying to %s..." % tweet_id)
|
|
||||||
try:
|
|
||||||
result = client.create_tweet(text, reply_to=tweet_id)
|
|
||||||
new_id = result["tweet_id"]
|
|
||||||
console.print("\n[green]✅ Reply posted![/green]")
|
|
||||||
console.print(" ID: %s" % new_id)
|
|
||||||
console.print(" URL: https://x.com/i/status/%s" % new_id)
|
|
||||||
console.print(' Text: "%s"' % result["text"][:100])
|
|
||||||
except RuntimeError as e:
|
|
||||||
console.print("[red]❌ %s[/red]" % e)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
|
|
||||||
@cli.command()
|
|
||||||
@click.argument("tweet_url")
|
|
||||||
@click.argument("text")
|
|
||||||
def quote(tweet_url, text):
|
|
||||||
# type: (str, str) -> None
|
|
||||||
"""Quote a tweet. TWEET_URL can be a full URL or tweet ID."""
|
|
||||||
# If user passes just an ID, convert to URL
|
|
||||||
if not tweet_url.startswith("http"):
|
|
||||||
tweet_url = "https://x.com/i/status/%s" % tweet_url
|
|
||||||
client = _get_client()
|
|
||||||
console.print("🔄 Quoting %s..." % tweet_url)
|
|
||||||
try:
|
|
||||||
result = client.create_tweet(text, quote_tweet_url=tweet_url)
|
|
||||||
new_id = result["tweet_id"]
|
|
||||||
console.print("\n[green]✅ Quote tweet posted![/green]")
|
|
||||||
console.print(" ID: %s" % new_id)
|
|
||||||
console.print(" URL: https://x.com/i/status/%s" % new_id)
|
|
||||||
console.print(' Text: "%s"' % result["text"][:100])
|
|
||||||
except RuntimeError as e:
|
|
||||||
console.print("[red]❌ %s[/red]" % e)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
|
|
||||||
@cli.command()
|
|
||||||
@click.argument("tweet_id")
|
|
||||||
@click.confirmation_option(prompt="Are you sure you want to delete this tweet?")
|
|
||||||
def delete(tweet_id):
|
|
||||||
# type: (str,) -> None
|
|
||||||
"""Delete a tweet by ID."""
|
|
||||||
client = _get_client()
|
|
||||||
console.print("🗑️ Deleting tweet %s..." % tweet_id)
|
|
||||||
try:
|
|
||||||
client.delete_tweet(tweet_id)
|
|
||||||
console.print("\n[green]✅ Tweet deleted![/green]")
|
|
||||||
console.print(" ID: %s" % tweet_id)
|
|
||||||
except RuntimeError as e:
|
|
||||||
console.print("[red]❌ %s[/red]" % e)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
cli()
|
cli()
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,15 +1,16 @@
|
|||||||
"""Configuration loader — reads config.yaml and merges with defaults.
|
"""Configuration loader with YAML parsing and normalization."""
|
||||||
|
|
||||||
Uses a simple built-in YAML parser to avoid adding PyYAML as a dependency.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import re
|
import copy
|
||||||
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Dict, List, Union
|
|
||||||
|
|
||||||
# Default configuration
|
import yaml
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_CONFIG = {
|
DEFAULT_CONFIG = {
|
||||||
"fetch": {
|
"fetch": {
|
||||||
"count": 50,
|
"count": 50,
|
||||||
@@ -31,131 +32,118 @@ DEFAULT_CONFIG = {
|
|||||||
} # type: Dict[str, Any]
|
} # type: Dict[str, Any]
|
||||||
|
|
||||||
|
|
||||||
def _parse_value(s):
|
def load_config(config_path=None):
|
||||||
# type: (str) -> Union[str, int, float, bool]
|
# type: (Optional[str]) -> Dict[str, Any]
|
||||||
"""Parse a scalar YAML value."""
|
"""Load and normalize config from YAML, merged with defaults."""
|
||||||
if s == "true":
|
config = copy.deepcopy(DEFAULT_CONFIG)
|
||||||
return True
|
path = _resolve_config_path(config_path)
|
||||||
if s == "false":
|
if not path:
|
||||||
return False
|
return config
|
||||||
# Remove surrounding quotes
|
|
||||||
if (s.startswith('"') and s.endswith('"')) or (s.startswith("'") and s.endswith("'")):
|
|
||||||
return s[1:-1]
|
|
||||||
# Try number
|
|
||||||
try:
|
try:
|
||||||
if "." in s:
|
raw = path.read_text(encoding="utf-8")
|
||||||
return float(s)
|
except OSError as exc:
|
||||||
return int(s)
|
logger.warning("Failed to read config file %s: %s", path, exc)
|
||||||
except ValueError:
|
return config
|
||||||
return s
|
|
||||||
|
try:
|
||||||
|
parsed = yaml.safe_load(raw) or {}
|
||||||
|
except yaml.YAMLError as exc:
|
||||||
|
logger.warning("Failed to parse YAML config %s: %s", path, exc)
|
||||||
|
return config
|
||||||
|
|
||||||
|
if not isinstance(parsed, dict):
|
||||||
|
logger.warning("Config root must be a mapping, got %s", type(parsed).__name__)
|
||||||
|
return config
|
||||||
|
|
||||||
|
merged = _deep_merge(config, parsed)
|
||||||
|
return _normalize_config(merged)
|
||||||
|
|
||||||
|
|
||||||
def _parse_yaml(text):
|
def _resolve_config_path(config_path):
|
||||||
# type: (str) -> Dict[str, Any]
|
# type: (Optional[str]) -> Optional[Path]
|
||||||
"""Minimal YAML parser for our flat config structure.
|
"""Find config path from explicit argument or default locations."""
|
||||||
|
if config_path:
|
||||||
|
path = Path(config_path)
|
||||||
|
return path if path.exists() else None
|
||||||
|
|
||||||
Supports: scalars, inline arrays [...], indented "- item" arrays,
|
candidates = [
|
||||||
nested objects via indentation.
|
Path.cwd() / "config.yaml",
|
||||||
"""
|
Path(__file__).parent.parent / "config.yaml",
|
||||||
result = {} # type: Dict[str, Any]
|
]
|
||||||
lines = text.split("\n")
|
for candidate in candidates:
|
||||||
stack = [{"indent": -1, "obj": result}] # type: List[Dict[str, Any]]
|
if candidate.exists():
|
||||||
|
return candidate
|
||||||
for line in lines:
|
return None
|
||||||
# Strip comments and trailing whitespace
|
|
||||||
trimmed = re.sub(r"#.*$", "", line).rstrip()
|
|
||||||
if not trimmed or not trimmed.strip():
|
|
||||||
continue
|
|
||||||
|
|
||||||
indent = len(line) - len(line.lstrip())
|
|
||||||
content = trimmed.strip()
|
|
||||||
|
|
||||||
# Handle "- item" array entries
|
|
||||||
if content.startswith("- "):
|
|
||||||
parent = stack[-1]["obj"]
|
|
||||||
keys = list(parent.keys())
|
|
||||||
if keys:
|
|
||||||
last_key = keys[-1]
|
|
||||||
if not isinstance(parent[last_key], list):
|
|
||||||
parent[last_key] = []
|
|
||||||
parent[last_key].append(_parse_value(content[2:].strip()))
|
|
||||||
continue
|
|
||||||
|
|
||||||
colon_idx = content.find(":")
|
|
||||||
if colon_idx == -1:
|
|
||||||
continue
|
|
||||||
|
|
||||||
key = content[:colon_idx].strip()
|
|
||||||
raw_value = content[colon_idx + 1:].strip()
|
|
||||||
|
|
||||||
# Pop stack to find parent at correct indentation
|
|
||||||
while len(stack) > 1 and stack[-1]["indent"] >= indent:
|
|
||||||
stack.pop()
|
|
||||||
parent = stack[-1]["obj"]
|
|
||||||
|
|
||||||
if raw_value == "" or raw_value == "|":
|
|
||||||
# Nested object
|
|
||||||
child = {} # type: Dict[str, Any]
|
|
||||||
parent[key] = child
|
|
||||||
stack.append({"indent": indent, "obj": child})
|
|
||||||
elif raw_value.startswith("[") and raw_value.endswith("]"):
|
|
||||||
# Inline array
|
|
||||||
inner = raw_value[1:-1].strip()
|
|
||||||
if inner == "":
|
|
||||||
parent[key] = []
|
|
||||||
else:
|
|
||||||
parent[key] = [_parse_value(s.strip()) for s in inner.split(",")]
|
|
||||||
else:
|
|
||||||
parent[key] = _parse_value(raw_value)
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
def _deep_merge(target, source):
|
def _deep_merge(target, source):
|
||||||
# type: (Dict[str, Any], Dict[str, Any]) -> Dict[str, Any]
|
# type: (Dict[str, Any], Mapping[str, Any]) -> Dict[str, Any]
|
||||||
"""Deep merge source into target (source values override target)."""
|
"""Deep merge source into target (source values override target)."""
|
||||||
result = dict(target)
|
result = copy.deepcopy(target)
|
||||||
for key in source:
|
for key, value in source.items():
|
||||||
if (
|
if isinstance(value, dict) and isinstance(result.get(key), dict):
|
||||||
isinstance(source[key], dict)
|
result[key] = _deep_merge(result[key], value)
|
||||||
and isinstance(result.get(key), dict)
|
|
||||||
):
|
|
||||||
result[key] = _deep_merge(result[key], source[key])
|
|
||||||
else:
|
else:
|
||||||
result[key] = source[key]
|
result[key] = copy.deepcopy(value)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def load_config(config_path=None):
|
def _normalize_config(config):
|
||||||
# type: (str) -> Dict[str, Any]
|
# type: (Dict[str, Any]) -> Dict[str, Any]
|
||||||
"""Load config from config.yaml, merged with defaults."""
|
"""Normalize shape and value types."""
|
||||||
if config_path is None:
|
normalized = copy.deepcopy(DEFAULT_CONFIG)
|
||||||
# Look in current directory first, then script directory
|
merged = _deep_merge(normalized, config)
|
||||||
candidates = [
|
|
||||||
Path.cwd() / "config.yaml",
|
|
||||||
Path(__file__).parent.parent / "config.yaml",
|
|
||||||
]
|
|
||||||
for p in candidates:
|
|
||||||
if p.exists():
|
|
||||||
config_path = str(p)
|
|
||||||
break
|
|
||||||
|
|
||||||
if config_path and Path(config_path).exists():
|
fetch = merged.get("fetch")
|
||||||
try:
|
if not isinstance(fetch, dict):
|
||||||
raw = Path(config_path).read_text(encoding="utf-8")
|
fetch = {}
|
||||||
parsed = _parse_yaml(raw)
|
fetch_count = _as_int(fetch.get("count"), DEFAULT_CONFIG["fetch"]["count"])
|
||||||
config = _deep_merge(DEFAULT_CONFIG, parsed)
|
fetch["count"] = max(fetch_count, 1)
|
||||||
except Exception:
|
merged["fetch"] = fetch
|
||||||
config = dict(DEFAULT_CONFIG)
|
|
||||||
else:
|
|
||||||
config = dict(DEFAULT_CONFIG)
|
|
||||||
|
|
||||||
# Ensure nested dicts exist
|
filter_config = merged.get("filter")
|
||||||
config.setdefault("fetch", DEFAULT_CONFIG["fetch"])
|
if not isinstance(filter_config, dict):
|
||||||
config.setdefault("filter", DEFAULT_CONFIG["filter"])
|
filter_config = {}
|
||||||
|
mode = str(filter_config.get("mode", "topN"))
|
||||||
|
if mode not in {"topN", "score", "all"}:
|
||||||
|
mode = "topN"
|
||||||
|
filter_config["mode"] = mode
|
||||||
|
filter_config["topN"] = max(_as_int(filter_config.get("topN"), 20), 1)
|
||||||
|
filter_config["minScore"] = _as_float(filter_config.get("minScore"), 50.0)
|
||||||
|
filter_config["excludeRetweets"] = bool(filter_config.get("excludeRetweets", False))
|
||||||
|
|
||||||
# Deep-copy filter weights if needed
|
langs = filter_config.get("lang", [])
|
||||||
if "filter" in config and "weights" not in config["filter"]:
|
if not isinstance(langs, list):
|
||||||
config["filter"]["weights"] = dict(DEFAULT_CONFIG["filter"]["weights"])
|
langs = []
|
||||||
|
filter_config["lang"] = [str(lang) for lang in langs if str(lang)]
|
||||||
|
|
||||||
return config
|
weights = filter_config.get("weights", {})
|
||||||
|
if not isinstance(weights, dict):
|
||||||
|
weights = {}
|
||||||
|
normalized_weights = {}
|
||||||
|
default_weights = DEFAULT_CONFIG["filter"]["weights"]
|
||||||
|
for key, default_value in default_weights.items():
|
||||||
|
normalized_weights[key] = _as_float(weights.get(key), float(default_value))
|
||||||
|
filter_config["weights"] = normalized_weights
|
||||||
|
merged["filter"] = filter_config
|
||||||
|
|
||||||
|
return merged
|
||||||
|
|
||||||
|
|
||||||
|
def _as_int(value, default):
|
||||||
|
# type: (Any, int) -> int
|
||||||
|
"""Best-effort int conversion."""
|
||||||
|
try:
|
||||||
|
return int(value)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return default
|
||||||
|
|
||||||
|
|
||||||
|
def _as_float(value, default):
|
||||||
|
# type: (Any, float) -> float
|
||||||
|
"""Best-effort float conversion."""
|
||||||
|
try:
|
||||||
|
return float(value)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return default
|
||||||
|
|||||||
@@ -6,14 +6,14 @@ configurable rules (topN, min score, language, etc.).
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import replace
|
||||||
import math
|
import math
|
||||||
from typing import Dict, List
|
from typing import Mapping
|
||||||
|
|
||||||
from .models import Tweet
|
|
||||||
|
|
||||||
|
|
||||||
# Type alias for filter weights dict
|
# Type alias for filter weights dict
|
||||||
FilterWeights = Dict[str, float]
|
FilterWeights = Mapping[str, float]
|
||||||
|
|
||||||
DEFAULT_WEIGHTS = {
|
DEFAULT_WEIGHTS = {
|
||||||
"likes": 1.0,
|
"likes": 1.0,
|
||||||
@@ -25,7 +25,7 @@ DEFAULT_WEIGHTS = {
|
|||||||
|
|
||||||
|
|
||||||
def score_tweet(tweet, weights=None):
|
def score_tweet(tweet, weights=None):
|
||||||
# type: (Tweet, FilterWeights) -> float
|
# type: (Tweet, Optional[FilterWeights]) -> float
|
||||||
"""Calculate engagement score for a single tweet.
|
"""Calculate engagement score for a single tweet.
|
||||||
|
|
||||||
Formula:
|
Formula:
|
||||||
@@ -35,20 +35,19 @@ def score_tweet(tweet, weights=None):
|
|||||||
+ w_bookmarks × bookmarks
|
+ w_bookmarks × bookmarks
|
||||||
+ w_views_log × log10(views)
|
+ w_views_log × log10(views)
|
||||||
"""
|
"""
|
||||||
if weights is None:
|
weight_map = _build_weights(weights or {})
|
||||||
weights = DEFAULT_WEIGHTS
|
|
||||||
m = tweet.metrics
|
m = tweet.metrics
|
||||||
return (
|
return (
|
||||||
weights.get("likes", 1.0) * m.likes
|
weight_map["likes"] * m.likes
|
||||||
+ weights.get("retweets", 3.0) * m.retweets
|
+ weight_map["retweets"] * m.retweets
|
||||||
+ weights.get("replies", 2.0) * m.replies
|
+ weight_map["replies"] * m.replies
|
||||||
+ weights.get("bookmarks", 5.0) * m.bookmarks
|
+ weight_map["bookmarks"] * m.bookmarks
|
||||||
+ weights.get("views_log", 0.5) * math.log10(max(m.views, 1))
|
+ weight_map["views_log"] * math.log10(max(m.views, 1))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def filter_tweets(tweets, config):
|
def filter_tweets(tweets, config):
|
||||||
# type: (List[Tweet], dict) -> List[Tweet]
|
# type: (Sequence[Tweet], Mapping[str, Any]) -> List[Tweet]
|
||||||
"""Filter and rank tweets according to config.
|
"""Filter and rank tweets according to config.
|
||||||
|
|
||||||
Config keys:
|
Config keys:
|
||||||
@@ -64,27 +63,53 @@ def filter_tweets(tweets, config):
|
|||||||
# 1. Language filter
|
# 1. Language filter
|
||||||
lang_filter = config.get("lang", [])
|
lang_filter = config.get("lang", [])
|
||||||
if lang_filter:
|
if lang_filter:
|
||||||
filtered = [t for t in filtered if t.lang in lang_filter]
|
lang_set = {str(lang) for lang in lang_filter if str(lang)}
|
||||||
|
filtered = [tweet for tweet in filtered if tweet.lang in lang_set]
|
||||||
|
|
||||||
# 2. Exclude retweets
|
# 2. Exclude retweets
|
||||||
if config.get("excludeRetweets", False):
|
if config.get("excludeRetweets", False):
|
||||||
filtered = [t for t in filtered if not t.is_retweet]
|
filtered = [tweet for tweet in filtered if not tweet.is_retweet]
|
||||||
|
|
||||||
# 3. Score all tweets
|
# 3. Score all tweets
|
||||||
weights = config.get("weights", DEFAULT_WEIGHTS)
|
weights = _build_weights(config.get("weights", {}))
|
||||||
for t in filtered:
|
scored = [replace(tweet, score=round(score_tweet(tweet, weights), 1)) for tweet in filtered]
|
||||||
t.score = round(score_tweet(t, weights), 1)
|
|
||||||
|
|
||||||
# 4. Sort by score (descending)
|
# 4. Sort by score (descending)
|
||||||
filtered.sort(key=lambda t: t.score, reverse=True)
|
scored.sort(key=lambda tweet: tweet.score, reverse=True)
|
||||||
|
|
||||||
# 5. Apply filter mode
|
# 5. Apply filter mode
|
||||||
mode = config.get("mode", "topN")
|
mode = str(config.get("mode", "topN"))
|
||||||
if mode == "topN":
|
if mode == "topN":
|
||||||
top_n = config.get("topN", 20)
|
top_n = max(_as_int(config.get("topN"), 20), 1)
|
||||||
return filtered[:top_n]
|
return scored[:top_n]
|
||||||
elif mode == "score":
|
if mode == "score":
|
||||||
min_score = config.get("minScore", 50)
|
min_score = _as_float(config.get("minScore"), 50.0)
|
||||||
return [t for t in filtered if t.score >= min_score]
|
return [tweet for tweet in scored if tweet.score >= min_score]
|
||||||
else:
|
return scored
|
||||||
return filtered
|
|
||||||
|
|
||||||
|
def _build_weights(raw_weights):
|
||||||
|
# type: (Mapping[str, Any]) -> Dict[str, float]
|
||||||
|
"""Merge custom weights with defaults and coerce to float."""
|
||||||
|
merged = {}
|
||||||
|
for key, default_value in DEFAULT_WEIGHTS.items():
|
||||||
|
merged[key] = _as_float(raw_weights.get(key), default_value)
|
||||||
|
return merged
|
||||||
|
|
||||||
|
|
||||||
|
def _as_int(value, default):
|
||||||
|
# type: (Any, int) -> int
|
||||||
|
"""Best-effort int conversion."""
|
||||||
|
try:
|
||||||
|
return int(value)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return default
|
||||||
|
|
||||||
|
|
||||||
|
def _as_float(value, default):
|
||||||
|
# type: (Any, float) -> float
|
||||||
|
"""Best-effort float conversion."""
|
||||||
|
try:
|
||||||
|
return float(value)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return default
|
||||||
|
|||||||
@@ -2,15 +2,12 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
|
||||||
from typing import List, Optional
|
|
||||||
|
|
||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
from rich.panel import Panel
|
from rich.panel import Panel
|
||||||
from rich.table import Table
|
from rich.table import Table
|
||||||
from rich.text import Text
|
|
||||||
|
|
||||||
from .models import Tweet, UserProfile
|
from .serialization import tweets_to_json as _tweets_to_json
|
||||||
|
|
||||||
|
|
||||||
def format_number(n):
|
def format_number(n):
|
||||||
@@ -168,46 +165,7 @@ def print_filter_stats(original_count, filtered, console=None):
|
|||||||
def tweets_to_json(tweets):
|
def tweets_to_json(tweets):
|
||||||
# type: (List[Tweet]) -> str
|
# type: (List[Tweet]) -> str
|
||||||
"""Export tweets as JSON string."""
|
"""Export tweets as JSON string."""
|
||||||
result = []
|
return _tweets_to_json(tweets)
|
||||||
for t in tweets:
|
|
||||||
d = {
|
|
||||||
"id": t.id,
|
|
||||||
"text": t.text,
|
|
||||||
"author": {
|
|
||||||
"id": t.author.id,
|
|
||||||
"name": t.author.name,
|
|
||||||
"screenName": t.author.screen_name,
|
|
||||||
"profileImageUrl": t.author.profile_image_url,
|
|
||||||
"verified": t.author.verified,
|
|
||||||
},
|
|
||||||
"metrics": {
|
|
||||||
"likes": t.metrics.likes,
|
|
||||||
"retweets": t.metrics.retweets,
|
|
||||||
"replies": t.metrics.replies,
|
|
||||||
"quotes": t.metrics.quotes,
|
|
||||||
"views": t.metrics.views,
|
|
||||||
"bookmarks": t.metrics.bookmarks,
|
|
||||||
},
|
|
||||||
"createdAt": t.created_at,
|
|
||||||
"media": [
|
|
||||||
{"type": m.type, "url": m.url, "width": m.width, "height": m.height}
|
|
||||||
for m in t.media
|
|
||||||
],
|
|
||||||
"urls": t.urls,
|
|
||||||
"isRetweet": t.is_retweet,
|
|
||||||
"retweetedBy": t.retweeted_by,
|
|
||||||
"lang": t.lang,
|
|
||||||
"score": t.score,
|
|
||||||
}
|
|
||||||
if t.quoted_tweet:
|
|
||||||
qt = t.quoted_tweet
|
|
||||||
d["quotedTweet"] = {
|
|
||||||
"id": qt.id,
|
|
||||||
"text": qt.text,
|
|
||||||
"author": {"screenName": qt.author.screen_name, "name": qt.author.name},
|
|
||||||
}
|
|
||||||
result.append(d)
|
|
||||||
return json.dumps(result, ensure_ascii=False, indent=2)
|
|
||||||
|
|
||||||
|
|
||||||
def print_user_profile(user, console=None):
|
def print_user_profile(user, console=None):
|
||||||
|
|||||||
147
twitter_cli/serialization.py
Normal file
147
twitter_cli/serialization.py
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
"""Serialization helpers for Tweet models."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
from typing import Any, Dict, Iterable, List, Optional
|
||||||
|
|
||||||
|
from .models import Author, Metrics, Tweet, TweetMedia
|
||||||
|
|
||||||
|
|
||||||
|
def tweet_to_dict(tweet: Tweet) -> Dict[str, Any]:
|
||||||
|
"""Convert a Tweet dataclass into a JSON-safe dict."""
|
||||||
|
data = {
|
||||||
|
"id": tweet.id,
|
||||||
|
"text": tweet.text,
|
||||||
|
"author": {
|
||||||
|
"id": tweet.author.id,
|
||||||
|
"name": tweet.author.name,
|
||||||
|
"screenName": tweet.author.screen_name,
|
||||||
|
"profileImageUrl": tweet.author.profile_image_url,
|
||||||
|
"verified": tweet.author.verified,
|
||||||
|
},
|
||||||
|
"metrics": {
|
||||||
|
"likes": tweet.metrics.likes,
|
||||||
|
"retweets": tweet.metrics.retweets,
|
||||||
|
"replies": tweet.metrics.replies,
|
||||||
|
"quotes": tweet.metrics.quotes,
|
||||||
|
"views": tweet.metrics.views,
|
||||||
|
"bookmarks": tweet.metrics.bookmarks,
|
||||||
|
},
|
||||||
|
"createdAt": tweet.created_at,
|
||||||
|
"media": [
|
||||||
|
{
|
||||||
|
"type": media.type,
|
||||||
|
"url": media.url,
|
||||||
|
"width": media.width,
|
||||||
|
"height": media.height,
|
||||||
|
}
|
||||||
|
for media in tweet.media
|
||||||
|
],
|
||||||
|
"urls": list(tweet.urls),
|
||||||
|
"isRetweet": tweet.is_retweet,
|
||||||
|
"retweetedBy": tweet.retweeted_by,
|
||||||
|
"lang": tweet.lang,
|
||||||
|
"score": tweet.score,
|
||||||
|
}
|
||||||
|
if tweet.quoted_tweet:
|
||||||
|
data["quotedTweet"] = {
|
||||||
|
"id": tweet.quoted_tweet.id,
|
||||||
|
"text": tweet.quoted_tweet.text,
|
||||||
|
"author": {
|
||||||
|
"screenName": tweet.quoted_tweet.author.screen_name,
|
||||||
|
"name": tweet.quoted_tweet.author.name,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def tweet_from_dict(data: Dict[str, Any]) -> Tweet:
|
||||||
|
"""Convert a dict into a Tweet dataclass."""
|
||||||
|
author_data = data.get("author") or {}
|
||||||
|
metrics_data = data.get("metrics") or {}
|
||||||
|
media_data = data.get("media") or []
|
||||||
|
quoted_data = data.get("quotedTweet")
|
||||||
|
|
||||||
|
quoted_tweet = None # type: Optional[Tweet]
|
||||||
|
if isinstance(quoted_data, dict):
|
||||||
|
quoted_author = quoted_data.get("author") or {}
|
||||||
|
quoted_tweet = Tweet(
|
||||||
|
id=str(quoted_data.get("id") or ""),
|
||||||
|
text=str(quoted_data.get("text") or ""),
|
||||||
|
author=Author(
|
||||||
|
id="",
|
||||||
|
name=str(quoted_author.get("name") or ""),
|
||||||
|
screen_name=str(quoted_author.get("screenName") or ""),
|
||||||
|
),
|
||||||
|
metrics=Metrics(),
|
||||||
|
created_at="",
|
||||||
|
)
|
||||||
|
|
||||||
|
return Tweet(
|
||||||
|
id=str(data.get("id") or ""),
|
||||||
|
text=str(data.get("text") or ""),
|
||||||
|
author=Author(
|
||||||
|
id=str(author_data.get("id") or ""),
|
||||||
|
name=str(author_data.get("name") or ""),
|
||||||
|
screen_name=str(author_data.get("screenName") or ""),
|
||||||
|
profile_image_url=str(author_data.get("profileImageUrl") or ""),
|
||||||
|
verified=bool(author_data.get("verified", False)),
|
||||||
|
),
|
||||||
|
metrics=Metrics(
|
||||||
|
likes=int(metrics_data.get("likes") or 0),
|
||||||
|
retweets=int(metrics_data.get("retweets") or 0),
|
||||||
|
replies=int(metrics_data.get("replies") or 0),
|
||||||
|
quotes=int(metrics_data.get("quotes") or 0),
|
||||||
|
views=int(metrics_data.get("views") or 0),
|
||||||
|
bookmarks=int(metrics_data.get("bookmarks") or 0),
|
||||||
|
),
|
||||||
|
created_at=str(data.get("createdAt") or ""),
|
||||||
|
media=[
|
||||||
|
TweetMedia(
|
||||||
|
type=str(item.get("type") or ""),
|
||||||
|
url=str(item.get("url") or ""),
|
||||||
|
width=_optional_int(item.get("width")),
|
||||||
|
height=_optional_int(item.get("height")),
|
||||||
|
)
|
||||||
|
for item in media_data
|
||||||
|
if isinstance(item, dict)
|
||||||
|
],
|
||||||
|
urls=[str(url) for url in (data.get("urls") or [])],
|
||||||
|
is_retweet=bool(data.get("isRetweet", False)),
|
||||||
|
lang=str(data.get("lang") or ""),
|
||||||
|
retweeted_by=_optional_str(data.get("retweetedBy")),
|
||||||
|
quoted_tweet=quoted_tweet,
|
||||||
|
score=float(data.get("score") or 0.0),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def tweets_from_json(raw: str) -> List[Tweet]:
|
||||||
|
"""Parse a JSON string into Tweet objects."""
|
||||||
|
payload = json.loads(raw)
|
||||||
|
if not isinstance(payload, list):
|
||||||
|
raise ValueError("Tweet JSON payload must be a list")
|
||||||
|
return [tweet_from_dict(item) for item in payload if isinstance(item, dict)]
|
||||||
|
|
||||||
|
|
||||||
|
def tweets_to_json(tweets: Iterable[Tweet]) -> str:
|
||||||
|
"""Serialize Tweet objects to pretty JSON."""
|
||||||
|
return json.dumps([tweet_to_dict(tweet) for tweet in tweets], ensure_ascii=False, indent=2)
|
||||||
|
|
||||||
|
|
||||||
|
def _optional_int(value: Any) -> Optional[int]:
|
||||||
|
"""Parse an optional integer value."""
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return int(value)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _optional_str(value: Any) -> Optional[str]:
|
||||||
|
"""Parse an optional string value."""
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
text = str(value)
|
||||||
|
return text if text else None
|
||||||
331
uv.lock
generated
331
uv.lock
generated
@@ -63,6 +63,44 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "exceptiongroup"
|
||||||
|
version = "1.3.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
|
||||||
|
{ name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9' and python_full_version < '3.13'" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iniconfig"
|
||||||
|
version = "2.1.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
resolution-markers = [
|
||||||
|
"python_full_version == '3.9.*'",
|
||||||
|
"python_full_version < '3.9'",
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iniconfig"
|
||||||
|
version = "2.3.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
resolution-markers = [
|
||||||
|
"python_full_version >= '3.10'",
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "jeepney"
|
name = "jeepney"
|
||||||
version = "0.9.0"
|
version = "0.9.0"
|
||||||
@@ -226,6 +264,40 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
|
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "packaging"
|
||||||
|
version = "26.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pluggy"
|
||||||
|
version = "1.5.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
resolution-markers = [
|
||||||
|
"python_full_version < '3.9'",
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955, upload-time = "2024-04-20T21:34:42.531Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556, upload-time = "2024-04-20T21:34:40.434Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pluggy"
|
||||||
|
version = "1.6.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
resolution-markers = [
|
||||||
|
"python_full_version >= '3.10'",
|
||||||
|
"python_full_version == '3.9.*'",
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pycryptodomex"
|
name = "pycryptodomex"
|
||||||
version = "3.23.0"
|
version = "3.23.0"
|
||||||
@@ -275,6 +347,68 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
|
{ url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pytest"
|
||||||
|
version = "8.3.5"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
resolution-markers = [
|
||||||
|
"python_full_version < '3.9'",
|
||||||
|
]
|
||||||
|
dependencies = [
|
||||||
|
{ name = "colorama", marker = "python_full_version < '3.9' and sys_platform == 'win32'" },
|
||||||
|
{ name = "exceptiongroup", marker = "python_full_version < '3.9'" },
|
||||||
|
{ name = "iniconfig", version = "2.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
|
||||||
|
{ name = "packaging", marker = "python_full_version < '3.9'" },
|
||||||
|
{ name = "pluggy", version = "1.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
|
||||||
|
{ name = "tomli", marker = "python_full_version < '3.9'" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891, upload-time = "2025-03-02T12:54:54.503Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634, upload-time = "2025-03-02T12:54:52.069Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pytest"
|
||||||
|
version = "8.4.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
resolution-markers = [
|
||||||
|
"python_full_version == '3.9.*'",
|
||||||
|
]
|
||||||
|
dependencies = [
|
||||||
|
{ name = "colorama", marker = "python_full_version == '3.9.*' and sys_platform == 'win32'" },
|
||||||
|
{ name = "exceptiongroup", marker = "python_full_version == '3.9.*'" },
|
||||||
|
{ name = "iniconfig", version = "2.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" },
|
||||||
|
{ name = "packaging", marker = "python_full_version == '3.9.*'" },
|
||||||
|
{ name = "pluggy", version = "1.6.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" },
|
||||||
|
{ name = "pygments", marker = "python_full_version == '3.9.*'" },
|
||||||
|
{ name = "tomli", marker = "python_full_version == '3.9.*'" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pytest"
|
||||||
|
version = "9.0.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
resolution-markers = [
|
||||||
|
"python_full_version >= '3.10'",
|
||||||
|
]
|
||||||
|
dependencies = [
|
||||||
|
{ name = "colorama", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" },
|
||||||
|
{ name = "exceptiongroup", marker = "python_full_version == '3.10.*'" },
|
||||||
|
{ name = "iniconfig", version = "2.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
|
||||||
|
{ name = "packaging", marker = "python_full_version >= '3.10'" },
|
||||||
|
{ name = "pluggy", version = "1.6.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
|
||||||
|
{ name = "pygments", marker = "python_full_version >= '3.10'" },
|
||||||
|
{ name = "tomli", marker = "python_full_version == '3.10.*'" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pywin32"
|
name = "pywin32"
|
||||||
version = "311"
|
version = "311"
|
||||||
@@ -302,6 +436,86 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/60/22/e0e8d802f124772cec9c75430b01a212f86f9de7546bda715e54140d5aeb/pywin32-311-cp39-cp39-win_arm64.whl", hash = "sha256:62ea666235135fee79bb154e695f3ff67370afefd71bd7fea7512fc70ef31e3d", size = 8778162, upload-time = "2025-07-14T20:13:03.544Z" },
|
{ url = "https://files.pythonhosted.org/packages/60/22/e0e8d802f124772cec9c75430b01a212f86f9de7546bda715e54140d5aeb/pywin32-311-cp39-cp39-win_arm64.whl", hash = "sha256:62ea666235135fee79bb154e695f3ff67370afefd71bd7fea7512fc70ef31e3d", size = 8778162, upload-time = "2025-07-14T20:13:03.544Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pyyaml"
|
||||||
|
version = "6.0.3"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0d/a2/09f67a3589cb4320fb5ce90d3fd4c9752636b8b6ad8f34b54d76c5a54693/PyYAML-6.0.3-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f", size = 186824, upload-time = "2025-09-29T20:27:35.918Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/02/72/d972384252432d57f248767556ac083793292a4adf4e2d85dfe785ec2659/PyYAML-6.0.3-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4", size = 795069, upload-time = "2025-09-29T20:27:38.15Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a7/3b/6c58ac0fa7c4e1b35e48024eb03d00817438310447f93ef4431673c24138/PyYAML-6.0.3-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:efd7b85f94a6f21e4932043973a7ba2613b059c4a000551892ac9f1d11f5baf3", size = 862585, upload-time = "2025-09-29T20:27:39.715Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/25/a2/b725b61ac76a75583ae7104b3209f75ea44b13cfd026aa535ece22b7f22e/PyYAML-6.0.3-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22ba7cfcad58ef3ecddc7ed1db3409af68d023b7f940da23c6c2a1890976eda6", size = 806018, upload-time = "2025-09-29T20:27:41.444Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6f/b0/b2227677b2d1036d84f5ee95eb948e7af53d59fe3e4328784e4d290607e0/PyYAML-6.0.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:6344df0d5755a2c9a276d4473ae6b90647e216ab4757f8426893b5dd2ac3f369", size = 802822, upload-time = "2025-09-29T20:27:42.885Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/99/a5/718a8ea22521e06ef19f91945766a892c5ceb1855df6adbde67d997ea7ed/PyYAML-6.0.3-cp38-cp38-win32.whl", hash = "sha256:3ff07ec89bae51176c0549bc4c63aa6202991da2d9a6129d7aef7f1407d3f295", size = 143744, upload-time = "2025-09-29T20:27:44.487Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/76/b2/2b69cee94c9eb215216fc05778675c393e3aa541131dc910df8e52c83776/PyYAML-6.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:5cf4e27da7e3fbed4d6c3d8e797387aaad68102272f8f9752883bc32d61cb87b", size = 160082, upload-time = "2025-09-29T20:27:46.049Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9f/62/67fc8e68a75f738c9200422bf65693fb79a4cd0dc5b23310e5202e978090/pyyaml-6.0.3-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da", size = 184450, upload-time = "2025-09-25T21:33:00.618Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ae/92/861f152ce87c452b11b9d0977952259aa7df792d71c1053365cc7b09cc08/pyyaml-6.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917", size = 174319, upload-time = "2025-09-25T21:33:02.086Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d0/cd/f0cfc8c74f8a030017a2b9c771b7f47e5dd702c3e28e5b2071374bda2948/pyyaml-6.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9", size = 737631, upload-time = "2025-09-25T21:33:03.25Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ef/b2/18f2bd28cd2055a79a46c9b0895c0b3d987ce40ee471cecf58a1a0199805/pyyaml-6.0.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5", size = 836795, upload-time = "2025-09-25T21:33:05.014Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/73/b9/793686b2d54b531203c160ef12bec60228a0109c79bae6c1277961026770/pyyaml-6.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a", size = 750767, upload-time = "2025-09-25T21:33:06.398Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a9/86/a137b39a611def2ed78b0e66ce2fe13ee701a07c07aebe55c340ed2a050e/pyyaml-6.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926", size = 727982, upload-time = "2025-09-25T21:33:08.708Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/dd/62/71c27c94f457cf4418ef8ccc71735324c549f7e3ea9d34aba50874563561/pyyaml-6.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7", size = 755677, upload-time = "2025-09-25T21:33:09.876Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/29/3d/6f5e0d58bd924fb0d06c3a6bad00effbdae2de5adb5cda5648006ffbd8d3/pyyaml-6.0.3-cp39-cp39-win32.whl", hash = "sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0", size = 142592, upload-time = "2025-09-25T21:33:10.983Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f0/0c/25113e0b5e103d7f1490c0e947e303fe4a696c10b501dea7a9f49d4e876c/pyyaml-6.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007", size = 158777, upload-time = "2025-09-25T21:33:15.55Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rich"
|
name = "rich"
|
||||||
version = "14.3.3"
|
version = "14.3.3"
|
||||||
@@ -316,6 +530,31 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/14/25/b208c5683343959b670dc001595f2f3737e051da617f66c31f7c4fa93abc/rich-14.3.3-py3-none-any.whl", hash = "sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d", size = 310458, upload-time = "2026-02-19T17:23:13.732Z" },
|
{ url = "https://files.pythonhosted.org/packages/14/25/b208c5683343959b670dc001595f2f3737e051da617f66c31f7c4fa93abc/rich-14.3.3-py3-none-any.whl", hash = "sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d", size = 310458, upload-time = "2026-02-19T17:23:13.732Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ruff"
|
||||||
|
version = "0.15.4"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/da/31/d6e536cdebb6568ae75a7f00e4b4819ae0ad2640c3604c305a0428680b0c/ruff-0.15.4.tar.gz", hash = "sha256:3412195319e42d634470cc97aa9803d07e9d5c9223b99bcb1518f0c725f26ae1", size = 4569550, upload-time = "2026-02-26T20:04:14.959Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f2/82/c11a03cfec3a4d26a0ea1e571f0f44be5993b923f905eeddfc397c13d360/ruff-0.15.4-py3-none-linux_armv6l.whl", hash = "sha256:a1810931c41606c686bae8b5b9a8072adac2f611bb433c0ba476acba17a332e0", size = 10453333, upload-time = "2026-02-26T20:04:20.093Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ce/5d/6a1f271f6e31dffb31855996493641edc3eef8077b883eaf007a2f1c2976/ruff-0.15.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:5a1632c66672b8b4d3e1d1782859e98d6e0b4e70829530666644286600a33992", size = 10853356, upload-time = "2026-02-26T20:04:05.808Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b1/d8/0fab9f8842b83b1a9c2bf81b85063f65e93fb512e60effa95b0be49bfc54/ruff-0.15.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:a4386ba2cd6c0f4ff75252845906acc7c7c8e1ac567b7bc3d373686ac8c222ba", size = 10187434, upload-time = "2026-02-26T20:03:54.656Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/85/cc/cc220fd9394eff5db8d94dec199eec56dd6c9f3651d8869d024867a91030/ruff-0.15.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2496488bdfd3732747558b6f95ae427ff066d1fcd054daf75f5a50674411e75", size = 10535456, upload-time = "2026-02-26T20:03:52.738Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fa/0f/bced38fa5cf24373ec767713c8e4cadc90247f3863605fb030e597878661/ruff-0.15.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3f1c4893841ff2d54cbda1b2860fa3260173df5ddd7b95d370186f8a5e66a4ac", size = 10287772, upload-time = "2026-02-26T20:04:08.138Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2b/90/58a1802d84fed15f8f281925b21ab3cecd813bde52a8ca033a4de8ab0e7a/ruff-0.15.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:820b8766bd65503b6c30aaa6331e8ef3a6e564f7999c844e9a547c40179e440a", size = 11049051, upload-time = "2026-02-26T20:04:03.53Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d2/ac/b7ad36703c35f3866584564dc15f12f91cb1a26a897dc2fd13d7cb3ae1af/ruff-0.15.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c9fb74bab47139c1751f900f857fa503987253c3ef89129b24ed375e72873e85", size = 11890494, upload-time = "2026-02-26T20:04:10.497Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/93/3d/3eb2f47a39a8b0da99faf9c54d3eb24720add1e886a5309d4d1be73a6380/ruff-0.15.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f80c98765949c518142b3a50a5db89343aa90f2c2bf7799de9986498ae6176db", size = 11326221, upload-time = "2026-02-26T20:04:12.84Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ff/90/bf134f4c1e5243e62690e09d63c55df948a74084c8ac3e48a88468314da6/ruff-0.15.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:451a2e224151729b3b6c9ffb36aed9091b2996fe4bdbd11f47e27d8f2e8888ec", size = 11168459, upload-time = "2026-02-26T20:04:00.969Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b5/e5/a64d27688789b06b5d55162aafc32059bb8c989c61a5139a36e1368285eb/ruff-0.15.4-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a8f157f2e583c513c4f5f896163a93198297371f34c04220daf40d133fdd4f7f", size = 11104366, upload-time = "2026-02-26T20:03:48.099Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f1/f6/32d1dcb66a2559763fc3027bdd65836cad9eb09d90f2ed6a63d8e9252b02/ruff-0.15.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:917cc68503357021f541e69b35361c99387cdbbf99bd0ea4aa6f28ca99ff5338", size = 10510887, upload-time = "2026-02-26T20:03:45.771Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ff/92/22d1ced50971c5b6433aed166fcef8c9343f567a94cf2b9d9089f6aa80fe/ruff-0.15.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:e9737c8161da79fd7cfec19f1e35620375bd8b2a50c3e77fa3d2c16f574105cc", size = 10285939, upload-time = "2026-02-26T20:04:22.42Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e6/f4/7c20aec3143837641a02509a4668fb146a642fd1211846634edc17eb5563/ruff-0.15.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:291258c917539e18f6ba40482fe31d6f5ac023994ee11d7bdafd716f2aab8a68", size = 10765471, upload-time = "2026-02-26T20:03:58.924Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d0/09/6d2f7586f09a16120aebdff8f64d962d7c4348313c77ebb29c566cefc357/ruff-0.15.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:3f83c45911da6f2cd5936c436cf86b9f09f09165f033a99dcf7477e34041cbc3", size = 11263382, upload-time = "2026-02-26T20:04:24.424Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1b/fa/2ef715a1cd329ef47c1a050e10dee91a9054b7ce2fcfdd6a06d139afb7ec/ruff-0.15.4-py3-none-win32.whl", hash = "sha256:65594a2d557d4ee9f02834fcdf0a28daa8b3b9f6cb2cb93846025a36db47ef22", size = 10506664, upload-time = "2026-02-26T20:03:50.56Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d0/a8/c688ef7e29983976820d18710f955751d9f4d4eb69df658af3d006e2ba3e/ruff-0.15.4-py3-none-win_amd64.whl", hash = "sha256:04196ad44f0df220c2ece5b0e959c2f37c777375ec744397d21d15b50a75264f", size = 11651048, upload-time = "2026-02-26T20:04:17.191Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3e/0a/9e1be9035b37448ce2e68c978f0591da94389ade5a5abafa4cf99985d1b2/ruff-0.15.4-py3-none-win_arm64.whl", hash = "sha256:60d5177e8cfc70e51b9c5fad936c634872a74209f934c1e79107d11787ad5453", size = 10966776, upload-time = "2026-02-26T20:03:56.908Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "shadowcopy"
|
name = "shadowcopy"
|
||||||
version = "0.0.4"
|
version = "0.0.4"
|
||||||
@@ -328,6 +567,60 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/ad/32/fdea8e7f7b2b8bae13ee6ab7df1a10d28a24dbeb03a62ff033563f95d77c/shadowcopy-0.0.4-py3-none-any.whl", hash = "sha256:fc51e59a639dc6a5a3a7a9b4e3ecadc71989e339f2d995d90aaa491acd4ba4eb", size = 4212, upload-time = "2023-07-08T00:01:34.042Z" },
|
{ url = "https://files.pythonhosted.org/packages/ad/32/fdea8e7f7b2b8bae13ee6ab7df1a10d28a24dbeb03a62ff033563f95d77c/shadowcopy-0.0.4-py3-none-any.whl", hash = "sha256:fc51e59a639dc6a5a3a7a9b4e3ecadc71989e339f2d995d90aaa491acd4ba4eb", size = 4212, upload-time = "2023-07-08T00:01:34.042Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tomli"
|
||||||
|
version = "2.4.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/82/30/31573e9457673ab10aa432461bee537ce6cef177667deca369efb79df071/tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c", size = 17477, upload-time = "2026-01-11T11:22:38.165Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3c/d9/3dc2289e1f3b32eb19b9785b6a006b28ee99acb37d1d47f78d4c10e28bf8/tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867", size = 153663, upload-time = "2026-01-11T11:21:45.27Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/51/32/ef9f6845e6b9ca392cd3f64f9ec185cc6f09f0a2df3db08cbe8809d1d435/tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9", size = 148469, upload-time = "2026-01-11T11:21:46.873Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d6/c2/506e44cce89a8b1b1e047d64bd495c22c9f71f21e05f380f1a950dd9c217/tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95", size = 236039, upload-time = "2026-01-11T11:21:48.503Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b3/40/e1b65986dbc861b7e986e8ec394598187fa8aee85b1650b01dd925ca0be8/tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76", size = 243007, upload-time = "2026-01-11T11:21:49.456Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9c/6f/6e39ce66b58a5b7ae572a0f4352ff40c71e8573633deda43f6a379d56b3e/tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d", size = 240875, upload-time = "2026-01-11T11:21:50.755Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/aa/ad/cb089cb190487caa80204d503c7fd0f4d443f90b95cf4ef5cf5aa0f439b0/tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576", size = 246271, upload-time = "2026-01-11T11:21:51.81Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0b/63/69125220e47fd7a3a27fd0de0c6398c89432fec41bc739823bcc66506af6/tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a", size = 96770, upload-time = "2026-01-11T11:21:52.647Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1e/0d/a22bb6c83f83386b0008425a6cd1fa1c14b5f3dd4bad05e98cf3dbbf4a64/tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa", size = 107626, upload-time = "2026-01-11T11:21:53.459Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2f/6d/77be674a3485e75cacbf2ddba2b146911477bd887dda9d8c9dfb2f15e871/tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614", size = 94842, upload-time = "2026-01-11T11:21:54.831Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3c/43/7389a1869f2f26dba52404e1ef13b4784b6b37dac93bac53457e3ff24ca3/tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1", size = 154894, upload-time = "2026-01-11T11:21:56.07Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e9/05/2f9bf110b5294132b2edf13fe6ca6ae456204f3d749f623307cbb7a946f2/tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8", size = 149053, upload-time = "2026-01-11T11:21:57.467Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e8/41/1eda3ca1abc6f6154a8db4d714a4d35c4ad90adc0bcf700657291593fbf3/tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a", size = 243481, upload-time = "2026-01-11T11:21:58.661Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d2/6d/02ff5ab6c8868b41e7d4b987ce2b5f6a51d3335a70aa144edd999e055a01/tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1", size = 251720, upload-time = "2026-01-11T11:22:00.178Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7b/57/0405c59a909c45d5b6f146107c6d997825aa87568b042042f7a9c0afed34/tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b", size = 247014, upload-time = "2026-01-11T11:22:01.238Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2c/0e/2e37568edd944b4165735687cbaf2fe3648129e440c26d02223672ee0630/tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51", size = 251820, upload-time = "2026-01-11T11:22:02.727Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5a/1c/ee3b707fdac82aeeb92d1a113f803cf6d0f37bdca0849cb489553e1f417a/tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729", size = 97712, upload-time = "2026-01-11T11:22:03.777Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/69/13/c07a9177d0b3bab7913299b9278845fc6eaaca14a02667c6be0b0a2270c8/tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da", size = 108296, upload-time = "2026-01-11T11:22:04.86Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/18/27/e267a60bbeeee343bcc279bb9e8fbed0cbe224bc7b2a3dc2975f22809a09/tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3", size = 94553, upload-time = "2026-01-11T11:22:05.854Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/34/91/7f65f9809f2936e1f4ce6268ae1903074563603b2a2bd969ebbda802744f/tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0", size = 154915, upload-time = "2026-01-11T11:22:06.703Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/20/aa/64dd73a5a849c2e8f216b755599c511badde80e91e9bc2271baa7b2cdbb1/tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e", size = 149038, upload-time = "2026-01-11T11:22:07.56Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9e/8a/6d38870bd3d52c8d1505ce054469a73f73a0fe62c0eaf5dddf61447e32fa/tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4", size = 242245, upload-time = "2026-01-11T11:22:08.344Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/59/bb/8002fadefb64ab2669e5b977df3f5e444febea60e717e755b38bb7c41029/tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e", size = 250335, upload-time = "2026-01-11T11:22:09.951Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a5/3d/4cdb6f791682b2ea916af2de96121b3cb1284d7c203d97d92d6003e91c8d/tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c", size = 245962, upload-time = "2026-01-11T11:22:11.27Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f2/4a/5f25789f9a460bd858ba9756ff52d0830d825b458e13f754952dd15fb7bb/tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f", size = 250396, upload-time = "2026-01-11T11:22:12.325Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/aa/2f/b73a36fea58dfa08e8b3a268750e6853a6aac2a349241a905ebd86f3047a/tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86", size = 97530, upload-time = "2026-01-11T11:22:13.865Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3b/af/ca18c134b5d75de7e8dc551c5234eaba2e8e951f6b30139599b53de9c187/tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87", size = 108227, upload-time = "2026-01-11T11:22:15.224Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/22/c3/b386b832f209fee8073c8138ec50f27b4460db2fdae9ffe022df89a57f9b/tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132", size = 94748, upload-time = "2026-01-11T11:22:16.009Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f3/c4/84047a97eb1004418bc10bdbcfebda209fca6338002eba2dc27cc6d13563/tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6", size = 154725, upload-time = "2026-01-11T11:22:17.269Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a8/5d/d39038e646060b9d76274078cddf146ced86dc2b9e8bbf737ad5983609a0/tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc", size = 148901, upload-time = "2026-01-11T11:22:18.287Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/73/e5/383be1724cb30f4ce44983d249645684a48c435e1cd4f8b5cded8a816d3c/tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66", size = 243375, upload-time = "2026-01-11T11:22:19.154Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/31/f0/bea80c17971c8d16d3cc109dc3585b0f2ce1036b5f4a8a183789023574f2/tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d", size = 250639, upload-time = "2026-01-11T11:22:20.168Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2c/8f/2853c36abbb7608e3f945d8a74e32ed3a74ee3a1f468f1ffc7d1cb3abba6/tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702", size = 246897, upload-time = "2026-01-11T11:22:21.544Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/49/f0/6c05e3196ed5337b9fe7ea003e95fd3819a840b7a0f2bf5a408ef1dad8ed/tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8", size = 254697, upload-time = "2026-01-11T11:22:23.058Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f3/f5/2922ef29c9f2951883525def7429967fc4d8208494e5ab524234f06b688b/tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776", size = 98567, upload-time = "2026-01-11T11:22:24.033Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7b/31/22b52e2e06dd2a5fdbc3ee73226d763b184ff21fc24e20316a44ccc4d96b/tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475", size = 108556, upload-time = "2026-01-11T11:22:25.378Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/48/3d/5058dff3255a3d01b705413f64f4306a141a8fd7a251e5a495e3f192a998/tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2", size = 96014, upload-time = "2026-01-11T11:22:26.138Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b8/4e/75dab8586e268424202d3a1997ef6014919c941b50642a1682df43204c22/tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9", size = 163339, upload-time = "2026-01-11T11:22:27.143Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/06/e3/b904d9ab1016829a776d97f163f183a48be6a4deb87304d1e0116a349519/tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0", size = 159490, upload-time = "2026-01-11T11:22:28.399Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e3/5a/fc3622c8b1ad823e8ea98a35e3c632ee316d48f66f80f9708ceb4f2a0322/tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df", size = 269398, upload-time = "2026-01-11T11:22:29.345Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fd/33/62bd6152c8bdd4c305ad9faca48f51d3acb2df1f8791b1477d46ff86e7f8/tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d", size = 276515, upload-time = "2026-01-11T11:22:30.327Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4b/ff/ae53619499f5235ee4211e62a8d7982ba9e439a0fb4f2f351a93d67c1dd2/tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f", size = 273806, upload-time = "2026-01-11T11:22:32.56Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/47/71/cbca7787fa68d4d0a9f7072821980b39fbb1b6faeb5f5cf02f4a5559fa28/tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b", size = 281340, upload-time = "2026-01-11T11:22:33.505Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f5/00/d595c120963ad42474cf6ee7771ad0d0e8a49d0f01e29576ee9195d9ecdf/tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087", size = 108106, upload-time = "2026-01-11T11:22:34.451Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/de/69/9aa0c6a505c2f80e519b43764f8b4ba93b5a0bbd2d9a9de6e2b24271b9a5/tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd", size = 120504, upload-time = "2026-01-11T11:22:35.764Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b3/9f/f1668c281c58cfae01482f7114a4b88d345e4c140386241a1a24dcc9e7bc/tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4", size = 99561, upload-time = "2026-01-11T11:22:36.624Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "twitter-cli"
|
name = "twitter-cli"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@@ -336,14 +629,52 @@ dependencies = [
|
|||||||
{ name = "browser-cookie3" },
|
{ name = "browser-cookie3" },
|
||||||
{ name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
|
{ name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
|
||||||
{ name = "click", version = "8.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
|
{ name = "click", version = "8.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
|
||||||
|
{ name = "pyyaml" },
|
||||||
{ name = "rich" },
|
{ name = "rich" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[package.optional-dependencies]
|
||||||
|
dev = [
|
||||||
|
{ name = "pytest", version = "8.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
|
||||||
|
{ name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" },
|
||||||
|
{ name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
|
||||||
|
{ name = "ruff" },
|
||||||
|
]
|
||||||
|
|
||||||
[package.metadata]
|
[package.metadata]
|
||||||
requires-dist = [
|
requires-dist = [
|
||||||
{ name = "browser-cookie3", specifier = ">=0.19" },
|
{ name = "browser-cookie3", specifier = ">=0.19" },
|
||||||
{ name = "click", specifier = ">=8.0" },
|
{ name = "click", specifier = ">=8.0" },
|
||||||
|
{ name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0" },
|
||||||
|
{ name = "pyyaml", specifier = ">=6.0" },
|
||||||
{ name = "rich", specifier = ">=13.0" },
|
{ name = "rich", specifier = ">=13.0" },
|
||||||
|
{ name = "ruff", marker = "extra == 'dev'", specifier = ">=0.8" },
|
||||||
|
]
|
||||||
|
provides-extras = ["dev"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typing-extensions"
|
||||||
|
version = "4.13.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
resolution-markers = [
|
||||||
|
"python_full_version < '3.9'",
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967, upload-time = "2025-04-10T14:19:05.416Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806, upload-time = "2025-04-10T14:19:03.967Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typing-extensions"
|
||||||
|
version = "4.15.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
resolution-markers = [
|
||||||
|
"python_full_version >= '3.10'",
|
||||||
|
"python_full_version == '3.9.*'",
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
Reference in New Issue
Block a user