Add JoinQuant browser backtest automation

This commit is contained in:
Yuxuan Yan
2026-07-04 18:08:31 +08:00
parent 5388359dc8
commit 39a93259e1
7 changed files with 740 additions and 1 deletions
+59
View File
@@ -238,6 +238,65 @@ The command downloads a tiny public daily-bar sample, builds a fixed-share
JoinQuant target files, writes a configured wrapper strategy, and creates JoinQuant target files, writes a configured wrapper strategy, and creates
`joinquant_smoke_manifest.json` with all output paths. `joinquant_smoke_manifest.json` with all output paths.
## Browser Backtest Automation
JoinQuant's public `jqdatasdk` is a data SDK. It supports authenticated data
calls such as `auth(username, password)` and `get_price(...)`, but cloud
strategy upload, backtest execution, and result export are web-application
workflows. The plugin therefore automates those remote steps through Playwright
with a saved browser session.
Install the optional browser runner in the uv environment:
```bash
uv sync --extra joinquant-browser
uv run playwright install chromium
```
Save a reusable login state. This opens a browser; log in normally, including
any CAPTCHA or 2FA, then press Enter in the terminal to save state:
```bash
uv run python cli.py joinquant browser-login \
--storage-state ~/.config/chinese-equity-quant/joinquant_storage_state.json
```
Create a selector/action config:
```bash
uv run python cli.py joinquant write-browser-config \
--out-path /tmp/chinese-equity-quant-realdata/joinquant_browser_config.json \
--strategy-url "https://www.joinquant.com/<your strategy page>"
```
If selectors need tuning, capture the logged-in page:
```bash
uv run python cli.py joinquant browser-snapshot \
--url "https://www.joinquant.com/<your strategy page>" \
--out-dir /tmp/chinese-equity-quant-realdata/browser_snapshot
```
Then run the remote backtest automation:
```bash
uv run python cli.py joinquant run-browser-backtest \
--manifest-path /tmp/chinese-equity-quant-realdata/joinquant_smoke_manifest.json \
--config-path /tmp/chinese-equity-quant-realdata/joinquant_browser_config.json \
--storage-state ~/.config/chinese-equity-quant/joinquant_storage_state.json \
--headed
```
The config is declarative: actions can navigate, paste the generated wrapper,
upload all target CSV files, fill dates, click run, wait for completion,
download result CSVs, and take screenshots. When the configured downloads
produce fills, positions, and PnL CSVs, the runner automatically calls
`joinquant ingest` and `joinquant reconcile`.
Do not store raw JoinQuant passwords in this repository. The browser state file
is created with `0600` permissions and should live under `~/.config`, outside
the repo.
## Recommended First Sanity Checks ## Recommended First Sanity Checks
1. One liquid stock with a fixed target share count. 1. One liquid stock with a fixed target share count.
+20
View File
@@ -21,6 +21,21 @@ The plugin validates system mechanics, not alpha quality:
uv run python cli.py joinquant prepare-smoke \ uv run python cli.py joinquant prepare-smoke \
--out-dir /tmp/chinese-equity-quant-realdata --out-dir /tmp/chinese-equity-quant-realdata
uv sync --extra joinquant-browser
uv run playwright install chromium
uv run python cli.py joinquant browser-login \
--storage-state ~/.config/chinese-equity-quant/joinquant_storage_state.json
uv run python cli.py joinquant write-browser-config \
--out-path /tmp/chinese-equity-quant-realdata/joinquant_browser_config.json \
--strategy-url "https://www.joinquant.com/..."
uv run python cli.py joinquant run-browser-backtest \
--manifest-path /tmp/chinese-equity-quant-realdata/joinquant_smoke_manifest.json \
--config-path /tmp/chinese-equity-quant-realdata/joinquant_browser_config.json \
--storage-state ~/.config/chinese-equity-quant/joinquant_storage_state.json
uv run python cli.py joinquant export-targets \ uv run python cli.py joinquant export-targets \
--positions-path portfolio/run1.pq \ --positions-path portfolio/run1.pq \
--portfolio-name run1 \ --portfolio-name run1 \
@@ -64,3 +79,8 @@ simulator's next-open convention.
data download, one-stock long-only position file, internal simulation, aligned data download, one-stock long-only position file, internal simulation, aligned
target export, wrapper generation, and a manifest with expected JoinQuant CSV target export, wrapper generation, and a manifest with expected JoinQuant CSV
export paths. export paths.
`run-browser-backtest` automates the remote JoinQuant web run through
Playwright. It reuses a saved browser login state, executes the configured UI
actions, downloads JoinQuant CSVs when configured, and runs ingest/reconcile
automatically once all three CSVs are present.
+386
View File
@@ -0,0 +1,386 @@
"""Browser automation for JoinQuant cloud backtest runs.
JoinQuant's public ``jqdatasdk`` is a data API; cloud strategy upload/run/export
is exposed through the web application. This module keeps that automation
optional and configurable so UI selector drift does not affect the core test
suite.
"""
from __future__ import annotations
import json
import re
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
from plugins.joinquant.ingest import ingest_joinquant_outputs
from plugins.joinquant.reconcile import reconcile_joinquant
DEFAULT_LOGIN_URL = "https://www.joinquant.com/user/login/index"
def _require_playwright():
try:
from playwright.sync_api import TimeoutError as PlaywrightTimeoutError
from playwright.sync_api import sync_playwright
except ImportError as exc:
raise RuntimeError(
"Playwright is required for JoinQuant browser automation. Install it "
"in this uv environment, then install Chromium:\n"
" uv pip install playwright\n"
" uv run playwright install chromium"
) from exc
return sync_playwright, PlaywrightTimeoutError
def default_browser_config(strategy_url: str = "") -> dict[str, Any]:
"""Return a selector/action template for JoinQuant browser automation."""
return {
"strategy_url": strategy_url,
"login_url": DEFAULT_LOGIN_URL,
"headless": False,
"timeout_ms": 120_000,
"notes": [
"Fill strategy_url and selectors after inspecting your JoinQuant strategy page.",
"Use `joinquant browser-snapshot` to capture HTML/screenshots for selector tuning.",
"The action list is declarative and runs in order.",
],
"actions": [
{"type": "goto", "url": "{strategy_url}"},
{
"type": "paste_text_file",
"selector": "textarea, .ace_text-input, .cm-content, .CodeMirror textarea",
"path": "{wrapper_path}",
"description": "Paste generated wrapper strategy into the code editor.",
},
{
"type": "set_input_files",
"selector": "input[type=file]",
"paths": "{target_csvs}",
"description": "Upload all aligned daily target CSV files.",
"optional": True,
},
{
"type": "fill",
"selector": "input[name=start_date], input[placeholder*=开始]",
"text": "{backtest_start_date}",
"optional": True,
},
{
"type": "fill",
"selector": "input[name=end_date], input[placeholder*=结束]",
"text": "{backtest_end_date}",
"optional": True,
},
{
"type": "click",
"selector": "text=/运行回测|开始回测|回测|Run Backtest|Run/i",
"description": "Start the JoinQuant backtest.",
},
{
"type": "wait_for_selector",
"selector": "text=/回测完成|运行完成|Backtest complete|Finished/i",
"timeout_ms": 600_000,
"optional": True,
},
{
"type": "download",
"selector": "text=/导出成交|下载成交|fills|trades/i",
"save_as": "{expected_joinquant_csvs.fills}",
"optional": True,
},
{
"type": "download",
"selector": "text=/导出持仓|下载持仓|positions/i",
"save_as": "{expected_joinquant_csvs.positions}",
"optional": True,
},
{
"type": "download",
"selector": "text=/导出收益|下载收益|pnl|收益/i",
"save_as": "{expected_joinquant_csvs.pnl}",
"optional": True,
},
{"type": "screenshot", "path": "{run_artifact_dir}/final.png"},
],
}
def write_browser_config_template(path: str | Path, *, strategy_url: str = "") -> Path:
"""Write a JSON config template for browser automation."""
out_path = Path(path)
out_path.parent.mkdir(parents=True, exist_ok=True)
out_path.write_text(
json.dumps(default_browser_config(strategy_url), indent=2, ensure_ascii=False) + "\n",
encoding="utf-8",
)
return out_path
def load_json(path: str | Path) -> dict[str, Any]:
"""Load a JSON object from disk."""
data = json.loads(Path(path).read_text(encoding="utf-8"))
if not isinstance(data, dict):
raise ValueError(f"Expected JSON object in {path}")
return data
def _get_dotted(data: dict[str, Any], dotted: str) -> Any:
current: Any = data
for part in dotted.split("."):
if isinstance(current, dict) and part in current:
current = current[part]
else:
raise KeyError(dotted)
return current
def _manifest_context(manifest: dict[str, Any], artifact_dir: Path, config: dict[str, Any]) -> dict[str, Any]:
targets_dir = Path(str(manifest["targets_dir"]))
target_csvs = [str(path) for path in sorted(targets_dir.glob("*.csv"))]
if not target_csvs:
raise ValueError(f"No target CSV files found under {targets_dir}")
target_dates = [
datetime.strptime(Path(path).stem, "%Y%m%d").strftime("%Y-%m-%d")
for path in target_csvs
]
context = dict(manifest)
context.update({
"strategy_url": config.get("strategy_url", ""),
"target_csvs": target_csvs,
"target_csvs_csv": ",".join(target_csvs),
"backtest_start_date": config.get("backtest_start_date") or min(target_dates),
"backtest_end_date": config.get("backtest_end_date") or max(target_dates),
"run_artifact_dir": str(artifact_dir),
})
return context
_TOKEN_RE = re.compile(r"^\{([^{}]+)\}$")
_PARTIAL_TOKEN_RE = re.compile(r"\{([^{}]+)\}")
def resolve_template(value: Any, context: dict[str, Any]) -> Any:
"""Resolve ``{tokens}`` in config values against the manifest context."""
if isinstance(value, str):
whole = _TOKEN_RE.match(value)
if whole:
return _get_dotted(context, whole.group(1))
def replace(match: re.Match[str]) -> str:
resolved = _get_dotted(context, match.group(1))
return str(resolved)
return _PARTIAL_TOKEN_RE.sub(replace, value)
if isinstance(value, list):
return [resolve_template(item, context) for item in value]
if isinstance(value, dict):
return {key: resolve_template(val, context) for key, val in value.items()}
return value
def save_login_state(
*,
storage_state: str | Path,
login_url: str = DEFAULT_LOGIN_URL,
headless: bool = False,
wait_seconds: int = 0,
) -> Path:
"""Open a browser for manual login and save the authenticated state."""
if headless and wait_seconds <= 0:
raise ValueError("headless browser-login requires --wait-seconds > 0")
sync_playwright, _ = _require_playwright()
state_path = Path(storage_state).expanduser()
state_path.parent.mkdir(parents=True, exist_ok=True)
with sync_playwright() as pw:
browser = pw.chromium.launch(headless=headless)
context = browser.new_context()
page = context.new_page()
page.goto(login_url, wait_until="domcontentloaded")
if wait_seconds > 0:
page.wait_for_timeout(wait_seconds * 1000)
else:
input("Log in to JoinQuant in the opened browser, then press Enter here to save state...")
context.storage_state(path=str(state_path))
browser.close()
state_path.chmod(0o600)
return state_path
def browser_snapshot(
*,
url: str,
storage_state: str | Path,
out_dir: str | Path,
headless: bool = True,
timeout_ms: int = 60_000,
) -> dict[str, Path]:
"""Save a logged-in page screenshot and HTML for selector discovery."""
sync_playwright, _ = _require_playwright()
root = Path(out_dir)
root.mkdir(parents=True, exist_ok=True)
html_path = root / "page.html"
screenshot_path = root / "page.png"
with sync_playwright() as pw:
browser = pw.chromium.launch(headless=headless)
context = browser.new_context(storage_state=str(Path(storage_state).expanduser()))
page = context.new_page()
page.set_default_timeout(timeout_ms)
page.goto(url, wait_until="networkidle")
html_path.write_text(page.content(), encoding="utf-8")
page.screenshot(path=str(screenshot_path), full_page=True)
browser.close()
return {"html": html_path, "screenshot": screenshot_path}
def _locator(page: Any, selector: str):
return page.locator(selector).first()
def _action_fail(action: dict[str, Any], exc: Exception) -> None:
if action.get("optional", False):
return
raise exc
def _run_action(page: Any, action: dict[str, Any], context: dict[str, Any], timeout_default: int) -> dict[str, Any]:
resolved = resolve_template(action, context)
kind = resolved["type"]
timeout_ms = int(resolved.get("timeout_ms", timeout_default))
record: dict[str, Any] = {"type": kind, "status": "ok"}
try:
if kind == "goto":
page.goto(str(resolved["url"]), wait_until=resolved.get("wait_until", "domcontentloaded"), timeout=timeout_ms)
elif kind == "click":
_locator(page, str(resolved["selector"])).click(timeout=timeout_ms)
elif kind == "fill":
_locator(page, str(resolved["selector"])).fill(str(resolved.get("text", "")), timeout=timeout_ms)
elif kind == "press":
_locator(page, str(resolved["selector"])).press(str(resolved["key"]), timeout=timeout_ms)
elif kind == "set_input_files":
paths = resolved.get("paths", [])
if isinstance(paths, str):
paths = [path for path in paths.split(",") if path]
_locator(page, str(resolved["selector"])).set_input_files(paths, timeout=timeout_ms)
record["n_files"] = len(paths)
elif kind == "paste_text_file":
text = Path(str(resolved["path"])).read_text(encoding="utf-8")
loc = _locator(page, str(resolved["selector"]))
loc.click(timeout=timeout_ms)
page.keyboard.press("Control+A")
page.keyboard.insert_text(text)
record["n_chars"] = len(text)
elif kind == "evaluate":
page.evaluate(str(resolved["script"]), resolved.get("arg"))
elif kind == "wait_for_selector":
page.wait_for_selector(str(resolved["selector"]), timeout=timeout_ms)
elif kind == "wait_for_timeout":
page.wait_for_timeout(int(resolved.get("timeout_ms", 1000)))
elif kind == "download":
save_as = Path(str(resolved["save_as"]))
save_as.parent.mkdir(parents=True, exist_ok=True)
with page.expect_download(timeout=timeout_ms) as download_info:
_locator(page, str(resolved["selector"])).click(timeout=timeout_ms)
download = download_info.value
download.save_as(str(save_as))
record["path"] = str(save_as)
elif kind == "screenshot":
path = Path(str(resolved["path"]))
path.parent.mkdir(parents=True, exist_ok=True)
page.screenshot(path=str(path), full_page=bool(resolved.get("full_page", True)))
record["path"] = str(path)
else:
raise ValueError(f"Unsupported browser action type: {kind}")
except Exception as exc: # pragma: no cover - exercised only with Playwright.
record["status"] = "skipped" if resolved.get("optional", False) else "failed"
record["error"] = str(exc)
_action_fail(resolved, exc)
return record
def run_browser_backtest(
*,
manifest_path: str | Path,
config_path: str | Path,
storage_state: str | Path,
out_dir: str | Path | None = None,
headless: bool | None = None,
auto_reconcile: bool = True,
) -> dict[str, Any]:
"""Run configured browser automation for a JoinQuant backtest."""
sync_playwright, _ = _require_playwright()
manifest = load_json(manifest_path)
config = load_json(config_path)
artifact_dir = Path(out_dir or Path(str(manifest["joinquant_export_dir"])).parent / "browser_run")
artifact_dir.mkdir(parents=True, exist_ok=True)
context_vars = _manifest_context(manifest, artifact_dir, config)
timeout_ms = int(config.get("timeout_ms", 120_000))
actions = config.get("actions") or []
if not actions:
raise ValueError("Browser config contains no actions")
records: list[dict[str, Any]] = []
with sync_playwright() as pw:
browser = pw.chromium.launch(
headless=bool(config.get("headless", False) if headless is None else headless)
)
context = browser.new_context(
storage_state=str(Path(storage_state).expanduser()),
accept_downloads=True,
)
page = context.new_page()
page.set_default_timeout(timeout_ms)
for action in actions:
records.append(_run_action(page, action, context_vars, timeout_ms))
browser.close()
expected = manifest.get("expected_joinquant_csvs", {})
downloaded = {
key: str(path)
for key, value in expected.items()
if (path := Path(str(value))).exists()
}
reconcile_paths: dict[str, str] = {}
if auto_reconcile and {"fills", "positions", "pnl"}.issubset(downloaded):
ingested = ingest_joinquant_outputs(
portfolio_name=str(manifest["portfolio_name"]),
fills_csv=downloaded["fills"],
positions_csv=downloaded["positions"],
pnl_csv=downloaded["pnl"],
out_dir=Path(str(manifest["joinquant_export_dir"])).parent / "ingested",
)
reconciled = reconcile_joinquant(
portfolio_name=str(manifest["portfolio_name"]),
targets_dir=str(manifest["targets_dir"]),
our_fills_path=str(manifest["fills_path"]),
our_positions_path=str(manifest["positions_path"]),
our_pnl_path=str(manifest["pnl_path"]),
jq_fills_path=str(ingested["fills"]),
jq_positions_path=str(ingested["positions"]),
jq_pnl_path=str(ingested["pnl"]),
out_dir=Path(str(manifest["joinquant_export_dir"])).parent / "reconcile",
)
reconcile_paths = {key: str(value) for key, value in reconciled.items()}
report = {
"created_at": datetime.now(timezone.utc).isoformat(),
"manifest_path": str(manifest_path),
"config_path": str(config_path),
"storage_state": str(storage_state),
"artifact_dir": str(artifact_dir),
"actions": records,
"downloaded": downloaded,
"reconcile_paths": reconcile_paths,
}
report_path = artifact_dir / "browser_run_report.json"
report["report_path"] = str(report_path)
report_path.write_text(
json.dumps(report, indent=2, ensure_ascii=False) + "\n",
encoding="utf-8",
)
return report
+106
View File
@@ -4,6 +4,12 @@ from __future__ import annotations
import click import click
from plugins.joinquant.browser import (
browser_snapshot,
run_browser_backtest,
save_login_state,
write_browser_config_template,
)
from plugins.joinquant.export_targets import export_targets from plugins.joinquant.export_targets import export_targets
from plugins.joinquant.ingest import ingest_joinquant_outputs from plugins.joinquant.ingest import ingest_joinquant_outputs
from plugins.joinquant.reconcile import reconcile_joinquant from plugins.joinquant.reconcile import reconcile_joinquant
@@ -217,3 +223,103 @@ def prepare_smoke_cmd(
f"PnL={summary['total_pnl']:,.2f}, cost={summary['total_cost']:,.2f}, " f"PnL={summary['total_pnl']:,.2f}, cost={summary['total_cost']:,.2f}, "
f"blocked={summary['blocked_trades']}" f"blocked={summary['blocked_trades']}"
) )
@joinquant.command("browser-login")
@click.option(
"--storage-state",
default="~/.config/chinese-equity-quant/joinquant_storage_state.json",
show_default=True,
help="Path where the authenticated browser state is stored",
)
@click.option(
"--login-url",
default="https://www.joinquant.com/user/login/index",
show_default=True,
)
@click.option("--headless", is_flag=True, help="Use a headless browser")
@click.option(
"--wait-seconds",
default=0,
show_default=True,
type=int,
help="Seconds to wait before saving state; 0 prompts for Enter after login",
)
def browser_login_cmd(storage_state, login_url, headless, wait_seconds):
"""Open JoinQuant login and save reusable browser session state."""
path = save_login_state(
storage_state=storage_state,
login_url=login_url,
headless=headless,
wait_seconds=wait_seconds,
)
click.echo(f"Saved JoinQuant browser state: {path}")
@joinquant.command("write-browser-config")
@click.option("--out-path", required=True, help="Path for JSON browser automation config")
@click.option("--strategy-url", default="", help="JoinQuant strategy edit/backtest URL")
def write_browser_config_cmd(out_path, strategy_url):
"""Write a browser automation selector/action config template."""
path = write_browser_config_template(out_path, strategy_url=strategy_url)
click.echo(f"Saved JoinQuant browser config template: {path}")
@joinquant.command("browser-snapshot")
@click.option("--url", required=True, help="Logged-in JoinQuant page URL to inspect")
@click.option(
"--storage-state",
default="~/.config/chinese-equity-quant/joinquant_storage_state.json",
show_default=True,
)
@click.option("--out-dir", required=True, help="Directory for page.html and page.png")
@click.option("--headed", is_flag=True, help="Run with a visible browser")
@click.option("--timeout-ms", default=60_000, show_default=True, type=int)
def browser_snapshot_cmd(url, storage_state, out_dir, headed, timeout_ms):
"""Save a page screenshot and HTML for selector discovery."""
paths = browser_snapshot(
url=url,
storage_state=storage_state,
out_dir=out_dir,
headless=not headed,
timeout_ms=timeout_ms,
)
click.echo(f"Saved HTML: {paths['html']}")
click.echo(f"Saved screenshot: {paths['screenshot']}")
@joinquant.command("run-browser-backtest")
@click.option("--manifest-path", required=True, help="Manifest from `joinquant prepare-smoke`")
@click.option("--config-path", required=True, help="JSON browser automation config")
@click.option(
"--storage-state",
default="~/.config/chinese-equity-quant/joinquant_storage_state.json",
show_default=True,
)
@click.option("--out-dir", default=None, help="Browser-run artifact directory")
@click.option("--headed", is_flag=True, help="Run with a visible browser")
@click.option("--no-auto-reconcile", is_flag=True, help="Skip ingest/reconcile after downloads")
def run_browser_backtest_cmd(
manifest_path,
config_path,
storage_state,
out_dir,
headed,
no_auto_reconcile,
):
"""Run JoinQuant browser automation from manifest and config."""
report = run_browser_backtest(
manifest_path=manifest_path,
config_path=config_path,
storage_state=storage_state,
out_dir=out_dir,
headless=not headed,
auto_reconcile=not no_auto_reconcile,
)
click.echo(f"Saved browser run report: {report['report_path']}")
if report["downloaded"]:
click.echo("Downloaded JoinQuant CSVs:")
for key, path in report["downloaded"].items():
click.echo(f" {key}: {path}")
if report["reconcile_paths"]:
click.echo(f"Saved reconciliation summary: {report['reconcile_paths']['summary_md']}")
+3
View File
@@ -16,6 +16,9 @@ dependencies = [
backtrader = [ backtrader = [
"backtrader>=1.9.76.123", "backtrader>=1.9.76.123",
] ]
joinquant-browser = [
"playwright>=1.61.0",
]
[dependency-groups] [dependency-groups]
dev = [ dev = [
+44
View File
@@ -12,6 +12,11 @@ from click.testing import CliRunner
from cli import cli from cli import cli
from pipeline.common.schema import FILL_COLUMNS, PNL_COLUMNS, POSITION_COLUMNS from pipeline.common.schema import FILL_COLUMNS, PNL_COLUMNS, POSITION_COLUMNS
from plugins.joinquant.browser import (
default_browser_config,
resolve_template,
write_browser_config_template,
)
from plugins.joinquant.export_targets import export_targets from plugins.joinquant.export_targets import export_targets
from plugins.joinquant.ingest import ( from plugins.joinquant.ingest import (
ingest_joinquant_outputs, ingest_joinquant_outputs,
@@ -530,3 +535,42 @@ def test_build_fixed_share_positions_excludes_final_executionless_date():
] ]
assert positions["position_shares"].tolist() == [1000, 1000] assert positions["position_shares"].tolist() == [1000, 1000]
assert positions["target_value"].tolist() == [10_000.0, 10_500.0] assert positions["target_value"].tolist() == [10_000.0, 10_500.0]
def test_browser_config_template_and_placeholder_resolution(tmp_path):
config_path = write_browser_config_template(
tmp_path / "browser_config.json",
strategy_url="https://www.joinquant.com/example",
)
config = json.loads(config_path.read_text())
assert config["strategy_url"] == "https://www.joinquant.com/example"
assert config["actions"][0]["type"] == "goto"
context = {
"wrapper_path": "/tmp/wrapper.py",
"target_csvs": ["/tmp/20240110.csv", "/tmp/20240111.csv"],
"expected_joinquant_csvs": {"fills": "/tmp/jq_fills.csv"},
}
assert resolve_template("{wrapper_path}", context) == "/tmp/wrapper.py"
assert resolve_template("{target_csvs}", context) == [
"/tmp/20240110.csv",
"/tmp/20240111.csv",
]
assert resolve_template("save:{expected_joinquant_csvs.fills}", context) == "save:/tmp/jq_fills.csv"
def test_joinquant_cli_browser_config_smoke(tmp_path):
runner = CliRunner()
config_path = tmp_path / "browser_config.json"
result = runner.invoke(cli, [
"joinquant",
"write-browser-config",
"--out-path",
str(config_path),
"--strategy-url",
"https://www.joinquant.com/example",
])
assert result.exit_code == 0, result.output
assert config_path.exists()
assert default_browser_config()["actions"]
Generated
+122 -1
View File
@@ -296,6 +296,9 @@ dependencies = [
backtrader = [ backtrader = [
{ name = "backtrader" }, { name = "backtrader" },
] ]
joinquant-browser = [
{ name = "playwright" },
]
[package.dev-dependencies] [package.dev-dependencies]
dev = [ dev = [
@@ -311,9 +314,10 @@ requires-dist = [
{ name = "click", specifier = ">=8.0.0" }, { name = "click", specifier = ">=8.0.0" },
{ name = "matplotlib", specifier = ">=3.7.0" }, { name = "matplotlib", specifier = ">=3.7.0" },
{ name = "pandas", specifier = ">=2.0.0" }, { name = "pandas", specifier = ">=2.0.0" },
{ name = "playwright", marker = "extra == 'joinquant-browser'", specifier = ">=1.61.0" },
{ name = "pyarrow", specifier = ">=14.0.0" }, { name = "pyarrow", specifier = ">=14.0.0" },
] ]
provides-extras = ["backtrader"] provides-extras = ["backtrader", "joinquant-browser"]
[package.metadata.requires-dev] [package.metadata.requires-dev]
dev = [ dev = [
@@ -744,6 +748,92 @@ wheels = [
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/2c/47/c99d5268f354002ce80f8d029cd9d7d872969da1de8b93d32de4dc56d6f4/fonttools-4.63.0-py3-none-any.whl", hash = "sha256:445af2eab030a16b9171ea8bdda7ebf7d96bda2df88ee182a464252f6e05e20d", size = 1164562, upload-time = "2026-05-14T12:04:29.092Z" }, { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/2c/47/c99d5268f354002ce80f8d029cd9d7d872969da1de8b93d32de4dc56d6f4/fonttools-4.63.0-py3-none-any.whl", hash = "sha256:445af2eab030a16b9171ea8bdda7ebf7d96bda2df88ee182a464252f6e05e20d", size = 1164562, upload-time = "2026-05-14T12:04:29.092Z" },
] ]
[[package]]
name = "greenlet"
version = "3.5.3"
source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple/" }
sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/e2/f1/fbbfef6af0bad0548f09bc28948ea3c275b4edb19e17fc5ca9900a6a634d/greenlet-3.5.3.tar.gz", hash = "sha256:a61efc018fd3eb317eeca31aba90ee9e7f26f22884a79b6c6ec715bf71bb62f1", size = 200270, upload-time = "2026-06-26T19:28:24.832Z" }
wheels = [
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/f5/a1/1f7f0c555f5858fd2906fe9f7b0a3554fddb85cb70df7a6aaec41dc292c2/greenlet-3.5.3-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:c180d22d325fb613956b443c3c6f4406eb70e6defc70d3974da2a7b59e06f48c", size = 285838, upload-time = "2026-06-26T18:21:05.167Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/0a/29/be9f43ed61677a5759b38c8a9389248133c8c731bbfc0574ecdff66c99fc/greenlet-3.5.3-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:483d08c11181c83a6ce1a7a61df0f624a208ec40817a3bb2302714592eee4f04", size = 602342, upload-time = "2026-06-26T19:07:06.908Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/b9/42/ba41c97ec36aa4b3ec25e5aa691d79561254805fad7f2f826dd6770587e2/greenlet-3.5.3-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1dae6e0091eae084317e411f047f0b7cb241c6db570f7c45fd6b900a274914ce", size = 615541, upload-time = "2026-06-26T19:10:04.909Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/2e/8c/231ca675b0df779816950ca66b40b1fa14dbff4a0ed9814a9a29ec399140/greenlet-3.5.3-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0f6ff50ff8dbd51fae9b37f4101648b04ea0df19b3f50ab2beb5061e7716a5c8", size = 622473, upload-time = "2026-06-26T19:24:12.786Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/f5/c7/28747042e1df8a9cd120a1ebe15529fc4be3b486e13e8d551ff307a82412/greenlet-3.5.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9bcd2d72ccd70a1ec68ba6ef93e7fbb4420ef9997dabc7010d893bd4015e0bec", size = 615675, upload-time = "2026-06-26T18:32:14.444Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/81/fe/dd97c483a3ff82849196ccd07851600edd3ac9de74669ca8a6022ada9ea1/greenlet-3.5.3-cp310-cp310-manylinux_2_39_riscv64.whl", hash = "sha256:37bf9c538f5ae6e63d643f88dec37c0c83bdf0e2ebc62961dedcf458822f7b71", size = 418421, upload-time = "2026-06-26T19:25:34.503Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/cc/a8/b85525a6c8fba9f009a5f7c8df1545de8fb0f0bf3e0179194ef4e500317f/greenlet-3.5.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:73f152c895e09907e0dbe24f6c2db37beb085cd63db91c3825a0fcd0064124a8", size = 1575057, upload-time = "2026-06-26T19:09:00.264Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/03/79/fb76edb218fe6735ab0edeba176c7ab80df9618f7c02ce4208979f3ae7db/greenlet-3.5.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8bdb43e1a1d1873721acab2be99c5befd4d2044ddfd52e4d610801019880a702", size = 1641692, upload-time = "2026-06-26T18:31:41.454Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/6b/79/86fe3ee50ed55d9b3907eecd3208b5c3fe8a79515519aae98b4753c3fa1d/greenlet-3.5.3-cp310-cp310-win_amd64.whl", hash = "sha256:0909f9355a9f24845d3299f3112e266a06afb68302041989fd26bd68894933db", size = 238742, upload-time = "2026-06-26T18:20:40.758Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/51/58/5404031044f55afad7aad1aff8be3f22b1bed03e237cfeabbc7e5c8cfde0/greenlet-3.5.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:aca9b4ce85b152b5524ef7d88170efdff80dc0032aa8b75f9aaf7f3479ea95b4", size = 287424, upload-time = "2026-06-26T18:20:31.469Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/b4/bf/1c65e9b94a54d547068fa5b5a8a06f221f3316b48908e08668d29c77cb50/greenlet-3.5.3-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f71be4920368fe1fabeeaa53d1e3548337e2b223d9565f8ad5e392a75ba23fc", size = 606523, upload-time = "2026-06-26T19:07:08.859Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/b8/c7/b66baacc95775ad511287acb0137b95574a9ce5491902372b7564799d790/greenlet-3.5.3-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4d77e67f65f98449e3fb83f795b5d0a8437aead2f874ca89c96576caf4be3af6", size = 618315, upload-time = "2026-06-26T19:10:06.055Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/b0/a0/68afd1ebad40db87dac0a28ffa120726b98bf9c7c40c481b0f63c105d298/greenlet-3.5.3-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e18619ba655ac05d78d80fc83cac4ba892bd6927b99e3b8237aee861aaacc8bb", size = 626155, upload-time = "2026-06-26T19:24:14.44Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/78/2b/28ed29463522fdbe4c15b1f63922041626a7478316b34ab4adda3f0a4aba/greenlet-3.5.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8540f1e6205bd13ca0ce685581037219ca54a1b41a0a15d228c6c9b8ad5903d7", size = 617381, upload-time = "2026-06-26T18:32:16.077Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/07/7f/e327d912239ec4b3b49999e3967389bcf1ee8722b9ee9194d2752ecd558a/greenlet-3.5.3-cp311-cp311-manylinux_2_39_riscv64.whl", hash = "sha256:d27c0c653a60d9535f690226474a5cc1036a8b0d7b57504d1c4f89c44a07a80c", size = 421083, upload-time = "2026-06-26T19:25:35.804Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/2a/7b/ad04e9d1337fc04965dc9fc616b6a72cb65a24b800a014c011ec812f5489/greenlet-3.5.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7ef56fe650f50575bf843acde967b9c567687f3c22340941a899b7bc56e956a8", size = 1577771, upload-time = "2026-06-26T19:09:01.537Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/d8/33/6c87ab7ba663f70ca21f3022aad1ffe56d3f3e0521e836c2415e13abcc3c/greenlet-3.5.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5121af01cf911e70056c00d4b46d5e9b5d1415550038573d744138bacb59e6b8", size = 1644048, upload-time = "2026-06-26T18:31:42.996Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/1c/35/f0d8ee998b422cf8693b270f098e55d8d4ec8006b061b333f54f177d28d9/greenlet-3.5.3-cp311-cp311-win_amd64.whl", hash = "sha256:0f41e4a05a3c0cb31b17023eff28dd111e1d16bf7d7d00406cd7df23f31398a7", size = 239137, upload-time = "2026-06-26T18:23:21.664Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/fb/96/b9820295576ef18c9edc404f10e260ae7215ceaf3781a54b720ed2627862/greenlet-3.5.3-cp311-cp311-win_arm64.whl", hash = "sha256:ec6f1af59f6b5f3fc9678e2ea062d8377d22ac644f7844cb7a292910cf12ff44", size = 237630, upload-time = "2026-06-26T18:24:00.281Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/5d/6e/4c37d51a2b7f82d2ff11bb6b5f7d766d9a011726624af255e843727627a3/greenlet-3.5.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:719757059f5a53fd0dde23f78cffeafcdd97b21c850ddb7ca684a3c1a1f122e2", size = 288685, upload-time = "2026-06-26T18:22:08.977Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/7a/73/815dd90131c1b71ebdf53dbc7c276cafec2a1173b97559f97aba72724a87/greenlet-3.5.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:efa9f765dd09f9d0cdac651ffdf631ee59ec5dc6ee7a73e0c012ba9c52fbdf5b", size = 604761, upload-time = "2026-06-26T19:07:10.114Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/9f/57/079cfe76bcef36b153b25607ee91c6fcb58f17f8b23c86bbbeabe0c88d72/greenlet-3.5.3-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7faba15ac005376e02a0384504e0243be3370ce010296a44a820feb342b505ab", size = 617044, upload-time = "2026-06-26T19:10:07.25Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/fb/fb/d97dc261209c80744b7c8132693a30d70ec6e7315e632cb0a10b3fec94dd/greenlet-3.5.3-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5795cd1101371140551c645f2d408b8d3c01a5a29cf8a9bce6e759c983682d23", size = 622351, upload-time = "2026-06-26T19:24:16.32Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/37/87/b4d095775a3fb1bcafbb483fc206b27ebb785724c83051447737085dc54e/greenlet-3.5.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:87142215824be6ac05e2e8e2786eec307ccbc27c36723c3881959df654af6861", size = 614244, upload-time = "2026-06-26T18:32:17.594Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/8e/ac/e5fee13cbbd0e8de312d9a146584b8a51891c68847330ef9dc8b5109d23f/greenlet-3.5.3-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:af4923b3096e26a36d7e9cf24ab88083a20f97d191e3b97f253731ce9b41b28c", size = 425395, upload-time = "2026-06-26T19:25:37.144Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/8a/70/7559b609683650fa2b95b8ab84b4ab0b26556a635d19675e12aa832d826d/greenlet-3.5.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:215275b1b49320987352e6c1b054acca0064f965a2c66992bed9a6f7d913f149", size = 1574210, upload-time = "2026-06-26T19:09:03.077Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/ae/73/be55392074c60fc37655ca40fa6022457bfbf6718e9e342a7b0b41f96dd2/greenlet-3.5.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6b1b0eed82364b0e32c4ea0f221452d33e6bb17ae094d9f72aed9851812747ea", size = 1638627, upload-time = "2026-06-26T18:31:44.748Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/14/40/c57489acf8e37d74e2913d4eff63aa0dba17acccc4bdeef874dde2dbbec9/greenlet-3.5.3-cp312-cp312-win_amd64.whl", hash = "sha256:cde8adafa2365676f74a979744629589999093bc86e2484214f58e61df08902c", size = 239882, upload-time = "2026-06-26T18:23:27.518Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/71/fd/6fea0e3d6600f785069481ee637e09378dd4118acdfd38ad88ae2db31c98/greenlet-3.5.3-cp312-cp312-win_arm64.whl", hash = "sha256:c4e7b79d83805475f0102008843f6eb45fd3bb0b2e88c774adab5fbaab27117d", size = 238211, upload-time = "2026-06-26T18:22:37.671Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/9b/ff/a620267401db30a50cc8450ee90730e2d4a85658c055c0e760d4ed47fb13/greenlet-3.5.3-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:c8d87c2134d871df96ecdea9cec7cbaab286dadab0f56476e57aaf9e8ac11550", size = 287609, upload-time = "2026-06-26T18:21:14.724Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/d6/fa/5401ac78021c826a25b6dde0c705e0a8f29b617509f9185a31dac15fbe1b/greenlet-3.5.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2d185dd1621757e70c3861cceffd5317ab4e7ed7eb09c82994828468527ade5", size = 607435, upload-time = "2026-06-26T19:07:11.412Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/e9/76/1dc144a2e56e65d36405078ed774224375ea520a1870a6e46e08bb4ac7bf/greenlet-3.5.3-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1c514a468149bf8fbbab874188a3535cd8a48a3e353eb53a3d424296f8dbacd3", size = 619787, upload-time = "2026-06-26T19:10:08.396Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/57/61/2f5b1adf256d039f5dab8005de8d3d7ad2b0070a3219c0e036b3fbfeb440/greenlet-3.5.3-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9ad04dd75458c6300b047c61b8639092433d205a25a14e310d6582a480efcca1", size = 625580, upload-time = "2026-06-26T19:24:18.344Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/bf/87/c298cee62df1de4ad7fec32abda73526cff347fd143a6ed4ac369246668a/greenlet-3.5.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:915f887cf2682b66419b879423a2e072634aa7b7dce6f3ada4957cfced3f1e9a", size = 616786, upload-time = "2026-06-26T18:32:19.128Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/3e/d9/ab7fc9e543e44d6879b0a6ef9a4b2188940fd180cc65d6f646883ddf7201/greenlet-3.5.3-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:afaabdd554cd7ae9bbb3ca070b0d7fdfd207dbf1d16865f7233837709d354bda", size = 427933, upload-time = "2026-06-26T19:25:38.219Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/9e/2e/e6f009885ed0705ccf33fe0583c117cfd03cde77e31a596dd5785a30762b/greenlet-3.5.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:766cfd421c13e450feb340cd472a3ed9957d438727b7b4593ad7c76c5d2b0deb", size = 1574316, upload-time = "2026-06-26T19:09:04.273Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/ef/fe/43fd110b01e40da0adb7c90ac7ea744bef2d43dca00de5095fd2351c2a68/greenlet-3.5.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2ecda9ec22edf38fa389369eaed8c3d37c05f3c54e69f69438dbb2cc1de1458b", size = 1638614, upload-time = "2026-06-26T18:31:46.297Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/0f/7c/062447147a61f8b4337b156fe70d32a165fcf2f89d7ca6255e572806705c/greenlet-3.5.3-cp313-cp313-win_amd64.whl", hash = "sha256:c82304750f057167ff60d188df1d0cc1764ce9567eadf03e6a7443bcedd0b30b", size = 239850, upload-time = "2026-06-26T18:21:54.613Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/c7/7e/220a7f5824a64a60443fc03b39dfac4ea63a7fb6d481efa27eafa928e7f4/greenlet-3.5.3-cp313-cp313-win_arm64.whl", hash = "sha256:dc133a1569ee667b2a6ef56ce551084aeefd87a5acbc4736d336d1e2edc6cfc4", size = 238141, upload-time = "2026-06-26T18:22:48.507Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/c3/93/43e116ee114b28737ba7e12952a0d4e2f55944d0f84e42bc91ba7192a3c9/greenlet-3.5.3-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:fd2e02fa07485778536a036222d616ab957b1d533f36b3ed98ce725d9c9d3117", size = 288202, upload-time = "2026-06-26T18:23:49.604Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/82/2f/146d218299046a43d1f029fd544b3d110d0f175a09c715c7e8da4a4a345d/greenlet-3.5.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df0a0628d1597eb0897b62f55d1343f772405fd25f3b2a796c76874b0c2e22e8", size = 654096, upload-time = "2026-06-26T19:07:12.71Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/a0/cc/04738cafb3f45fa991ea44f9de94c47dcec964f5a972300988a6751f49d9/greenlet-3.5.3-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ebd933a6adabc298bab47731a130fe6bfb888bd934eee37810f151159544540d", size = 666304, upload-time = "2026-06-26T19:10:09.503Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/86/a9/73fa62893d5b84b4205544e6b673c654cc43aa5b9899bac00f04d64af73d/greenlet-3.5.3-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8d19fe6c39ebff9259f07bcc685d3290f8fa4ea2278e51dd0008e4d6b0f2d814", size = 670657, upload-time = "2026-06-26T19:24:19.967Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/ce/aa/4e0dad5e605c270c784ab911c43da6adb136ccd4d81180f763ca429a723d/greenlet-3.5.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4b9d501b40e80b70e32323c799dd9b420a5577a9601469d362ae1ffb690f3a7c", size = 663635, upload-time = "2026-06-26T18:32:20.802Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/29/7e/2ffce64929fb3cab7b65d5a0b20aaf9764e227681d731b041077fc9a525a/greenlet-3.5.3-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:962c5df2db8cb446da51edf1ca5296c389d93b99c9d8aa2ee4c7d0d8f1218260", size = 473497, upload-time = "2026-06-26T19:25:39.421Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/d1/50/13efdbea246fe3d3b735e191fec08fb50809f53cd2383ebe123d0809e44b/greenlet-3.5.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a1fad1d11e7d6aab184107baa8e4ece11ccba3ec9599cd7efa5ff4d70d43256a", size = 1621252, upload-time = "2026-06-26T19:09:05.647Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/f7/22/c0a336ae4a1410fd5f5121098e5bfbf1865f64c5ef80b4b5412886c4a332/greenlet-3.5.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:fad5aec764399f1b5cc347ad250a59660f20c8f8888ea6bae1f93b769cce1154", size = 1684824, upload-time = "2026-06-26T18:31:47.738Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/7a/94/91aec0030bea75c4b3244251d0de60a1f3432d1ecb53ab6c437fb5c3ba61/greenlet-3.5.3-cp314-cp314-win_amd64.whl", hash = "sha256:7669aa24cf2a1041d6f7899575b494a3ab4cf68bfcc8609b1dc0be7272db835e", size = 240754, upload-time = "2026-06-26T18:22:15.669Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/e5/06/68d0983e79e02138f64b4d303c500c27ddb48e5e77f3debb80888a921eae/greenlet-3.5.3-cp314-cp314-win_arm64.whl", hash = "sha256:5b4807c4082c9d1b6d9eed56fcd041863e37f2228106eef24c30ca096e238605", size = 239549, upload-time = "2026-06-26T18:22:42.996Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/91/95/3e161213d7f1d378d15aa9e792093e9bfe01844680d04b7fd6e0107c9098/greenlet-3.5.3-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:271a8ea7c1024e8a0d7dd2be66dd66dda8a07193f41a17b9e924f7600f5b62be", size = 296389, upload-time = "2026-06-26T18:22:20.657Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/00/92/715c44721abe2b4d1ae9abde4179411868a5bff312479f54e105d372f131/greenlet-3.5.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:19131729ae0ddc3c2e1ef85e650169b5e37ee32e400f215f78b94d7b0d567310", size = 653382, upload-time = "2026-06-26T19:07:14.209Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/a0/83/37a10372a1090a6624cca8e74c12df1a36c2dc36429ed0255b7fb1aeee23/greenlet-3.5.3-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1540dd8e5fc2a5aec40fbb98ef8e149fa47c89a4b4a1cf2575a14d3d1869d7a8", size = 659401, upload-time = "2026-06-26T19:10:10.876Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/cb/73/8faec206b851c22b1733545fda900829a1f3f5b1c78ae7e0fb3dba57d9f4/greenlet-3.5.3-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b897d97759425953f69a9c0fac67f8fe333ec0ce7377ef186fb2b0c3ad5e354d", size = 659582, upload-time = "2026-06-26T19:24:21.357Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/db/e2/d1509cad4207da559cc42986ecdd8fc67ad0d1bba2bf03023c467fd5e0f3/greenlet-3.5.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e81fa194a1d20967877bdf9c7794db2bc99063e5be36aee710c08f04c5bb087f", size = 656969, upload-time = "2026-06-26T18:32:22.272Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/b4/55/50c19e49f8045834ada71ef12f8ad048eba8517c6aa41161bed676328fae/greenlet-3.5.3-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:3236754d423955ea08e9bb5f6c04a7895f9e22c290b66aa7653fcb922d839eb0", size = 491037, upload-time = "2026-06-26T19:25:40.672Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/86/7d/eaf70de20aadca3a5884aec58362861c64ce45e7b277f47ed026926a3b89/greenlet-3.5.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:55cf4d777485d43110e47133cbba6d74a8885a87ec1227ef0267f9ee80c5aa21", size = 1617822, upload-time = "2026-06-26T19:09:06.893Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/8a/f9/414d38fc400ae4350d4185eaad1827676f7cf5287b9136e0ed1cbbe20a7f/greenlet-3.5.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:12a248ba75f6a9a236375f52296c498c89ff1d8badf32deb9eca7abd5853f7da", size = 1677983, upload-time = "2026-06-26T18:31:49.396Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/e4/15/7edb977e08f9bff702fe42d6c902702786ff6b9694058b4e6a2a6ac90e57/greenlet-3.5.3-cp314-cp314t-win_amd64.whl", hash = "sha256:efc6bd60ea02e085862c74a3ef64b147ffc6f1a5ea7d9f26e7a939943f68c1e3", size = 243626, upload-time = "2026-06-26T18:24:41.485Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/2c/8a/93928dce91e6b3598b5e779e8d1fd6576a504640c58e78627077f6a7a91a/greenlet-3.5.3-cp315-cp315-macosx_11_0_universal2.whl", hash = "sha256:ea03f2f04367845d6b58eeed276e1e56e51f0b97d8ad5a88a7d20a91dc9056cc", size = 288860, upload-time = "2026-06-26T18:22:48.07Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/4f/ca/69db42d447a1378043e2c8f19c09cbbd1263371505053c496b49066d3d16/greenlet-3.5.3-cp315-cp315-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78dbef602fda6d97d957eb7937f70c9ce9e9527330347f8f6b6f9e554a9e7a47", size = 659747, upload-time = "2026-06-26T19:07:15.565Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/a8/0b/af7ac2ef8dd41e3da1a40dda6305c23b9a03e13ba975ec916357b50f8575/greenlet-3.5.3-cp315-cp315-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f73857adb8fee13fa56c172bd11262f888c0c648f9fea113e777bb2c7904a81", size = 670419, upload-time = "2026-06-26T19:10:12.293Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/25/aa/952cf28c2ff949a8c971134fb43854dd7eaa737218723aaef758f8c9aead/greenlet-3.5.3-cp315-cp315-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:cefa9cef4b371f9844c6053db71f1138bc6807bab1578b0dae5149c1f1141357", size = 674261, upload-time = "2026-06-26T19:24:22.79Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/51/1e/1d51640cacbfc455dbe9f9a9f594c49e4e244f63b9971a2f4764e46cc53d/greenlet-3.5.3-cp315-cp315-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:232fec92e823addaf02d9472cf7381e24a1d046a6ced1103c5caa4c21b9dfc1d", size = 668787, upload-time = "2026-06-26T18:32:24.298Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/dc/f2/b00d6f5e63e531a93562b2ec1a4c320fbee91f580fc42e6417af69d706e5/greenlet-3.5.3-cp315-cp315-manylinux_2_39_riscv64.whl", hash = "sha256:6219b6d04dbf6ba6084d77dc609e8473060dc55f759cbf626d512122781fa128", size = 480322, upload-time = "2026-06-26T19:25:41.852Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/21/66/4030d5b0b5894500023f003bb054d9bb354dfbd1e186c3a296759172f5f5/greenlet-3.5.3-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:2421c3564da9429d5586d46ca31ebb26516b5498a802cf65c041a8e8a8980d34", size = 1626305, upload-time = "2026-06-26T19:09:08.281Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/0e/50/5221371c7550108dfa3c378debc41d032aa9c78e89abb01d8011cfc93289/greenlet-3.5.3-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:e0f0d160f0b2e558e6c75f7930967183255dc9735e5f5b8cae58ee09c9576d8b", size = 1688631, upload-time = "2026-06-26T18:31:51.278Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/68/5d/00d469daae3c65d2bf620b10eee82eb022127d483c6bc8c69fae6f3fbf17/greenlet-3.5.3-cp315-cp315-win_amd64.whl", hash = "sha256:dd99329bbc15ca78dcc583dba05d0b1b0bae01ab6c2174989f5aaee3e41ac930", size = 241027, upload-time = "2026-06-26T18:22:38.203Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/e7/e8/883785b44c5780ed71e83d3e4437e710470be17a2e181e8b601e2da0dc4a/greenlet-3.5.3-cp315-cp315-win_arm64.whl", hash = "sha256:499fef2acede88c1864a57bb586b4bf533c81e1b82df7ab93451cdb47dfec227", size = 240085, upload-time = "2026-06-26T18:23:54.217Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/1c/da/4f4a8450962fad137c1c8981a3f1b8919d06c829993d4d476f9c525d5173/greenlet-3.5.3-cp315-cp315t-macosx_11_0_universal2.whl", hash = "sha256:176bc16a721fa5fc294d70b87b4dfa5fbdd251b3da5d5372735ecef9bd7d6d0c", size = 297221, upload-time = "2026-06-26T18:23:27.176Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/57/66/b3bfae3e220a9b63ea539a0eea681800c69ab1aada757eae8789f183e7ce/greenlet-3.5.3-cp315-cp315t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:629b614d2b786e89c50440e246f33eea78f58a962d0bdbbcc809e6d13605903f", size = 657221, upload-time = "2026-06-26T19:07:16.973Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/7b/81/b6d4d73a709684fc77e7fa034d7c2fe82cffa9fc920fadcaa659c2626213/greenlet-3.5.3-cp315-cp315t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2b2e857ae16f5f72142edf75f9f176fe7526ba19a2841df1420516f83831c9f2", size = 663226, upload-time = "2026-06-26T19:10:13.723Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/e9/39/0e0938a75115b939d42733a2a12e1d349653c9531fe6fe563e8a681f04e6/greenlet-3.5.3-cp315-cp315t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:16d192579ed281051396dddd7f7754dac6259e6b1fb26378c87b66622f8e3f91", size = 663706, upload-time = "2026-06-26T19:24:24.312Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/f5/07/e210b02b589f16e74ff48b730690e4a34ffe984219fce4f3c1a0e7ec8545/greenlet-3.5.3-cp315-cp315t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e515757e2e36bcbf1fad09a46e1557e8b1ae1797d4b44d09da7deed88ad28608", size = 660802, upload-time = "2026-06-26T18:32:26.081Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/5b/41/35d1c678cdb3c3b9e6bee691728e563cfb294202b23c7a4c3c2ccc343589/greenlet-3.5.3-cp315-cp315t-manylinux_2_39_riscv64.whl", hash = "sha256:4399eb8d041f20b68d943918bc55502a93d6fdc0a37c14da7881c04139acee9d", size = 498803, upload-time = "2026-06-26T19:25:43.063Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/eb/2e/5303eb3fa06bca089060f479707182a93e360683bc252acf846c3090d34e/greenlet-3.5.3-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:b363d46ed1ea431825fdb01471bb024fc08399bad1572a616e853c7684415adb", size = 1622157, upload-time = "2026-06-26T19:09:09.527Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/54/70/50de47a488f14df260b50ae34fb5d56016e308b098eab02c878b5223c26a/greenlet-3.5.3-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:e44da2f5bbdaabaf7d80b73dbb430c7035771e9f244e3c8b769715c9d8fa0a16", size = 1681159, upload-time = "2026-06-26T18:31:52.986Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/a7/13/1055e1dda7882073eda533e2b96c62e55bbd2db7fda6d5ece992febc7071/greenlet-3.5.3-cp315-cp315t-win_amd64.whl", hash = "sha256:8ff8bed3e3baa20a3ea261ce00526f1898ad4801d4886fd2220580ee0ad8fadf", size = 244007, upload-time = "2026-06-26T18:22:04.353Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/b4/0d/ca7d15afbdc397e3401134c9e1800d51d12b829661786187a4ad08fe484f/greenlet-3.5.3-cp315-cp315t-win_arm64.whl", hash = "sha256:b7068bd09f761f3f5b4d214c2bed063186b2a86148c740b3873e3f56d79bac31", size = 242586, upload-time = "2026-06-26T18:23:37.93Z" },
]
[[package]] [[package]]
name = "html5lib" name = "html5lib"
version = "1.1" version = "1.1"
@@ -1534,6 +1624,25 @@ wheels = [
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/bc/60/5382c03e1970de634027cee8e1b7d39776b778b81812aaf45b694dfe9e28/pillow-12.2.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:bfa9c230d2fe991bed5318a5f119bd6780cda2915cca595393649fc118ab895e", size = 7080946, upload-time = "2026-04-01T14:46:11.734Z" }, { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/bc/60/5382c03e1970de634027cee8e1b7d39776b778b81812aaf45b694dfe9e28/pillow-12.2.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:bfa9c230d2fe991bed5318a5f119bd6780cda2915cca595393649fc118ab895e", size = 7080946, upload-time = "2026-04-01T14:46:11.734Z" },
] ]
[[package]]
name = "playwright"
version = "1.61.0"
source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple/" }
dependencies = [
{ name = "greenlet" },
{ name = "pyee" },
]
wheels = [
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/44/ee/31e4e0db36588b817a10b299a0285082545fde7d36543c2abe498bb3d61a/playwright-1.61.0-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:ff138c3a604f69911e9d42fd036e55c2a171e5616edf04c1e7f60a2a285540b0", size = 43421877, upload-time = "2026-06-29T10:32:48.428Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/42/35/71395dd3ecc798965be4a3ef8c443217d4abca168e7cb34536304f9489e6/playwright-1.61.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:009588c2a7e499bc5a8b425b61fa65490968bbda9cd69e0cf2cff10f8304659a", size = 42205016, upload-time = "2026-06-29T10:32:52.104Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/f4/44/323164cf5cd1647bdefce76ffce27651aadb959d089b48f53ea40918276e/playwright-1.61.0-py3-none-macosx_11_0_universal2.whl", hash = "sha256:9f7de4536088d12037c13a52b7ea34b59270b78926bb56935070597ffac6b1af", size = 43421884, upload-time = "2026-06-29T10:32:55.773Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/ab/f8/a35bf179e4ba2522c1893635094a64e407572547bd61528820fc0abc87fe/playwright-1.61.0-py3-none-manylinux1_x86_64.whl", hash = "sha256:54f3b39f6eab832e33458c1dd7da0b5682aedab3b09ae731b5c59fa12fd2024e", size = 47421381, upload-time = "2026-06-29T10:32:59.903Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/b7/eb/e3f922348ec17c315f98c463f72faa1181a1c3de0bfe31a8d2edf6561723/playwright-1.61.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93454322ade8c11d5d6c211bfd91bdfb9ffb4810e3e026371bcbc4bec1b7ee4c", size = 47120545, upload-time = "2026-06-29T10:33:03.574Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/c2/a6/5be4e52b40a9c0c8a073e7c5b0785c05cf5a9ea8f8a7b5b260e32d970342/playwright-1.61.0-py3-none-win32.whl", hash = "sha256:372d55a6f1248fa1dd47599686980cb8fb5bbe6fcda59eab793eb657c11d8a9b", size = 37844841, upload-time = "2026-06-29T10:33:07.361Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/6c/fd/2b78036e5fbe9d5f5645bbe08a1eac7160c51243c0093963edbcf67c35d9/playwright-1.61.0-py3-none-win_amd64.whl", hash = "sha256:35c6cc4589a5d00964a59d7b3e59641e0aac0c02f15479a7af77d20f6bc79597", size = 37844846, upload-time = "2026-06-29T10:33:10.637Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/27/0d/1b0f3c4ee4eb0514bc805b5c2f9a223e5b6de4f11a926f5235d51d0fc81b/playwright-1.61.0-py3-none-win_arm64.whl", hash = "sha256:e9fcbffcf557a8620fdedd92491eb59a32d18e23d6f3b4f6214b952be324fe51", size = 33955127, upload-time = "2026-06-29T10:33:14.008Z" },
]
[[package]] [[package]]
name = "pluggy" name = "pluggy"
version = "1.6.0" version = "1.6.0"
@@ -1618,6 +1727,18 @@ wheels = [
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" },
] ]
[[package]]
name = "pyee"
version = "13.0.1"
source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple/" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/8b/04/e7c1fe4dc78a6fdbfd6c337b1c3732ff543b8a397683ab38378447baa331/pyee-13.0.1.tar.gz", hash = "sha256:0b931f7c14535667ed4c7e0d531716368715e860b988770fc7eb8578d1f67fc8", size = 31655, upload-time = "2026-02-14T21:12:28.044Z" }
wheels = [
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/a0/c4/b4d4827c93ef43c01f599ef31453ccc1c132b353284fc6c87d535c233129/pyee-13.0.1-py3-none-any.whl", hash = "sha256:af2f8fede4171ef667dfded53f96e2ed0d6e6bd7ee3bb46437f77e3b57689228", size = 15659, upload-time = "2026-02-14T21:12:26.263Z" },
]
[[package]] [[package]]
name = "pygments" name = "pygments"
version = "2.20.0" version = "2.20.0"